state

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package state provides the durability core — an append-only Log and content-addressed Blobs — that the engine (Phase 2) sits on for commit/resume and that obs (Phase 6) reads to project OTel spans.

The Log is etcd-style framed (length + CRC32C + payload + 8-byte pad — see event.go); the fold scans from offset 0 and silently truncates on the first short read or CRC mismatch, which is the torn-tail recovery the design requires. The Blobs store is content-addressed (sha256 v1; the ref format carries the algorithm name so BLAKE3 can land additively).

Slice 1.5 ships the primitives; the engine in Phase 2 orchestrates them into the content-address-then-pointer-swap commit boundary (CLAUDE.md invariant: artifacts in Blobs first, then a `node.completed` event appended + fsync'd).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrBadRef wraps every refusal to parse a ref (wrong prefix, wrong length, non-hex,
	// wrong algorithm). Callers use errors.Is to distinguish from missing / corruption.
	ErrBadRef = errors.New("state: malformed blob ref")
	// ErrCorruption fires when Get reads a file whose recomputed sha256 doesn't match the
	// ref's address. Detects silent disk corruption.
	ErrCorruption = errors.New("state: blob content does not match its address")
)

Functions

func RefFor

func RefFor(content []byte) string

RefFor returns the ref string a Put of content would produce. Pure; no I/O. Callers (the engine, when building events that point to soon-to-be-put payloads) can pre-compute.

Types

type Blobs

type Blobs interface {
	Put(content []byte) (ref string, err error)
	Get(ref string) ([]byte, error)
}

Blobs is the content-addressed artifact store seam. One concrete impl (FSBlobs) + one fake (InMemoryBlobs). Hash is hidden behind the ref string so a future BLAKE3 impl can land additively (refs would be "awf-d1:blake3:<hex>" and parse via the same prefix split).

type Event

type Event struct {
	Seq        uint64          `json:"seq"`
	Epoch      uint32          `json:"epoch"`
	TS         time.Time       `json:"ts"`
	Path       string          `json:"path"`
	Type       string          `json:"type"`
	PayloadRef string          `json:"payload_ref,omitempty"`
	Data       json.RawMessage `json:"data,omitempty"`
}

Event is the on-disk record per phase-1 design §D. The JSON shape is the wire format; the frame (encodeFrame/decodeFrame) is the bytes that land on disk.

Field semantics:

  • Seq, Epoch, TS: assigned by the Log on Append (caller leaves them zero-valued).
  • Path: opaque attribution — the engine's addressing path (Phase 2's engine/path package produces it). Slice 1.5 never parses it.
  • Type: opaque event-kind name. Phase 2's engine defines the vocabulary (run.started, node.completed, branch.taken, …); slice 1.5 carries arbitrary strings.
  • PayloadRef, Data: optional. PayloadRef is a content-addressed pointer into Blobs (large payloads live there); Data is inline structured payload for small events. Both omitempty.

func FoldFile

func FoldFile(path string) ([]Event, error)

FoldFile reads + decodes every committed event from the log at path WITHOUT taking a writable file handle. Use this when you need to observe a log that another process or goroutine may be actively writing to — unlike OpenLog, FoldFile never truncates a torn tail (the file is opened read-only). Torn-tail records at the tail are silently skipped per scanFile's semantics.

Single-writer discipline is preserved: only OpenLog/OpenLogExclusive take a write handle; FoldFile is observer-only.

type FSBlobs

type FSBlobs struct {
	// contains filtered or unexported fields
}

FSBlobs is the filesystem-backed CAS — one file per content address, sharded by the first two hex chars of the address.

func OpenBlobs

func OpenBlobs(root string) (*FSBlobs, error)

OpenBlobs prepares the blob store rooted at the given directory. Creates the root directory (and the `sha256/` subdirectory) if they don't already exist.

func (*FSBlobs) Get

func (b *FSBlobs) Get(ref string) ([]byte, error)

Get reads the blob at ref and re-verifies its sha256 against the ref's address. Returns ErrBadRef for malformed refs, ErrCorruption for content/address mismatch, or a wrapped fs.ErrNotExist for refs the caller didn't Put.

func (*FSBlobs) Put

func (b *FSBlobs) Put(content []byte) (string, error)

Put writes content to the store under its sha256 address and returns the ref string. Idempotent: putting the same bytes a second time is a no-op (atomic rename onto the existing file produces an identical file). Atomic on POSIX (same-directory rename).

type FileLog

type FileLog struct {
	// contains filtered or unexported fields
}

FileLog is the production impl — one append-only file per run. Single-writer per instance; the interpreter is single-threaded for commits (runtime-design §5).

func OpenLog

func OpenLog(path string, clk clock.Clock) (*FileLog, error)

OpenLog opens (or creates) the log file at path. On an existing file, it scans to the first short read or CRC mismatch and truncates the file there (torn-tail recovery), then initializes the next Seq to (max-seen + 1) and the Epoch to (max-seen + 1). On a fresh file, Seq starts at 0 (first Append gets Seq=1) and Epoch is 0.

