Documentation
¶
Overview ¶
Applying an admitted command is §10.4's durable protocol around ONE call into a runtime, and this file is that protocol. The consumer decided which command is next; everything from here is about doing it exactly once.
THE SHAPE IS FIXED BY THE STATE MACHINE, not chosen: a command is claimed under the current lease epoch, its private payload is loaded from SessionStore only after that claim, it is revalidated, it enters applying, the application prefix that correlates CommandID and RuntimeCommandID with the epoch is appended to the journal BEFORE the runtime-visible effect, the runtime is driven, and the record is then terminally settled. Every one of those steps exists because the process can stop between any two of them.
THE SEAMS ARE NARROW LOCAL INTERFACES OVER METHODS THAT EXIST IN THE LEGACY FAMILY, AND ONLY THERE. That qualification is the whole of this paragraph and an earlier version of it was missing — it said flatly that "sessionstore v0.6.0 HAS all of them", which is true of one family and misleads exactly the reader trying to work out why apply does not work in disposition mode.
sessionstore v0.7.0 has all of them for a LEGACY session — GetCommand, the inbox record's private Payload/PayloadRef with GetObject behind a reference, FindCommandApplication, ReadGates, ClaimCommand, BeginApplyingCommand, CompleteCommand, RejectCommand, and the journal's application-prefix envelope — and the shapes below are modelled on those rather than invented beside them. They are declared locally for the reason O4.1 declared Inbox and Cursors locally: naming a looprig module in go.mod is the same decision as depending on it, and that decision is a release-ordering commitment recorded elsewhere. What O4.1's declaration could also say, and this one cannot, is that the module lacks the operation; consulting the module is what shows a shape is wrong, and not consulting it is what let a terminal transition the store refuses sit behind a green test.
A HOST CANNOT REACH THAT FAMILY. A Host takes residency through AcquireResidency, which pins ProtocolModeDisposition, and a disposition session refuses the journal grant this protocol appends its prefix under. So every seam below is bound to calls that exist and that this Host cannot use on a session it can actually hold. The disposition family has no counterpart to bind to yet either: it has NO CLAIM EDGE — pending -> claimed has no entry point and there is no in-package writer of a DispositionClaim at all — and it settles from durable evidence the caller does not supply, which harness v0.33.0 cannot produce because runtimecommand.Admitted carries no attempt identity. Closing this is a three-repository sequence and not a Host change; the README's limitations section is where that is stated for a release reader.
ONE CONSEQUENCE BELONGS HERE RATHER THAN ONLY THERE, because this file is where "exactly once" is claimed. In disposition mode THE STORE DECIDES THE TERMINAL ARM, so there is no Host-authored rejection once an attempt exists, and MISSING EVIDENCE IS NOT PROOF THAT NOTHING WAS APPLIED — a Host must never re-dispatch on its absence. The honest claim for a disposition Host is "at most one authorized attempt, settled only from the runtime's durable disposition", which is narrower than what this file's legacy shapes express.
WHAT SETTLES A COMMAND AS APPLIED IS DURABLE EVIDENCE, not this Host's confidence. §10.4 ends an application at a correlated journal effect and the store requires the terminal write to NAME that effect, so the applier reads the correlation back after driving the runtime and completes with what the journal says. A command whose runtime call returned while committing nothing is not applied, and saying so is the difference between exactly-once and a durable no-op recorded as success.
Package commands consumes a session's durable command inbox in the immutable order the inbox itself recorded, and advances one durable consumption cursor behind it.
THE ORDER IS THE STORE'S, NOT THE TRANSPORT'S. §10.4 accepts a command with CreateOrdered under ordering_scope=(TenantID, SessionID), which fixes an immutable per-session acceptance order at the moment of acceptance; a consumer reads a bounded ListOrdered page from its cursor and consumes that. HostLink delivery is a LOW-LATENCY HINT and nothing else: it carries a CommandID that has already been admitted, it may arrive out of order, twice, or not at all, and none of those change what this package does. Hint's parameter is unnamed for that reason — see Consumer.Hint.
THE CURSOR IS NOT A QUEUE POSITION, it is an attestation. It advances past a command only once that command is TERMINAL or its recoverable application prefix is DURABLY OWNED, which are §10.4's two ways a command stops needing to be re-driven from the inbox. A command that is neither stops the pass where it stands: its successors are not applied, and the pass NAMES it. Both halves matter. Skipping the predecessor would apply commands in an order the session never accepted; skipping it quietly would leave a session wedged with nothing to read but a cursor that stopped moving.
WHAT THIS PACKAGE DOES NOT DO is claim, apply, recover or finalize a command. That is O4.2's, behind the Processor seam, and the split is deliberate: the consumer decides WHICH command is next and whether the cursor may pass it, and it decides that from durable state alone.
Applying a command in the DISPOSITION family is a different durable protocol from apply.go's, and this file is that protocol.
THE DIFFERENCE IS WHO AUTHORS THE TERMINAL ARM. In the legacy family a Host reads the journal correlation itself and then writes `applied` or `rejected` from what it concluded. Here it cannot: SettleDispositionCommand takes a command, a revision and a residency and NOTHING ELSE — no outcome, no absence, no proof structure — and the store derives what it expects from its own immutable record and obtains the evidence through its configured reader. A Host's whole authority is "at most one authorized attempt"; what that attempt produced is the runtime's durable statement, not this Host's opinion.
SO THE SHAPE IS CLAIM, AUTHORIZE, DISPATCH, SETTLE, and each edge exists because the process can stop between any two of them:
- CLAIM under the residency GRANT. The claim edge takes a *ResidencyGrant rather than an epoch, so a caller cannot name one; DispositionClaim below therefore carries no epoch member at all, and the binding of a grant to a session is the concrete edge's, not this package's.
- AUTHORIZE — mint an attempt identity, read the RUNTIME's own journal grant, and write both immutably into the record. After this the command is `applying` and is closed by EVIDENCE rather than by any timer.
- DISPATCH, carrying the attempt identity to the runtime, which is what makes the runtime's durable disposition be about THIS attempt.
- SETTLE from that evidence.
WHAT "APPLIED" MEANS, and it is narrower than the word: under the attempt's journal grant, the runtime durably recorded that it accepted this command into its execution path. It does NOT mean a turn started, does NOT mean a turn folded, does NOT mean no later rejection can follow, and does NOT mean a queued input survives a crash — the runtime's loop mailbox is in memory. Do not widen this sentence anywhere, including in a user-facing vocabulary.
MISSING EVIDENCE IS NOT PROOF THAT NOTHING HAPPENED. A settlement that cannot read a disposition leaves the command `applying` at an unmoved revision and blocks the pass. That is the safe failure and the only honest one: a Host that re-dispatched on an absent record would be re-driving an effect that may already have committed.
Recovery is the half of §10.4 that exists because a Host can stop between any two durable writes, and this file is the decision it makes when it starts again.
THERE IS ONE QUESTION AND THE RECORD CANNOT ANSWER IT. An `applying` record MAY have a committed effect and may equally have been moved into applying a moment before the process died, with nothing appended and nothing begun. What separates them is the JOURNAL: a correlated prefix followed by the public event that carried the effect means the command has been applied, whichever lease did it; a prefix followed by a fence above its own epoch means the application started and its writer lost the stream; no prefix at all means nothing happened. Those are different settlements, and a seam that could only say "a prefix exists" could not tell them apart.
THE APPLY DEADLINE ASYMMETRY FOLLOWS FROM THE SAME LINE. A new claim may not start at or after the deadline, and finishing an application may, because §10.4 says recovering an expired `applying` prefix is CONTINUATION of an existing application rather than creation of a new claim. A rule that refused both would strand exactly the commands whose effects already reached a runtime.
WHAT THIS FILE NEVER DOES IS RE-CLAIM AN APPLYING RECORD. Applying is a fortress: it cannot be re-claimed at any epoch and it cannot be renewed, so the only moves out of it are completing it against a committed effect and rejecting it against proof that none committed. An earlier version of this file claimed an expired applying record and applied it afresh, which the store this runs on refuses outright — and refuses for the reason the rule exists: a second application of a command whose first may still commit.
Index ¶
- Constants
- Variables
- type Application
- type ApplicationOutcome
- type Applications
- type Applier
- type ApplierOptions
- type ApplyError
- type ApplyRefusal
- type Applying
- type AttemptClosers
- type AttemptID
- type AttemptIDs
- type BlockedCommand
- type Claim
- type Command
- type CommandRecords
- type Completion
- type Consumer
- type CursorRegressionError
- type CursorWrites
- type Cursors
- type DispositionApplier
- type DispositionApplierOptions
- type DispositionAttempt
- type DispositionClaim
- type DispositionRecord
- type DispositionRecords
- type DispositionSettlement
- type DispositionWrites
- type Effect
- type Fence
- type Gate
- type Gates
- type Inbox
- type InboxWrites
- type InvalidConsumerOptionsError
- type JournalWrites
- type Kind
- type Options
- type Outcome
- type PageError
- type PageProblem
- type PassResult
- type Payload
- type Prefix
- type Processor
- type Record
- type Rejection
- type State
- type UUIDAttemptIDs
- type UnusableHostError
Constants ¶
const MaxAttemptIDBytes = 256
MaxAttemptIDBytes bounds a dispatch-attempt identity.
IT IS BOTH DOWNSTREAM BOUNDS AT ONCE, which is why it is a constant here rather than a deferral to either. sessionstore bounds DispositionAttemptID and harness bounds runtimecommand.AttemptID, both at Core's MaxIDBytes, and an identity travels to BOTH: the store writes it into the immutable attempt and the runtime writes it into its durable disposition. An identity either of them refused would be refused after a durable write on one side, so it is refused before either.
Variables ¶
var ( // ErrEnduringEffect reports a closure refused because the runtime's journal // holds a durable effect caused by the command being closed. IT IS TERMINAL // and must never be retried into a tombstone. ErrEnduringEffect = department.ErrEnduringEffect // ErrDispositionUnsupported reports a dispatch refused because the runtime's // durable log cannot record a disposition. NOTHING DURABLE WAS WRITTEN, so // the command MAY be re-offered, which is not what a transport failure means. ErrDispositionUnsupported = department.ErrDispositionUnsupported // ErrNoAttemptCloser reports a runtime that offers no recovery closure. It is // a sentinel rather than an absent capability because a wrapper that forwards // an optional capability satisfies the interface for every runtime it wraps, // so an assertion can no longer answer the question. ErrNoAttemptCloser = department.ErrNoAttemptCloser )
ErrEnduringEffect and ErrDispositionUnsupported are department's sentinels, aliased here so this package's own callers name one vocabulary.
THEY ARE ALIASES AND NOT COPIES. An independently-declared sentinel with the same name would compare unequal to the one the adapter actually raises, so every errors.Is in this package would silently answer false and both refusals would collapse into RefusalRuntime — the exact conflation harness exported its error type to prevent.
var ErrEvidenceUnroutable = errors.New("commands: this session's settlement evidence could not be obtained from any configured reader, which is a wiring failure rather than a runtime that recorded nothing")
ErrEvidenceUnroutable reports a settlement whose evidence could not be OBTAINED because of this deployment's wiring.
IT IS DECLARED HERE AND RAISED BY THE CONCRETE EDGE, which is the direction the other sentinels in this file do NOT go — they are department's, because a runtime raises them. This one is a STORE fact, and internal/sessionstoreadapter imports this package rather than the other way round, so here is the one place both the raiser and the reader can name it.
WHY IT IS A SENTINEL AT ALL. Both classifiable causes — an unregistered binding and a reader that refuses to resolve the session — already arrive at the applier as typed errors with their cause intact; the information was being discarded by one unconditional mapping, not missing. Recovering it costs no upstream API and leaks nothing: a refusal code names no tenant, session, binding or body.
Functions ¶
This section is empty.
Types ¶
type Application ¶
type Application struct {
CommandID sessionwire.CommandID
RuntimeCommandID uuid.UUID
Outcome ApplicationOutcome
// PrefixEpoch is the lease epoch the correlated prefix was appended under,
// and is zero when the outcome is Absent.
PrefixEpoch uint64
// EffectEventID and EffectSeq name the public event that carried the effect,
// and are zero unless the outcome is Committed.
EffectEventID sessionwire.EventID
EffectSeq uint64
// SupersedingEpoch is the highest opening-fence epoch the walk observed. A
// writer at or below it is provably fenced out of the stream, which is the
// only thing that makes a negative answer about an in-flight applier safe.
SupersedingEpoch uint64
}
Application is what one session's journal proves about one command.
type ApplicationOutcome ¶
type ApplicationOutcome string
ApplicationOutcome is what a session's journal proves about one command's application.
IT IS FIVE-VALUED AND NOT A BOOLEAN, and the earlier two-valued shape (`(Prefix, bool, error)`) is why this is stated at length: each value unlocks a DIFFERENT settlement, and a seam that could only say "a prefix exists" forced a caller to guess between finishing a command and refusing it. The vocabulary is sessionstore's, because the answers are the store's to give.
const ( // ApplicationAbsent means no record in the journal names this command. // Nothing has been applied under it. ApplicationAbsent ApplicationOutcome = "absent" // ApplicationCommitted means a correlated prefix is immediately followed by // the public event that carried its effect. The command has been applied, // whichever lease did it, and that event is what a completion names. ApplicationCommitted ApplicationOutcome = "committed" // ApplicationAbandoned means a correlated prefix is immediately followed by // an opening fence above its own epoch: the application started and its // writer lost the stream before committing anything more. ApplicationAbandoned ApplicationOutcome = "abandoned" // ApplicationUnresolved means a correlated prefix exists whose outcome // cannot be read yet. Both settlements refuse; the answer may become // readable later. ApplicationUnresolved ApplicationOutcome = "unresolved" // ApplicationConflicted means a prefix names this command's public identity // with a different runtime identity or kind. The durable mapping is broken, // so no settlement is safe and an operator has to look. ApplicationConflicted ApplicationOutcome = "conflicted" )
type Applications ¶
type Applications interface {
// FindApplication correlates the journal against the command's OWN durable
// identities. A caller does not supply them: one that could name the runtime
// identity to correlate on could ask about a mapping that was never
// accepted, and the answer would be evidence about nothing.
FindApplication(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID) (Application, error)
}
Applications reads what a session's journal proves about a command.
type Applier ¶
type Applier struct {
// contains filtered or unexported fields
}
Applier claims, applies, recovers and settles one command at a time. It is the Processor the consumer hands non-terminal records to.
func NewApplier ¶
func NewApplier(options ApplierOptions) (*Applier, error)
NewApplier validates options and returns an Applier.
func (*Applier) Process ¶
Process claims, applies, recovers or settles one command.
IT READS THE RECORD FRESH, AND THEN THE JOURNAL. The Command it is handed is the ordering decision's three fields, taken from a page that may be a whole pass old; every transition below is a compare-and-swap against the record as it is now, and every settlement is decided against what the journal proves rather than against what the record's state suggests.
EVERY KIND IS DRIVEN INTO THE RUNTIME, including create and restore. An earlier version settled those two as applied on the ground that residency is their consequence and had already happened — which was sound about the consequence and skipped the question of what the durable record may legally say. It may say applied only from applying, and only naming a committed journal effect; a Host's knowledge that a runtime exists is neither. There is no shortcut to write.
type ApplierOptions ¶
type ApplierOptions struct {
// Host supplies the clock, the claim TTL and this Host's identity. Nothing
// here restates a value host.New already checked.
Host *host.Host
// Key is the session whose commands this applier applies.
Key registry.Key
// LeaseEpoch is the grant every durable write is stamped with.
LeaseEpoch uint64
Records CommandRecords
Applications Applications
Gates Gates
Writes InboxWrites
Journal JournalWrites
// Runtime is Host's control path into the resident runtime.
Runtime department.CommandApplier
// Fence is the lease-epoch guard. AN APPLIER HAS ITS OWN, and that is the
// answer to the hand-off O4.1 left on Processor: the applier's transitions
// are fenced writes, and they are fenced by the same one mechanism the
// cursor write goes through rather than by a second discipline.
Fence Fence
}
ApplierOptions configures an Applier.
type ApplyError ¶
type ApplyError struct {
Refusal ApplyRefusal
CommandID sessionwire.CommandID
Reason string
Cause error
}
ApplyError reports why an applier declined to finish a command.
func (*ApplyError) Error ¶
func (e *ApplyError) Error() string
func (*ApplyError) Unwrap ¶
func (e *ApplyError) Unwrap() error
Unwrap returns the typed cause a lower layer produced, if any.
type ApplyRefusal ¶
type ApplyRefusal string
ApplyRefusal is a stable, machine-readable reason an applier declined to finish a command. It is a code rather than a sentence for the reason PageProblem is.
A REFUSAL IS NOT A REJECTION. Every value here leaves the durable record where it was and blocks the pass at that command, which the consumer names in PassResult.Blocked. The durable rejections this package writes are Rejection values, not refusals.
const ( // RefusalStaleEpoch reports a record or a prefix under an epoch later than // this applier's, which means this Host no longer owns the session. RefusalStaleEpoch ApplyRefusal = "stale_epoch" // RefusalClaimHeld reports another epoch's unexpired claim. RefusalClaimHeld ApplyRefusal = "claim_held" // RefusalDeadlinePassed reports a new claim refused at or after the apply // deadline. Factory's deadline reconciler is what makes such a record // terminal; a Host never rejects for a deadline it merely observed. RefusalDeadlinePassed ApplyRefusal = "apply_deadline_passed" // RefusalUnsupportedKind reports a command kind this Host cannot apply. RefusalUnsupportedKind ApplyRefusal = "unsupported_kind" // RefusalUnknownState reports a durable state this package cannot reason // about. RefusalUnknownState ApplyRefusal = "unknown_state" // RefusalUnrevisionedRecord reports a record carrying no provider revision. // // Zero is not a revision any provider assigns, so it cannot be a revision a // caller read, and a compare-and-swap naming it is unconditional. The store // refuses it as an invalid request; this Host refuses to ISSUE it, which is // the half a fake cannot supply. RefusalUnrevisionedRecord ApplyRefusal = "unrevisioned_record" // RefusalForeignRecord reports a record that is not the one asked for, or // not this session's. RefusalForeignRecord ApplyRefusal = "foreign_record" // RefusalCorrelation reports identities that do not correlate: an // unallocated RuntimeCommandID, a journal prefix naming another runtime // command, or a committed effect against a record that never entered // applying. RefusalCorrelation ApplyRefusal = "correlation_mismatch" // RefusalUnresolvedApplication reports an application whose outcome the // journal cannot yet decide. Neither settlement is safe and the answer may // become readable later, so the pass stops rather than guessing. RefusalUnresolvedApplication ApplyRefusal = "unresolved_application" // RefusalUnfencedApplier reports an abandoned-looking application whose // writer the journal has not yet fenced out. Only the fence proves the // applier can no longer commit the effect a rejection would orphan. RefusalUnfencedApplier ApplyRefusal = "unfenced_applier" // RefusalMissingPayload reports a kind whose private payload is required and // absent, or a payload the store's own one-of-two invariant refuses. RefusalMissingPayload ApplyRefusal = "missing_payload" // RefusalUnreadableGateResponse reports a gate response whose private body // this Host cannot read the gate identity out of, so §9.4's recheck cannot // be run. RefusalUnreadableGateResponse ApplyRefusal = "unreadable_gate_response" // RefusalReferencedGateResponse reports a gate response whose body is // stored behind an object reference this Host does not dereference. // // IT IS NOT THE SAME FINDING as an unreadable body, though both stop the // same check, and giving them one code made the difference unobservable: // deleting the reference branch left the JSON decode to fail on empty bytes // with the same code, so the whole branch was an equivalent mutant. The // repairs differ — a malformed body is corruption or a Factory bug, and a // referenced one is a body over its inline threshold plus a Host that // deliberately cannot fetch it — and an operator reading "not a Core // gate-response request" about a perfectly well-formed spilled body is // being misdirected. RefusalReferencedGateResponse ApplyRefusal = "referenced_gate_response" // RefusalInvalidEffect reports a terminal application that names no durable // event. RefusalInvalidEffect ApplyRefusal = "invalid_effect" // RefusalNoCommittedEffect reports a runtime call that returned while the // journal shows no committed effect for the command. Nothing was applied, so // nothing may be recorded as applied. RefusalNoCommittedEffect ApplyRefusal = "no_committed_effect" // RefusalStore reports an ambiguous durable-store failure. RefusalStore ApplyRefusal = "store_failure" // RefusalRuntime reports that the runtime refused or failed the command // after its application prefix was committed. RefusalRuntime ApplyRefusal = "runtime_failure" // RefusalInvalidAttempt reports an attempt identity neither downstream // module would accept. It is refused before any durable write, because an // identity the store wrote and the runtime then refused would authorize a // dispatch nothing could ever be evidence about. RefusalInvalidAttempt ApplyRefusal = "invalid_attempt" // RefusalNoJournalGrant reports a runtime holding no single-writer journal // lease, so there is no grant an attempt could name. // // IT IS NOT A STALE EPOCH. The runtime's grant and this Host's residency are // different authorities over different stores, and a Host that substituted // its own would author an attempt no evidence could ever match. RefusalNoJournalGrant ApplyRefusal = "no_journal_grant" // RefusalDispositionUnsupported reports a dispatch the runtime refused // because its durable log cannot record a disposition. // // IT IS DELIBERATELY NOT RefusalRuntime, and the distinction is the one // harness exported the error type to make expressible: NOTHING DURABLE WAS // WRITTEN, so the command may be re-offered elsewhere. A transport failure // says the opposite — something may have happened — so collapsing the two // would either strand a re-offerable command or re-drive a real effect. RefusalDispositionUnsupported ApplyRefusal = "disposition_unsupported" // runtime's durable disposition. // // THE COMMAND STAYS APPLYING AT AN UNMOVED REVISION, and that is the safe // failure rather than a missing feature. Absence is not a disposition: a // Host that concluded "nothing happened" from an unreadable journal would // re-drive an effect that may already have committed. RefusalEvidenceUnavailable ApplyRefusal = "evidence_unavailable" // RefusalEvidenceUnroutable reports a settlement whose evidence could not be // OBTAINED because of this deployment's WIRING, rather than because the // runtime recorded nothing. // // IT IS A DIFFERENT REFUSAL FROM RefusalEvidenceUnavailable BECAUSE IT NAMES A // DIFFERENT PERSON. Both leave the command applying at an unmoved revision and // both are safe; what differs is who has to act. `evidence_unavailable` points // an operator at the runtime, which is right when the runtime wrote nothing // and WRONG — expensively, since a real effect has already been committed — // when the cause is a binding no reader is registered for, or a reader that // refuses to resolve the session at all. // // IT COVERS ONLY THE CAUSES HOST CAN ACTUALLY NAME. A router pointed at the // WRONG journal store is not one of them: that store is healthy, at the // correct tenant, and truthfully reports it holds no such record, which is // indistinguishable from the benign case at settlement time and correctly so. // Closing that needs a construction-time answer no released API offers, and it // is booked rather than papered over here. RefusalEvidenceUnroutable ApplyRefusal = "evidence_unroutable" // RefusalEnduringEffect reports a recovery closure refused because the // runtime's journal holds a durable effect for the command. // // IT IS TERMINAL AND MUST NOT BE RETRIED INTO A TOMBSTONE. The predecessor's // effect committed and only its evidence is missing; an operator has to // look, and the pass blocks until one does. RefusalEnduringEffect ApplyRefusal = "enduring_effect" // RefusalNoAttemptCloser reports a predecessor's stranded attempt this // composition has no closer for. The pass blocks; nothing is concluded. RefusalNoAttemptCloser ApplyRefusal = "no_attempt_closer" )
type Applying ¶
Applying moves a claimed command into applying: the statement that application is starting now, not that capacity has been reserved.
type AttemptClosers ¶
type AttemptClosers interface {
// CloseAttempt durably records that the named attempt was NOT applied, under
// a grant strictly later than the attempt's own.
//
// IT IS REFUSED RATHER THAN FORCED when the runtime's journal holds an
// enduring event caused by that command: a tombstone over a committed effect
// is the one error this protocol cannot recover from. Such a refusal is NOT
// retryable and must never be retried into a tombstone.
CloseAttempt(ctx context.Context, command sessionwire.CommandID, runtimeCommand uuid.UUID, kind Kind, attempt AttemptID, attemptJournalEpoch uint64) error
}
AttemptClosers drives a successor's recovery closure for a PREDECESSOR's stranded attempt. It is department.AttemptCloser, restated in this package's own vocabulary — a commands.Kind and a commands.AttemptID rather than two strings — so a mis-ordered pair of same-typed parameters is a compile failure here rather than a silently wrong closure.
IT IS DISCOVERED BY ASSERTION AND IS OPTIONAL, exactly as harness's runtimecommand.AttemptCloser is: folding it into the ordinary dispatch seam would make every implementer of that seam carry a capability only a recovery path uses. A composition without one simply never closes a predecessor's attempt, and the consequence is a blocked pass rather than a wrong settlement.
type AttemptID ¶
type AttemptID string
AttemptID is the immutable identity of ONE authorized dispatch.
IT IS MINTED BEFORE THE ATTEMPT IS WRITTEN AND NEVER AFTERWARDS. The store writes it into the record immutably at BeginDispositionAttempt and no transition may rewrite one, which is what lets a successor tell the attempt it is recovering from the attempt it would have made.
type AttemptIDs ¶
AttemptIDs mints one identity per authorized dispatch.
IT IS A SEAM RATHER THAN A FUNCTION CALL because the identity must be UNIQUE across every dispatch this deployment ever makes, and uniqueness is a property of the source rather than of this package. A test needs a source it can name; production needs one that cannot collide. Both are the same shape.
A MINTER THAT REPEATS ITSELF IS A CORRECTNESS FAILURE AND NOT A COSMETIC ONE: two attempts sharing an identity would let one attempt's evidence settle the other's command, which is precisely the correlation the identity exists to make impossible.
type BlockedCommand ¶
type BlockedCommand struct {
CommandID sessionwire.CommandID
AcceptedOrder uint64
// State is the state the pass LAST OBSERVED for this command: the
// Processor's reported state when it answered, and the record's own durable
// state when it did not.
State State
// Cause is the Processor's error, or nil when the command simply is not yet
// terminal and owns no prefix. Those are different situations and a caller
// diagnosing a wedged session needs to tell them apart.
Cause error
}
BlockedCommand names the command a pass stopped at.
IT IS THE "NOT SILENTLY" IN STEP 4. A pass that declines to skip a pending predecessor and reports nothing is indistinguishable from a pass that found an empty inbox, and the difference is a session that is stuck. This value is carried on the result and is readable from the loop through Consumer.LastPass.
type Command ¶
type Command struct {
// TenantID and SessionID are the scope the record claims to belong to.
// They are carried so a page can be checked against the session this
// consumer owns, rather than trusted.
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
// CommandID is the public, retry-stable identity, and the inbox's stable
// key.
CommandID sessionwire.CommandID
// AcceptedOrder is the immutable per-session acceptance order CreateOrdered
// fixed. It is the ONLY ordering authority: not due_at, not the CommandID,
// not the order a Factory replica forwarded a hint in.
AcceptedOrder uint64
// State is the durable processing state.
State State
}
Command is the part of a SessionInbox record this consumer reads.
IT IS DELIBERATELY NOT THE WHOLE AGGREGATE. §10.4's record also holds the private payload or object reference, the allocated RuntimeCommandID, the claim epoch and expiry and the terminal result; none of those decide what is next or whether the cursor may pass, and the payload in particular is loaded from SessionStore AFTER a claim rather than carried around before one. What is here is the identity, the immutable acceptance order, and the durable state — the three things an ordering decision is made from.
type CommandRecords ¶
type CommandRecords interface {
// LoadCommand returns the full inbox record for one command, including the
// revision a later compare-and-swap names.
LoadCommand(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID) (Record, error)
// LoadPayload returns the private body or its object reference. It is called
// only after a claim has been committed.
LoadPayload(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID) (Payload, error)
}
CommandRecords reads the private durable command state.
type Completion ¶
Completion records that an applying command's effect committed.
type Consumer ¶
type Consumer struct {
// contains filtered or unexported fields
}
Consumer consumes one session's durable command inbox.
func NewConsumer ¶
NewConsumer validates options and returns a Consumer.
func (*Consumer) Hint ¶
func (c *Consumer) Hint(sessionwire.CommandID)
Hint wakes the consumer because a command was admitted.
THE PARAMETER IS UNNAMED, AND THAT IS THE MECHANISM. §10.5 makes HostLink delivery a low-latency path and not a source of truth: the CommandID it carries has already been admitted and is already in the durable order, so using it to select or prioritise work would be consuming by arrival order — the exact thing step 3 forbids. A convention saying "do not read it" is a convention someone will forget; an unnamed parameter cannot be read at all, and TestHintCarriesNoAuthority keeps it unnamed.
It never blocks and it may drop: a hint arriving while one is already pending asks for a pass that is already about to happen.
func (*Consumer) LastPass ¶
func (c *Consumer) LastPass() PassResult
LastPass reports what the most recent pass did.
IT IS THE LOOP'S ONLY READER. Run discards each PassResult, so without this a blocked predecessor is reported to nobody once the consumer is running by itself — which would make BlockedCommand's whole argument vacuous.
func (*Consumer) Reconcile ¶
func (c *Consumer) Reconcile(ctx context.Context) (PassResult, error)
Reconcile performs ONE bounded pass over the durable order.
It is exported because attach-time reconciliation is a caller's decision: O3.1's sequence begins inbox ownership at step 7 and reports attached at step 8, and a composition that wants the first pass to have completed before it answers a Factory bind calls this directly rather than racing the loop.
IT IS SAFE TO CALL WHILE Run IS RUNNING, and that is stated here because here is where a caller reads it. Passes are serialized: a second one waits for the first rather than reading the same cursor and listing the same page. The sentence lived only on the unexported field that implements it, which no caller can see.
func (*Consumer) Run ¶
Run reconciles the inbox until the session context ends, the grant is lost, or Stop is called.
THE FIRST PASS HAPPENS BEFORE ANY TIMER. §10.5 says an owning Host reconciles its inbox ON ATTACH and again on a bounded interval while resident; a loop that armed its timer first would leave a command accepted before the attach unapplied for a whole interval, on the one path that exists to make a cold session recoverable without a wake signal.
A FULL PAGE COSTS NO INTERVAL. When a pass consumed its entire bounded page there is more durable work available now, and the loop takes it. This cannot spin: More is reported only when the whole page was consumed, and consuming a whole page advances the cursor by the batch, so every immediate repetition makes strict progress through a finite order.
EVERY REASON TO STOP IS CONSULTED BEFORE A PASS, which is precedence rather than a race. Go's select chooses uniformly among ready cases, so a hint that arrived at the same moment the grant was lost would otherwise drive a pass under a lease this Host no longer holds, half the time.
THE TIMER IS NEVER STOPPED, and that is a cost rather than an oversight. host.Clock hands back a *time.Timer, and the only test double that can hand back a timer a test fires is &time.Timer{C: ch}, which PANICS on Stop. So an unfired timer outlives its round by at most one interval. The remedy when it matters is host.Clock gaining a channel-returning After.
type CursorRegressionError ¶
CursorRegressionError reports a durable cursor lower than one this consumer has already had acknowledged.
It is REFUSED rather than obeyed. This Host is the only writer of the cursor while it holds the fence, so a lower value is the store contradicting itself; obeying it would re-drive every command in between.
THE HIGH-WATER COMES FROM READS AS WELL AS WRITES, which is wider than an earlier version of this comment said. Held is set by every successful load, not only by an acknowledged write, so a store that answers 10 and then 5 is refused even though this consumer wrote neither. That is deliberate: the contradiction is the store's either way, and the consequence of obeying it is the same.
func (*CursorRegressionError) Error ¶
func (e *CursorRegressionError) Error() string
type CursorWrites ¶
type CursorWrites interface {
// SaveCursor records the cursor under the writer's lease epoch. The
// implementation rejects an epoch lower than the greatest already
// committed for the record.
//
// THE REJECTION MUST CARRY A SENTINEL THE FENCE CLASSIFIES, and that is a
// contract rather than a courtesy. Every cursor write goes through
// Fence.Write, and the fence records ownership as gone only for the errors
// it recognises — residency.epochFence classifies exactly
// residency.ErrEpochSuperseded and residency.ErrFenceConflict and treats
// anything else as an ambiguous store failure. So an implementation that
// reports a superseded epoch as an untyped error leaves this Host believing
// it still owns a session a successor has taken, retrying once per
// ReconcileInterval and counting the refusals as store trouble. Wrap or
// return residency.ErrEpochSuperseded.
SaveCursor(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, epoch uint64, order uint64) error
}
CursorWrites is the durable cursor's one write.
IT IS ITS OWN INTERFACE BECAUSE THE FENCING GUARD READS THESE DECLARATIONS. TestEveryDurableWriteGoesThroughTheFence collects the methods of every interface in this package whose name ends in "Writes" and requires each call to one of them to be inside a Fence.Write, so the set of durable writes is derived from the seams rather than from a list somebody has to remember to widen. Cursors below still has both methods, so nothing that implements it changed.
type Cursors ¶
type Cursors interface {
// LoadCursor returns the greatest acceptance order durably consumed for a
// session THIS STORE WILL VOUCH FOR, or zero when that session has recorded
// none.
//
// THE QUALIFICATION REPLACES A SENTENCE THAT WAS WIDER THAN THE STORE — it
// used to read "or zero when none has been recorded", full stop, and
// sessionstore v0.7.0's reader does not behave that way. A session with no
// disposition catalog — absent, or bound to another protocol — is REFUSED
// rather than answered about, and the store says why: absence licenses a
// save to create a fresh record at whatever epoch and order the caller
// names, so a reader that reported an unvouched-for session as absence
// would let any caller reset the fence.
//
// SO ZERO AND A REFUSAL ARE TWO DIFFERENT ANSWERS AND A CALLER MUST NOT
// COLLAPSE THEM. Zero means "this session exists and has consumed nothing",
// and the correct response is to list from the head of its stream. A
// refusal means the stream is not this Host's to read, and the correct
// response is to stop and report — never to list from the head, which would
// re-drive every command in a session this Host has no authority over.
//
// Both of Host's call sites now do that: Consumer.loadCursor propagates,
// and Service.cursorFor propagates into the warm release's step 1, which
// aborts and names the step. See finding C-1.
LoadCursor(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID) (uint64, error)
CursorWrites
}
Cursors is the durable command-consumption cursor of §10.4.
It is a SEPARATE interface from Inbox because it is a different durable object with a different writer discipline: the inbox is written by whoever accepts and applies commands, and the cursor is written only by the current lease holder, through the fence.
type DispositionApplier ¶
type DispositionApplier struct {
// contains filtered or unexported fields
}
DispositionApplier claims, authorizes, dispatches and settles one command at a time in the disposition family. It is the Processor a composed Host runs, and it replaced the commands.NoDispatch refusal, deleted in the same change, when harness gained an attempt-aware journal writer.
func NewDispositionApplier ¶
func NewDispositionApplier(options DispositionApplierOptions) (*DispositionApplier, error)
NewDispositionApplier validates options and returns a DispositionApplier.
func (*DispositionApplier) Process ¶
Process claims, authorizes, dispatches, recovers or settles one command.
IT READS THE RECORD FRESH. The Command it is handed is the ordering decision's three fields, taken from a page that may be a whole pass old, and every transition below is a compare-and-swap against the record as it is now.
THE FOUR ARMS ARE THE FOUR DURABLE STATES AND NOTHING ELSE. A terminal record is reported; an applying record has a durably authorized attempt and is settled or recovered; a claimed record already holds this Host's claim and resumes at the attempt; a pending record starts at the claim.
type DispositionApplierOptions ¶
type DispositionApplierOptions struct {
// Host supplies the clock, the claim TTL and this Host's identity.
Host *host.Host
// Key is the session whose commands this applier applies.
Key registry.Key
// ResidencyEpoch is HOST's grant over the session. It is settlement and
// authorization CONTEXT and is never used as a journal epoch.
ResidencyEpoch uint64
Records DispositionRecords
Writes DispositionWrites
// Runtime is Host's control path into the resident runtime.
Runtime department.CommandApplier
// JournalEpochs reports the RUNTIME's own grant over its journal. It is a
// separate collaborator from Runtime because it is a separate capability,
// discovered by assertion in department, and because the two answers must be
// visibly sourced differently: an attempt naming this Host's residency as a
// journal epoch is an attempt no evidence could ever verify.
JournalEpochs department.LeaseEpochReporter
// Attempts mints the identity of each authorized dispatch.
Attempts AttemptIDs
// Closer drives a successor's recovery closure. IT IS OPTIONAL: a
// composition without one blocks on a predecessor's stranded attempt rather
// than concluding anything about it.
Closer AttemptClosers
// Fence is the lease-epoch guard every durable write goes through.
Fence Fence
}
DispositionApplierOptions configures a DispositionApplier.
type DispositionAttempt ¶
type DispositionAttempt struct {
ExpectedRevision uint64
// AttemptID is the minted identity this dispatch will carry to the runtime.
AttemptID AttemptID
// JournalEpoch is the RUNTIME's own grant over its journal, read from the
// runtime and never from this Host's residency.
JournalEpoch uint64
// ResidencyEpoch is the Host grant the dispatch is authorized under. The
// store fences it to EQUAL the claim's own, so it cannot raise the mark.
ResidencyEpoch uint64
StartedAt time.Time
}
DispositionAttempt authorizes exactly one dispatch and records it immutably.
type DispositionClaim ¶
DispositionClaim takes a short-lived claim so one writer works on a command at a time.
IT NAMES NO EPOCH, AND THE ABSENCE IS STRUCTURAL RATHER THAN TIDY. ClaimDispositionCommand takes a *ResidencyGrant and derives the epoch from it, refusing anything that is not a live-looking grant THAT STORE issued for THAT session — so no number a caller chose can reach the record's high-water mark. A member here would be a number this package could get wrong, for a value the store would not read.
type DispositionRecord ¶
type DispositionRecord struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
CommandID sessionwire.CommandID
// RuntimeCommandID is the UUID the winning acceptance record allocated. It
// is read from the durable record and never minted here.
RuntimeCommandID uuid.UUID
Kind Kind
State State
AcceptedOrder uint64
Revision uint64
ApplyDeadline time.Time
// ClaimResidencyEpoch and ClaimExpiresAt are the current claim, or the zero
// values when there is none. A terminal record keeps the claim that settled
// it.
ClaimResidencyEpoch uint64
ClaimExpiresAt time.Time
// AttemptID, AttemptJournalEpoch and AttemptResidencyEpoch are the ONE
// authorized dispatch, or the zero values when none was authorized. They are
// immutable once written.
AttemptID AttemptID
AttemptJournalEpoch uint64
AttemptResidencyEpoch uint64
}
DispositionRecord is the disposition-family inbox record as an applier reads it. It is Record's counterpart and deliberately not Record itself: the two families carry different members, and one type spanning both would let a legacy field be read on a disposition record and answer a plausible zero.
THE TWO EPOCHS ARE TWO TYPES OF NUMBER AND ARE NEVER COMPARED. ClaimResidency is Host's own grant over the session; AttemptJournalEpoch is the RUNTIME's grant over its journal, returned by the runtime itself. sessionstore says in terms that a residency epoch "must never be compared with, or used as, a journal epoch", and a Host that copied one into the other would author an attempt no evidence could ever match.
type DispositionRecords ¶
type DispositionRecords interface {
// LoadDispositionCommand returns the full record for one command, including
// the revision a later compare-and-swap names.
LoadDispositionCommand(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID) (DispositionRecord, error)
// LoadDispositionPayload returns the private body or its object reference.
// It is called only after a claim has been committed.
LoadDispositionPayload(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID) (Payload, error)
}
DispositionRecords reads the private durable command state of one disposition-family command.
type DispositionSettlement ¶
type DispositionSettlement struct {
ExpectedRevision uint64
// ResidencyEpoch is settlement CONTEXT and not authority. It is recorded in
// the outcome, fenced against the claim's high-water mark, and compared with
// no journal epoch. The evidence is what authorizes the settlement.
ResidencyEpoch uint64
}
DispositionSettlement asks the store to settle from evidence.
IT CARRIES NO OUTCOME, and that is the protocol rather than a narrow request shape. The store derives the expected descriptor from its own record, obtains the evidence through its configured reader and verifies it before the write; a caller that could supply an outcome could talk a settlement into a state the journal does not support.
type DispositionWrites ¶
type DispositionWrites interface {
// ClaimDisposition CASes a pending record into claimed under the caller's
// residency GRANT and returns its new revision. The grant is the
// implementation's, bound to this session; see DispositionClaim.
ClaimDisposition(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID, claim DispositionClaim) (uint64, error)
// BeginAttempt CASes a claimed record into applying and records the attempt
// immutably. Only the holder of a LIVE claim at the claim's own residency may
// make it, and there is no deadline check: an applying record is closed by
// evidence rather than by a timer.
BeginAttempt(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID, attempt DispositionAttempt) (uint64, error)
// SettleDisposition settles an applying command from durable evidence and
// reports the terminal state the STORE chose. A caller does not choose it.
SettleDisposition(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID, settlement DispositionSettlement) (State, error)
}
DispositionWrites is every durable transition a disposition applier makes.
EVERY METHOD HERE IS FENCED, by the same structural guard that covers InboxWrites and CursorWrites: TestEveryDurableWriteGoesThroughTheFence collects the methods of every interface in this package whose name ends in "Writes" and requires each call to one of them to be lexically inside a Fence.Write. DO NOT RENAME THIS TYPE OUT OF THE CONVENTION.
type Effect ¶
type Effect struct {
CompletedAt time.Time
EventID sessionwire.EventID
JournalSeq uint64
}
Effect is the durable journal event that carried a command's effect, which is what a terminal application RECORDS.
"Applied" with no event is a claim that something happened with nothing to point at, so this is required rather than optional, and it is read out of the journal correlation rather than composed by the caller.
type Fence ¶
type Fence interface {
// Held reports the typed reason ownership is gone, or nil.
Held() error
// Lost closes when ownership is gone.
Lost() <-chan struct{}
// Write performs one fenced write: refuse before it when ownership is
// already gone, and record the loss when the store reports a later owner.
Write(func() error) error
}
Fence is the lease-epoch guard every durable write goes through.
It is the SHAPE residency.epochFence already has, declared here because that type's methods are unexported and this is a different package. An adapter is owed at the composition root; it is mechanical, and the shape is stated here so it stays mechanical. THE THREE METHODS ARE NOT INTERCHANGEABLE: Lost is the channel a loop selects on, Held is the point check a write makes immediately before acting, and Write is the only one that can record the OTHER way ownership ends — a store refusing a write because a later epoch has committed, which closes no channel.
type Gate ¶
type Gate struct {
GateID sessionwire.GateID
// Open reports an unresolved, unexpired durable gate.
Open bool
// OwnerHostID and OwnerEpoch are the resident owner the gate was opened
// under. A gate response is resumable only while both still name this Host's
// current residency, because continuation is out of scope: a runtime that was
// released and relaunched no longer holds what the gate suspended.
OwnerHostID sessionwire.HostID
OwnerEpoch uint64
}
Gate is the durable gate state §9.4's release-race backstop is decided from.
type Gates ¶
type Gates interface {
// LoadGate returns the durable gate and whether it exists.
LoadGate(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, gate sessionwire.GateID) (Gate, bool, error)
}
Gates reads durable gate state for the §9.4 release-race backstop.
type Inbox ¶
type Inbox interface {
// ListOrdered returns at most limit records STRICTLY AFTER afterOrder, in
// ascending immutable acceptance order.
//
// The bound is the caller's and the ordering is the store's; a consumer
// that sorted the page itself would be inferring an order rather than
// reading one, which is the thing §10.4 forbids.
ListOrdered(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, afterOrder uint64, limit int) ([]Command, error)
}
Inbox is the durable per-session command inbox, read in acceptance order.
type InboxWrites ¶
type InboxWrites interface {
// ClaimCommand CASes a record into StateClaimed and returns its new
// revision. It is refused for a terminal record, for an applying record at
// any epoch, for an epoch below the record's high-water mark, at or after
// the apply deadline, and against a live claim at the same epoch — A CLAIM
// CANNOT BE RENEWED.
ClaimCommand(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID, claim Claim) (uint64, error)
// BeginApplying CASes a claimed record into StateApplying and returns its
// new revision. Only the holder of a LIVE claim at the claim's own epoch may
// make it; there is no deadline check, which is what stops a reconciler's
// clock from cancelling work that is about to commit.
BeginApplying(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID, applying Applying) (uint64, error)
// CompleteCommand settles an applying command as applied. It is admitted
// only from StateApplying, and a caller whose epoch is not the claim's must
// name the effect the journal correlation found.
CompleteCommand(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID, completion Completion) error
// RejectCommand settles a command with a durable typed reason. It is
// admitted only where the journal proves no effect committed, and for an
// applying record only to a strictly later epoch that the journal has fenced
// the applier out under.
RejectCommand(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, command sessionwire.CommandID, rejection Rejection) error
}
InboxWrites is every durable transition an applier makes to the command record.
EVERY METHOD HERE IS FENCED. The name is load-bearing: the structural guard collects the methods of each interface in this package whose name ends in "Writes" and requires every call to one of them to be lexically inside a Fence.Write. A durable write added to this interface is guarded the moment it is declared; a durable write declared on a seam named otherwise is not, which is the guard's one stated limit — DO NOT RENAME THIS TYPE OUT OF THE CONVENTION. Each write also has a behavioural test that loses the grant immediately before it, so the structural guard is the forward claim rather than the only one.
APPLIED AND REJECTED ARE MUTUALLY EXCLUSIVE BY CONSTRUCTION HERE: they are two methods with different required evidence — a journal effect for one, a typed public reason for the other — so no single call can commit both, and the store refuses either against a record that is already terminal.
type InvalidConsumerOptionsError ¶
InvalidConsumerOptionsError reports an option a Consumer may not run with.
func (*InvalidConsumerOptionsError) Error ¶
func (e *InvalidConsumerOptionsError) Error() string
type JournalWrites ¶
type JournalWrites interface {
// AppendApplicationPrefix commits the private correlation record before the
// runtime-visible effect begins. The lease epoch is the writer's own and is
// not a parameter; see Prefix.
AppendApplicationPrefix(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, prefix Prefix) error
}
JournalWrites is the one journal append an applier makes.
It is a SEPARATE seam from InboxWrites because it is a different durable object with a different writer: the inbox record is a keyed CAS, and this is an append to the session's own stream under the journal fence. Both are fenced, and both are covered by the same structural guard, which is what the naming convention buys — a second write seam needed no edit to the guard.
SO THE SAME PROHIBITION APPLIES HERE: DO NOT RENAME THIS TYPE OUT OF THE CONVENTION. The guard's one measured survivor is exactly that rename, and it spans both write seams; a warning on only one of them would leave the newer seam looking unclaimed.
type Kind ¶
type Kind string
Kind is the command kind §10.4's SessionInbox record carries.
It is Host's own enumeration rather than a projection of a Core request type, because what Host branches on is what it must REVALIDATE before applying, and Core's records are shaped for admission rather than for that. It travels to the runtime opaque: see department.RuntimeCommand.
const ( // KindCreate and KindRestore ask for a session that, by the time the // command reaches this Host, exists. They are still DRIVEN INTO THE RUNTIME // like every other kind — see the note on Process — because what settles a // command is a correlated durable effect, and residency is not one. KindCreate Kind = "create" KindRestore Kind = "restore" // KindInput carries private input blocks for a resident runtime. KindInput Kind = "input" // KindInterrupt asks the resident runtime to interrupt. KindInterrupt Kind = "interrupt" // KindGateResponse answers one open durable gate. KindGateResponse Kind = "gate_response" )
type Options ¶
type Options struct {
// Host supplies the clock, the reconcile interval and the reconcile batch.
// Nothing here restates a value host.New already checked.
Host *host.Host
// Key is the session this consumer owns the inbox of.
Key registry.Key
// LeaseEpoch is the grant every cursor write is stamped with.
LeaseEpoch uint64
Inbox Inbox
Cursors Cursors
Processor Processor
Fence Fence
}
Options configures a Consumer.
type Outcome ¶
type Outcome struct {
// State is the durable state the Processor last observed for the command.
State State
// PrefixOwned reports that the command's recoverable application prefix is
// durably committed and correlated with a lease epoch.
//
// IT IS THE SECOND HALF OF THE CURSOR RULE, and it is a separate field
// rather than an inference from State because §10.4 gives it a separate
// meaning: an `applying` record MAY have a committed application prefix and
// may equally have been claimed a moment ago with nothing written. The
// first is recoverable from the durable correlation by whoever holds the
// lease next; the second is not, and only the Processor knows which it is.
PrefixOwned bool
}
Outcome is what a Processor reports about one command, reduced to the two facts the cursor rule is made of.
type PageError ¶
type PageError struct {
Problem PageProblem
// Index is the position in the page, or -1 when the problem is the page's
// as a whole.
Index int
CommandID sessionwire.CommandID
AcceptedOrder uint64
Reason string
Cause error
}
PageError reports a ListOrdered page this consumer refused.
NOTHING FROM SUCH A PAGE IS APPLIED, including the records before the bad one. A page whose order is already known to be wrong cannot be trusted for its prefix either: applying it would be applying commands in an order the session never accepted, which is the one thing the immutable order exists to prevent.
type PageProblem ¶
type PageProblem string
PageProblem is a stable, machine-readable way a ListOrdered page broke the ordering contract.
It is a code rather than a sentence for the reason host.OptionErrorCode is: Reason is free text a caller must not branch on, and two of these rules already read similarly enough that a substring match would confuse them.
const ( // PageProblemOverLimit reports a page wider than the limit asked for. PageProblemOverLimit PageProblem = "over_limit" // PageProblemNotAfterCursor reports a record at or below the cursor the // page was requested after. A store answering with one is replaying // history the cursor exists to skip. PageProblemNotAfterCursor PageProblem = "not_after_cursor" // PageProblemNotMonotonic reports a record whose acceptance order does not // strictly increase. §10.4 makes the order immutable and monotone, so this // is the store contradicting its own contract. PageProblemNotMonotonic PageProblem = "not_monotonic" // PageProblemForeignSession reports a record scoped to another tenant or // session. PageProblemForeignSession PageProblem = "foreign_session" // PageProblemInvalidCommandID reports a record whose public identity Core // refuses. PageProblemInvalidCommandID PageProblem = "invalid_command_id" // PageProblemUnknownState reports a durable state this consumer cannot // reason about. PageProblemUnknownState PageProblem = "unknown_state" // PageProblemDuplicateCommandID reports the same public command identity // twice in one page. // // §10.4 makes CommandID the inbox's STABLE KEY, so two records carrying one // is the store contradicting itself in the same way a repeated acceptance // order does — and it is not caught by the order checks, because the two // copies may sit at strictly increasing orders. Left unchecked it hands one // command to the Processor twice inside a single pass, which is what the // serialization prevents ACROSS passes. PageProblemDuplicateCommandID PageProblem = "duplicate_command_id" )
type PassResult ¶
type PassResult struct {
// Cursor is the greatest durable consumption cursor this consumer knows of
// at the moment the pass ended.
//
// IT IS ONE RULE ON EVERY EXIT, which it was not: three exits reported the
// cursor the pass had loaded and two reported a bare zero, so a pass
// refused before it could read anything told an operator reading LastPass
// that the session had consumed nothing — the exact misdiagnosis the
// Consumed/Cursor split below exists to prevent. Every exit now reports the
// held cursor.
//
// ZERO MEANS ONE THING: this consumer has never had a cursor acknowledged,
// either because the session has consumed nothing or because no pass has
// yet managed to read one.
Cursor uint64
// Examined is how many records the page held.
Examined int
// Consumed is how many of them this pass consumed.
//
// It may exceed what Cursor reflects, and exactly when: a pass aborted by
// cancellation or by a lost grant writes no cursor at all, so the work it
// did is re-derived on the next pass. Naming the two separately is what
// makes that visible rather than a discrepancy a reader has to derive.
Consumed int
// More reports that the page came back FULL and was fully consumed, so
// there is more durable work available immediately.
More bool
// Blocked names the command the pass stopped at, if any.
Blocked *BlockedCommand
}
PassResult is what one reconciliation pass did.
type Payload ¶
type Payload struct {
// Body is the inline private request body.
Body []byte
// Ref is the object reference a body too large to inline is stored behind.
Ref sessionwire.ObjectReference
}
Payload is the PRIVATE part of the record, loaded from SessionStore after a claim and handed to the runtime opaque.
It is a separate load rather than a field of Record for one reason: step 4 says the payload is loaded AFTER the claim, and a field on the record a claim is decided from would be loaded before it. Two calls make the ordering a thing a test can see.
AT MOST ONE OF THE TWO IS SET. §10.1 gives a private body an independent immutable object reference once it exceeds its inline threshold; Host passes the reference on rather than dereferencing it, so a large body never travels through Host's memory.
type Prefix ¶
type Prefix struct {
CommandID sessionwire.CommandID
RuntimeCommandID uuid.UUID
Kind Kind
}
Prefix is §10.4's private application-prefix record, which correlates a command with its runtime allocation before the runtime-visible effect begins.
ITS EXISTENCE IS THE WHOLE RECOVERY SIGNAL. A crash before it means no effect began and the command may be applied afresh; a crash after it means an effect may have begun, and what happened is then a question for the journal rather than for the record's state.
IT CARRIES NO LEASE EPOCH, and that is stronger than carrying one: the journal writer stamps its own grant onto the record and refuses one a caller chose, so the epoch on a prefix is the epoch that actually held the stream rather than the epoch a caller believed it held.
type Processor ¶
type Processor interface {
// Process handles one command and reports what the cursor may conclude.
Process(context.Context, Command) (Outcome, error)
}
Processor claims, applies, recovers and finalizes ONE command. O4.2 implements it; this package only decides which command it is handed and whether the cursor may pass afterwards.
It is not department.CommandApplier. That interface is Host's control path INTO a runtime and takes one admitted command; this one is the whole durable protocol around such a call, including the claim, the payload load, the journal correlation and the terminal settlement.
WHAT O4.2 DID WITH THE HAND-OFFS THIS COMMENT USED TO CARRY. Two of the four were closed, and the other two are open and are O5's; they are stated in the present tense because the next implementer reads them here.
- CLOSED. A Processor is still handed no Fence — an Applier holds its own, supplied at its own construction — and the structural guard no longer covers SaveCursor alone: TestEveryDurableWriteGoesThroughTheFence derives its subject from every `…Writes` interface in this package and covers every production file, so the applier's transitions are inside it. Both halves were done rather than either.
- OPEN. A PANICKING Processor TAKES THE HOST DOWN. Run calls this on its own goroutine and there is no recover anywhere in the path; releasePass is deferred, so the pass slot is freed and the process is not.
- OPEN. acquirePass HAS NO CONTEXT OR STOP ESCAPE, so Stop cannot bound Run's exit while another caller holds the slot. Harmless while every pass is context-bounded; it becomes a drain question at O5.
- CLOSED, as predicted. Inbox and Cursors survived O4.2 without widening: the applier's transitions write the SAME durable SessionInbox record this Inbox reads, under the same one-writer-per-lease discipline, through a separate InboxWrites seam. Two seams over one object is fine. Drifting into a second writer discipline without noticing is not.
type Record ¶
type Record struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
CommandID sessionwire.CommandID
// RuntimeCommandID is the UUID the WINNING acceptance record allocated.
// §10.4 allows two Factory replicas to propose different UUIDs while racing
// one public ID and makes only the winner's usable, so this is read from the
// durable record, never minted here, and §16 forwards it to the runtime.
RuntimeCommandID uuid.UUID
Kind Kind
State State
// AcceptedOrder is the immutable acceptance order, carried so a record read
// fresh can be checked against the page record it came from.
AcceptedOrder uint64
// Revision is the provider revision every compare-and-swap names.
//
// IT IS REQUIRED, NOT AN OPTIMISATION. A transition is a decision about a
// record the caller has read; one that named no revision would be a blind
// write dressed as a compare-and-swap, and two writers that both read a
// pending record would both succeed.
Revision uint64
// ApplyDeadline is the outer bound §10.4 puts on applying this command. A
// NEW claim may not start at or after it; continuing an application may.
ApplyDeadline time.Time
// ClaimEpoch and ClaimExpiresAt are the current claim, or the zero values
// when there is none. A terminal record keeps the claim that settled it.
ClaimEpoch uint64
ClaimExpiresAt time.Time
}
Record is the SessionInbox record §10.4 describes, as an applier reads it.
IT IS THE AGGREGATE Command IS NOT. The consumer's Command is the ordering decision's three fields; this is what a claim, a deadline check, a correlation and a terminal settlement are made from — and it is read FRESH inside Process rather than taken from the page, because a page is a snapshot and every transition below is a compare-and-swap against the record as it is now.
It carries no payload. That is not an omission: see Payload.
type Rejection ¶
type Rejection struct {
ExpectedRevision uint64
Epoch uint64
Detail sessionwire.ErrorDetail
}
Rejection settles a command with a durable typed reason.
type State ¶
type State string
State is a command's DURABLE processing state, from §10.4's one state machine.
It is NOT sessionwire.CommandState, and the difference is the reason this type exists rather than a reuse. Core's type is the PUBLIC projection a caller polls — accepted, pending, applied, rejected — and it cannot express claimed or applying at all, which are exactly the two states this consumer has to distinguish: a claimed command is somebody's in-flight work and an applying one may have a recoverable prefix. Projecting one onto the other would collapse the distinction the cursor rule is written over.
const ( // StatePending is an accepted command nobody has claimed. StatePending State = "pending" // StateClaimed is a command claimed under a lease epoch, not yet applying. StateClaimed State = "claimed" // StateApplying is a command whose application has begun. StateApplying State = "applying" // StateApplied is one of the two mutually exclusive terminal states. StateApplied State = "applied" // StateRejected is the other, carrying a typed reason. StateRejected State = "rejected" )
type UUIDAttemptIDs ¶
type UUIDAttemptIDs struct{}
UUIDAttemptIDs mints an attempt identity per dispatch from a fresh random UUID.
THE ONE PROPERTY THAT MATTERS IS THAT IT NEVER REPEATS, across processes and across Hosts, and a random v4 UUID is what this workspace already uses for exactly that. A counter would be wrong rather than merely weaker: two Host replicas racing one session would mint the same names, and an attempt identity is what decides whether a piece of evidence is about THIS attempt — so a collision would let one attempt's disposition settle another's command.
IT IS NOT DERIVED FROM THE COMMAND, and that is also a correctness point. A successor recovering a stranded attempt must be able to tell the attempt it is closing from the attempt it would itself have made, and a deterministic function of the command identity would give both the same name.
func (UUIDAttemptIDs) NewAttemptID ¶
func (UUIDAttemptIDs) NewAttemptID() (AttemptID, error)
NewAttemptID returns a fresh identity, or the entropy failure that stopped it.
A FAILURE IS REPORTED RATHER THAN WORKED AROUND. There is no fallback to a counter or to a clock: an identity that might repeat is worse than no dispatch, because the dispatch it would authorize is one whose evidence could settle somebody else's command.
type UnusableHostError ¶
type UnusableHostError struct {
// Accessors are the offending accessor names, in a fixed order.
Accessors []string
}
UnusableHostError reports a *host.Host this package cannot run over.
IT EXISTS BECAUSE A NON-NIL HOST IS NOT A VALIDATED ONE. Every field of host.Host is unexported, which restricts NAMING a field from another package and nothing else: the empty composite literal &host.Host{} is legal everywhere, compiles outside package host, and yields a nil Clock, a zero ReconcileInterval and a zero ReconcileBatch. NewConsumer checked only for nil, so such a value was accepted, and a zero batch is not cosmetic — see Reconcile's note on what "full" means.
Accessors names every accessor whose value is unusable rather than the first, and that is what makes each check individually killable. The two ways to obtain a Host are host.New, which produces all three valid, and an empty literal, which produces all three invalid; a first-violation-wins error would therefore exercise exactly one of the three checks no matter what a test did.
func (*UnusableHostError) Error ¶
func (e *UnusableHostError) Error() string