Write Tuple
Append a tuple to the Forum and learn the durability and indexing guarantees.
What this page covers
The write operation: how a tuple becomes durable, the sequence number it receives, and what the in-memory index promises.
Construction
A Forum wraps a single Strand. Multiple Forum instances can share the same Strand if you want a single audit log, but each instance manages its own live_tuples index — and indices may briefly diverge until they re-converge on the next Strand sync.
A Forum wraps a single Strand. Multiple Forums can share the same Strand if you want a single audit log, but each Forum instance manages its own live_tuples index.
use std::sync::Arc;
use tokio::sync::RwLock;
use strand::{Strand, StrandConfig};
use forum::Forum;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let cfg = StrandConfig::default().with_storage(dir.path());
let strand = Arc::new(RwLock::new(Strand::new(cfg).await?));
let forum = Forum::new(strand);
// Write a 7-byte tuple. The Forum returns its entry id (also the strand
// sequence number assigned to this write).
let id = forum.write(b"hello".to_vec()).await?;
println!("wrote tuple at entry id {id}");
Ok(())
}Step-by-step
- The Strand write lock is taken.
- The current
strand.len()is read; that value becomes theEntryIdfor this tuple. - A
TupleEntry::Write { id, tuple }is serialized withserde_json::to_vec. - The bytes are appended to the Strand. On return, the new block is durable on disk.
- The
live_tupleswrite lock is taken; the tuple is inserted under its id. - Both locks are released.
The two locks are taken in sequence (Strand first, then index). They are never held simultaneously across await boundaries.
Durability guarantees
| Property | Guarantee |
|---|---|
Survival after write returns | The Strand block is written through to the configured storage backend |
| Order among writes | Strictly increasing EntryId |
Visibility to concurrent takes | Immediate, once the index write lock is released |
| Visibility to peers | Eventual, after Strand replication |
Errors
| Variant | Cause | Recovery |
|---|---|---|
ForumError::Strand(String) | Strand append failed (disk, quorum) | Retry the write; the operation is idempotent only if you reuse the same payload — the new attempt will receive a new EntryId |
ForumError::Serialization(serde_json::Error) | TupleEntry failed to encode | Should not happen for byte vectors; check for changes to the enum definition |
Practical patterns
Job queue producer
use forum::Forum;
use serde::Serialize;
#[derive(Serialize)]
struct Job {
kind: String,
payload: serde_json::Value,
}
async fn enqueue(forum: &Forum, job: &Job) -> Result<(), Box<dyn std::error::Error>> {
let bytes = serde_json::to_vec(job)?;
forum.write(bytes).await?;
Ok(())
}Idempotent producer (deduplicate before write)
use forum::{Forum, Pattern};
async fn enqueue_once(forum: &Forum, payload: &[u8]) -> Result<bool, Box<dyn std::error::Error>> {
if forum.read(&Pattern(payload.to_vec())).await?.is_some() {
return Ok(false); // already present, skip
}
forum.write(payload.to_vec()).await?;
Ok(true)
}read does not consume; this is the right primitive for "is this already queued?" checks. Be aware of the race: another writer may insert between the read and the write. If you need strict deduplication, key tuples with a content-addressed prefix and have the consumer drop duplicates.
Limits
- The in-memory
live_tuplesmap holds every live (not-yet-taken) tuple. Memory grows linearly with queue depth. Take consumed tuples aggressively if your workload allows it. - Tuples are arbitrary byte vectors; the Forum does not impose a maximum size, but Strand block-size limits apply (see Strand's storage page).