OpenLog does NOT create intermediate directories — the parent must exist. (The engine in Phase 2 owns the run directory layout, so plumbing MkdirAll here would couple the primitive to a layout it doesn't own.)

func OpenLogExclusive

func OpenLogExclusive(path string, clk clock.Clock) (*FileLog, error)

OpenLogExclusive opens (or creates) the log file at path WITH O_EXCL — i.e. the open atomically fails if the file already exists. This is the race-free first-run primitive: `awf run` calls it to mint a new run.id's log, and the existing OpenLog (which tolerates and torn-tail-recovers an existing file) stays as the resume primitive.

On collision, the returned error wraps fs.ErrExist — callers route with errors.Is(err, fs.ErrExist) for the "run id already exists, use awf resume" message.

Unlike OpenLog, there's no torn-tail-recovery branch here: a fresh file can't have a torn tail. The function is intentionally minimal.

func (*FileLog) Append

func (lg *FileLog) Append(e Event) error

Append assigns Seq/Epoch/TS and writes the framed JSON record. Does NOT fsync — caller invokes Sync at durability-critical events.

func (*FileLog) Close

func (lg *FileLog) Close() error

Close fsyncs and closes. If Sync fails we still attempt Close (releasing the FD is more important than the trailing close-error), but the Sync error is what we return.

func (*FileLog) Fold

func (lg *FileLog) Fold() ([]Event, error)

Fold re-reads the file from offset 0 and returns every record decoded as an Event. Torn-tail records are silently skipped (Open already truncated them; this is the post-truncation read).

func (*FileLog) Sync

func (lg *FileLog) Sync() error

Sync fsyncs the underlying file.

type InMemoryBlobs

type InMemoryBlobs struct {
	// contains filtered or unexported fields
}

InMemoryBlobs is the Blobs fake: a map from hex-hash to content. Matches FSBlobs' error contract (ErrBadRef for malformed, wrapped fs.ErrNotExist for missing, ErrCorruption never fires in-memory because content can't be tampered with). Safe for concurrent Put/Get — a mutex serializes store access so parallel branch goroutines (e.g. T10 fan-out) can commit concurrently without racing.

func NewInMemoryBlobs

func NewInMemoryBlobs() *InMemoryBlobs

NewInMemoryBlobs mints an empty in-memory blob store.

func (*InMemoryBlobs) ClearFault

func (b *InMemoryBlobs) ClearFault()

ClearFault resets the FailPutAfterN hook. See InMemoryLog.ClearFault.

func (*InMemoryBlobs) FailPutAfterN

func (b *InMemoryBlobs) FailPutAfterN(n int)

FailPutAfterN — see InMemoryLog.FailAppendAfterN.

func (*InMemoryBlobs) Get

func (b *InMemoryBlobs) Get(ref string) ([]byte, error)

func (*InMemoryBlobs) Put

func (b *InMemoryBlobs) Put(content []byte) (string, error)

type InMemoryLog

type InMemoryLog struct {
	// contains filtered or unexported fields
}

InMemoryLog is the Log fake: events live in a slice, Sync/Close are no-ops. Used by Phase 2's engine tests (the fake backend's conformance suite). Single-writer; not safe for concurrent Append from multiple goroutines (matches FileLog's contract).

func NewInMemoryLog

func NewInMemoryLog(clk clock.Clock) *InMemoryLog

NewInMemoryLog mints a fresh in-memory log. The optional clk must be non-nil; pass clock.System{} for "real" time or a clock.Fake for deterministic tests.

func (*InMemoryLog) Append

func (l *InMemoryLog) Append(e Event) error

func (*InMemoryLog) ClearFault

func (l *InMemoryLog) ClearFault()

ClearFault resets the FailAppendAfterN hook to "no fault programmed." Idempotent on already-cleared. Conformance harness calls this before the resume so a programmed crash doesn't replay on the resume's own Appends.

func (*InMemoryLog) Close

func (*InMemoryLog) Close() error

func (*InMemoryLog) FailAppendAfterN

func (l *InMemoryLog) FailAppendAfterN(n int)

FailAppendAfterN configures the fake so the first n Appends succeed and the (n+1)-th fails with an "induced fault" error. FailAppendAfterN(0) fails the very first call. One-shot: call #(n+1) and beyond succeed normally.

Mirrors container.Fake.FailExecAfterN's semantics; lets slice 2.4's commit test crash *between* Blobs.Put and Log.Append for a chosen attempt.

func (*InMemoryLog) Fold

func (l *InMemoryLog) Fold() ([]Event, error)

func (*InMemoryLog) Reopen

func (l *InMemoryLog) Reopen() error

Reopen simulates a FileLog.OpenLog of an existing file: bumps the internal epoch counter to (last-event.Epoch + 1) so subsequent Appends carry the new epoch. On an empty log (no events) Reopen is a no-op — matches FileLog's "fresh file → epoch=0" semantic.

Conformance harness's crash-then-resume choreography calls this between the simulated crash (programmed via Fail*AfterN) and the resume's first Append (run.resumed). Mirrors what production FileLog.OpenLog does for free on an existing file; the fake doesn't get that for free because it doesn't open files.

Returns error for signature symmetry with FileLog.OpenLog (which can fail on I/O); the in-mem impl can't actually fail, but callers shouldn't rely on that — Phase 4 Docker conformance may swap in a file-backed log where the signature matters.

func (*InMemoryLog) Sync

func (*InMemoryLog) Sync() error

type Log

type Log interface {
	Append(e Event) error
	Sync() error
	Fold() ([]Event, error)
	Close() error
}

Log is the durability seam. One concrete impl (FileLog) + one fake (InMemoryLog).

  • Append assigns Seq (monotonic across reopens), Epoch (incremented on each open of an existing log), and TS (from the injected Clock); the caller fills Type, Path, PayloadRef, Data.
  • Sync fsyncs the file; caller decides per-event criticality (the engine in Phase 2 calls Sync on node.completed, run.*, signal.received — the design's durability-critical events — and lets high-frequency io.chunk/agent.event ride the next fsync).
  • Fold rescans the file and returns the full event sequence. No internal cache (the access pattern is dominated by Append; Fold is called on resume / for debugging).
  • Close fsyncs and releases the file handle.

Jump to

Keyboard shortcuts

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