stream

package
v0.30.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 13, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package stream declares the SDK's durable, partitioned, offset-addressed log: the Stream a producer appends records to and any number of readers read at their own pace, each record kept until its retention ends rather than until somebody acknowledges it (ADR 0231).

It is the left-hand column of a frontier core/data/queue draws on its right (ADR 0054). A queue removes a message at its acknowledgement, leases it to one consumer at a time, keeps no order past the first retry and dead-letters what fails; a stream keeps every record for its retention, lets every reader name the offset it reads from, keeps the order of each partition, and leaves a failing record where it is — the reader that cannot handle it parks, and every other reader goes on.

value, err := s.Append(ctx, []stream.RecordValue{{Key: order, Payload: body}})
entries, err := s.Read(ctx, value.Partition, from, 100)

A record's partition is its key's, by PartitionOf — FNV-1a modulo the partition count —, the same on every engine, so a key's records are read in the order they were appended wherever they are kept. Ordering holds within a partition and nowhere else.

The port is FROZEN at four methods (ADR 0039); every other capability is a sibling reached by type assertion: Waker, Seeker, Trimmer, Compactor and Committer.

Package stream — range 0.2.38.* (ADR 0231 core/data/stream block), and the engines' and the subscriber's 0.3.94.* (ADR 0231 service/data/stream block, declared here as ADR 0160 requires).

Package stream — hosts the stream policy every engine is configured with, and the guard every engine runs so that all of them refuse identical inputs identically (ADR 0031).

Index

Constants

View Source
const (

	// CodeStreamMisconfigured identifies a PolicyValue field, or a call's
	// argument, an engine cannot honour, refused before anything is written.
	CodeStreamMisconfigured errs.Code = 0x00_02_26_01 // 0.2.38.1

	// CodeRecordTooLarge identifies an Append holding a record whose key and
	// payload together exceed PolicyValue.MaxRecordBytes.
	CodeRecordTooLarge errs.Code = 0x00_02_26_02 // 0.2.38.2

	// CodeOffsetTrimmed identifies a read from an offset below the first one
	// its partition still retains.
	CodeOffsetTrimmed errs.Code = 0x00_02_26_03 // 0.2.38.3

	// CodeOffsetAhead identifies an offset past the next one its partition
	// will assign.
	CodeOffsetAhead errs.Code = 0x00_02_26_04 // 0.2.38.4

	// CodeUnknownPartition identifies a partition outside [0, Partitions()).
	CodeUnknownPartition errs.Code = 0x00_02_26_05 // 0.2.38.5

	// CodeInvalidBatchSize identifies an Append of no record, or a Read asked
	// for a non-positive number of entries.
	CodeInvalidBatchSize errs.Code = 0x00_02_26_06 // 0.2.38.6

	// CodeStreamFull identifies an Append refused because the stream held
	// PolicyValue.MaxBytes for longer than PolicyValue.BlockTimeout.
	CodeStreamFull errs.Code = 0x00_02_26_07 // 0.2.38.7

	// CodeUnknownGroup identifies a Committed for a group that never committed
	// an offset on that partition.
	CodeUnknownGroup errs.Code = 0x00_02_26_08 // 0.2.38.8

	// CodePartitionsMixed identifies an Append whose records' keys hash to more
	// than one partition: a batch is appended to one partition, under one sync.
	CodePartitionsMixed errs.Code = 0x00_02_26_09 // 0.2.38.9
)
View Source
const (

	// CodeStreamBackendFailed identifies an operation a durable engine depends
	// on that its storage refused: a filesystem call the operating system
	// refused, or a statement the SQL engine's database did not complete.
	CodeStreamBackendFailed errs.Code = 0x00_03_5E_01 // 0.3.94.1

	// CodeStreamDirectoryUnusable identifies a stream directory that cannot be
	// prepared, is not a directory, or is writable by accounts that must not be
	// able to inject or remove records.
	CodeStreamDirectoryUnusable errs.Code = 0x00_03_5E_02 // 0.3.94.2

	// CodeSQLStreamMisconfigured identifies an SQL engine configuration refused
	// at construction: no transactor, one it cannot join a transaction of, a
	// dialect it cannot spell, or a table name it cannot interpolate safely.
	CodeSQLStreamMisconfigured errs.Code = 0x00_03_5E_03 // 0.3.94.3

	// CodeSubscriberMisconfigured identifies a subscriber configuration the
	// engine refuses: no handler, no group, or a handler whose author has not
	// asserted that it is idempotent.
	CodeSubscriberMisconfigured errs.Code = 0x00_03_5E_04 // 0.3.94.4
)
View Source
const DefaultMaxRecordBytes int = 1 << 20

