Skip to content

Producer

Producer ingests data by opening stream (implicitly) on feed, appending batches of records, and sealing stream into commit (optional). What belongs in one stream and when to seal are producer decisions. See Architecture.

StreamStack producer write path

Lifecycle

Step API What Happens
Open open(feed) Allocates new stream, acquires writer lease, returns AppendSession
Append append / appendAsync Submits batch, assigns sequence numbers, persists to WAL, returns after durability
Seal seal(message) Closes stream, finalizes artifacts, publishes commit, trims WAL

Feed holds many sealed streams over time. Typically one open stream per feed at time, enforced by writer lease.

WAL (Durability)

Batches are not written to bucket one at a time. Server buffers them into WAL segments and flushes when either threshold is reached:

Trigger Default
Time ~250 ms
Buffered size ~8 MiB

Each segment is one atomic object PUT. When PUT succeeds, every batch in that segment is acknowledged. When PUT fails, those batches fail.

After ack, data survives process crash. May not yet appear in pack or manifest form.

Publish / Append

append and appendAsync complete when batch is durable in WAL segment. Response is AppendResult with assigned sequence range (first, last, recordCount).

Unsealed records are readable on feed before seal. Seal publishes them into commit history with full storage artifacts.

API Behavior
append Blocks until WAL ack
appendAsync Returns future that completes on WAL ack

On server, sequence assignment and fence or match_seq checks run at submit time. On client, appendAsync respects maxUnackedBytes (default 16 MiB) and blocks when unacked payload would exceed cap. seal drains all in-flight async appends before calling server.

Continuous Flush

After WAL ack, records are handed to background flusher that incrementally builds pack, manifest, and key index state while stream stays open. Append throughput is not blocked on artifact upload.

Seal waits for this lane to drain, then publishes commit. Seal latency can exceed last append ack on large streams.

Seal Stream

On seal, server:

Operation What Happens
Seal Marks stream sealed and rejects further appends
Flush Flushes remaining WAL data
Finalize Drains continuous flusher and finalizes artifacts
Publish Publishes commit record
Trim Trims WAL segments for that stream

Batches

AppendBatch carries one or more records plus optional coordination fields:

Field Purpose
Records Payload bytes, optional key, version, tombstone marker, optional fingerprint
fencingToken Must match stream writer epoch or append is rejected
matchSequence Optimistic tail check: must equal current last sequence

Fingerprint is an opaque 32-byte identity. Omit it and the server defaults to SHA-256 of the payload. If supplied, the server stores it as-is.

CLI binds writer fencing token automatically. Java client does same when batch omits one.

Keyed and keyless records can mix in one stream when feed allows keys. Key index artifacts produced only for keyed feeds.

Leases & Fencing

Each feed has one active writer lease at time. Opening or resuming stream acquires lease and bumps writer epoch. Superseded writer receives WriterFencedException on later appends or seal.

In multi-node deployment, write-path requests that hit non-owner may proxy to lease holder. See Installation.

Recovery

Operation Purpose
resume(feed) Reattach to latest unsealed stream after restart
admin resume Server-side equivalent for abandoned open stream
admin discard Drop unsealed streams without publishing commit
admin repair Finish seal that failed mid-publish

Resume discards staged flush state for that stream and continues from WAL.

Examples

CLI one-shot append and seal:

./streamstack append orders \
  --record k1=hello --record k2=world --message 'hourly poll'

Java SDK with explicit session control:

AppendSession session = client.open("orders");
session.append(new AppendBatch(List.of(
    AppendRecord.of("hello".getBytes()),
    AppendRecord.keyed("k2".getBytes(), 0L, "world".getBytes())
), Optional.empty(), Optional.empty()));
Commit commit = session.seal("hourly poll");

Pipelined ingest with byte window before seal:

AppendSession session = client.open("orders").maxUnackedBytes(4 * 1024 * 1024);
for (AppendBatch batch : batches) {
    session.appendAsync(batch);
}
Commit commit = session.seal("bulk load");