Documentation
¶
Overview ¶
Package state is the chassis's durable state-record primitive: a named record (tenant, machine, id) with a state, a version and opaque JSON data; an atomic compare-and-swap transition that checks BOTH the current state and the expected version; and a durable event for every committed transition, written in the same transaction (a transactional outbox). The `state` personality presents each event at least once into the tenant's `_state/0` stack.
It is not a workflow engine: no machine definitions, no timers, no retries of the caller's work, no watchers, no deletion. See docs/advanced/protocols/state.md.
Index ¶
- Constants
- func ErrorCode(err error) string
- func Register(name string, c Constructor)
- func ValidID(id string) error
- func ValidMachine(m string) error
- func ValidState(field, st string) error
- type Cause
- type CodedError
- type Config
- type ConflictError
- type Constructor
- type CreateReq
- type Event
- type ExistsError
- type InvalidArgError
- type NotFoundError
- type Record
- type Store
- func (s *Store) ClaimDue(ctx context.Context, node string, limit int) ([]Event, error)
- func (s *Store) Close() error
- func (s *Store) Create(ctx context.Context, req CreateReq) (Record, error)
- func (s *Store) DB() *sql.DB
- func (s *Store) EnsureSchema(ctx context.Context) error
- func (s *Store) EventsForRecord(ctx context.Context, tenant, machine, id string) ([]Event, error)
- func (s *Store) Get(ctx context.Context, tenant, machine, id string) (Record, error)
- func (s *Store) GetEvent(ctx context.Context, eventID string) (Event, error)
- func (s *Store) MarkDone(ctx context.Context, eventID, rid string) error
- func (s *Store) MarkSkipped(ctx context.Context, eventID, reason string) error
- func (s *Store) Purge(ctx context.Context, retention time.Duration) (int64, error)
- func (s *Store) ReclaimStale(ctx context.Context, staleAfter time.Duration, maxAttempts int) (int64, error)
- func (s *Store) Retry(ctx context.Context, eventID string, backoff time.Duration, reason string, ...) error
- func (s *Store) SetClock(now func() time.Time)
- func (s *Store) SetMaxDataBytes(n int)
- func (s *Store) Transition(ctx context.Context, req TransitionReq) (Record, Event, error)
- type StoreError
- type TransitionReq
Constants ¶
const ( MaxMachineBytes = 64 MaxIDBytes = 256 MaxStateBytes = 64 MaxTenantBytes = 128 // DefaultMaxDataBytes caps `data` unless SetMaxDataBytes changes it. DefaultMaxDataBytes = 64 * 1024 )
Grammar. `machine` plays the role a KV namespace does: a lowercase dotted family name the consumer owns (`onepony.task`). `id` is free text inside the tenant's machine (`acme:t_123`), shaped like a notebook name segment. States are short tokens.
const ( StatusPending = "pending" StatusClaimed = "claimed" StatusDone = "done" StatusSkipped = "skipped" StatusDead = "dead" )
Event statuses. pending → claimed → done | skipped | dead; a claim that did not reach acceptance goes back to pending (with a next_attempt_at) or, past the attempt cap, to dead.
const AtLayout = "2006-01-02T15:04:05.000000000Z"
AtLayout is the fixed-width UTC timestamp stored in every *_at column (the notebook store's), so text comparison is chronological on both engines.
Variables ¶
This section is empty.
Functions ¶
func Register ¶
func Register(name string, c Constructor)
Register adds a backend constructor. Called from a backend package's init().
func ValidID ¶
ValidID reports whether id is a record id: 1–MaxIDBytes bytes of UTF-8 with no '/' and no control characters.
func ValidMachine ¶
ValidMachine reports whether m is a machine name.
func ValidState ¶
ValidState reports whether st is a state token.
Types ¶
type Cause ¶
type Cause struct {
Source string `json:"source"` // the committing run's @src (web, lmtp, scheduled, state, …)
Stack string `json:"stack"` // the committing run's stack
Trace string `json:"trace"` // the committing run's rid
Run string `json:"run"` // its continuation run id, when it had one
}
Cause is the provenance of a transition: which run committed it. It is filled by the op layer from trusted context, never from arguments, and rides on the event into `_state/0` as `@state.cause.*`.
type CodedError ¶
CodedError carries a stable txco_state_* code. The op layer surfaces it as `<into>.error.{code,message}` with a nil Go error, so a stack branches on the code and the run continues: a conflict is ordinary control flow, not a failure.
type Config ¶
type Config struct {
// DBPath is the bundled SQLite backend's file path (--state-db-path).
DBPath string
}
Config carries backend-selecting options resolved from chassis config. The bundled "sqlite" backend uses DBPath (same posture as notebook.Config).
type ConflictError ¶
ConflictError: the compare-and-swap did not match. Reason is "version" when ExpectedVersion is stale (checked first — a stale actor, doc §10) and "state" when the version matched but the record is not in From. Current is the record as it is now.
func (*ConflictError) Code ¶
func (e *ConflictError) Code() string
func (*ConflictError) Error ¶
func (e *ConflictError) Error() string
type Constructor ¶
Constructor builds a Store from resolved config. It is expected to open its backing DB and call Store.EnsureSchema before returning.
type Event ¶
type Event struct {
ID string `json:"event_id"` // stev_<hxid>
Tenant string `json:"-"`
Machine string `json:"machine"`
RecordID string `json:"id"`
Version int64 `json:"version"` // the version the transition produced
From string `json:"from"`
To string `json:"to"`
Cause Cause `json:"cause"`
CreatedAt time.Time `json:"created_at"`
Status string `json:"status"`
// Attempts counts presentations that consumed an attempt: an accept
// timeout or a claim reclaimed from a dead node. A denial or a
// refused handoff does not count. The stack sees Attempts+1 as
// `@state.attempt`.
Attempts int `json:"attempts"`
NextAttemptAt time.Time `json:"-"`
ClaimedBy string `json:"-"`
ClaimedAt time.Time `json:"-"`
DeliveredAt time.Time `json:"-"`
DeliveredRid string `json:"-"`
LastError string `json:"-"`
}
Event is one committed transition and its delivery bookkeeping.
type ExistsError ¶
type ExistsError struct{ Current Record }
ExistsError: Create found a record already there. Current is what is stored, so the caller can decide whether that is the record it wanted.
func (*ExistsError) Code ¶
func (e *ExistsError) Code() string
func (*ExistsError) Error ¶
func (e *ExistsError) Error() string
type InvalidArgError ¶
type InvalidArgError struct{ Reason string }
InvalidArgError flags a malformed request: a name outside its grammar, data that is not JSON or not UTF-8 or over the cap, a version below 1.
func (*InvalidArgError) Code ¶
func (e *InvalidArgError) Code() string
func (*InvalidArgError) Error ¶
func (e *InvalidArgError) Error() string
type NotFoundError ¶
type NotFoundError struct{ Machine, ID string }
NotFoundError: no record at (tenant, machine, id).
func (*NotFoundError) Code ¶
func (e *NotFoundError) Code() string
func (*NotFoundError) Error ¶
func (e *NotFoundError) Error() string
type Record ¶
type Record struct {
Tenant string `json:"-"`
Machine string `json:"machine"`
ID string `json:"id"`
State string `json:"state"`
Version int64 `json:"version"`
Data json.RawMessage `json:"data"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Record is one durable state record.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the façade over the two tables. It carries the dialect (for `?`→`$n` rebinding), the data cap, and a clock seam for tests, mirroring notebook.Store.
func NewStore ¶
NewStore builds a Store over the opened DB and its dialect. A nil dialect defaults to SQLite (the in-tree default).
func Open ¶
Open constructs the named backend. An unknown name is a startup error listing what is available (so a misconfigured --state-store fails loudly).
func (*Store) ClaimDue ¶
ClaimDue atomically moves up to limit due pending events to 'claimed' for node and returns them — one statement, so concurrent claimers (SKIP LOCKED on Postgres; the single writer on SQLite) partition the due set instead of double-claiming.
Presentation order per record: an event is due only when no earlier version of the same record is still pending or claimed. Version 9 is never presented until version 8 has reached a terminal status (done, skipped or dead), even across nodes. Their RUNS may still overlap, because acceptance is not completion; handlers compare versions.
Attempts are not consumed here: the outcome methods decide what counted (see Event.Attempts).
func (*Store) Create ¶
Create stores a record at version 1. A record already at (tenant, machine, id) is an ExistsError carrying what is there; nothing is changed and no event is written (creation is not a transition).
func (*Store) EnsureSchema ¶
EnsureSchema creates the tables + indexes if absent. Portable DDL (the notebook store's rules): TEXT ids, TEXT fixed-width timestamps, JSON as TEXT, BIGINT counters, native partial indexes — one statement set serves SQLite and Postgres. There is no migration runner for these stores; changes must be additive.
func (*Store) EventsForRecord ¶
EventsForRecord lists a record's events in version order (tests, inspection).
func (*Store) MarkDone ¶
MarkDone closes a claim: the chassis accepted the event for execution in the tenant (rid is the run's). Terminal.
func (*Store) MarkSkipped ¶
MarkSkipped closes a claim without a presentation: the tenant has no active _state stack, or the run never left _sys. Terminal; not replayed if a stack appears later.
func (*Store) Purge ¶
Purge deletes terminal events (done, skipped, dead) created before retention ago. Pending and claimed rows are never purged.
func (*Store) ReclaimStale ¶
func (s *Store) ReclaimStale(ctx context.Context, staleAfter time.Duration, maxAttempts int) (int64, error)
ReclaimStale returns claims older than staleAfter to pending — crash recovery for a node that died after claiming and before an outcome. Each reclaim consumes an attempt (a node that dies on the same event every time must not loop forever), and at maxAttempts the event is dead. Returns the number of rows touched.
func (*Store) Retry ¶
func (s *Store) Retry(ctx context.Context, eventID string, backoff time.Duration, reason string, consumeAttempt bool, maxAttempts int) error
Retry returns a claim to pending, due again after backoff. With consumeAttempt the presentation counted (an accept timeout: the run may have accepted late, so this is where duplicates come from) and, at maxAttempts, the event is dead instead. Without it (an admission denial, a refused handoff) nothing was presented and nothing is counted.
func (*Store) SetMaxDataBytes ¶
SetMaxDataBytes installs the node's cap on `data` (0 = uncapped).
func (*Store) Transition ¶
Transition is the compare-and-swap. In one write transaction it (1) updates the record only where state = From AND version = ExpectedVersion, (2) classifies a miss against the current row (NotFoundError; ConflictError "version" first, then "state"), (3) inserts the event for the new version, and (4) commits. The event and the new version are visible together or not at all: a crash between them leaves neither.
Under Postgres READ COMMITTED a second UPDATE racing on the same version blocks on the row, re-evaluates against the winner's new version and matches nothing; on SQLite the _txlock=immediate DSN serializes the two transactions outright. The CAS holds without SERIALIZABLE.
type StoreError ¶
StoreError wraps a database or driver failure.
func (*StoreError) Code ¶
func (e *StoreError) Code() string
func (*StoreError) Error ¶
func (e *StoreError) Error() string
func (*StoreError) Unwrap ¶
func (e *StoreError) Unwrap() error
type TransitionReq ¶
type TransitionReq struct {
Tenant string
Machine string
ID string
From string
To string
ExpectedVersion int64
Data json.RawMessage
Cause Cause
}
TransitionReq moves a record From → To when its current state is From AND its version is ExpectedVersion. Nil Data keeps the stored data; any other value (including `{}`) replaces it. From may equal To: a data-bearing self-transition still bumps the version and emits an event.