DefaultMaxRecordBytes bounds one record when PolicyValue.MaxRecordBytes is left at zero: one mebibyte, the queue's DefaultMaxMessageBytes and Kafka's default message.max.bytes.

View Source
const DefaultSegmentBytes int64 = 64 << 20

DefaultSegmentBytes is a segment's size when PolicyValue.SegmentBytes is left at zero: 64 MiB, kernel/fs/seglog's and the consensus log's.

Variables

View Source
var (
	// StreamMisconfigured refuses a PolicyValue an engine cannot honour — a
	// negative bound, a BlockTimeout with no MaxBytes, a MaxBytes below one
	// record — and an argument a call cannot honour, such as an empty group
	// name. The "field" field names which value.
	StreamMisconfigured = errs.Static(CodeStreamMisconfigured, "STREAM_MISCONFIGURED",
		"The stream policy is not one this engine can honour",
		"core/data/stream: a policy field or an argument has no usable value; the field names which",
		0,
		78)

	// RecordTooLarge refuses an Append holding a record above the policy's
	// bound, at the producer that can still make it smaller. Its fields carry
	// the two sizes and the record's index in the batch, never its bytes.
	RecordTooLarge = errs.Static(CodeRecordTooLarge, "RECORD_TOO_LARGE",
		"The record exceeds the stream's configured maximum size",
		"core/data/stream: a record larger than PolicyValue.MaxRecordBytes; the fields carry both sizes, never the bytes",
		413,
		78)

	// OffsetTrimmed refuses a read below the first offset the partition retains:
	// the records were trimmed by retention or Trim — never by compaction,
	// whose holes a read skips. It is a refusal and not a silent skip because a reader that
	// resumed later than it asked has lost records without knowing; its fields
	// name the offset asked and the first retained, so the reader decides.
	OffsetTrimmed = errs.Static(CodeOffsetTrimmed, "OFFSET_TRIMMED",
		"The requested records are no longer retained",
		"core/data/stream: read below the first retained offset; the fields carry both offsets",
		409,
		65)

	// OffsetAhead refuses an offset past the partition's next one: a read, a
	// commit or a trim naming a record that does not exist yet. It is usually a
	// cursor carried over from another stream, or from before a rebuild.
	OffsetAhead = errs.Static(CodeOffsetAhead, "OFFSET_AHEAD",
		"The requested offset is past the end of the stream",
		"core/data/stream: offset past the partition's next offset; the fields carry both offsets",
		409,
		65)

	// UnknownPartition refuses a partition outside [0, Partitions()).
	UnknownPartition = errs.Static(CodeUnknownPartition, "UNKNOWN_PARTITION",
		"The stream has no such partition",
		"core/data/stream: partition outside [0, Partitions()); the fields carry it and the count",
		400,
		65)

	// InvalidBatchSize refuses an Append of no record and a Read asked for a
	// non-positive number of entries, rather than an empty answer forever from
	// a loop that processes nothing.
	InvalidBatchSize = errs.Static(CodeInvalidBatchSize, "INVALID_BATCH_SIZE",
		"The batch size is not a positive number",
		"core/data/stream: empty Append or non-positive Read max",
		0,
		78)

	// StreamFull refuses an Append that waited PolicyValue.BlockTimeout for
	// room under PolicyValue.MaxBytes and found none: the readers, or the
	// retention, are behind. It is the producer's backpressure, and nothing was
	// written.
	StreamFull = errs.Static(CodeStreamFull, "STREAM_FULL",
		"The stream is full",
		"core/data/stream: MaxBytes held for BlockTimeout; nothing was written",
		503,
		75)

	// UnknownGroup answers a Committed for a group that never committed an
	// offset on that partition: where it starts is the reader's decision.
	UnknownGroup = errs.Static(CodeUnknownGroup, "UNKNOWN_GROUP",
		"The consumer group has no committed offset",
		"core/data/stream: no offset committed by this group on this partition",
		404,
		66)

	// PartitionsMixed refuses an Append whose keyed records hash to more than
	// one partition: a batch is one partition's, written under one sync, and
	// splitting it would make a crash keep half of it. The producer groups its
	// records by PartitionOf first.
	PartitionsMixed = errs.Static(CodePartitionsMixed, "PARTITIONS_MIXED",
		"The batch's records belong to different partitions",
		"core/data/stream: a batch's keys name more than one partition; nothing was written",
		400,
		65)

	// StreamBackendFailed reports an operation a durable engine depends on that
	// its storage refused. The fields name the operation; the cause is wrapped.
	StreamBackendFailed = errs.Static(CodeStreamBackendFailed, "STREAM_BACKEND_FAILED",
		"The stream's storage failed",
		"service/data/stream: the engine's storage refused an operation; the fields name it",
		0,
		74)

	// StreamDirectoryUnusable refuses a stream directory that cannot be
	// prepared, is not a directory, or is writable by another account.
	StreamDirectoryUnusable = errs.Static(CodeStreamDirectoryUnusable, "STREAM_DIRECTORY_UNUSABLE",
		"The stream directory cannot be used",
		"service/data/stream: the stream directory cannot be prepared or is writable by others",
		0,
		78)

	// SQLStreamMisconfigured refuses an SQL engine configuration at
	// construction; the "field" field names which value.
	SQLStreamMisconfigured = errs.Static(CodeSQLStreamMisconfigured, "SQL_STREAM_MISCONFIGURED",
		"The SQL stream configuration is not usable",
		"service/data/stream: SQL engine configuration refused at construction; the field names which",
		0,
		78)

	// SubscriberMisconfigured refuses a subscriber configuration: no handler,
	// no group, or a handler not asserted idempotent; the "field" field names
	// which value.
	SubscriberMisconfigured = errs.Static(CodeSubscriberMisconfigured, "SUBSCRIBER_MISCONFIGURED",
		"The stream subscriber configuration is not usable",
		"service/data/stream: subscriber configuration refused; the field names which",
		0,
		78)
)

