novadocs
Concepts

Concepts

Producers

Producer policy, sessions and one-shot appends, automatic batching, and what happens when the leader moves

There are two ways to append: open a persistent producer session and stream batches through it, or append records one off with a single call. This page covers both, along with how a stream admits writers (its producer policy), how the SDK batches, and how producers follow the stream's leader. Conditional appends with match_seq_num and fencing tokens are on the concurrency page.

Producer policy: Any or Fenced

A stream's producer policy is its sharing discipline for writers, set at creation from the request or the bucket's defaults (and changeable later by a reconfigure, which hands the leadership off):

any (default)fenced
Concurrent sessionsMany; their batches interleave in arrival order through one sequencerExactly one
Fencing tokenCooperative: binds only writers that present oneMandatory while a token is installed
Last handle closesLeadership stays warm for the idle timeout (30 s) so a reconnect rejoins cheaplyLeadership releases at once, so a handoff never waits
One-shot appendsInterleave freely with sessionsSequential: each briefly holds the one handle; a concurrent one is refused (retryable)

Choose fenced when your application has a single logical writer per stream and wants nova to refuse a second one; choose any when many uncoordinated clients append to one stream.

Sessions and one-shot appends

A producer session is a bidirectional gRPC stream (AppendSession): an open frame naming the stream by (bucket, key) and an optional fencing token, then batches, each answered in order by an ack or a rejection. The session is the wire: it dies with the connection, and a reconnect re-handshakes. Sessions exist only on gRPC; the HTTP surface has no session verb.

A one-shot append (Append) is one unary call: one batch, one durable result. It is what POST /v1/streams/{bucket}/{key}/records maps to. On the star it joins the stream's leadership per call; on any streams the leadership stays warm between calls, so a burst pays the join once.

Session (pipelined) versus one-shot
p, err := c.OpenStream(ctx, client.Address{Bucket: "demo", Key: "events"})
res, err := p.Append(ctx, client.Bodies([]byte("a"), []byte("b"))...)
err = p.Close(ctx)

res, err = c.Append(ctx, client.Address{Bucket: "demo", Key: "events"},
	client.AppendRecord{Body: []byte("c"), Headers: []client.Header{{Name: []byte("k"), Value: []byte("v")}}})

Both return an AppendResult{Start, End, Tail}: Start is the first appended record, End one past the last, Tail the committed tail as of the ack. Hold a session for pipelined throughput; use a one-shot for convenience or from a request handler.

The SDK batches for you

A Producer stages appends and a single send loop cuts wire batches whenever a slot in the in-flight window frees: natural batching. An idle append goes out immediately, so batching adds no latency; under load, whatever accumulated while the window was full rides the next slot as one batch. The numbers:

PropertyValue
Wire batch cap1 MiB
In-flight windowAdvertised by the star at open (default 16 batches)
Staged buffer bound1 MiB; a caller blocks when that much is staged behind a full window
WithBatchLinger(d)Wait up to d (at most 1 s) for a fuller batch; default 0
WithBatchSize(n)Lower the accumulation target from 1 MiB

Every caller resolves with exactly its own records' range, even when its append was merged with others. Append blocks until durable; AppendAsync returns a channel. Close flushes what is staged and waits for the window to drain.

nova append uses a session with a 5 ms linger by default (--linger 0 cuts immediately).

Producers follow the leader

The stream's leader, the star holding its leadership, serves appends. Any other star answers NOT_OWNER with the holder's address, and the SDK follows it below your call, remembering the holder for the stream until a failure from it. Over HTTP you never see a redirect: the data bridge proxies a foreign-led stream one hop to its holder, and the Gateway follows redirects itself. Redirects dial the star's advertised address verbatim, so it must resolve from wherever clients run; one that cannot surfaces at once as client.ErrHolderUnresolvable.

When the leader moves

Leadership moves on a drain, a rebalance, a handoff, or a crash. The session breaks; the SDK reconnects within its retry budget (Options.RetryBudget, default 30 s), re-handshakes at the stream's new leader, and resends the unacknowledged tail in order: plain batches as fresh appends (at-least-once), conditional batches re-evaluated (exactly-once). Past the budget the producer is broken: every pending and future append resolves client.ErrUnavailable, and you open a new one. A refusal no retry can cure (fenced out, not found, an invalid record) surfaces immediately instead.

An append whose outcome is ambiguous (the wire died between send and ack) resolves the same way on every plane: CheckTail, read the suffix from the last position you know, look for your record, and resend only if it is absent.

Source documents

  • docs/adr/0020-client-natural-batching.md
  • proto/nova/v1/dataplane.proto
  • client/producer.go, client/oneshot.go

On this page