Functions

func PartitionOf

func PartitionOf(key []byte, partitions int) int

PartitionOf returns the partition a record keyed key is appended to among partitions: the 32-bit FNV-1a hash of the key modulo partitions. It is the one placement every engine applies, so a key lands on the same partition in memory, on a directory and in a table.

A partitions below one is read as one. An empty key has no partition of its own: an engine spreads unkeyed batches round-robin and does not ask; asked anyway, PartitionOf answers the hash of no bytes.

Types

type AppendValue

type AppendValue struct {
	// Partition is the partition the whole batch was appended to.
	Partition int
	// First is the offset of the batch's first record.
	First Offset
	// Last is the offset of the batch's last record.
	Last Offset
}

AppendValue is where Stream.Append put a batch: its partition and the offsets of its first and last records, the batch's records holding every offset between them in order.

type BoundsValue

type BoundsValue struct {
	// First is the lowest offset still readable. Retention and Trim raise
	// it, compaction never moves it; nothing lowers it.
	First Offset
	// Next is the offset the partition's next record will take.
	Next Offset
}

BoundsValue is a partition's retained range: records from First up to Next excluded. The partition holds none when First equals Next.

type Committer

type Committer interface {
	// Commit records next as the offset group reads next on partition,
	// replacing whatever the group committed there before — moving it back is
	// legitimate: it is a replay. next past the partition's next offset is
	// [OffsetAhead]; below its first retained offset it is kept, and the
	// group's next Read reports [OffsetTrimmed]. An empty group is
	// [StreamMisconfigured]; a partition outside [0, Partitions()) is
	// [UnknownPartition].
	Commit(ctx context.Context, group string, partition int, next Offset) error
	// Committed returns the offset group committed last on partition.
	//
	// A group that never committed there is [UnknownGroup] — never a zero
	// offset, because "start at the beginning" and "start at the end" are both
	// legitimate first positions and only the reader knows which it wants.
	Committed(ctx context.Context, group string, partition int) (Offset, error)
}

Committer keeps, per consumer group and partition, the offset the group reads next: the durable cursor that lets a group's reader stop and another one resume where it was.

It is a sibling because keeping cursors is a capability of the engine's storage: an engine that keeps them in the same store as the records lets a handler commit its offset in its own transaction (exactly-once in effect, ADR 0231 and kitsunium/sdk#823), which is the reason they are kept there and not beside it.

type CompactValue

type CompactValue struct {
	// Scanned is how many retained records were at least
	// [PolicyValue.CompactAfter] old, the only ones a compaction may drop.
	Scanned int64
	// Kept is how many of them survived.
	Kept int64
	// Dropped is how many of them were removed: superseded by a later
	// record of their key, or a tombstone past its second window.
	Dropped int64
}

CompactValue is what one Compactor.Compact did to a partition: the records it looked at, those it kept and those it dropped; Kept plus Dropped is Scanned.

type Compactor

type Compactor interface {
	// Compact removes from partition every keyed record at least
	// [PolicyValue.CompactAfter] old that a later record of the same key, as
	// old, supersedes, and every tombstone at least twice CompactAfter old with
	// them; a record younger than CompactAfter is never touched, and an unkeyed
	// record is left as it is. Every survivor keeps its offset: offsets are
	// never renumbered and [Stream.Bounds] does not move, so a reader inside a
	// compacted range reads on from the next survivor and is refused
	// [OffsetTrimmed] only below the first retained offset, never in a hole.
	//
	// It reports what it looked at — every retained record at least
	// CompactAfter old —, what it kept and what it dropped. A zero CompactAfter
	// never compacts: Compact returns a zero [CompactValue]. A partition outside
	// [0, Partitions()) is [UnknownPartition].
	Compact(ctx context.Context, partition int) (CompactValue, error)
}

Compactor rewrites a partition so that it keeps, of every key, only its last record: the shape of a changelog read from the start to rebuild a state, with no need to keep every intermediate value.

It is a sibling because compaction is a policy only some engines afford. It never runs on its own — no timer, no goroutine —: a caller runs it, from a scheduler's job for instance, and PolicyValue.CompactAfter says which records it may touch.

Tombstones

A keyed record with an empty payload is a TOMBSTONE: the key's deletion. A compaction keeps it, as the last record of its key, for one more CompactAfter — a reader that bootstraps within that window still learns the key is gone —, and the first compaction after drops it with every earlier record of its key.

type EntryValue

type EntryValue struct {
	// AppendedAt is the engine's clock when the batch holding the record was
	// appended; every record of one batch carries the same instant. It is
	// what [Seeker.OffsetAt] and retention by age read.
	AppendedAt time.Time
	// Record is the record as it was appended.
	Record RecordValue
	// Partition is the partition the record was appended to.
	Partition int
	// Offset is the record's position in its partition, unique and never reused.
	Offset Offset
}

EntryValue is one record as a partition keeps it: where it is, when it arrived, and what it holds.

type Offset

type Offset uint64

Offset is a record's position in its partition: assigned by the engine at Stream.Append, consecutive within a partition from zero, and never reused — a trimmed offset stays trimmed and the next one keeps counting. A Compactor may leave holes between the offsets a partition still holds; it never renumbers them.

type PolicyValue

type PolicyValue struct {
	// Partitions is how many partitions the stream has, fixed for its life.
	// Zero means one; negative is refused.
	Partitions int
	// MaxRecordBytes bounds one record, its key and payload together. A
	// larger record is refused at [Stream.Append] with [RecordTooLarge]. Zero
	// means [DefaultMaxRecordBytes]; negative is refused.
	MaxRecordBytes int
	// RetainFor is how long a record is kept, measured from its
	// [EntryValue.AppendedAt]: older records are trimmed by the engine, a
	// file engine dropping whole segments only. Zero keeps every record for
	// good; negative is refused.
	RetainFor time.Duration
	// RetainBytes is how many bytes of records a partition keeps: above it
	// the engine trims the oldest. Zero is unbounded; negative is refused.
	RetainBytes int64
	// MaxBytes is how many bytes of records the stream may hold before
	// [Stream.Append] waits for room — the producers' backpressure, for a
	// stream whose retention is bounded by its readers rather than by time.
	// Zero is unbounded; negative is refused, and so is a positive value
	// below the normalized MaxRecordBytes, under which no record could fit.
	MaxBytes int64
	// BlockTimeout is how long an Append waits for room under MaxBytes before
	// it is refused with [StreamFull]. Zero refuses at once. Negative is
	// refused, and so is a positive value with no MaxBytes, which would wait
	// for nothing.
	BlockTimeout time.Duration
	// SegmentBytes is the size of the segments a file engine writes, and the
	// unit its retention releases. Zero means [DefaultSegmentBytes]; negative
	// is refused. Engines that keep no segments ignore it.
	SegmentBytes int64
	// CompactAfter is how old a record must be before a [Compactor] may
	// drop it, measured from its [EntryValue.AppendedAt]: a reader that
	// stays within that window behind the head sees every update, and a
	// tombstone is kept one more CompactAfter after it became compactable.
	// Zero never compacts; negative is refused.
	CompactAfter time.Duration
}

PolicyValue is what an engine enforces on a stream: how many partitions, how large a record, how long and how much it keeps, how much it may hold before producers wait, and how large a segment a file engine writes.

Its zero value WORKS, and that is the decision

A stream's zero policy is one partition, records of up to DefaultMaxRecordBytes, kept forever, never full, never compacted, in segments of DefaultSegmentBytes. Every zero here has one reading (ADR 0031's clamp branch): unlike the queue's visibility timeout, no field has two opposite readings of zero, so none is refused at zero. What is refused is a negative value — it is not a bound, and the caller who wrote it meant something —, a PolicyValue.BlockTimeout with no PolicyValue.MaxBytes to wait under, and a MaxBytes below one record, under which no Append could ever succeed.

func (PolicyValue) Normalized

func (p PolicyValue) Normalized() PolicyValue

Normalized returns the policy with its clamped fields resolved, so that every engine reads the same numbers rather than each applying the same default. It does NOT validate: call PolicyValue.Validate first, at construction.

func (PolicyValue) Validate

func (p PolicyValue) Validate() error

Validate reports whether the policy is one an engine can honour, and refuses it BY FIELD (StreamMisconfigured, the "field" field naming which) when it is not.

It lives in core so that the memory, file and SQL engines refuse identical inputs identically, which is what makes the first usable as a test double for the other two.

type RecordValue

type RecordValue struct {
	// Key places the record: records of one key go to one partition
	// ([PartitionOf]) and are read in the order they were appended. It may
	// be empty, for a record whose order relative to others does not matter.
	Key []byte
	// Payload is the record's bytes. It may be empty; under a key, an empty
	// payload is a tombstone, the key's deletion for a [Compactor].
	Payload []byte
}

RecordValue is what a producer appends: a key that places it, and the bytes it carries.

type Seeker

type Seeker interface {
	// OffsetAt returns the first offset of partition whose [EntryValue.AppendedAt]
	// is at or after t: the partition's first retained offset when t precedes
	// every record it retains, its next offset when t follows every one. A
	// partition outside [0, Partitions()) is [UnknownPartition].
	OffsetAt(ctx context.Context, partition int, t time.Time) (Offset, error)
}

Seeker finds an offset by time: where a reader that wants "everything since nine o'clock" starts.

It is a sibling because an engine over a store that keeps no append instant cannot implement it.

type Stream

type Stream interface {
	// Append writes records to one partition as one batch and returns where
	// they landed.
	//
	// The partition is the records' keys': [PartitionOf] of the first keyed
	// record, every other keyed record required to hash to the same partition
	// ([PartitionsMixed] otherwise, nothing written), and an unkeyed record
	// following the batch. A batch with no key at all goes to the partitions in
	// turn. The records take consecutive offsets in their order.
	//
	// When it returns nil, a durable implementation has flushed the whole batch
	// to its device under one sync: a crash one instruction later loses none of
	// it, and a crash before loses all of it, never a part. An implementation
	// that joins the transaction ctx carries (ADR 0151) has instead made the
	// batch part of that transaction.
	//
	// An empty batch is [InvalidBatchSize]; a record whose key and payload
	// together exceed [PolicyValue.MaxRecordBytes] is [RecordTooLarge], the
	// whole batch refused. A stream holding [PolicyValue.MaxBytes] waits up to
	// [PolicyValue.BlockTimeout] for retention to make room, then refuses with
	// [StreamFull]; ctx ends the wait earlier with its own cause.
	Append(ctx context.Context, records []RecordValue) (AppendValue, error)
	// Read returns up to max entries of partition, from offset from on, in
	// offset order. It does NOT block waiting for records: a read at the
	// partition's next offset yields an empty slice and a nil error, and
	// waiting is the caller's ([Waker] tells it when to look again). The
	// offsets a [Compactor] emptied are skipped: the entries carry their own
	// offsets, which need not be consecutive.
	//
	// from below the partition's first retained offset is [OffsetTrimmed] —
	// the records are gone, and silently starting later would hide the gap; one
	// past the next offset is [OffsetAhead]. A partition outside
	// [0, Partitions()) is [UnknownPartition], and a non-positive max is
	// [InvalidBatchSize].
	Read(ctx context.Context, partition int, from Offset, max int) ([]EntryValue, error)
	// Bounds returns the first offset partition still retains and the next one
	// it will assign. A partition outside [0, Partitions()) is
	// [UnknownPartition].
	Bounds(ctx context.Context, partition int) (BoundsValue, error)
	// Partitions returns how many partitions the stream has, at least one. It
	// never changes for the life of a stream: a key's partition is its hash
	// modulo this number, and changing it would move every key.
	Partitions() int
}

Stream is one named, partitioned log: producers append batches of records to it, and readers read each partition from an offset of their own choosing. Implementations MUST be safe for concurrent use by multiple goroutines AND, where the implementation is durable, by every process that opens the same storage.

IFACE-PLUGIN: the concrete engines stay unexported behind their constructors in internal/service/data/stream.

The interface is FROZEN at these four methods. A published alias makes it structural for every downstream implementation, and a fifth method would break each one at compile time with no deprecation window (ADR 0039). A new capability arrives as a sibling reached by type assertion — Waker, Seeker, Trimmer, Compactor and Committer are the ones that ship.

Why the READER owns its offset

Because a stream has many readers and each reads at its own pace: the record is not consumed by being read, so nothing about a read can live in the record. A reader that wants its position to survive it keeps it through Committer; a reader that does not — a replay, an audit, a backfill — just reads from wherever it likes.

type Trimmer

type Trimmer interface {
	// Trim makes before the first offset partition retains: every record below
	// it becomes unreadable at once ([OffsetTrimmed]) and its storage is
	// released when the engine can — a file engine releases whole segments only.
	// A before at or below the first offset changes nothing; one past the next
	// offset is [OffsetAhead]; a partition outside [0, Partitions()) is
	// [UnknownPartition]. Offsets are never reused: the next offset does not
	// move.
	Trim(ctx context.Context, partition int, before Offset) error
}

Trimmer drops a partition's prefix before its retention would: the operator's lever, and the one a reader uses once every record below an offset is known to be useless.

It is a sibling because an engine over a store whose retention is its own cannot honour it.

type Waker

type Waker interface {
	// Wake returns a channel closed by the next Append through this stream value
	// in this process, to any partition. Call it BEFORE the Read that may come
	// back empty: an append landing between that Read and the wait closes the
	// channel already handed out, so it is not missed.
	Wake() <-chan struct{}
}

Waker lets an idle reader sleep until there may be records to read.

It is a SIBLING of Stream for the reason queue's Waker is: an engine that cannot see its own appends — or whose appends come from another process — has no honest answer to give. A reader asks by type assertion and polls when the answer is no, so an engine that omits it loses latency, never records.

A wake is a HINT: a signal may come with nothing new on the reader's partition, and records appended by another process close no signal, so a reader that uses it still polls at the cadence that bounds the second case.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL