Documentation
¶
Overview ¶
Package sessionstore provides transport-neutral durable session persistence over Looprig's Core wire records and Storage primitives.
Index ¶
- Constants
- func EncodeEnvelope(env Envelope) ([]byte, error)
- type AcquireReconciliationClaimRequest
- type AcquireResidencyRequest
- type AdmitCommandRequest
- type BeginApplyingCommandRequest
- type BodyReference
- type BodySlot
- type CatalogEntry
- type CatalogError
- type CatalogErrorCode
- type CatalogRecord
- type CheckpointSummary
- type ClaimCommandRequest
- type ClearHostRegistrationRequest
- type ClearSessionPointerRequest
- type Clock
- type CommandApplication
- type CommandApplicationOutcome
- type CommandClaim
- type CommandKind
- type CommandResult
- type CompleteCommandRequest
- type CreateCatalogEntryRequest
- type DesiredWorkload
- type DrainHostTargetRequest
- type DueCommand
- type DueCommandPage
- type DueGate
- type DueGatePage
- type Envelope
- type EnvelopeError
- type EnvelopeErrorCode
- type EnvelopeKind
- type FindCommandApplicationRequest
- type GetCatalogEntryRequest
- type GetCommandRequest
- type GetHostRegistrationRequest
- type GetObjectMetadataRequest
- type GetObjectRequest
- type GetReconciliationClaimRequest
- type GetSessionPointerRequest
- type HostAdvertisement
- type HostRegistration
- type HostRegistrationEntry
- type HostRoute
- type HostTarget
- type HostTargetEntry
- type HostTargetError
- type HostTargetErrorCode
- type HostTargetKey
- type HostTargetPage
- type HostTargetReconcileResult
- type InboxEntry
- type InboxError
- type InboxErrorCode
- type InboxRecord
- type InboxState
- type InvalidBackendError
- type InvalidBackgroundWorkError
- type InvalidIdentityError
- type InvalidLimitError
- type InvalidOptionError
- type JournalError
- type JournalErrorCode
- type JournalWriter
- type KeyspaceError
- type KeyspaceErrorCode
- type Limits
- type ListCompatibleHostsRequest
- type ListDueCommandsRequest
- type ListDueGatesRequest
- type ListSessionsRequest
- type ObjectError
- type ObjectErrorCode
- type ObjectKind
- type OpenGateRequest
- type OpenJournalRequest
- type Option
- func WithClock(clock Clock) Option
- func WithControlShards(shards int) Option
- func WithIOProviderOwnership(closer io.Closer) Option
- func WithLegacySingleTenant(defaultTenant sessionwire.TenantID) Option
- func WithLimits(limits Limits) Option
- func WithLogger(logger *slog.Logger) Option
- func WithProviderOwnership(closer ProviderCloser) Option
- func WithShutdownTimeout(timeout time.Duration) Option
- type PlacementIntent
- type PointerError
- type PointerErrorCode
- type ProtocolMode
- type ProviderCloser
- type PublishHostTargetRequest
- type PutHostRegistrationRequest
- type PutObjectRequest
- type ReadGatesRequest
- type ReadPublicJournalRequest
- type ReadRuntimeJournalRequest
- type ReconcileError
- type ReconcileErrorCode
- type ReconcileHostTargetsRequest
- type ReconciliationClaim
- type ReconciliationClaimEntry
- type RegistryError
- type RegistryErrorCode
- type RejectCommandRequest
- type ReleaseReconciliationClaimRequest
- type RemnantGateIntent
- type ResidencyAcquireCleanupError
- type ResidencyEpoch
- type ResidencyError
- type ResidencyGrant
- type ResolveGateRequest
- type RetireGateDeadlineIntentRequest
- type RuntimeCommandID
- type RuntimePage
- type RuntimeRecord
- type SessionBinding
- type SessionPage
- type SessionPointer
- type SessionPointerEntry
- type SessionPointerKind
- type SetSessionPointerRequest
- type Store
- func (s *Store) AcquireReconciliationClaim(ctx context.Context, req AcquireReconciliationClaimRequest) (ReconciliationClaimEntry, error)
- func (s *Store) AcquireResidency(ctx context.Context, req AcquireResidencyRequest) (*ResidencyGrant, error)
- func (s *Store) AdmitCommand(ctx context.Context, req AdmitCommandRequest) (InboxEntry, bool, error)
- func (s *Store) BeginApplyingCommand(ctx context.Context, req BeginApplyingCommandRequest) (InboxEntry, error)
- func (s *Store) ClaimCommand(ctx context.Context, req ClaimCommandRequest) (InboxEntry, error)
- func (s *Store) ClearActiveContinuationPointer(ctx context.Context, req ClearSessionPointerRequest) (SessionPointerEntry, error)
- func (s *Store) ClearHostRegistration(ctx context.Context, req ClearHostRegistrationRequest) (HostRegistrationEntry, error)
- func (s *Store) ClearRuntimeCheckpointPointer(ctx context.Context, req ClearSessionPointerRequest) (SessionPointerEntry, error)
- func (s *Store) ClearWorkspaceCheckpointPointer(ctx context.Context, req ClearSessionPointerRequest) (SessionPointerEntry, error)
- func (s *Store) Close(ctx context.Context) error
- func (s *Store) CompleteCommand(ctx context.Context, req CompleteCommandRequest) (InboxEntry, error)
- func (s *Store) ControlShards() int
- func (s *Store) CreateCatalogEntry(ctx context.Context, req CreateCatalogEntryRequest) (CatalogEntry, bool, error)
- func (s *Store) DrainHostTarget(ctx context.Context, req DrainHostTargetRequest) (HostTargetEntry, error)
- func (s *Store) FindCommandApplication(ctx context.Context, req FindCommandApplicationRequest) (CommandApplication, error)
- func (s *Store) GetActiveContinuationPointer(ctx context.Context, req GetSessionPointerRequest) (SessionPointerEntry, error)
- func (s *Store) GetCatalogEntry(ctx context.Context, req GetCatalogEntryRequest) (CatalogEntry, error)
- func (s *Store) GetCommand(ctx context.Context, req GetCommandRequest) (InboxEntry, error)
- func (s *Store) GetHostRegistration(ctx context.Context, req GetHostRegistrationRequest) (HostRegistrationEntry, error)
- func (s *Store) GetObject(ctx context.Context, req GetObjectRequest) (io.ReadCloser, error)
- func (s *Store) GetObjectMetadata(ctx context.Context, req GetObjectMetadataRequest) (sessionwire.ObjectMetadata, error)
- func (s *Store) GetReconciliationClaim(ctx context.Context, req GetReconciliationClaimRequest) (ReconciliationClaimEntry, error)
- func (s *Store) GetRuntimeCheckpointPointer(ctx context.Context, req GetSessionPointerRequest) (SessionPointerEntry, error)
- func (s *Store) GetWorkspaceCheckpointPointer(ctx context.Context, req GetSessionPointerRequest) (SessionPointerEntry, error)
- func (s *Store) ListCompatibleHosts(ctx context.Context, req ListCompatibleHostsRequest) (HostTargetPage, error)
- func (s *Store) ListDueCommands(ctx context.Context, req ListDueCommandsRequest) (DueCommandPage, error)
- func (s *Store) ListDueGates(ctx context.Context, req ListDueGatesRequest) (DueGatePage, error)
- func (s *Store) ListSessions(ctx context.Context, req ListSessionsRequest) (SessionPage, error)
- func (s *Store) OpenGate(ctx context.Context, req OpenGateRequest) (CatalogEntry, error)
- func (s *Store) OpenJournal(ctx context.Context, req OpenJournalRequest) (*JournalWriter, error)
- func (s *Store) PublishHostTarget(ctx context.Context, req PublishHostTargetRequest) (HostTargetEntry, error)
- func (s *Store) PutHostRegistration(ctx context.Context, req PutHostRegistrationRequest) (HostRegistrationEntry, error)
- func (s *Store) PutObject(ctx context.Context, req PutObjectRequest) (sessionwire.ObjectMetadata, error)
- func (s *Store) ReadGates(ctx context.Context, req ReadGatesRequest) (sessionwire.GatePage, error)
- func (s *Store) ReadPublicJournal(ctx context.Context, req ReadPublicJournalRequest) (sessionwire.JournalPage, error)
- func (s *Store) ReadRuntimeJournal(ctx context.Context, req ReadRuntimeJournalRequest) (RuntimePage, error)
- func (s *Store) ReconcileHostTargets(ctx context.Context, req ReconcileHostTargetsRequest) (HostTargetReconcileResult, error)
- func (s *Store) RejectCommand(ctx context.Context, req RejectCommandRequest) (InboxEntry, error)
- func (s *Store) ReleaseReconciliationClaim(ctx context.Context, req ReleaseReconciliationClaimRequest) (ReconciliationClaimEntry, error)
- func (s *Store) ResolveGate(ctx context.Context, req ResolveGateRequest) (CatalogEntry, error)
- func (s *Store) RetireGateDeadlineIntent(ctx context.Context, req RetireGateDeadlineIntentRequest) error
- func (s *Store) SetActiveContinuationPointer(ctx context.Context, req SetSessionPointerRequest) (SessionPointerEntry, error)
- func (s *Store) SetRuntimeCheckpointPointer(ctx context.Context, req SetSessionPointerRequest) (SessionPointerEntry, error)
- func (s *Store) SetWorkspaceCheckpointPointer(ctx context.Context, req SetSessionPointerRequest) (SessionPointerEntry, error)
- func (s *Store) UpdateCatalogDesiredState(ctx context.Context, req UpdateCatalogDesiredStateRequest) (CatalogEntry, error)
- func (s *Store) UpdateCatalogHostState(ctx context.Context, req UpdateCatalogHostStateRequest) (CatalogEntry, error)
- type StoreClosedError
- type UpdateCatalogDesiredStateRequest
- type UpdateCatalogHostStateRequest
Examples ¶
Constants ¶
const ( // CatalogRecordVersion is the stored legacy catalog version. Bound records // use CatalogBindingRecordVersion; readers refuse all other versions. CatalogRecordVersion uint8 = 1 // MaxCatalogOpenGates bounds the open-gate projections one catalog record // carries. The catalog is a replay-free status projection, not a gate // store: a session with more simultaneously open gates than this is read // through the gate API instead. MaxCatalogOpenGates = 16 // MaxCatalogRecordBytes bounds an encoded catalog record. It is well below // storage.MaxOrderedValueBytes so a record that this package accepts always // fits in the provider, leaving no state that can be written but not // rewritten. MaxCatalogRecordBytes = 256 << 10 )
const ( // EnvelopeVersion is the independent version of the raw journal frame. EnvelopeVersion uint8 = 1 // MaxEnvelopeBytes is the maximum encoded frame size accepted or produced. MaxEnvelopeBytes = 1 << 20 // MaxInlineBodyBytes is the maximum size of either independent inline body. MaxInlineBodyBytes = 512 << 10 )
const ( // GateIntentRecordVersion is the independent version of the stored gate // deadline intent. A reader fails closed on any other version rather than // guessing which members a future encoder meant. GateIntentRecordVersion uint8 = 1 // MaxGateIntentBytes bounds an encoded deadline intent. An intent holds // identities and one timestamp, never a prompt, so this is far above what a // legitimate record needs and far below the provider's own value bound. MaxGateIntentBytes = 8 << 10 )
const ( // HostTargetRecordVersion is the independent version of the stored // advertisement. A reader fails closed on any other version rather than // guessing which members a future encoder meant. HostTargetRecordVersion uint8 = 1 // MaxHostTargetRecordBytes bounds an encoded advertisement. Like the // registration's bound it is far tighter than the catalog's, because this // record has no open-ended member: it is a fixed tuple of bounded // identities, two instants, and three scalars. // // It allows for JSON ESCAPING, which is what sizes it rather than the sum // of the identity lengths. An identity is any valid UTF-8 of at most // MaxIDBytes bytes, control characters included, and Go escapes each of // those as \u00XX — six bytes for one — so the worst acceptable record is // about six times what an ASCII fixture measures. See // TestLargestAcceptableHostTargetFitsTheBound, which builds that record // rather than an ASCII one. // // It sits below storage.MaxOrderedValueBytes, so a record this package // accepts always fits in the provider and there is no state that can be // written but not rewritten. A heartbeat rewrites this row on a fixed // cadence forever, so a row that could be created but not updated would be // a row frozen at whatever capacity it last reported. // // On the ENCODE path the refusal is unreachable, and that is the point // rather than a gap: every member is bounded by Core's identity ceiling, so // the largest record the validators accept is a small multiple of it. What // holds the relationship is therefore not a test that reaches the branch — // none can — but the unsigned constant below, which fails to compile if the // bound ever exceeds the provider's, and // TestLargestAcceptableHostTargetFitsTheBound, which fails if the members // ever grow into it. On the DECODE path it is live: those bytes are not // this package's to bound. MaxHostTargetRecordBytes = 16 << 10 )
const ( DefaultHostTargetReconcilePages = 16 MaxHostTargetReconcilePages = 1024 )
DefaultHostTargetReconcilePages is the number of due pages one sweep walks when a caller names no budget, and MaxHostTargetReconcilePages is the most it may name.
A sweep is bounded by pages as well as by page size because it must be able to STEP OVER a row it cannot handle. A row this sweep cannot decode stays due, so it sits at the head of every later ascending due page; a single-page sweep would spend every pass on that row and never reach the rows behind it.
THE BUDGET ALONE IS NOT THE ANSWER, and believing it was is how this record nearly shipped with the head-of-line failure its own deadline view exists not to have. A budget bounds the work ONE PASS does; it does nothing about progress, because every pass restarts at the head of the same ascending view and nothing ever removes an unreadable row, so that population is monotonically non-decreasing. Once it reaches MaxPages x Limit rows, every later pass spends its whole budget on them and withdraws nothing, forever.
What actually supplies progress is the CONTINUATION on the result: a sweep that runs out of budget hands back where it stopped, and a caller that pages until Exhausted reaches every row however many unreadable ones precede them. The budget then means what it says — a bound on one call — and HostTargetReconcileResult.Unreadable is what makes the cost of those rows visible rather than merely survivable.
const ( // InboxRecordVersion is the independent version of the stored command // record. A reader fails closed on any other version rather than guessing // which members a future encoder meant. InboxRecordVersion uint8 = 1 // MaxInboxPayloadBytes bounds an INLINE private command payload. A body // larger than this is stored as an object and referenced, which is what // InboxRecord.PayloadRef is for: the inbox is a control record that a // reconciler pages through, not a blob store. MaxInboxPayloadBytes = 64 << 10 // MaxInboxRecordBytes bounds an encoded command record. Like the catalog's // bound it is well below storage.MaxOrderedValueBytes, so a record this // package accepts always fits in the provider and there is no state that // can be written but not rewritten. MaxInboxRecordBytes = 256 << 10 )
const ( // SessionPointerRecordVersion is the independent version of the stored // pointer. A reader fails closed on any other version rather than guessing // which members a future encoder meant. SessionPointerRecordVersion uint8 = 1 // MaxSessionPointerRecordBytes bounds an encoded pointer. // // The record has no open-ended member. Two of its members are identities // bounded by sessionwire.MaxIDBytes, and the ceiling has to allow for JSON // ESCAPING of those, which is what sizes it: an identity is any valid UTF-8 // of at most that many bytes — control characters included, which both // TenantID.Validate and SessionID.Validate accept — and Go escapes each of // those as \u00XX, six bytes for one. // // The target is NOT escaping-sensitive and is the reason this bound is // half the registry's rather than equal to it: an ObjectID that // parseObjectReference accepts is canonical lowercase ASCII of a fixed // shape, so it encodes one byte per byte. // TestLargestAcceptableSessionPointerFitsTheBound builds the worst case // over every declared kind and reports what it measures. // // Like the other bounds it sits below storage.MaxOrderedValueBytes, so a // record this package accepts always fits in the provider and there is no // state that can be written but not rewritten. MaxSessionPointerRecordBytes = 8 << 10 )
const ( // ReconciliationClaimRecordVersion is the independent version of the stored // claim. A reader fails closed on any other version rather than guessing // which members a future encoder meant. ReconciliationClaimRecordVersion uint8 = 1 // MaxReconciliationClaimRecordBytes bounds an encoded claim. The record has // no open-ended member — it is three identities and two instants — but the // ceiling still has to allow for JSON ESCAPING, which is what sizes it: an // identity is any valid UTF-8 of at most sessionwire.MaxIDBytes bytes, // control characters included, and Go escapes each of those as \u00XX, six // bytes for one. TestLargestAcceptableReconciliationClaimFitsTheBound // builds that worst case and reports what it measures, because a bound // measured with ASCII is a sixth of what it claims to be. // // Like the other bounds it sits below storage.MaxOrderedValueBytes, so a // record this package accepts always fits in the provider and there is no // state that can be written but not rewritten. MaxReconciliationClaimRecordBytes = 16 << 10 )
const ( // HostRegistrationRecordVersion is the independent version of the stored // registration. A reader fails closed on any other version rather than // guessing which members a future encoder meant. HostRegistrationRecordVersion uint8 = 1 // MaxHostRegistrationRecordBytes bounds an encoded registration. It is far // tighter than the catalog's and the inbox's bounds because this record has // no open-ended member: it is a fixed tuple of identities, each of them // bounded by sessionwire.MaxIDBytes. // // The ceiling has to allow for JSON ESCAPING, and that is what sizes it. An // identity is any valid UTF-8 of at most MaxIDBytes bytes — control // characters included, which both TenantID.Validate and validateOpaque // accept — and Go escapes each of those as \u00XX, six bytes for one. The // worst acceptable registration is therefore about six times the sum of its // identity lengths, which at 8 KiB was 95 bytes OVER the bound: records the // validators accept were refused here, and the comment that used to sit // here said a spelling near the ceiling was one nothing could produce. It // was measuring ASCII. TestLargestAcceptableRegistrationFitsTheBound now // builds the real worst case and reports the measured size. // // Like the other bounds it sits below storage.MaxOrderedValueBytes, so a // record this package accepts always fits in the provider and there is no // state that can be written but not rewritten. MaxHostRegistrationRecordBytes = 16 << 10 )
const ( // DefaultControlShards is the shard count a store adopts when its backend // is first initialized and no count was named. DefaultControlShards = 16 // MaxControlShards bounds the count. It is a ceiling on a REPLICA'S SWEEP // COST rather than on the provider: a sweep visits every shard round-robin, // so the count is a per-pass floor on the number of provider queries even // when nothing at all is due. It is also bounded below by the marker's // two-byte field and by controlShardToken's fixed width. MaxControlShards = 4096 // MinControlShards is one — an unsharded deployment, which is a legitimate // configuration and the one a single-replica local Factory wants. MinControlShards = 1 )
const CatalogBindingRecordVersion uint8 = 2
CatalogBindingRecordVersion adds a required complete immutable binding to the catalog. Legacy records retain CatalogRecordVersion and their bytes.
const DefaultJournalOverflowThresholdBytes = 64 << 10
DefaultJournalOverflowThresholdBytes is the encoded body size above which a journal body is uploaded as an immutable object and replaced in the record by a fixed-integrity reference. It is well below MaxInlineBodyBytes so an ordinary record stays small enough that a whole page of them fits inside one ledger read.
const DefaultJournalPageBytes = 1 << 20
DefaultJournalPageBytes bounds the resolved bytes one journal page may return. It exceeds MaxInlineBodyBytes, so a page always makes progress: the largest single public body a writer can commit still fits in one page.
const DefaultShutdownTimeout = 30 * time.Second
DefaultShutdownTimeout bounds provider cleanup after Store-owned work drains. It matches the remote provider drain bound used by the released NATS backend.
const MaxCommandClaimTTL = time.Hour
MaxCommandClaimTTL bounds how far ahead of the store's clock a claim may lapse. It is a ceiling on caller error and clock skew, not a policy TTL: a caller chooses its own TTL well below this, and nothing here is a recommendation of an hour.
It exists because an over-long claim is a durable liveness fault that one caller can commit alone. A claim may legitimately outlive the apply deadline — that is what lets an unexpired claim win the deadline race — and inboxDue caps the due horizon at the deadline, so from the deadline onward the command is DUE, is paged by every reconciler pass, and can be settled by nobody until the claim lapses. Unbounded, "until the claim lapses" is bounded only by rankableTime, which is centuries: one caller with a skewed clock parks a row in the deadline view for the life of the deployment. The bound turns that into at most one TTL, which is the same shape of exposure a crashed claimer already has.
It is stated as a package constant with no deployment knob for the reason MaxInboxPayloadBytes is: it is a bound on what this record may mean, not a tuning parameter, and a deployment that needed a longer one would be saying something about the machine rather than about its own capacity.
const MaxDesiredWorkloadPayloadBytes = 16 << 10
MaxDesiredWorkloadPayloadBytes bounds the opaque platform payload.
It is far below MaxCatalogRecordBytes, and the gap is the point. The payload is the only open-ended member a Factory controls on this record, so without a bound of its own an oversized one would be refused as "the record is too large" — a limit naming a member the caller did not write and cannot shrink. Bounding it here reports the member that was actually too big.
The value is sized for a workload SPEC rather than for workload data: a pod template with resources, a workspace policy, and labels is a few kilobytes. A payload wanting more than this is carrying data that belongs in an object, referenced by the spec.
A byte payload has no escaping worst case to allow for, unlike the identity tuples the registry and the target directory bound: Go's JSON encoder writes a []byte as base64, so the encoded cost is a fixed 4/3 of the payload plus the version label, and it cannot be inflated six-fold by a caller choosing control characters.
const MaxHostRegistrationTTL = time.Hour
MaxHostRegistrationTTL bounds how far ahead of the store's clock a caller may place a registration's expiry.
It exists because an over-long expiry is a durable ROUTING fault one caller can commit alone, and the damage is the opposite shape from a claim's. A claim that outlives its usefulness parks a row nobody may settle; a registration that outlives its Host is worse, because every reader treats it as a live route and keeps sending sessions to a process that is gone. The registration is refreshed by heartbeat, so a legitimate one is short — this is a ceiling on how long a single skewed clock reading can misroute a session, not a tuning parameter, which is why it is a package constant with no deployment knob for the reason MaxCommandClaimTTL is.
const MaxHostTargetAvailableCapacity uint64 = 1 << 20
MaxHostTargetAvailableCapacity bounds the free capacity one Host may advertise for one target.
It exists so that hostTargetRank is TOTAL. The rank a provider orders on is a signed int64 and the reported capacity is an unsigned uint64, so without a ceiling the conversion has an undefined region: a capacity above MaxInt64 converts to a NEGATIVE rank, which would sort a Host claiming absurd capacity BELOW every real one — a wrong answer that looks like a working directory rather than like a refusal. Bounding the input instead makes that region unreachable, and the ceiling is checked where the value enters the record, not where the rank is computed, so no future rank expression can reintroduce it.
The value is orders of magnitude above any real Host and is not a tuning parameter; a Host near it is already reporting something no process could serve.
const MaxHostTargetTTL = 15 * time.Minute
MaxHostTargetTTL bounds how far ahead of the store's clock a Host may place its next heartbeat.
It is tighter than MaxHostRegistrationTTL, and the asymmetry is the whole difference between the two records. A registration is consulted BY NAME, for one session a caller already knows about; a stale one misroutes that session. An advertisement is offered to EVERY placement decision for its target, and it is offered PREFERENTIALLY when its capacity ranks high — so a crashed Host that advertised generous capacity is the first row every placement page returns. This ceiling bounds how long a single skewed clock reading can hold that position before the row is even eligible to be reconciled away.
THE COUNTER-ARGUMENT, recorded so the next reader sees both. The ceiling is measured against THIS STORE's clock, so it binds two populations rather than one: a Host whose clock runs fast, and a perfectly-clocked Host whose heartbeat interval is simply longer than the ceiling. Whichever it is, that Host's row can be offered to placement for up to the whole window after the process behind it has died, and five minutes was argued for on exactly that basis. Fifteen is kept because the two populations pull in opposite directions: tightening the ceiling shortens the dead-endpoint window for the first, and REFUSES the second outright — which removes capacity rather than merely mis-offering it — and this package cannot see a deployment's heartbeat interval to tell them apart. The choice is a judgement call within a factor of three, it is pinned at both boundaries by TestPublishHostTargetBoundsTheHeartbeatPromise, and it is cheap to change: nothing derives from it and no stored record embeds it.
const MaxReconciliationClaimTTL = 5 * time.Minute
MaxReconciliationClaimTTL bounds how far ahead of the store's clock a caller may place a claim's expiry.
It exists because an over-long claim is a durable liveness fault one caller can commit alone, and the shape of the damage is the one MaxCommandClaimTTL describes: nothing removes a claim, and every other replica declines to reconcile the session for as long as it lasts. Unbounded, "as long as it lasts" is bounded only by rankableTime, which is centuries — one replica with a skewed clock takes a session out of reconciliation for the life of the deployment.
The consequence is delay rather than incorrectness, because the claim is not a fence and a replica that decided to reconcile anyway would still be safe. That is exactly why the bound has to be here rather than in a caller's policy: the failure is invisible, so nothing would ever report it.
It is a ceiling on caller error and clock skew, not a policy TTL. A reconciler chooses its own TTL far below this, and five minutes is not a recommendation.
const MinGateIntentRemnantAge = 5 * time.Minute
MinGateIntentRemnantAge is how long a gate deadline intent must have been durable before this store will retire it as a remnant.
IT IS A CEILING ON HOW LONG ONE OpenGate CALL CAN TAKE, not a policy delay. OpenGate writes the intent, then commits the open projection, and between those two writes the intent looks exactly like a remnant. Nothing in the two records distinguishes "the open crashed" from "the open is in flight", so a retirement inside that window can tombstone the deadline of a gate that is about to become publicly open — leaving it waiting with nothing to expire it, under an identity that can never be reused because this package's tombstones are permanent.
Five minutes is chosen against the cost of being wrong in each direction, and the two costs are not symmetric. Waiting too long leaves a remnant row in a due page for longer; the continuation steps past it, so the cost is a row per page, not a stalled sweep. Waiting too little destroys a live gate's deadline. So the window is set far above any plausible span of the interval it actually covers, rather than close to it.
THE INTERVAL IS THE CLOCK READING TO THE PROJECTION COMMIT, not "between two writes". OpenGate reads the clock at the top, before it is admitted and before it reads the catalog, because this package reads the clock once ahead of any provider work. So the exposed span is a mutex acquisition, the session-scope verification and catalog read, the intent write and the projection write — several round trips rather than the gap between two of them. Leaving the reading where it is remains right: moving it after the catalog read would buy a shorter interval by breaking the rule that keeps every operation's decisions evaluated at one instant.
WHICH CLOCK, AND WHAT THAT DOES NOT BUY. RecordedAt is stamped by the store that opened the gate and the age is evaluated by the store that sweeps — different processes, each with its own injected clock, with no shared time available (see WithClock). The comparison is therefore skew-relative: a fast sweeper reaches the window early by the skew, a slow one late. That is affordable only because the window is minutes and the skew a deployment tolerates is seconds; it would not be affordable for a window of seconds, and shrinking this constant without a real clock is the way to make it unsafe.
Variables ¶
This section is empty.
Functions ¶
func EncodeEnvelope ¶
EncodeEnvelope validates and deterministically encodes an envelope. The returned frame does not alias any caller-owned body.
Types ¶
type AcquireReconciliationClaimRequest ¶
type AcquireReconciliationClaimRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
HolderID string
ExpiresAt time.Time
}
AcquireReconciliationClaimRequest takes or extends the claim on one session.
HolderID is the calling replica's own identity. ExpiresAt is that replica's promise about when it will be done, and it must lie in the store's future and within MaxReconciliationClaimTTL of it.
There is no claim instant and no expected revision. The claim instant is the store's, for the reason ReconciliationClaim states; the revision is not the caller's business because a claim is not a decision about a record the caller has read — it is "am I the one doing this?", and the answer is decided by the holder and the clock, closed by a compare-and-swap this store reads for itself.
There is deliberately no lease epoch. Requiring one would say that holding a session's lease is relevant to doing scaling work for it, which is exactly backwards: reconciliation runs when NO Host owns the session.
type AcquireResidencyRequest ¶ added in v0.4.0
type AcquireResidencyRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
}
AcquireResidencyRequest names an existing disposition-mode catalog session.
type AdmitCommandRequest ¶
type AdmitCommandRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
CommandID sessionwire.CommandID
ProposedRuntimeCommandID RuntimeCommandID
Kind CommandKind
Payload []byte
PayloadRef sessionwire.ObjectReference
AcceptedAt time.Time
ApplyDeadline time.Time
}
AdmitCommandRequest accepts one client command into a session's inbox. It is idempotent by (TenantID, SessionID, CommandID): a repeat returns the stored record unchanged with created false.
ProposedRuntimeCommandID is a PROPOSAL. Racing replicas may propose different runtime identities for one public CommandID; only the one in the winning record is stored, returned, and used, and a losing replica receives the winner's rather than its own. A caller must therefore use the returned mapping and never the value it sent.
AcceptedAt and ApplyDeadline are the caller's clock readings, as every other timestamp this package stores is. They belong to the WINNER: a duplicate returns the accepted instant and deadline that were durably committed, not the ones it just sent, and they take no part in deciding whether a duplicate mismatches — see AdmitCommand.
type BeginApplyingCommandRequest ¶
type BeginApplyingCommandRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
CommandID sessionwire.CommandID
ExpectedRevision uint64
LeaseEpoch uint64
ClaimExpiresAt time.Time
}
BeginApplyingCommandRequest moves a claimed command into applying. Its members mean what ClaimCommandRequest's mean; ClaimExpiresAt replaces the claim's expiry, because the bound that mattered while the claimer was preparing is not the bound that matters while it is applying.
It is the machine's ONE IRREVERSIBLE expiry choice, and a caller should size it for the whole application rather than for the next step. Applying is a fortress: it cannot be re-claimed at any epoch and it cannot be renewed, so once this expiry lapses the command can be completed only by this same lease epoch and settled by nobody else until a later task's recovery reads the journal correlation. A value chosen too small does not fail the application — it parks the command.
type BodyReference ¶
type BodyReference struct {
Reference sessionwire.ObjectReference
SizeBytes uint64
SHA256 [32]byte
}
BodyReference is the fixed-integrity representation stored in an envelope. Reference is a logical Core identity, never a provider key or signed URL.
func BodyReferenceFromObjectMetadata ¶
func BodyReferenceFromObjectMetadata(metadata sessionwire.ObjectMetadata) (BodyReference, error)
BodyReferenceFromObjectMetadata converts canonical SHA-256 Core metadata to the journal's fixed binary reference.
func (BodyReference) ObjectMetadata ¶
func (r BodyReference) ObjectMetadata() (sessionwire.ObjectMetadata, error)
ObjectMetadata converts a valid fixed reference to Core's public metadata shape with a canonical lowercase SHA-256 digest.
type BodySlot ¶
type BodySlot struct {
Inline []byte
Reference *BodyReference
}
BodySlot is one independent inline or object-backed body. A nil Inline is absent; a non-nil, zero-length Inline is present. Inline and Reference are mutually exclusive.
type CatalogEntry ¶
type CatalogEntry struct {
Record CatalogRecord
Revision uint64
}
CatalogEntry is a catalog record together with the revision a caller passes to a subsequent Factory-owned compare-and-swap. The provider's immutable order is deliberately not exposed: it is sparse and scope-relative, so no caller can correctly infer a position or a count from it.
type CatalogError ¶
type CatalogError struct {
Code CatalogErrorCode
Field string
Epoch uint64
Revision uint64
Cause error
}
CatalogError is a typed, redacted catalog failure. Field names the offending input or stage and never carries a provider name, key, or record payload. Epoch is populated only for CatalogErrorEpoch, where the committed high-water epoch is itself the answer, and Revision only for CatalogErrorConflict, where a backend that can safely disclose the current revision did so.
func (*CatalogError) Error ¶
func (e *CatalogError) Error() string
func (*CatalogError) Unwrap ¶
func (e *CatalogError) Unwrap() error
type CatalogErrorCode ¶
type CatalogErrorCode string
CatalogErrorCode classifies a session catalog record failure.
Cursor is separate from Invalid because the two name different owners. An invalid limit is a caller mistake in the request this package validates; Cursor means a continuation token was not one this store issued for this query, whether the envelope or the provider token inside it failed, and a caller's only recovery is to restart the walk from the first page.
Epoch and Conflict are deliberately distinct, and the distinction is the whole point of the catalog's two ownership mechanisms. Epoch means a Host-owned write named a lease epoch below the record's committed high-water mark: that writer has provably been superseded and must not retry with the same epoch. Conflict means a compare-and-swap lost a race on the record's revision without any statement about ownership; the caller may re-read and retry. Unknown means the mutation's outcome could not be resolved at all.
const ( CatalogErrorInvalid CatalogErrorCode = "invalid" CatalogErrorCursor CatalogErrorCode = "cursor" CatalogErrorNotFound CatalogErrorCode = "not_found" CatalogErrorDeleted CatalogErrorCode = "deleted" CatalogErrorIdentity CatalogErrorCode = "identity" CatalogErrorEpoch CatalogErrorCode = "epoch" CatalogErrorSequence CatalogErrorCode = "sequence" CatalogErrorTooSoon CatalogErrorCode = "too_soon" CatalogErrorConflict CatalogErrorCode = "conflict" CatalogErrorUnknown CatalogErrorCode = "unknown" CatalogErrorBackend CatalogErrorCode = "backend" CatalogErrorMalformed CatalogErrorCode = "malformed" CatalogErrorVersion CatalogErrorCode = "version" CatalogErrorTooLarge CatalogErrorCode = "too_large" )
type CatalogRecord ¶
type CatalogRecord struct {
// Binding is immutable after creation. Zero preserves the legacy v1 record.
Binding SessionBinding
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
AgentID sessionwire.AgentID
RuntimeCompatibilityID string
CreatedAt time.Time
LastActiveAt time.Time
State sessionwire.SessionState
Residency sessionwire.SessionResidency
DesiredPlacement sessionwire.HostPlacement
LastJournalSeq uint64
LastEventID sessionwire.EventID
Checkpoint CheckpointSummary
OpenGates []sessionwire.GateProjection
LeaseEpoch uint64
DesiredIdempotencyKey string
DesiredGeneration uint64
DesiredWorkload DesiredWorkload
}
CatalogRecord is the neutral, replay-free durable projection of one session.
Its fields have two different owners, and the difference is enforced rather than documented. LeaseEpoch, State, Residency, LastActiveAt, the journal summary, the checkpoint summary, and the open-gate projections are written by the Host that holds the session's lease, and a write naming an epoch below the committed high-water mark is refused. DesiredPlacement, RuntimeCompatibilityID, and DesiredIdempotencyKey are Factory-authored desired state, guarded by revision compare-and-swap and an idempotency key; Factory never names a lease epoch, so it cannot claim ownership it does not have.
func (CatalogRecord) PlacementIntent ¶
func (r CatalogRecord) PlacementIntent() (PlacementIntent, error)
PlacementIntent projects the record's desired state.
It canonicalizes first, as Summary and Status do, so an intent can never be produced from a record this package would refuse to store — and so the payload it hands back is this package's copy rather than the record's own.
func (CatalogRecord) Status ¶
func (r CatalogRecord) Status() (sessionwire.SessionStatus, error)
Status projects the record into Core's replay-free status shape. WaitingGateID is the first open gate in the record's canonical (opened_seq, gate_id) order, so two readers of the same record always name the same gate. Canonicalizing here rather than assuming a canonical caller keeps the gate comparator stated exactly once; a second defensive sort in this method is precisely the kind of restatement that later drifts.
func (CatalogRecord) Summary ¶
func (r CatalogRecord) Summary() (sessionwire.SessionSummary, error)
Summary projects the record into Core's recent-first list shape.
type CheckpointSummary ¶
type CheckpointSummary struct {
JournalSeq uint64
Reference sessionwire.ObjectReference
CapturedAt time.Time
}
CheckpointSummary is the bounded durable description of the active workspace checkpoint. Its zero value means no checkpoint has been committed. It names a logical Core object reference, never a provider key or signed URL.
type ClaimCommandRequest ¶
type ClaimCommandRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
CommandID sessionwire.CommandID
ExpectedRevision uint64
LeaseEpoch uint64
ClaimExpiresAt time.Time
}
ClaimCommandRequest takes a short-lived claim on one accepted command.
ExpectedRevision is the revision the caller decided on, which is the revision the compare-and-swap names. It is required: a transition is a decision about a record the caller has read, and a claim that named no revision would be a blind write dressed as a compare-and-swap.
LeaseEpoch is the session lease epoch the claimer is acting under, and ClaimExpiresAt is the caller's own reading of when the claim lapses. The claim's expiry may fall after the command's apply deadline — that is what lets an unexpired claim win the deadline race — but it may not fall in the past.
type ClearHostRegistrationRequest ¶
type ClearHostRegistrationRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
LeaseEpoch uint64
}
ClearHostRegistrationRequest releases one session's route, leaving the epoch-fenced tombstone behind.
It carries no timestamp, and that is deliberate rather than an omission. A tombstone's instants are not an observation of anything — they record that THIS STORE wrote the tombstone — and a caller-supplied instant could place a tombstone's expiry in the future, producing a record that reads as released by structure and as live by time. The store's own clock cannot.
type ClearSessionPointerRequest ¶
type ClearSessionPointerRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
LeaseEpoch uint64
}
ClearSessionPointerRequest gives up one role's object, leaving the epoch-fenced tombstone behind.
It names no sequence, and that is the shape of the operation rather than an omission: clearing does not publish a capture, so there is nothing for a sequence to describe, and the stored one is RETAINED rather than replaced. A request that could name one could lower it.
type CommandApplication ¶
type CommandApplication struct {
CommandID sessionwire.CommandID
RuntimeCommandID RuntimeCommandID
Outcome CommandApplicationOutcome
// PrefixSeq and PrefixEpoch locate the prefix the outcome is about, and are
// zero when the outcome is Absent.
PrefixSeq uint64
PrefixEpoch uint64
// EffectSeq and EffectEventID name the public event that carried the
// effect, and are zero unless the outcome is Committed.
EffectSeq uint64
EffectEventID sessionwire.EventID
// SupersedingEpoch is the highest opening-fence epoch THE WALK OBSERVED,
// and a writer at or below it is provably fenced out of the stream.
//
// It is deliberately not described as the highest lease that ever owned the
// session, which is what a walk from sequence one happens to find today.
// The two come apart the moment the walk is bounded — the admission-tip
// bound above is the obvious way, and it would leave every fence written
// before the command was accepted unobserved — and the predicate this
// member exists for stays correct under that, because a fence observed
// LATER than some other fence is still a fence. Promising the maximum over
// the whole journal would make a bound that is otherwise fine look like a
// breaking change.
SupersedingEpoch uint64
CapturedTip uint64
}
CommandApplication is what one session's journal proves about one command.
Every member describes DURABLE STATE this store read, never a conclusion about a caller. The sequences are journal sequences in the session's own stream; the epochs are session lease epochs.
CapturedTip is the tip the correlation was taken at, and it exists to say how long the answer is good for. The answer is: FOUR OF THE FIVE OUTCOMES ARE NOT STABLE ACROSS TIPS, and the transitions are ordinary rather than exotic — absent becomes committed when the applier commits its prefix and effect a moment later, abandoned becomes committed when a later lease retries the application to completion, unresolved becomes either as soon as one more record lands, and committed becomes conflicted when a prefix breaking the mapping appears anywhere later in the stream. Two of those are what this package's own tests do on purpose.
Only CONFLICTED cannot be superseded, because nothing outranks a broken mapping in precedence, which is also why it is the one finding an operator can act on without re-reading.
What IS monotone is not an outcome but a pair of NEGATIVE facts, and they are exactly the two a settlement rests on:
- no effect for this command had committed by CapturedTip, and
- the writer at a given epoch was already fenced out by CapturedTip.
Both are properties of a PREFIX OF THE STREAM, and the journal only appends, so no later record undoes either. That is the whole of why the settlements are safe, and it is also why they RE-SCAN rather than accept a correlation a caller took earlier: a caller's older answer still carries true negative facts, but the store cannot tell from the value alone which tip they were true of relative to the record it is about to write.
A caller may therefore hold a correlation to decide WHAT TO DO — finish or settle — and must not hold one as a licence. Caching an ABSENT answer and rejecting on it later is precisely the overwrite this file exists to prevent: absence is the least stable finding there is, because every application starts from it.
type CommandApplicationOutcome ¶
type CommandApplicationOutcome string
CommandApplicationOutcome is what a session's journal proves about one command's application. It is a closed vocabulary because each value unlocks a different settlement, and a caller that met an unlisted one would have no safe default.
const ( // CommandApplicationAbsent means no record in the journal names this // command. Nothing has been applied under it. CommandApplicationAbsent CommandApplicationOutcome = "absent" // CommandApplicationCommitted means a correlated prefix is immediately // followed by the public event that carried its effect. The command has // been applied, whichever lease did it. CommandApplicationCommitted CommandApplicationOutcome = "committed" // CommandApplicationAbandoned 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. CommandApplicationAbandoned CommandApplicationOutcome = "abandoned" // CommandApplicationUnresolved means a correlated prefix exists whose // outcome cannot be read: it is still at the tip with its writer possibly // alive, or the record after it is neither its effect nor its writer's // fence. Both settlements refuse; the answer may become readable later. CommandApplicationUnresolved CommandApplicationOutcome = "unresolved" // CommandApplicationConflicted 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. CommandApplicationConflicted CommandApplicationOutcome = "conflicted" )
type CommandClaim ¶
CommandClaim is the current short-lived claim on a command. Its zero value means the command is unclaimed.
LeaseEpoch is the claiming Host's grant epoch. It is recorded here rather than fencing anything in this file: admission never writes a claim.
type CommandKind ¶
type CommandKind string
CommandKind names what the command asks the session to do.
The set is deliberately not enumerated here. Core accepts any non-empty state, residency, and gate kind so a later wire version can add one, and a closed set in this package would refuse a command Core itself considers valid. It is validated as a bounded opaque UTF-8 value.
type CommandResult ¶
type CommandResult struct {
CompletedAt time.Time
EventID sessionwire.EventID
JournalSeq uint64
}
CommandResult is the terminal application result: the durable journal event that carried the command's effect. Its zero value means the command has not been applied.
type CompleteCommandRequest ¶
type CompleteCommandRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
CommandID sessionwire.CommandID
ExpectedRevision uint64
LeaseEpoch uint64
Result CommandResult
}
CompleteCommandRequest records the terminal application of a command.
Result names the durable journal event that carried the command's effect. It is required, because "applied" with no event is a claim that something happened with nothing to point at.
type CreateCatalogEntryRequest ¶
type CreateCatalogEntryRequest struct {
Binding SessionBinding
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
AgentID sessionwire.AgentID
RuntimeCompatibilityID string
CreatedAt time.Time
LastActiveAt time.Time
State sessionwire.SessionState
Residency sessionwire.SessionResidency
DesiredPlacement sessionwire.HostPlacement
DesiredWorkload DesiredWorkload
IdempotencyKey string
}
CreateCatalogEntryRequest creates the authoritative record for one session. Legacy creation is idempotent by (TenantID, SessionID). A bound retry must also match the immutable Binding and AgentID; a mismatch is CatalogErrorConflict. A legacy request cannot adopt a bound row. Matching retries return the stored record unchanged with created false, even when mutable desired fields differ.
type DesiredWorkload ¶
DesiredWorkload is the platform workload a Factory wants reconciled for a dedicated session, carried opaquely.
Its zero value means no workload is desired, which is the ordinary case for a pooled session: pooled capacity is reconciled by scaling a Department's Hosts, not by creating anything per session.
The two members are present together or absent together, and canonicalization enforces that rather than documenting it. A payload with no version is a document no reconciler can interpret — it would have to guess a schema — and a version with no payload is a claim about nothing. Requiring both also gives "absent" exactly one spelling, which is what keeps a record's stored bytes independent of which writer produced them.
PayloadVersion is caller-owned text and is never interpreted here. It exists so that a reconciler reading a payload it does not understand can say so instead of misreading it.
A NOTE OWED TO WHOEVER ADDS A BYTES-IDENTITY CHECK TO THE CATALOG. This member makes the catalog record's stored bytes a normalizer's output rather than a fixed point of the caller's input: encoding/json decodes a []byte with non-strict base64, so a stored "AR==" decodes to one byte and re-encodes as "AQ==". The record still round-trips CANONICALLY — decode, encode, decode again is stable, which is what the codec fuzzer asserts — but a check comparing a provider's reply against the exact bytes handed to it, as verifyReconciliationClaimBytes and verifyRegistrationBytes do for records with no []byte member, would refuse a faithful reply to a value some other writer had stored non-canonically. Compare re-encoded forms there, not raw bytes.
type DrainHostTargetRequest ¶
type DrainHostTargetRequest struct {
Key HostTargetKey
HostID sessionwire.HostID
HostGeneration uint64
}
DrainHostTargetRequest withdraws one Host's advertisement for one target, gracefully and immediately.
It carries no timestamp, and that is deliberate rather than an omission. A withdrawal's instant is not an observation of anything — it records that THIS STORE wrote the withdrawal — and a caller-supplied instant would let a Host place its own withdrawal in the future or the distant past, in a record that nothing afterwards re-derives an expiry from. The store's own clock cannot.
type DueCommand ¶
type DueCommand struct {
Entry InboxEntry
}
DueCommand is one outstanding command. The identities are read from the RECORD rather than restated beside it — a sweep learns them from the row — and Entry is the same value a named read of that command returns, held to the same filing checks.
It wraps a single member rather than being one, for the reason DueGatePage is a type rather than a second return value: what a reconciler needs BESIDE the record is not settled yet, and adding a field is a smaller change to make than changing the element type of a public slice.
type DueCommandPage ¶
type DueCommandPage struct {
Commands []DueCommand
Examined int
Unreadable int
Limit int
NextCursor sessionwire.Cursor
}
DueCommandPage is one bounded page of outstanding commands together with what producing it cost. It is where the three cost members both sweeps carry are defined; DueGatePage refers here rather than restating them.
EXAMINED is the number of rows the provider returned, and LIMIT is the EFFECTIVE limit after a zero request limit has been resolved to the store's page size, so the comparison is available to a caller that named no limit. Together they answer a question the results alone cannot: Examined == Limit with nothing reported means this page was full and none of it said anything, which is a different state from "nothing is due".
UNREADABLE counts rows this reader could not decode, that disagreed with the filing they were found under, or that belong in a different shard. Each is SKIPPED rather than failing the page, and that is the strongest rule here rather than leniency: this view is ascending by an instant that never moves and nothing rewrites such a row, so a reader that failed on one would switch reconciliation off for every tenant in the shard until someone repaired the row by hand.
LOCATING AN UNREADABLE ROW IS OUT OF BAND, and that is a real limitation rather than an oversight to be discovered. Unreadable is a count; this package has no logger and no channel to report a row's identity through, and adding one is public surface a later task should design rather than something to bolt on here. What an operator has instead is the SHARD and the DUE BOUND the page was read at, which narrow the row to one namespace and one prefix of an ordered view. The row's stable key and ordering scope are in hand at both skip sites, so a reporting channel is cheap to add when something exists to receive it.
NEXTCURSOR is what keeps that skipping from becoming starvation. An unreadable row is stepped over by the provider's own continuation, which resumes from the tuple the page ended on, so a row left in place is passed rather than met again on the next page.
type DueGate ¶
type DueGate struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
Gate sessionwire.GateProjection
}
DueGate is one gate whose deadline has passed, together with the session it belongs to. Gate is the projection read back from that session's durable catalog record, not from the intent: the intent is an index into the projection and never a second copy of it.
type DueGatePage ¶
type DueGatePage struct {
Gates []DueGate
Remnants []RemnantGateIntent
Examined int
Unreadable int
Limit int
// NextCursor resumes this shard's sweep after the position this page ended
// at. It is empty when the view is exhausted.
//
// It is what turns the head-of-line hazard this reader used to have from
// permanent into transient. A row that reports nothing — a remnant, or one
// this reader could not read at all — is stepped over by the provider's own
// continuation, which resumes from the frozen (due_at, stable_key,
// ordering_scope) tuple the page ended on. So a blocking row is PASSED
// rather than met again at the head of every page, and a gate behind it is
// reached on the next page instead of never.
//
// A still-open gate past its deadline is deliberately NOT stepped over
// permanently: it stays in the view, so every fresh pass reports it again,
// because it is current due work that nothing has dealt with. It does not
// block, because the continuation moves past it within a pass.
NextCursor sessionwire.Cursor
}
DueGatePage is one bounded page of due gates together with what producing it cost.
Examined, Limit and Unreadable are defined on DueCommandPage, including why an unreadable row is skipped rather than failing the page and why locating one is out of band. They mean the same things here.
What is specific to this page is the distinction between the two ways a row can report no gate. A REMNANT was read and understood and can be retired, so it is reported in Remnants with the revision a retirement names; an UNREADABLE row is one nothing in this package can vouch for, so it is counted and left alone. Reporting the second as the first would aim a retirement at a row whose gate may well be open in a record nobody could decode.
type Envelope ¶
type Envelope struct {
Kind EnvelopeKind
EventID sessionwire.EventID
RecordID string
Public BodySlot
Runtime BodySlot
LeaseEpoch uint64
CommandID sessionwire.CommandID
RuntimeCommandID uuid.UUID
CommandKind string
}
Envelope is one deterministic raw journal record. Public and Runtime remain separate so a public reader can select the public slot without inspecting or resolving private runtime bytes.
func DecodeEnvelope ¶
DecodeEnvelope strictly decodes one complete frame. It checks the one-MiB envelope bound before allocating and returns caller-owned body copies.
type EnvelopeError ¶
type EnvelopeError struct {
Code EnvelopeErrorCode
Field string
Cause error
}
EnvelopeError reports a bounded codec failure and preserves its cause without placing attacker-controlled cause text in Error().
func (*EnvelopeError) Error ¶
func (e *EnvelopeError) Error() string
func (*EnvelopeError) Unwrap ¶
func (e *EnvelopeError) Unwrap() error
type EnvelopeErrorCode ¶
type EnvelopeErrorCode string
EnvelopeErrorCode is a stable machine-readable envelope failure reason.
const ( EnvelopeErrorMalformed EnvelopeErrorCode = "malformed" EnvelopeErrorVersion EnvelopeErrorCode = "version" EnvelopeErrorKind EnvelopeErrorCode = "kind" EnvelopeErrorField EnvelopeErrorCode = "field" EnvelopeErrorOrder EnvelopeErrorCode = "order" EnvelopeErrorMissing EnvelopeErrorCode = "missing" EnvelopeErrorInvalid EnvelopeErrorCode = "invalid" EnvelopeErrorLength EnvelopeErrorCode = "length" EnvelopeErrorTooLarge EnvelopeErrorCode = "too-large" EnvelopeErrorDigest EnvelopeErrorCode = "digest" EnvelopeErrorTrailing EnvelopeErrorCode = "trailing" )
type EnvelopeKind ¶
type EnvelopeKind uint8
EnvelopeKind identifies the closed set of raw journal record shapes.
const ( EnvelopeKindPublicEvent EnvelopeKind = 1 EnvelopeKindRuntimeControl EnvelopeKind = 2 EnvelopeKindOpeningFence EnvelopeKind = 3 EnvelopeKindApplicationPrefix EnvelopeKind = 4 )
type FindCommandApplicationRequest ¶
type FindCommandApplicationRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
CommandID sessionwire.CommandID
}
FindCommandApplicationRequest names one accepted command whose journal evidence a caller wants. It carries no positioning members: the correlation is a question about the whole of a session's history, and a caller that could bound the walk could bound away the evidence.
type GetCatalogEntryRequest ¶
type GetCatalogEntryRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
}
GetCatalogEntryRequest reads one session's authoritative catalog record.
type GetCommandRequest ¶
type GetCommandRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
CommandID sessionwire.CommandID
}
GetCommandRequest reads one accepted command by its public identity.
type GetHostRegistrationRequest ¶
type GetHostRegistrationRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
}
GetHostRegistrationRequest reads one session's current route.
type GetObjectMetadataRequest ¶ added in v0.4.0
type GetObjectMetadataRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
ExpectedKind ObjectKind
Reference sessionwire.ObjectReference
}
GetObjectMetadataRequest resolves one logical reference in a caller-authorized tenant/session scope and an explicit semantic kind.
type GetObjectRequest ¶
type GetObjectRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
ExpectedKind ObjectKind
Metadata sessionwire.ObjectMetadata
}
GetObjectRequest names a verified object and the semantic kind the caller is authorized to consume.
type GetReconciliationClaimRequest ¶
type GetReconciliationClaimRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
}
GetReconciliationClaimRequest reads one session's current claim.
type GetSessionPointerRequest ¶
type GetSessionPointerRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
}
GetSessionPointerRequest reads one session's current pointer of one role.
type HostAdvertisement ¶
type HostAdvertisement struct {
InternalEndpoint sessionwire.InternalEndpoint
IsolationClass sessionwire.HostIsolationClass
Accepting bool
AvailableCapacity uint64
// ExpiresAt is the promise the Host makes about its next heartbeat, and it
// is the row's due time. It lives on the advertisement rather than on the
// record because a withdrawn row has no next heartbeat to promise: putting
// it here is what makes "withdrawn implies not due" a statement about the
// record's SHAPE instead of a rule someone has to remember.
ExpiresAt time.Time
}
HostAdvertisement is the offered half of a Host target row: everything a Factory needs to decide whether to place a NEW session on this Host, and nothing that would let it claim an existing one.
It is a separate pointer member rather than a group of optional fields, and that is what makes withdrawal correct BY CONSTRUCTION rather than by convention. A withdrawn row has no advertisement, and "no advertisement" is one nil rather than an enumeration of five zero values that a sixth member would silently escape — and, more importantly here, both derived views read that one nil, so a withdrawn row cannot be ranked and cannot be due.
type HostRegistration ¶
type HostRegistration struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
LeaseEpoch uint64
ObservedAt time.Time
ExpiresAt time.Time
// Route is nil exactly when this registration is a released tombstone.
Route *HostRoute
}
HostRegistration is the authoritative durable record of where one session is currently running, and of the lease epoch that fact was observed under.
It is an EXPIRING ROUTING HINT over a PERMANENT FENCE, and those two halves have opposite lifetimes:
- The route expires. A reader past ExpiresAt must treat the registration as absent, because the Host that published it may have died at any instant after ObservedAt and nothing will tell this record about it.
- LeaseEpoch does not expire, ever. It is the high-water mark that refuses a superseded Host's write, and it is the reason an expired registration and a released one are RETAINED rather than deleted. Dropping the row would drop the fence, and the next write from a lease that has already lost the session would be admitted.
A nil Route is the released tombstone: the registration a graceful shutdown leaves behind. It carries the fence and nothing else, so no reader can route to it however its timestamps read.
The accumulation that follows is intentional and affordable: one small, permanent row per session that has ever been registered, never listed, never ranked, never due, and never read except by name. The reader cost is nil, which is what makes permanence the right answer rather than a debt.
CARRY-FORWARD CONTRACT for whoever adds retention: THE ONLY SAFE REAPER IS ONE THAT REMOVES THE SESSION'S WHOLE SCOPE AT ONCE — this row, its catalog record, its journal, its commands, and its collision witnesses — because deleting this row ALONE destroys the fence while leaving the session registrable, which is precisely the state the retention exists to prevent. A sweep that walks record kinds independently and reclaims the cheapest one first will reach this one first, and it must not.
WHAT THIS RECORD OWNS, AND WHAT THE CATALOG OWNS. Three members appear in both records, and in every case the catalog's is Factory-authored DESIRED state or a durable status projection while this one's is the Host's OBSERVED answer:
- Placement. CatalogRecord.DesiredPlacement is what a Factory ASKED for under its own revision/idempotency CAS. HostRoute.Placement is the admission model the session is actually running under. The two disagree for the whole interval between a desired change and its reconciliation, and a router that used the desired value during that interval would bind to a Host that is not serving the session.
- Residency. CatalogRecord.Residency is the session's last known status, which is what a picker renders and which must survive this record's expiry. HostRoute.Residency is the ROUTABLE residency: it is only ever read together with the route it qualifies, and it disappears with it. A session whose registration has expired is cold no matter what the catalog's projection last said, which is exactly the statement the catalog cannot make because it does not expire.
- RuntimeCompatibilityID and AgentID. The catalog's are what the session was created for and what a Factory later desired; the route's are what the running Host actually loaded. A Factory reusing a route must check the observed one, because a Host that has been restarted onto a newer runtime is not compatible with a session pinned to the older one.
LeaseEpoch appears in both too, and there the duplication is real and deliberate: each record carries the epoch ITS OWN writes are fenced at. Neither is derived from the other, neither is read to decide the other, and the two advance independently — a Host that updates its catalog projection without refreshing its registration leaves this record at the older epoch, which is correct, because the fence protects the record it lives on.
func (HostRegistration) Observation ¶
func (r HostRegistration) Observation() (sessionwire.HostLinkRegistryObservation, error)
Observation projects a live registration into Core's HostLink vocabulary, which is the form a Factory sends over a HostLink binding.
It is also the record's OWN validator for every member of the route, and that is the point of routing both through one function. Core defines what a Host route means — that the endpoint is a credential-free WebSocket address, that the placement is one of two admission models, that the residency of a routed session is attaching, resident, or releasing rather than cold, that the expiry falls after the observation — and a second enumeration of those rules here would be free to drift from the one a Factory's peer actually applies. A registration this package stores is therefore always projectable, and the day Core adds a member to the observation this stops compiling rather than silently storing a record that cannot be projected.
A released registration has no projection at all: a tombstone is not a route, and there is no version of Core's observation that expresses one.
It is a projection of the BYTES and not a routing decision. It does not know the store's clock and therefore does not know whether the route has lapsed; GetHostRegistration makes that decision, and it is the only thing that does.
type HostRegistrationEntry ¶
type HostRegistrationEntry struct {
Registration HostRegistration
Revision uint64
}
HostRegistrationEntry is a registration together with the revision a later compare-and-swap names.
The provider's immutable acceptance order is deliberately NOT exposed, for the reason CatalogEntry's is not: a session's position in a stream of registrations is not a fact any caller acts on. Commands expose theirs because arrival order is the thing consumers sort by.
type HostRoute ¶
type HostRoute struct {
HostID sessionwire.HostID
HostGeneration uint64
AgentID sessionwire.AgentID
RuntimeCompatibilityID string
Placement sessionwire.HostPlacement
InternalEndpoint sessionwire.InternalEndpoint
Residency sessionwire.SessionResidency
Accepting bool
}
HostRoute is the routable half of a Host registration: everything a Factory needs to establish a HostLink to the process currently holding the session.
It is a separate pointer member rather than a group of optional fields on the record, and that is what makes the tombstone below correct BY CONSTRUCTION. A released registration has no route, and "no route" is one nil rather than an enumeration of eight zero values that a ninth member would silently escape.
Every member is OBSERVED — what the Host reports it is actually doing — which is the whole difference from the catalog record. See HostRegistration.
type HostTarget ¶
type HostTarget struct {
Key HostTargetKey
HostID sessionwire.HostID
HostGeneration uint64
ObservedAt time.Time
// Advertisement is nil exactly when this row is withdrawn.
Advertisement *HostAdvertisement
}
HostTarget is one Host's current advertisement for one target: the durable row behind a placement decision.
IT IS CAPACITY, NOT AUTHORITY, AND THAT BOUNDARY IS STRUCTURAL. This record names no tenant, no session, and no lease epoch — there is nowhere in it to spell one — and the projection it publishes to a reader, core's HostLinkCapacityReport, has no such member either. A Factory that has read this row knows a Host said it could take work; it knows nothing whatsoever about who owns any session. The record that answers THAT question is HostRegistration, whose LeaseEpoch is the fence, and no code path leads from this file to it. TestHostTargetsCannotSpellSessionOwnership pins the structural half of that claim so it cannot decay into prose.
HostGeneration is on the record rather than on the advertisement because a withdrawn row keeps it. It is a WRITE-ORDERING high-water mark over one row, and the distinction from the registry's epoch is worth stating precisely, because the two would otherwise look like the same mechanism:
- HostID is part of this row's identity, so the only writers of this row are incarnations of ONE Host. The generation orders that Host's own writes against each other and against nothing else.
- What it prevents is a liveness fault, not a safety one. A restarted Host publishes at a higher generation; a heartbeat or a drain still in flight from the dead incarnation would otherwise overwrite live capacity with a dead process's view of it, and the target would flap.
- It confers NO right over any session. A Host holding the highest generation on a capacity row has proven only that it is the newest incarnation of itself.
The rows are removable, which is the other half of the difference from the registry. See hostTargetDue for what removes one and why nothing here is retained forever.
func (HostTarget) Report ¶
func (t HostTarget) Report() (sessionwire.HostLinkCapacityReport, error)
Report projects an advertised target into Core's capacity vocabulary, which is the form a Factory reads when choosing where to place a session.
It is also the record's OWN validator for every member of the advertisement, and that is the point of routing both through one function. Core defines what a capacity report means — that the endpoint is a credential-free WebSocket address, that the placement is one of two admission models, that the isolation class is one of two boundaries, that a dedicated target cannot advertise more than one seat, that the expiry falls after the observation — and a second enumeration of those rules here would be free to drift from the one a Factory's peer actually applies. A target this package stores is therefore always projectable, and the day Core adds a member to the report this stops compiling rather than silently storing a record that cannot be projected.
A withdrawn row has no projection at all: a withdrawal is not an offer of capacity, and there is no version of Core's report that expresses one.
It is a projection of the BYTES and not a placement decision. It does not know the store's clock and therefore does not know whether the advertisement has lapsed; hostTargetLiveness makes that decision, and it is the only thing that does.
type HostTargetEntry ¶
type HostTargetEntry struct {
Target HostTarget
Revision uint64
}
HostTargetEntry is one advertisement together with the revision a later compare-and-swap names.
It is returned to the HOST that owns the row — from a publish, a drain, and the reconciler's own bookkeeping — and deliberately not to a placement reader, which receives projections instead. See HostTargetPage.
The provider's immutable acceptance order is not exposed, for the reason CatalogEntry's is not: a row's position in a stream of advertisements is not a fact any caller acts on.
type HostTargetError ¶
type HostTargetError struct {
Code HostTargetErrorCode
Field string
Generation uint64
Revision uint64
Cause error
}
HostTargetError is a typed, redacted Host target directory failure. Field names the offending input or stage and never carries a provider name, a key, or a record payload.
Revision is populated only for HostTargetErrorConflict, carrying the value that is itself the answer, as the other record kinds do for that code.
Generation is populated only for HostTargetErrorGeneration, carrying the high-water mark that refused the write so a superseded incarnation learns it has been superseded rather than retrying forever.
There is deliberately no epoch member of any kind. A caller cannot obtain a lease epoch from this type because there is no lease epoch in this record to obtain.
func (*HostTargetError) Error ¶
func (e *HostTargetError) Error() string
func (*HostTargetError) Unwrap ¶
func (e *HostTargetError) Unwrap() error
type HostTargetErrorCode ¶
type HostTargetErrorCode string
HostTargetErrorCode classifies a Host target directory failure.
It is its own vocabulary rather than the registry's, and the reason is not merely that they are separate aggregates. It is that a caller MUST NOT be able to write one handler for both. A RegistryError is the public account of who is running a session; a HostTargetError is the public account of who might be able to take one. Sharing a type would let a caller branch on "expired" without knowing which of those two questions it had just asked, and the whole discipline of this record is that capacity is never authority.
The codes are the ones the ordinary vocabulary supplies, with three that need their reasons stated:
- Withdrawn — the row exists and offers no capacity. It is what a drain leaves and what the reconciler writes; it is not an error in a caller and it is not a claim about any session.
- Generation — the caller named a Host generation BELOW the row's committed high-water mark, so the write comes from a superseded incarnation of that same Host. It is deliberately NOT called Epoch: RegistryErrorEpoch means a lease has provably lost a SESSION, and a code sharing that name would invite a reader to believe this record fences ownership. It does not. See HostTarget.
- Cursor — a page token this store did not issue for this exact target. The walk restarts from the first page; nothing is wrong with the store.
Conflict means a lost revision compare-and-swap and nothing else — re-read and retry — which is the meaning it has for every other record kind here.
TWO ERROR TYPES REACH A CALLER OF THIS RECORD, and a consumer must handle both. Validating a target reports *InvalidIdentityError for AgentID, because that is what every identity derivation in this package reports for a sessionwire identity, while the opaque runtime id and the placement enum report *HostTargetError — so one malformed request surfaces as either type depending on which member is wrong. That is the package's convention rather than this record's choice, and it is written down here because this record is the first one a Host or a Factory calls.
const ( HostTargetErrorInvalid HostTargetErrorCode = "invalid" HostTargetErrorNotFound HostTargetErrorCode = "not_found" HostTargetErrorWithdrawn HostTargetErrorCode = "withdrawn" HostTargetErrorDeleted HostTargetErrorCode = "deleted" HostTargetErrorIdentity HostTargetErrorCode = "identity" HostTargetErrorGeneration HostTargetErrorCode = "generation" HostTargetErrorConflict HostTargetErrorCode = "conflict" HostTargetErrorCursor HostTargetErrorCode = "cursor" HostTargetErrorUnknown HostTargetErrorCode = "unknown" HostTargetErrorBackend HostTargetErrorCode = "backend" HostTargetErrorMalformed HostTargetErrorCode = "malformed" HostTargetErrorVersion HostTargetErrorCode = "version" HostTargetErrorTooLarge HostTargetErrorCode = "too_large" )
type HostTargetKey ¶
type HostTargetKey struct {
AgentID sessionwire.AgentID
RuntimeCompatibilityID string
Placement sessionwire.HostPlacement
}
HostTargetKey is the target half of an advertisement's identity: the (agent, runtime, placement) triple a Host offers capacity FOR.
It is a struct rather than three parameters because it is threaded through the derivation, the request types, and the page, and because the whole triple is what selects a provider scope. A caller that dropped one member would otherwise be selecting a different target while looking correct.
This is derived capacity, not a definition catalogue. Nothing here says an agent or a runtime EXISTS; it says a Host is currently willing to serve one.
type HostTargetPage ¶
type HostTargetPage struct {
Hosts []sessionwire.HostLinkCapacityReport
LapsedSkipped int
UnreadableSkipped int
NextCursor sessionwire.Cursor
}
HostTargetPage is one bounded page of live capacity for one target.
Hosts carries core's capacity reports rather than this package's entries, and that is the capacity/authority boundary made structural on the read side. A HostLinkCapacityReport has no tenant member, no session member, and no epoch member, so a Factory holding one cannot mistake it for a claim on anything — there is nothing in it to mistake. The revision a compare-and-swap needs goes to the HOST that owns the row, through publish and drain, and a placement reader has no business rewriting another process's advertisement.
TWO COUNTS say why a page is shorter than its limit, and neither is a diagnostic afterthought. A page that silently returned fewer entries would leave a caller unable to tell "this target has little capacity" from "this target is full of rows I passed over", and the two causes want different responses:
- LapsedSkipped counts rows whose heartbeat promise had already lapsed at the store's clock. Such a row is still RANKED, so it still occupies a position in every later page; a nonzero count means the directory is owed a ReconcileHostTargets pass, which is the only thing that clears it.
- UnreadableSkipped counts rows this build could not decode, or that disagreed with the filing they were found under.
THE RULE FOR AN UNREADABLE ROW IS STATED HERE AND NOWHERE ELSE, because a rule restated in five places is a rule that drifts in four of them. Such a row is skipped rather than FAILING THE PAGE, and that is load-bearing rather than lenient: nothing in this package ever rewrites a row it cannot read — a newer writer may have produced it — so the row is permanent, and a reader that failed the page on one would take every Host serving that target out of service for as long as it existed, with no recovery path anywhere in the system. Skipping keeps the newer writer's row untouched and starts publishing it the instant a reader that understands it asks; the count keeps the condition visible; and README.md records that a genuinely corrupt row has no in-band repair at all. ListDueGates and ListSessions obey the same rule for the same reason.
A page may therefore contain fewer than Limit entries while still issuing a continuation. A caller that wants a specific number of candidates pages until NextCursor is empty; it must not treat a short page as the end of the target.
type HostTargetReconcileResult ¶
type HostTargetReconcileResult struct {
Scanned int
Withdrawn int
StillLive int
Contended int
Unreadable int
Unverified int
Exhausted bool
NextCursor sessionwire.Cursor
}
HostTargetReconcileResult accounts for every row one sweep scanned. The five outcomes sum to Scanned, which is asserted rather than assumed: a sweep that silently dropped a row would otherwise look like a sweep that had nothing to do.
- Withdrawn — the row's stored expiry had genuinely lapsed and the sweep removed it from both views.
- StillLive — the row named by the due page had not actually lapsed when the sweep revalidated its stored expiry. The due view is weakly consistent and a Host may have heartbeated since; this is the count of rows the revalidation SAVED.
- Contended — the compare-and-swap lost to a concurrent write, or the row moved out from under it. Nothing was decided and a later sweep will see the row again if it is still lapsed.
- Unreadable — the row could not be decoded, or disagreed with the filing it was found under. The sweep steps over it and does NOT rewrite it, for the reason HostTargetPage gives. A nonzero count is an operator's signal, not a transient: nothing retires such a row.
- Unverified — the compare-and-swap COMMITTED and the provider's reply then failed this package's checks on it. The withdrawal is durable, so the row is handled and no later sweep will revisit it, but this sweep cannot say that it is: the reply it was given does not describe what it wrote. It is its own outcome rather than folded into Contended, which means the opposite — that nothing was decided.
Exhausted reports whether the sweep reached the end of the due view within its page budget. NextCursor is nonempty exactly when it did not, and a caller that wants the whole view hands it back — see ReconcileHostTargetsRequest for why that is a correctness property rather than a convenience.
type InboxEntry ¶
type InboxEntry struct {
Record InboxRecord
Revision uint64
AcceptedOrder uint64
}
InboxEntry is a command record together with the provider state a caller needs: the revision a later compare-and-swap names, and the immutable acceptance order.
AcceptedOrder is exposed, and CatalogEntry's order deliberately is not. The difference is that a catalog record's order means nothing to anyone — a session's position in a creation stream is not a fact any caller acts on — while a command's acceptance order is the durable arrival order of commands within a session, which consumers sort a bounded ListOrdered page by and which a retry must receive unchanged as evidence that it is the same acceptance.
It is an OPAQUE COMPARISON KEY and nothing else:
- It is strictly increasing within one session's order scope: once a command has order 12, no command in that session becomes newly observable at 11.
- It is NOT contiguous and NOT one-based. A provider may allocate from a JetStream stream sequence or a shared SQL sequence, so a session's first command can have order 5000 and its second 9000. Nothing may derive a count, a position, or "the next" order from it.
- It is not comparable across sessions or tenants. Two sessions' orders come from different scopes and may interleave arbitrarily.
func (InboxEntry) CommandStatus ¶
func (e InboxEntry) CommandStatus() (sessionwire.CommandStatus, error)
CommandStatus projects the durable record onto core's public command status.
The durable machine has five states and the public vocabulary has four, so the projection makes one semantic choice, and it is this: an UNCLAIMED pending command is accepted, while claimed and applying are pending.
Core's own words settle it. "Accepted means the inbox commit succeeded; it does not promise that a Host has already applied the command" — which is exactly and only what this store knows about a command nobody has picked up. Once a writer has claimed it, something more than the commit is true: the command is being worked on, and the public state that says so is pending. The alternative — reporting every non-terminal command as accepted — would make the public status say nothing that the acknowledgement of the original request had not already said, for the whole life of the command.
The claim's LIVENESS deliberately does not enter into it, and the reason is a property the mapping has and would otherwise lose: it is MONOTONE. No transition in this file writes a state that projects backwards — nothing returns a claimed or applying command to pending — so a public status never regresses from pending to accepted, and a caller polling one sees a sequence that only moves forward. A liveness-sensitive mapping would break exactly that: a command whose claim lapsed would report accepted again, and would flap between the two as claims were taken and expired, on a schedule that is a property of the clock rather than of the command.
It lives here rather than beside the record because the mapping is a statement about the machine, and it is offered here rather than left to each consumer because Factory, Host and any later reader answering "what happened to my command" must not each invent their own answer.
type InboxError ¶
type InboxError struct {
Code InboxErrorCode
Field string
Epoch uint64
Revision uint64
Cause error
}
InboxError is a typed, redacted command failure. Field names the offending input or stage and never carries a provider name, a key, or any part of the command's private payload.
Epoch is populated only for InboxErrorEpoch and Revision only for InboxErrorConflict, each carrying the value that is itself the answer, as CatalogError does for the same two codes.
func (*InboxError) Error ¶
func (e *InboxError) Error() string
func (*InboxError) Unwrap ¶
func (e *InboxError) Unwrap() error
type InboxErrorCode ¶
type InboxErrorCode string
InboxErrorCode classifies a durable command record failure.
It is the inbox's own vocabulary rather than the catalog's, and the reason is ownership. Gates reuse CatalogError because a gate IS catalog state: the projection lives in the catalog record and the deadline intent is an index into it, so a gate failure is a statement about that record. A command is a separate aggregate — its own namespace, its own record, its own identity, its own lifecycle — and admission never reads or writes a session's catalog record. A caller branching on an inbox failure should not have to match the catalog's type to learn that its command was not stored, and the command lifecycle's later states need failures the catalog has no business naming.
CommandMismatch is definite and caller-caused: one command id was reused for a DIFFERENT command, and the stored command is untouched. No retry helps — the caller must mint a new id or send the command it originally sent.
It is deliberately NOT called "conflict", and the omission is the point. This file already spells "conflict" two ways. CatalogErrorConflict means a lost revision compare-and-swap: recoverable, provider-caused, carrying the actual revision, and explicitly inviting a re-read and a retry. ObjectErrorConflict means a key already holding DIFFERENT CONTENT, which is the near-twin of what a reused command id is — so a reader who met that one first would reasonably expect "conflict" here and get the catalog's recovery advice instead.
The tie goes to the catalog's meaning because of what this record IS: an OrderedIndex row has a Revision, so the command transition machine will compare-and-swap it and will need a name for losing that race. The object aggregate never will. InboxErrorConflict therefore carries the catalog's meaning exactly — a lost revision compare-and-swap, recoverable, reporting the actual revision, inviting a re-read and a retry — and the caller-caused content case takes a name that cannot be mistaken for either neighbour.
Unknown means the mutation's outcome could not be resolved at all, so the caller learns nothing about what is stored and must retry the same identity to find out.
Identity means a stored record disagreed with the identity it was filed under or asked for. It is not a caller error and not a conflict: it means the provider's answer cannot be trusted, and no retry of the caller's fixes it.
The transition machine adds the rest, and each one names a DIFFERENT recovery so that a caller can branch without reading prose:
- NotFound — no command has ever been admitted under that identity. The caller is asking about something it never accepted.
- Conflict — the record moved under the caller. Re-read and decide again. Revision carries what the record is at now when the store could see it.
- Epoch — the caller named a lease epoch BELOW the epoch the record's claim was taken under. That lease has provably been superseded and must not retry under the same epoch; Epoch carries the committed high-water mark, as CatalogErrorEpoch does.
- ClaimHeld — a LIVE claim is held on this command and is not provably the caller's. It cannot say "someone else": a claim records the lease epoch it was taken under and no claimant identity, so two writers under one epoch are indistinguishable to it. That matters for the likeliest recipient, which is not a rival but a claimer meeting its OWN live claim: a claim cannot be renewed, so a caller that wants more time must enter applying before its claim lapses, and waiting for the claim to expire — the advice that fits a rival — is the one thing that caller must not do.
- ClaimLost — the caller does not hold the live claim the transition requires, and Field says which of the two situations it is. "lease_epoch" means the claim is held under another epoch, so the caller never held this command; "claim" means the caller's own claim lapsed. Both are answered by claiming again, which the apply deadline may no longer permit, but they read very differently to an operator: the first is a writer working on a command that is not its own, the second is a writer that was too slow.
ClaimHeld and ClaimLost are the pair the journal already spells for lease ownership, and they mean the corresponding two things here.
- Deadline — a NEW claim was attempted at or after the command's apply deadline. No retry helps: the command is now the deadline reconciler's, and the caller learns its answer by reading the terminal record.
- State — the record is in a state this transition has no edge out of, and the caller had a current revision when it asked. It is a caller mistake about the machine rather than a race.
- Evidence — the journal does not support the settlement asked for, and Field says which question it failed. "application" means the correlation did not establish that no effect committed — either one did, or the evidence is not readable — so the command must be finished or left alone rather than rejected. "result" means a recovering successor named a terminal result that is not the effect its prefix is correlated with. "applying_lease" means the lease holding the record's claim is not yet provably fenced out of the journal, so it could still commit the effect this settlement would orphan. None of the three is a race and none is answered by retrying the same call unchanged: the first two are permanent for the journal as it stands, and the third becomes settleable only once a later lease has opened the stream.
- Terminal — the command's outcome is already settled. It is separate from State because it is the one state failure that is PERMANENT and that carries an answer: a caller meeting it should read the record and report the outcome rather than re-deciding anything.
const ( InboxErrorInvalid InboxErrorCode = "invalid" InboxErrorCursor InboxErrorCode = "cursor" InboxErrorCommandMismatch InboxErrorCode = "command_mismatch" InboxErrorNotFound InboxErrorCode = "not_found" InboxErrorDeleted InboxErrorCode = "deleted" InboxErrorIdentity InboxErrorCode = "identity" InboxErrorConflict InboxErrorCode = "conflict" InboxErrorEpoch InboxErrorCode = "epoch" InboxErrorClaimHeld InboxErrorCode = "claim_held" InboxErrorClaimLost InboxErrorCode = "claim_lost" InboxErrorDeadline InboxErrorCode = "deadline" InboxErrorState InboxErrorCode = "state" InboxErrorEvidence InboxErrorCode = "evidence" InboxErrorTerminal InboxErrorCode = "terminal" InboxErrorUnknown InboxErrorCode = "unknown" InboxErrorBackend InboxErrorCode = "backend" InboxErrorMalformed InboxErrorCode = "malformed" InboxErrorVersion InboxErrorCode = "version" InboxErrorTooLarge InboxErrorCode = "too_large" )
type InboxRecord ¶
type InboxRecord struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
CommandID sessionwire.CommandID
RuntimeCommandID RuntimeCommandID
Kind CommandKind
Payload []byte
PayloadRef sessionwire.ObjectReference
AcceptedAt time.Time
ApplyDeadline time.Time
State InboxState
Claim CommandClaim
Result CommandResult
Rejection *sessionwire.ErrorDetail
}
InboxRecord is the authoritative durable record of one accepted command.
Payload and PayloadRef are PRIVATE: they are the command's body, they are never part of a public projection, and no failure this package returns carries either of them. At most one of the two is set — an inline body up to MaxInboxPayloadBytes, or a reference to an object persisted first — and neither is set for a command that has no body. They are also IMMUTABLE for the life of the record, as Kind is: emptying or rewriting one turns every later retry of the command into a permanent mismatch — see sameCommandAs.
The acceptance order is deliberately NOT a member here. It is allocated by the provider at Create and is therefore not part of the bytes this record encodes; it is reported on InboxEntry, where its origin is unambiguous.
type InboxState ¶
type InboxState string
InboxState is the durable processing state of one accepted command.
The transitions between them — including which writer may make each one, what a claim epoch fences, and what a terminal CAS must set — are inbox_claim.go's. Admission produces InboxStatePending and never anything else. What each state must and must not carry is validateInboxState, below, because that is a property of the stored record rather than of the operation that wrote it.
const ( InboxStatePending InboxState = "pending" InboxStateClaimed InboxState = "claimed" InboxStateApplying InboxState = "applying" InboxStateApplied InboxState = "applied" InboxStateRejected InboxState = "rejected" )
type InvalidBackendError ¶
type InvalidBackendError struct {
Component string
}
InvalidBackendError reports a storage component or required capability that was not wired at Open. Component is one of Composite, Ledger, Leaser, KV, OrderedIndex, Blobs, or BlobReaderLifecycle.
func (*InvalidBackendError) Error ¶
func (e *InvalidBackendError) Error() string
type InvalidBackgroundWorkError ¶
type InvalidBackgroundWorkError struct{}
InvalidBackgroundWorkError reports a nil internal background work function.
func (*InvalidBackgroundWorkError) Error ¶
func (*InvalidBackgroundWorkError) Error() string
type InvalidIdentityError ¶
InvalidIdentityError identifies which opaque identity failed validation without retaining or rendering its value.
func (*InvalidIdentityError) Error ¶
func (e *InvalidIdentityError) Error() string
func (*InvalidIdentityError) Unwrap ¶
func (e *InvalidIdentityError) Unwrap() error
type InvalidLimitError ¶
InvalidLimitError reports a limit outside its inclusive valid range.
func (*InvalidLimitError) Error ¶
func (e *InvalidLimitError) Error() string
type InvalidOptionError ¶
InvalidOptionError reports an invalid Open option. When Cause is non-nil it is preserved for errors.Is and errors.As.
func (*InvalidOptionError) Error ¶
func (e *InvalidOptionError) Error() string
func (*InvalidOptionError) Unwrap ¶
func (e *InvalidOptionError) Unwrap() error
type JournalError ¶
type JournalError struct {
Code JournalErrorCode
Field string
Epoch uint64
Cause error
}
JournalError is a typed, redacted journal failure. Field names the offending input or stage and never carries a provider name, key, or record payload. Epoch is populated only where a fencing epoch is itself the answer — the live holder's epoch for lease_held, this writer's epoch for lease_lost.
func (*JournalError) Error ¶
func (e *JournalError) Error() string
func (*JournalError) Unwrap ¶
func (e *JournalError) Unwrap() error
type JournalErrorCode ¶
type JournalErrorCode string
JournalErrorCode classifies a journal ownership, append, or read failure.
Fenced and Unknown are deliberately distinct outcomes of one CAS append. Fenced is definite — a successor's record occupies the contested sequence, so this writer has provably lost the stream. Unknown means the outcome could not be resolved at all, so the writer's own tip is no longer trustworthy. Both end the writer permanently; only Fenced asserts that someone else won.
const ( JournalErrorInvalid JournalErrorCode = "invalid" JournalErrorLeaseHeld JournalErrorCode = "lease_held" JournalErrorLeaseLost JournalErrorCode = "lease_lost" JournalErrorFenced JournalErrorCode = "fenced" JournalErrorUnknown JournalErrorCode = "unknown" JournalErrorClosed JournalErrorCode = "closed" JournalErrorBackend JournalErrorCode = "backend" JournalErrorIntegrity JournalErrorCode = "integrity" JournalErrorTooLarge JournalErrorCode = "too_large" JournalErrorCursor JournalErrorCode = "cursor" )
type JournalWriter ¶
type JournalWriter struct {
// contains filtered or unexported fields
}
JournalWriter is one epoch-fenced single-writer grant over a session's journal. It is safe for concurrent use; every append is serialized.
Ownership algorithm. OpenJournal acquires a lease grant, reads the tip exactly once, and appends an opening fence at precisely that tip stamped with the grant's epoch. If that CAS conflicts the grant is spent: OpenJournal releases the lease and returns a typed conflict, and the caller may acquire a fresh, strictly higher epoch and reopen. It deliberately does NOT refresh the tip and retry, because a retry loop lets a writer silently reorder itself behind records it never observed.
After a successful open the writer tracks only its own committed sequence and CASes every later append on it. It never re-reads the tip, so a successor's opening fence permanently fails this writer at its next append. An append whose outcome could not be resolved is equally terminal: rather than re-read and rebase onto whatever is now durable, the writer latches the failure and refuses every later append.
func (*JournalWriter) Append ¶
Append commits one record and returns its journal sequence.
The writer owns the fields that carry ownership: an opening fence may not be appended by a caller at all, and an application prefix must leave LeaseEpoch zero for the writer to stamp.
Stamping the epoch is ALL this file checks about a prefix. What a prefix may be appended for — which command, in what position relative to its effect, and never for a command claimed under a lower grant than this writer's — is the writer obligation stated in inbox_recovery.go, and it is unenforceable here: this writer has no view of the inbox. A prefix appended against it does not fail, it makes its command unrecoverable, so read that list before emitting one. A body above the overflow threshold is uploaded as an immutable object and verified before its reference is appended; if the append then fails the verified object is deliberately left behind as an orphan for garbage collection rather than deleted against a provider that has just proved unreliable.
A conflicting, unresolved, or ownership-lost append ends the writer: the failure is latched and every later Append returns it. A definite backend failure leaves the tracked tip untouched and does not latch, so the caller may retry the same record.
func (*JournalWriter) Close ¶
func (w *JournalWriter) Close(ctx context.Context) error
Close releases the lease grant and the Store admission. It is idempotent and refuses every later append.
func (*JournalWriter) Epoch ¶
func (w *JournalWriter) Epoch() uint64
Epoch returns the fencing epoch of this writer's lease grant.
func (*JournalWriter) Sequence ¶
func (w *JournalWriter) Sequence() uint64
Sequence returns the last journal sequence this writer committed, starting at its own opening fence. It is never refreshed from the provider.
type KeyspaceError ¶
type KeyspaceError struct {
Code KeyspaceErrorCode
Cause error
}
KeyspaceError reports a fail-closed layout or physical-key failure. Cause is available to errors.Is/As, while Error deliberately omits provider and raw ID details.
func (*KeyspaceError) Error ¶
func (e *KeyspaceError) Error() string
func (*KeyspaceError) Unwrap ¶
func (e *KeyspaceError) Unwrap() error
type KeyspaceErrorCode ¶
type KeyspaceErrorCode string
KeyspaceErrorCode is a stable machine-readable keyspace failure class.
const ( KeyspaceBackend KeyspaceErrorCode = "backend" KeyspaceMarkerMalformed KeyspaceErrorCode = "marker_malformed" KeyspaceLayoutMismatch KeyspaceErrorCode = "layout_mismatch" KeyspaceMarkerAmbiguous KeyspaceErrorCode = "marker_ambiguous" KeyspaceBindingNotFound KeyspaceErrorCode = "binding_not_found" KeyspaceBindingAmbiguous KeyspaceErrorCode = "binding_ambiguous" KeyspaceScopeInvalid KeyspaceErrorCode = "scope_invalid" KeyspaceHashCollision KeyspaceErrorCode = "hash_collision" KeyspaceLegacyTenant KeyspaceErrorCode = "legacy_tenant" KeyspaceLegacySession KeyspaceErrorCode = "legacy_session" )
type Limits ¶
type Limits struct {
MaxPageSize int
}
Limits contains Store-wide ceilings. MaxPageSize bounds every provider page requested by SessionStore; individual operations may request a smaller page.
func DefaultLimits ¶
func DefaultLimits() Limits
DefaultLimits returns bounded defaults for provider queries.
type ListCompatibleHostsRequest ¶
type ListCompatibleHostsRequest struct {
Key HostTargetKey
// Cursor is a token a previous page of THIS target issued. It is opaque:
// retain it and hand it back, but do not parse it or derive ordering,
// identity, or authority from it. Possessing one authorizes nothing, and a
// cursor this store did not issue for this target is refused with
// HostTargetErrorCursor, which means the walk restarts from the first page
// rather than that anything is wrong with the store.
Cursor sessionwire.Cursor
// Limit is the page's record ceiling. Zero means the store's configured
// page size. It bounds the rows the PROVIDER returns, not the reports this
// page publishes; see HostTargetPage.
Limit int
}
ListCompatibleHostsRequest positions one bounded page of the Hosts currently offering capacity for one target, most free capacity first.
type ListDueCommandsRequest ¶
type ListDueCommandsRequest struct {
Shard int
DueAtOrBefore time.Time
Limit int
Cursor sessionwire.Cursor
}
ListDueCommandsRequest positions one bounded page of one shard's outstanding commands.
Shard names the control shard to read and must be below the store's ControlShards. A caller sweeps by visiting every shard round-robin; the store deliberately does not do that for it, because a replica that swept every shard in one call would hold the whole deployment's reconciliation in one request's latency and one caller's failure.
DueAtOrBefore is the inclusive wall-clock bound and is the FIRST page's query. A continuation carries its own bound, so a resumed request must leave this zero: presenting both would be two answers to one question, and silently preferring either is how a resumed sweep starts querying a bound it was never bound to.
type ListDueGatesRequest ¶
type ListDueGatesRequest struct {
// Shard names the control shard to read and must be below the store's
// ControlShards. A caller sweeps by visiting every shard round-robin.
Shard int
// DueAtOrBefore is the inclusive wall-clock bound and is the FIRST page's
// query. A continuation carries its own bound, so a resumed request must
// leave this zero; see dueGatePosition.
DueAtOrBefore time.Time
// Limit is the page's record ceiling. Zero means the store's configured
// page size.
Limit int
// Cursor resumes a sweep of this shard from the position a previous page
// ended at. It is opaque and is bound to this cursor kind and this shard.
Cursor sessionwire.Cursor
}
ListDueGatesRequest positions one bounded page of one control shard's gates whose deadline has passed. It is cross-tenant rather than tenant-scoped, because the due view is: see gateNamespace, and see shards.go for what a shard is and for what "service-only" does and does not mean here.
type ListSessionsRequest ¶
type ListSessionsRequest struct {
TenantID sessionwire.TenantID
// Cursor is a token a previous page of THIS tenant issued. It is opaque:
// retain it and hand it back, but do not parse it or derive ordering,
// tenancy, or authority from it. Possessing one authorizes nothing — a
// caller must authorize TenantID on its own — and a cursor this store did
// not issue for this tenant is refused with CatalogErrorCursor, which
// means the walk restarts from the first page rather than that anything is
// wrong with the store.
Cursor sessionwire.Cursor
// Limit is the page's record ceiling. Zero means the store's configured
// page size.
Limit int
}
ListSessionsRequest positions one bounded recent-first page of a tenant's sessions. Cursor is a token a previous page issued; Limit is that page's record ceiling, and zero means the store's configured page size.
type ObjectError ¶
type ObjectError struct {
Code ObjectErrorCode
Field string
Cause error
}
ObjectError is a typed, redacted object operation failure. Field names the offending input or stage and never carries a provider path, key, or payload.
Terminating an object stream can produce more than one failure at once — the cause that ended the stream, a read error observed by a racing reader, and a provider Close error — and those are reported as an errors.Join tree, so a returned error may contain several *ObjectError values. The FIRST one found by errors.As is the primary classification: it is the failure that caused termination, and later ones are subsidiary consequences of it. A caller that genuinely needs every code can walk the tree itself through the `Unwrap() []error` that errors.Join returns; the package deliberately does not export a set extractor, because classifying on the primary cause is the supported contract and an exported extractor would freeze the join shape.
func (*ObjectError) Error ¶
func (e *ObjectError) Error() string
func (*ObjectError) Unwrap ¶
func (e *ObjectError) Unwrap() error
type ObjectErrorCode ¶
type ObjectErrorCode string
ObjectErrorCode classifies redacted object operation failures.
Digest and Integrity are deliberately distinct: Digest means the caller's own metadata is self-inconsistent (its Digest field disagrees with the digest inside its ObjectID), while Integrity means bytes or a stored key did not match what the object identity promised.
const ( ObjectErrorInvalid ObjectErrorCode = "invalid" ObjectErrorSize ObjectErrorCode = "size" ObjectErrorDigest ObjectErrorCode = "digest" ObjectErrorSource ObjectErrorCode = "source" ObjectErrorBackend ObjectErrorCode = "backend" ObjectErrorConflict ObjectErrorCode = "conflict" ObjectErrorIntegrity ObjectErrorCode = "integrity" ObjectErrorCanceled ObjectErrorCode = "canceled" // bytes may still exist (for example, objects written before the index). ObjectErrorMetadataUnavailable ObjectErrorCode = "metadata_unavailable" )
type ObjectKind ¶
type ObjectKind string
ObjectKind is a closed semantic class for immutable session objects.
const ( ObjectKindJournalPublic ObjectKind = "journal-public" ObjectKindJournalRuntime ObjectKind = "journal-runtime" ObjectKindCommandPayload ObjectKind = "command-payload" ObjectKindToolResult ObjectKind = "tool-result" ObjectKindWorkspaceCheckpoint ObjectKind = "workspace-checkpoint" ObjectKindRuntimeCheckpoint ObjectKind = "runtime-checkpoint" ObjectKindArtifact ObjectKind = "artifact" ObjectKindAttachment ObjectKind = "attachment" ObjectKindContinuation ObjectKind = "continuation" ObjectKindRuntimeObject ObjectKind = "runtime-object" )
type OpenGateRequest ¶
type OpenGateRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
LeaseEpoch uint64
Gate sessionwire.GateProjection
}
OpenGateRequest projects one gate as publicly open and records its absolute deadline. LeaseEpoch is the writing Host's grant epoch, compared against the record's committed high-water mark exactly as UpdateCatalogHostState's is: open gates are Host-owned state.
Gate.OpenedJournalSeq must name an event at or below the record's durable journal tip. A gate whose opening event is not yet durable is refused rather than stored, because a reader would otherwise be handed a page claiming an event its own tip says does not exist.
type OpenJournalRequest ¶
type OpenJournalRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
}
OpenJournalRequest names the session whose stream a writer wants to own.
type Option ¶
type Option func(*config) error
Option configures Open.
func WithClock ¶
WithClock supplies the clock used by Store.
Only the clock VALUE is validated, and only for being non-nil. Nothing checks what it returns: there is no monotonicity requirement, no bound, and no comparison against the machine's own clock. That is a deliberate limit on what this package claims, and it has a consequence worth stating where the clock is supplied rather than leaving it to be discovered from whichever guard survived it.
The rule the package holds itself to instead is that a guard is a function of the RECORD, never of the clock alone. A stored record is validated against rankableTime, which bounds the instants a record may CARRY; it says nothing about a clock, so a predicate that compares against an absent or zero instant has to be total on its own — see claimLive in inbox_claim.go, whose zero-claim conjunct exists for exactly that reason.
What a wrong clock costs is therefore LIVENESS rather than safety. A clock running slow leaves claims looking live and deadlines looking distant, so work waits; one running fast expires claims early, so work is redone. Neither puts two writers on one command, because ownership is decided by the lease epoch and by the record's revision, and neither of those is a clock reading.
func WithControlShards ¶
WithControlShards names the number of service-control shards outstanding work is spread across.
IT IS NOT A RUNTIME SETTING, and the option is where that has to be said, because the name reads like one. The count is an input to controlShardOf, so it decides the namespace every inbox command and every gate deadline intent is FILED IN. Open persists it in the backend's layout marker and refuses a later Open of the same backend that names a different one; changing it for a backend that already holds records is an offline migration that must move them, not a redeploy with a new flag.
A larger count spreads a sweep across more replicas and makes any one shard's due page shorter. It is not free: a sweep visits every shard, so the count is a floor on the provider queries one pass costs even when nothing is due.
func WithIOProviderOwnership ¶
WithIOProviderOwnership explicitly transfers ownership of a provider whose released lifecycle contract is the standard io.Closer shape. Because io.Closer has no context, ShutdownTimeout can release the Store lifecycle but cannot force the underlying Close to return; its adapter goroutine may outlive the Store until the provider eventually returns. Transfer takes effect only after Open succeeds; a failed Open never closes the provider.
func WithLegacySingleTenant ¶
func WithLegacySingleTenant(defaultTenant sessionwire.TenantID) Option
WithLegacySingleTenant explicitly adopts the historical unscoped layout for one tenant. It never probes for legacy data; Open atomically persists the choice and exact tenant in the backend layout marker.
func WithLogger ¶
WithLogger supplies the structured logger used by Store.
func WithProviderOwnership ¶
func WithProviderOwnership(closer ProviderCloser) Option
WithProviderOwnership explicitly transfers provider lifecycle ownership to Store after Open succeeds. A failed Open leaves the provider caller-owned and never closes it. Without this option Close never closes caller-supplied storage.
func WithShutdownTimeout ¶
WithShutdownTimeout bounds explicitly owned provider cleanup after Store background work drains.
type PlacementIntent ¶
type PlacementIntent struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
AgentID sessionwire.AgentID
RuntimeCompatibilityID string
Placement sessionwire.HostPlacement
Workload DesiredWorkload
Generation uint64
}
PlacementIntent is the complete Factory-authored answer to "what should exist for this session", and nothing else.
COMPLETE AS TO AUTHORSHIP, NOT AS TO SUFFICIENCY, and the difference decides whether a controller acting on one alone is correct. This is the only one of the record's three projections that drops State — Summary and Status both carry it — so an intent cannot say whether the session it describes is still alive. A controller holding only this could create a dedicated workload for a session that has ended. Read CatalogRecord.State, or Status(), from the SAME entry: one read of one record answers both questions, and taking them from one entry is what makes the pair consistent.
Excluding the observation is nonetheless right, and is this type's whole discipline — an intent that carried liveness would be a request and a fact in one value, and the next reader would not know which half it was acting on.
It is what a placement controller reads, and its shape is the reason it exists as a type rather than as a handful of catalog members a caller picks out. Every member here is a REQUEST. There is no lease epoch, no HostID, no endpoint, no residency and no journal position, so a consumer cannot read an observation out of it and cannot be one refactor away from treating a request as a fact — the Host registry's tuple is where observed placement lives, and it is fenced by an epoch this intent structurally cannot name.
Generation is what a controller records against the workload it created. A controller that reconciled generation 7 and reads 7 again has nothing to do, however many times the record has been rewritten in between.
type PointerError ¶
type PointerError struct {
Code PointerErrorCode
Field string
Epoch uint64
Sequence uint64
Revision uint64
Cause error
}
PointerError is a typed, redacted object pointer failure. Field names the offending input or stage and never carries a provider name, a key, or a record payload.
Revision is populated only for PointerErrorConflict, carrying the value that is itself the answer, as the other record kinds do for that code.
Epoch and Sequence are the two high-water marks the record retains, and they are populated by exactly the three codes that refuse a write or withhold a target: Epoch, Sequence and Cleared. Disclosing them is not a courtesy. They are the only durable facts a caller's next attempt has to satisfy, nothing else in this package reads them out, and a cleared pointer in particular hands back no record at all — so withholding them would leave probing by rejected write as a caller's only way to learn what it must name. The registry discloses its epoch on its two no-route codes for the same reason.
A DELIBERATE ASYMMETRY WITH THE TARGET: neither the refusals nor the cleared report names the object that IS stored. A caller learns what it must beat, never what it lost to.
func (*PointerError) Error ¶
func (e *PointerError) Error() string
func (*PointerError) Unwrap ¶
func (e *PointerError) Unwrap() error
type PointerErrorCode ¶
type PointerErrorCode string
PointerErrorCode classifies an object pointer failure.
It is its own vocabulary rather than the catalog's, and the reason is the one this whole record exists for: the catalog carries a checkpoint SUMMARY that a projection write replaces wholesale, and this record carries the authoritative name. A caller that could handle both with one code set would be one refactor away from treating a failure to write the copy as a failure to write the truth, or the reverse.
The codes that need their reasons stated:
- Cleared — the pointer exists and names nothing. It is not an error in the caller and not an absence: the record is there, it holds both high-water marks, and it is those marks the failure carries. Reporting it as NotFound would tell a caller that a session had never had this pointer, which licenses a first write at any epoch and any sequence.
- Epoch — the request names a strictly lower lease epoch than the record's committed high-water mark, so the caller has provably lost the session.
- Sequence — the epoch was accepted and the target is OLDER than the one already named. Distinct from Epoch because the caller's authority is not in question and retrying will not help: what it holds is stale.
- NotFound — no pointer record of this kind exists at all. Distinct from Cleared for the reason above, and the distinction is what stops a clear from being confused with a session that has never checkpointed.
Conflict means a lost revision compare-and-swap and nothing else — re-read and retry — which is the meaning it has for every other record kind here.
const ( PointerErrorInvalid PointerErrorCode = "invalid" PointerErrorNotFound PointerErrorCode = "not_found" PointerErrorCleared PointerErrorCode = "cleared" PointerErrorEpoch PointerErrorCode = "epoch" PointerErrorSequence PointerErrorCode = "sequence" PointerErrorDeleted PointerErrorCode = "deleted" PointerErrorIdentity PointerErrorCode = "identity" PointerErrorConflict PointerErrorCode = "conflict" PointerErrorUnknown PointerErrorCode = "unknown" PointerErrorBackend PointerErrorCode = "backend" PointerErrorMalformed PointerErrorCode = "malformed" PointerErrorVersion PointerErrorCode = "version" PointerErrorTooLarge PointerErrorCode = "too_large" )
type ProtocolMode ¶ added in v0.4.0
type ProtocolMode string
ProtocolMode identifies a session's immutable dispatch protocol. Record upgrades do not convert this mode; conversion requires an offline migration.
const ( // ProtocolModeLegacy uses the released single-store epoch protocol. ProtocolModeLegacy ProtocolMode = "legacy" // ProtocolModeDisposition reserves the independent ownership/settlement // protocol. Its execution APIs are not implemented by this prerequisite. ProtocolModeDisposition ProtocolMode = "disposition" )
type ProviderCloser ¶
ProviderCloser is the lifecycle boundary for a provider whose ownership is explicitly transferred to Store.
type PublishHostTargetRequest ¶
type PublishHostTargetRequest struct {
Key HostTargetKey
HostID sessionwire.HostID
HostGeneration uint64
ObservedAt time.Time
Advertisement HostAdvertisement
}
PublishHostTargetRequest advertises one Host's current capacity for one target. It is also the HEARTBEAT: a Host republishes on its own cadence and this one operation moves the stored value, the rank, and the due time in a single compare-and-swap.
It carries a HostAdvertisement by VALUE, which is what makes "publish" and "drain" two different operations rather than one operation with a nil argument. A caller cannot accidentally remove its own capacity from every placement page by forgetting to set a member, and the withdrawn row has exactly two writers: DrainHostTarget and ReconcileHostTargets.
ObservedAt and the advertisement's ExpiresAt are the Host's own clock readings, as every other timestamp this package stores is. ExpiresAt is the promise the Host makes about its next heartbeat, so it must lie in the store's future and within MaxHostTargetTTL of it.
There is no expected revision. An advertisement is not a decision a caller makes about a row it has read — it is the current truth about one process's spare capacity — and the write is closed against the revision this store reads for itself. The generation is what establishes the right to write at all, and it establishes nothing else; see HostTarget.
type PutHostRegistrationRequest ¶
type PutHostRegistrationRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
LeaseEpoch uint64
ObservedAt time.Time
ExpiresAt time.Time
Route HostRoute
}
PutHostRegistrationRequest publishes the current observed route to one session, creating the registration if no Host has ever registered it.
It carries a HostRoute by VALUE, which is what makes "publish" and "release" two different operations rather than one operation with a nil argument. A caller cannot accidentally erase a session's route by forgetting to set a member, and the tombstone has exactly one writer: ClearHostRegistration.
ObservedAt and ExpiresAt are the Host's own clock readings, as every other timestamp this package stores is. ExpiresAt is the promise the Host makes about its next heartbeat, so it must lie in the store's future and within MaxHostRegistrationTTL of it.
There is no expected revision. A registration is not a decision a caller makes about a record it has read — it is the current truth about where the session is running — and the write is closed against the revision this store reads for itself, exactly as UpdateCatalogHostState is. The epoch is what establishes the right to write at all.
type PutObjectRequest ¶
type PutObjectRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
Kind ObjectKind
SizeBytes uint64
SHA256 [32]byte
MediaType string
Body io.Reader
}
PutObjectRequest declares an immutable object's exact content properties. MediaType is optional, bounded, validated descriptive metadata; it is untrusted and does not participate in object identity.
SizeBytes is the exact byte length of Body, not a hint: a body that ends early or runs long is rejected. SessionStore imposes no ceiling on it, by design — the effective bound is whatever the storage provider accepts. Verification streams through a fixed buffer and nothing here allocates in proportion to SizeBytes, so a large declared size costs a rejected write, not memory.
type ReadGatesRequest ¶
type ReadGatesRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
}
ReadGatesRequest reads one session's open public gates.
It has no cursor and no limit. The catalog holds at most MaxCatalogOpenGates gates for a session, so the whole answer is bounded by construction and is read from one record; a continuation would be a token that could never be issued. A future API that pages a larger open-gate set is what GatePage.OpenGateCount is reserved for.
type ReadPublicJournalRequest ¶
type ReadPublicJournalRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
FromSeq uint64
// Tail starts within the last Limit sequence positions at the tip captured
// by this read. Limit zero uses the store's configured page size. Private
// records occupy positions without returning events, so a tail may contain
// fewer events than Limit. The byte budget can shorten the page further;
// its continuation cursor remains pinned to the same captured tip.
Tail bool
// ScanLimit bounds examined journal records, including withheld private
// records. Zero preserves the unbounded-by-records legacy scan; a positive
// value must not exceed storage.MaxOrderedPageLimit. Exhaustion may return
// an empty event page with advanced coverage and a continuation cursor.
// Supply this budget again on each continuation; it is not part of the
// cursor. Event and byte limits may stop earlier, deferring the next event
// without counting it as covered. This bounds record work, not total bytes
// fetched to resolve the public bodies of the examined records.
ScanLimit int
// Cursor is a token a previous page of THIS session issued. It is opaque:
// retain it and hand it back, but do not parse it or derive position,
// tenancy, or authority from it. Possessing one authorizes nothing — a
// caller must authorize TenantID and SessionID on its own — and a cursor
// this store did not issue for this session and this projection is refused
// with JournalErrorCursor, which means the walk restarts rather than that
// anything is wrong with the journal.
Cursor sessionwire.Cursor
Limit int
}
ReadPublicJournalRequest positions one bounded public journal page. FromSeq (inclusive, zero meaning the first record), Cursor and Tail are mutually exclusive. Continue a page with its returned Cursor and Tail false.
type ReadRuntimeJournalRequest ¶
type ReadRuntimeJournalRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
FromSeq uint64
// Cursor carries the same rules as ReadPublicJournalRequest.Cursor, and the
// two are not interchangeable: a public token presented here, or a runtime
// token presented to the public read, is refused.
Cursor sessionwire.Cursor
Limit int
}
ReadRuntimeJournalRequest positions one bounded privileged replay page. Its FromSeq and Cursor positioning rules match ReadPublicJournalRequest; runtime replay does not offer tail positioning.
type ReconcileError ¶
type ReconcileError struct {
Code ReconcileErrorCode
Field string
ExpiresAt time.Time
Revision uint64
Cause error
}
ReconcileError is a typed, redacted reconciliation claim failure. Field names the offending input or stage and never carries a provider name, a key, or a record payload.
Revision is populated only for ReconcileErrorConflict, carrying the value that is itself the answer, as the other record kinds do for that code.
ExpiresAt is populated only for ReconcileErrorHeld, and it is the whole reason the code is useful: a replica told only "someone else has it" can do nothing but poll, while one told when the claim lapses can wait exactly that long. It deliberately does NOT disclose the holder — knowing which replica is working is not a fact any decision here turns on.
func (*ReconcileError) Error ¶
func (e *ReconcileError) Error() string
func (*ReconcileError) Unwrap ¶
func (e *ReconcileError) Unwrap() error
type ReconcileErrorCode ¶
type ReconcileErrorCode string
ReconcileErrorCode classifies a reconciliation claim failure.
It is its own vocabulary rather than the registry's or the catalog's, and for the reason HostTargetErrorCode is: a caller must not be able to write one handler for both. A RegistryError is the public account of who OWNS a session; a ReconcileError is the public account of who is currently doing scaling work for one. Sharing a type would let a caller branch on "held" without knowing which of those two questions it had asked, and the whole discipline of this record is that a claim is not ownership.
The codes that need their reasons stated:
- Held — another holder's claim is LIVE. It is not a failure in the caller and says nothing about any session's lease; it means the work is already being done, so do it later or not at all. ExpiresAt carries the horizon.
- Lapsed — no live claim is present. GetReconciliationClaim reports it against expires_at, meaning the stored claim has run out; a release reports it against holder_id, meaning the claim on this session is not the caller's and has run out, so there is nothing of the caller's to release. Both say the same thing about the world and differ in which of the caller's assumptions was wrong, which is why the field distinguishes them rather than a fourth code.
- NotFound — no claim record exists at all. Distinct from Lapsed because a session nobody has ever reconciled and one whose reconciler crashed are different operational facts, and identical to a caller that only wants to know whether it may proceed.
Conflict means a lost revision compare-and-swap and nothing else — re-read and retry — which is the meaning it has for every other record kind here.
There is deliberately no epoch code and no epoch member. A caller cannot obtain a lease epoch from this vocabulary because there is no lease epoch in this record to obtain.
const ( ReconcileErrorInvalid ReconcileErrorCode = "invalid" ReconcileErrorNotFound ReconcileErrorCode = "not_found" ReconcileErrorHeld ReconcileErrorCode = "held" ReconcileErrorLapsed ReconcileErrorCode = "lapsed" ReconcileErrorDeleted ReconcileErrorCode = "deleted" ReconcileErrorIdentity ReconcileErrorCode = "identity" ReconcileErrorConflict ReconcileErrorCode = "conflict" ReconcileErrorUnknown ReconcileErrorCode = "unknown" ReconcileErrorBackend ReconcileErrorCode = "backend" ReconcileErrorMalformed ReconcileErrorCode = "malformed" ReconcileErrorVersion ReconcileErrorCode = "version" ReconcileErrorTooLarge ReconcileErrorCode = "too_large" )
type ReconcileHostTargetsRequest ¶
type ReconcileHostTargetsRequest struct {
Limit int
MaxPages int
// Cursor resumes a sweep that ran out of page budget. It is opaque: retain
// it and hand it back, but do not parse it. Possessing one authorizes
// nothing — this operation is service-owned and a caller must establish
// that on its own — and a token this store did not issue for a sweep is
// refused with HostTargetErrorCursor.
//
// Resuming is not an optimization; see DefaultHostTargetReconcilePages for
// why a page budget alone leaves this sweep able to make no progress at
// all.
Cursor sessionwire.Cursor
}
ReconcileHostTargetsRequest bounds one service-owned sweep of the directory's deadline view. Zero means the default in Limit and MaxPages.
type ReconciliationClaim ¶
type ReconciliationClaim struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
HolderID string
ClaimedAt time.Time
ExpiresAt time.Time
}
ReconciliationClaim is the durable record of which Factory replica is currently doing reconciliation work for one session, and until when.
HolderID names the replica, opaquely. It is the only thing a later write is compared against, and it is not authority: two replicas that chose the same holder string are indistinguishable here, which costs exactly the duplicate work this record exists to reduce and costs nothing else.
ClaimedAt is the store's own clock reading at the moment the claim was accepted; ExpiresAt is the holder's promise about when it will be finished. The two are stamped from different clocks on purpose. A caller-supplied claim instant could be placed after its own expiry, or before a takeover that has already happened, and nothing could tell; the horizon must be the caller's because only the caller knows how long its work takes, and it is bounded above for that reason.
WHICH CLOCK, AND WHAT THAT DOES NOT BUY. Taking the claim instant from the store rather than the request removes a degree of freedom from the REQUEST; it does not make the instant authoritative. The clock is caller-injected and unchecked (see WithClock), and a Factory replica embeds its own Store, so s.clock is that replica's clock one level down. Liveness is therefore evaluated against the READING replica's clock and is skew-relative in both directions: a slow replica sees another's claim live longer than its holder meant, and a fast one takes it over early. There is no shared time here and none is available.
That is affordable for the reason this file's header gives and for no other: the claim licenses nothing, so the worst either direction produces is duplicated or delayed work that was already safe to do concurrently. It would NOT be affordable for a record whose expiry decided who may write, which is why the Host lease is a lease and this is not one.
A claim whose expiry EQUALS its claim instant has lapsed on arrival, which is exactly what ReleaseReconciliationClaim writes. That is not a second state needing a marker of its own: "released" and "expired" are the same fact to every reader — no live claim — and giving them one spelling means no reader has to know which one it is looking at.
type ReconciliationClaimEntry ¶
type ReconciliationClaimEntry struct {
Claim ReconciliationClaim
Revision uint64
}
ReconciliationClaimEntry is a claim together with the revision a later compare-and-swap names.
The provider's immutable acceptance order is deliberately not exposed, for the reason HostRegistrationEntry's is not: a session's position in a stream of claims is not a fact any caller acts on.
type RegistryError ¶
type RegistryError struct {
Code RegistryErrorCode
Field string
Epoch uint64
Revision uint64
Cause error
}
RegistryError is a typed, redacted Host registration failure. Field names the offending input or stage and never carries a provider name, a key, or a record payload.
Revision is populated only for RegistryErrorConflict, carrying the value that is itself the answer, as CatalogError and InboxError do for that code.
Epoch is populated more widely than the catalog's and the inbox's, and the extra two codes are the point rather than an inconsistency. For RegistryErrorEpoch it is the high-water mark that refused the write, as it is there. For RegistryErrorExpired and RegistryErrorReleased it is the fence the retained record still carries — because those two codes are the whole public account of a session that has no route, they are what a retention sweep acts on, and this package offers no other way to observe a registration's epoch. Without it a caller's only route to the record's most consequential permanent state would be to attempt a write it expects to fail and read the refusal. RegistryErrorNotFound carries no epoch: there is no record and so no fence, and a zero there means exactly that.
func (*RegistryError) Error ¶
func (e *RegistryError) Error() string
func (*RegistryError) Unwrap ¶
func (e *RegistryError) Unwrap() error
type RegistryErrorCode ¶
type RegistryErrorCode string
RegistryErrorCode classifies a Host registration failure.
It is the registry's own vocabulary rather than the catalog's, and the reason is the one InboxErrorCode gives. Gates reuse CatalogError because a gate IS catalog state. A registration is a separate aggregate: its own namespace, its own record, its own identity, its own fencing high-water mark, and — unlike every other record in this package — its own LIFETIME, because it expires while nothing else here does. Its writes never read or write a session's catalog record, and a caller branching on "there is no live route" should not have to match the type that reports "there is no such session".
The three ways a registration can fail to be a route are deliberately separate codes, and none of them is an error in the caller:
- NotFound — no registration has ever been written for this session. No Host has ever held it, or none has ever reported holding it.
- Expired — a registration exists and names a Host, but its expiry has passed. The Host may be alive and merely slow to heartbeat, or it may be gone; this record cannot tell the difference and neither may its reader.
- Released — a registration exists and is the tombstone a graceful shutdown left behind. The Host that held the session let it go on purpose.
A ROUTER MUST TREAT ALL THREE ALIKE: none of them is a route, and the difference between them is diagnostic. They are separate rather than collapsed into NotFound because of what an undifferentiated "absent" invites. A future writer that reads a registration, sees "not found", and creates a fresh record has just dropped the fencing high-water mark of whatever was really there — which is precisely the write the epoch fence exists to refuse. Nothing in this package reaches a write path through a reader that reports these codes, and the codes being distinct is what makes a future one that tries to look wrong rather than plausible.
The rest name the same failures the catalog's and the inbox's codes do:
- Invalid — a caller mistake in the request or a stored record that no longer satisfies its own rules.
- Deleted — the provider holds a TOMBSTONE for this identity. This package never deletes a registration, so it means the fencing high-water mark has been physically destroyed by something outside it; it is reported and never worked around, because the alternative is admitting a write from a lease that has already lost the session.
- Identity — a stored record disagreed with the identity it was filed under or asked for. It is not a caller error and no retry fixes it.
- Epoch — the caller named a lease epoch BELOW the record's committed high-water mark. That lease has provably been superseded and must not retry under the same epoch; Epoch carries the high-water mark, as CatalogErrorEpoch does.
- Conflict — a compare-and-swap lost a race on the record's revision, with no statement about ownership. Re-read and retry. Revision carries what the record is at now when the store could see it.
- Unknown — the mutation's outcome could not be resolved at all.
const ( RegistryErrorInvalid RegistryErrorCode = "invalid" RegistryErrorNotFound RegistryErrorCode = "not_found" RegistryErrorExpired RegistryErrorCode = "expired" RegistryErrorReleased RegistryErrorCode = "released" RegistryErrorDeleted RegistryErrorCode = "deleted" RegistryErrorIdentity RegistryErrorCode = "identity" RegistryErrorEpoch RegistryErrorCode = "epoch" RegistryErrorConflict RegistryErrorCode = "conflict" RegistryErrorUnknown RegistryErrorCode = "unknown" RegistryErrorBackend RegistryErrorCode = "backend" RegistryErrorMalformed RegistryErrorCode = "malformed" RegistryErrorVersion RegistryErrorCode = "version" RegistryErrorTooLarge RegistryErrorCode = "too_large" )
type RejectCommandRequest ¶
type RejectCommandRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
CommandID sessionwire.CommandID
ExpectedRevision uint64
LeaseEpoch uint64
Rejection sessionwire.ErrorDetail
}
RejectCommandRequest records the terminal typed rejection of a command.
LeaseEpoch is OPTIONAL here and required everywhere else in this file, and the asymmetry is the deadline reconciler's. Claiming, applying, and completing are things a session's lease holder does; rejecting is also what a Factory replica does to a command that has run out of deadline, and such a replica may be reconciling a session that has never had a lease at all. A zero epoch is therefore the honest statement "I am not acting under a session lease", and it buys exactly the authority the state machine grants that caller: it may settle a command that nobody is working on, and nothing else.
A NONZERO epoch here is a consistency check on a view the caller asserts, not an authority boundary. Nothing forces a caller to name one — a superseded Host obtains the reconciler's authority simply by passing zero — so the fence cannot be what keeps a stale lease out. What keeps it out is the claim rule below it, which is a property of the RECORD and applies identically at every epoch: a live claim admits only its own, and an applying record admits nobody. The fence's job is narrower and still worth doing: a caller that volunteers an epoch below the record's high-water mark is telling the store its view of the session is stale, and is told so rather than acting on it.
Rejection is a value rather than a pointer because a rejection without a reason is not a state this record has. It is validated as a public projection, so it carries a stable typed cause rather than a provider or runtime message.
type ReleaseReconciliationClaimRequest ¶
type ReleaseReconciliationClaimRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
HolderID string
}
ReleaseReconciliationClaimRequest gives one session's claim back early.
It carries no timestamp, and that is deliberate rather than an omission, for the reason ClearHostRegistrationRequest carries none: a released claim's instants record that THIS STORE released it, and a caller-supplied one could place the release in the future, producing a record that reads as released by intent and as live by time.
type RemnantGateIntent ¶
type RemnantGateIntent struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
GateID sessionwire.GateID
Revision uint64
}
RemnantGateIntent is one deadline intent whose gate the session's durable record does not project as open, together with the revision a retirement names.
It is reported rather than acted on, and rather than merely dropped, because this reader cannot decide the question a retirement has to answer: whether the open that wrote the intent crashed or is still in flight. Only elapsed time can, and the service that sweeps is the one holding the clock the retirement is evaluated against. See RetireGateDeadlineIntent.
The revision travels with it so the retirement is a compare-and-swap onto the row this page actually saw. Without it a sweeper would have to re-read, and a re-read is a second decision point at which a gate could have been reopened.
IT IS DELIBERATELY NOT RetireGateDeadlineIntentRequest, though the two spell the same four members today and staticcheck reports the conversion (S1016). One is a SWEEP RESULT and the other an OPERATION REQUEST, and collapsing them would make the request hostage to whatever a future page decides to report: a member added here for a caller's convenience would silently become a member the operation accepts. The conversion is used once, in the test that drives the whole sweeper path, which is the right place for it — it holds the two shapes together where they are meant to line up, and stops compiling if they ever diverge, which is the loud failure rather than a silent one.
type ResidencyAcquireCleanupError ¶ added in v0.4.0
type ResidencyAcquireCleanupError struct {
// contains filtered or unexported fields
}
ResidencyAcquireCleanupError means acquisition was canceled after a provider grant arrived and its release also failed. No ownership grant is returned. Use errors.As to retain this cleanup obligation and retry Release; until a release succeeds the Store admission remains held and Close may time out. Unwrap exposes both the acquisition refusal and provider cleanup failure.
func (*ResidencyAcquireCleanupError) Error ¶ added in v0.4.0
func (e *ResidencyAcquireCleanupError) Error() string
func (*ResidencyAcquireCleanupError) Release ¶ added in v0.4.0
func (e *ResidencyAcquireCleanupError) Release(ctx context.Context) error
Release retries cleanup only; this error exposes no ownership capability.
func (*ResidencyAcquireCleanupError) Unwrap ¶ added in v0.4.0
func (e *ResidencyAcquireCleanupError) Unwrap() error
type ResidencyEpoch ¶ added in v0.4.0
type ResidencyEpoch uint64
ResidencyEpoch identifies a Host residency grant. It is meaningful only in that session's residency namespace and must never be compared with, or used as, a journal epoch. Residency alone authorizes no journal writes or command application; the disposition protocol is not activated by this API.
type ResidencyError ¶ added in v0.4.0
ResidencyError is a provider failure acquiring or releasing residency. Cause preserves provider errors, including storage.LeaseHeldError for contention. Catalog, scope and Store lifecycle refusals retain their existing typed errors.
func (*ResidencyError) Error ¶ added in v0.4.0
func (e *ResidencyError) Error() string
func (*ResidencyError) Unwrap ¶ added in v0.4.0
func (e *ResidencyError) Unwrap() error
type ResidencyGrant ¶ added in v0.4.0
type ResidencyGrant struct {
// contains filtered or unexported fields
}
ResidencyGrant owns only an orchestration residency lease. Epoch and Lost come directly from Storage; Lost is the provider's actual notification, never inferred from journal events, release errors or caller cancellation.
Liveness is provider-dependent. In particular memstore provides neither TTL nor crash takeover. Residency loss does not fence a journal: a successor must independently acquire the journal grant before application.
The grant is safe for concurrent use and holds a Store admission until a successful Release. Store shutdown attempts bounded cleanup once; failed cleanup retains the admission for an explicit retry. Providers must honor Release context cancellation. There is no background retry loop.
func (*ResidencyGrant) Epoch ¶ added in v0.4.0
func (g *ResidencyGrant) Epoch() ResidencyEpoch
Epoch returns the provider's epoch in the residency domain.
func (*ResidencyGrant) Lost ¶ added in v0.4.0
func (g *ResidencyGrant) Lost() <-chan struct{}
Lost returns the provider's loss signal, including successful release.
func (*ResidencyGrant) Release ¶ added in v0.4.0
func (g *ResidencyGrant) Release(ctx context.Context) error
Release returns this specific grant to the provider. Success is idempotent; failure remains retryable and does not pretend cleanup completed or free the Store admission. Each provider attempt is bounded by the caller context and the Store shutdown timeout. It cannot release a successor's grant.
type ResolveGateRequest ¶
type ResolveGateRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
LeaseEpoch uint64
GateID sessionwire.GateID
}
ResolveGateRequest retires one open gate. It records only that the gate is no longer open and awaiting an answer: it carries no response, decides nothing about what the session does next, and starts no continuation.
It is idempotent, and it must be: a resolve interrupted between clearing the projection and retiring the intent is completed by repeating it.
type RetireGateDeadlineIntentRequest ¶
type RetireGateDeadlineIntentRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
GateID sessionwire.GateID
Revision uint64
}
RetireGateDeadlineIntentRequest tombstones one gate's deadline intent.
Revision is the revision the caller observed on the row, in a RemnantGateIntent from ListDueGates. The write is a compare-and-swap onto it, so a row that moved between the page and this call is refused rather than retired on stale evidence.
It is the counterpart of RemnantGateIntent, which is what a sweep reports and what a caller builds this from; the two are deliberately separate types for the reason stated there.
It names a tenant and a session because it must: the intent is filed under the session's scope and there is no way to reach it without them. So this operation cannot carry the structural hint the sweeps carry — a request that names no tenant — and its service-only status is prose alone. What protects it is that there is nothing a tenant could aim it at productively: it refuses any gate the session's own durable record still projects as open.
type RuntimeCommandID ¶
type RuntimeCommandID string
RuntimeCommandID is the runtime-facing identity a Host forwards into the Harness API. It is a named type rather than a bare string because it travels beside the public CommandID in almost every signature, and two adjacent strings of one type are silently swappable.
This package does not impose a UUID grammar on it. Harness allocates UUIDs today, but the durable MAPPING is what this record exists to make authoritative; a grammar check here would be a second statement of a rule this package does not own, and it would refuse a future runtime identity without adding any safety. It is validated as a bounded opaque UTF-8 value, exactly as the catalog validates the identities it does not own.
type RuntimePage ¶
type RuntimePage struct {
Records []RuntimeRecord
CapturedTip uint64
CoveredThrough uint64
NextCursor sessionwire.Cursor
}
RuntimePage is a bounded privileged replay page captured at CapturedTip.
type RuntimeRecord ¶
RuntimeRecord is one raw journal record as it is stored. Object-backed bodies stay unresolved: the caller decides which of them to fetch, through the ordinary object API, so a replay never pays for bytes it does not want.
type SessionBinding ¶ added in v0.4.0
type SessionBinding struct {
StorageBindingID string `json:"storage_binding_id"`
BindingVersion string `json:"binding_version"`
RuntimeSessionID string `json:"runtime_session_id"`
ProtocolMode ProtocolMode `json:"protocol_mode"`
}
SessionBinding pins the storage configuration and runtime identity chosen at creation. IDs are bounded opaque configuration names, not provider keys or credentials. BindingVersion identifies immutable configuration, never the current agent default. The all-zero value denotes an unbound legacy record. A nonzero binding must contain all four members.
type SessionPage ¶
type SessionPage struct {
sessionwire.SessionPage
UnreadableSkipped int
}
SessionPage is one bounded page of a tenant's sessions together with what producing it cost, as DueGatePage is for the deadline view.
It embeds Core's page rather than replacing it, so a caller still reads Sessions and NextCursor directly and can hand the embedded value to anything that takes a sessionwire.SessionPage.
UnreadableSkipped counts rows this reader could not hold to their own identity and therefore did not publish. It is not a diagnostic afterthought: it is what makes skipping such a row safe to do at all, because it leaves the caller able to tell "this tenant has three sessions" from "this tenant has three sessions and one row I could not vouch for". A nonzero count is durable — nothing in this package rewrites such a row — so it means a build that understands the row is needed, not that a retry will help.
type SessionPointer ¶
type SessionPointer struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
Kind SessionPointerKind
LeaseEpoch uint64
Sequence uint64
UpdatedAt time.Time
// Target is nil exactly when this pointer has been cleared.
Target *sessionwire.ObjectReference
}
SessionPointer is the durable record of which immutable object currently fills one role for one session, and of the two high-water marks that decide whether a later write may replace it.
The two are different questions and both are permanent:
- LeaseEpoch answers MAY YOU WRITE. It is the epoch of the grant that last wrote this record, and a write naming a strictly lower one has provably lost the session. An equal one is admitted because one grant writes many times. It never falls, which is why this record is never deleted.
- Sequence answers IS THIS NEWER. It is the journal position the target was captured at, and a write naming a strictly lower one would republish an object that a later capture has already superseded. It never falls either, and it is the one thing the epoch cannot supply: two writes under ONE grant are ordered only by their revision compare-and-swap, so a losing writer that retried would otherwise reinstate its older checkpoint over the newer one and every restore afterwards would silently lose the work in between. The catalog refuses a regressing LastJournalSeq for exactly this reason.
A nil Target is the cleared tombstone. It is one nil rather than an enumeration of zero values, so a LIVE TOMBSTONE IS UNREPRESENTABLE rather than excluded by a check somebody has to remember, and a cleared pointer carries both high-waters and nothing else.
UpdatedAt is the STORE's clock, not a caller's, for the reason ClearHostRegistrationRequest carries no timestamp: nothing in this record has an expiry, so no decision turns on this instant, and a caller-supplied one could only be wrong. It is the record's audit line and the summary's capture instant.
It is read when the request is VALIDATED, before this store issues any provider call — not when the write was accepted. A set reads it before admission, the witness binding, the read and the compare-and-swap; a clear reads it after its read and before its one swap, so the two paths do not even stamp from the same point in their own sequences. That is precisely because nothing decides on it: validating before any provider work is the property worth having, and paying a round trip to make an audit line a few milliseconds truer is not.
The accumulation is one small permanent row per role per session that has ever had one: never listed, never ranked, never due, and never read except by name. The registry's carry-forward contract about retention applies here word for word — the only safe reaper is one that removes the session's whole scope at once, because deleting this row alone destroys a fence while leaving the session writable.
func (SessionPointer) CheckpointSummary ¶
func (p SessionPointer) CheckpointSummary() (CheckpointSummary, error)
CheckpointSummary projects a workspace checkpoint pointer into the catalog's summary shape, and it is the ONLY way to build one from durable state.
This is where "the catalog holds a summary while the pointer holds the truth" stops being prose. A Host that publishes a projection reads this pointer and projects it; it does not compose a second answer from whatever it happens to remember, and UpdateCatalogHostState's replace-everything semantics are therefore harmless — what it replaces is a copy.
A CLEARED pointer projects to the ZERO summary with no error, because the zero summary is precisely what CheckpointSummary documents as "no checkpoint has been committed". That is the whole propagation path for a clear: the authoritative record says there is none, and the copy the catalog carries says the same thing on the next projection write.
It refuses any other KIND. The catalog validates a summary's reference as an opaque ObjectID and no more — it cannot tell a workspace checkpoint from a runtime one — so a runtime-checkpoint pointer projected into the catalog's workspace-checkpoint summary would put an object no restore can use where one it can use belongs, and nothing downstream would notice.
CapturedAt is this record's UpdatedAt, which is neither the instant the Host finished writing the object nor the instant the pointer was committed: it is the store's clock as the pointer request was validated, which SessionPointer states exactly. It therefore sits somewhere between the capture and the commit. It is the only instant this record has, and it is a rendering field: no decision in this package or in the catalog turns on it.
type SessionPointerEntry ¶
type SessionPointerEntry struct {
Pointer SessionPointer
Revision uint64
}
SessionPointerEntry is a pointer together with the revision a later compare-and-swap names.
The provider's immutable acceptance order is deliberately not exposed, for the reason HostRegistrationEntry's is not: a session's position in a stream of pointer writes is not a fact any caller acts on.
type SessionPointerKind ¶
type SessionPointerKind string
SessionPointerKind is the closed set of roles a session's current object can fill. It is the vocabulary the generic kernel below is parameterized by, and each member has exactly one public triple of methods.
ActiveContinuation is declared here and stored here and is otherwise RESERVED for the gate suspension/resume plan. This file stores a NAME; it does not define what a continuation contains, does not resume one, and no operation in this package reads one — which TestNothingElseInThisPackageReadsAPointer keeps true.
const ( SessionPointerActiveContinuation SessionPointerKind = "active-continuation" SessionPointerWorkspaceCheckpoint SessionPointerKind = "workspace-checkpoint" SessionPointerRuntimeCheckpoint SessionPointerKind = "runtime-checkpoint" )
type SetSessionPointerRequest ¶
type SetSessionPointerRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
LeaseEpoch uint64
Sequence uint64
Target sessionwire.ObjectReference
}
SetSessionPointerRequest names the object one role should now point at.
It carries a Target by VALUE, which is what makes "set" and "clear" two different operations rather than one operation with a nil argument: a caller cannot erase a session's checkpoint by forgetting to set a member, and the tombstone has exactly one writer.
It carries no KIND. The kind is spelled by the METHOD, so there is no way to name a role that has no method and no way to hand the wrong role a reference that the method's own object-kind rule would then have to catch by luck.
LeaseEpoch is the grant's epoch and is compared against the record's committed high-water mark. Sequence is the journal position the target was captured at and is compared against the record's committed sequence; both are high-water marks and neither ever falls.
There is no expected revision and no timestamp. A pointer is not a decision a caller makes about a record it has read — it is the current truth about which object fills a role — so the write is closed against the revision this store reads for itself, exactly as UpdateCatalogHostState and PutHostRegistration are, and the instant is the store's for the reason SessionPointer states.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the durable session aggregate over one complete storage backend.
func Open ¶
Open constructs a Store over a complete storage composite whose Blobs primitive provides bounded reader shutdown. Before provider I/O, publishing a Store, or starting its lifecycle, it validates that capability; it then atomically establishes the immutable backend keyspace marker.
Example ¶
ExampleOpen is the compiled twin of the composition example in README.md. It exists so that snippet cannot go stale: it is the whole shape of a composition root — pick a provider, hand its complete storage.Composite to Open, and close the Store — and if the signature or the flow changes, this stops compiling.
memstore is used because it is the in-process oracle Storage ships and it satisfies Open's bounded Blob reader lifecycle requirement. A product picks a durable provider here instead; nothing else in this function changes.
package main
import (
"context"
"fmt"
"log"
"time"
sessionwire "github.com/looprig/core/sessionwire/v1"
"github.com/looprig/sessionstore"
"github.com/looprig/storage/memstore"
)
func main() {
ctx := context.Background()
store, err := sessionstore.Open(ctx, memstore.New())
if err != nil {
log.Fatal(err)
}
defer func() {
if err := store.Close(ctx); err != nil {
log.Fatal(err)
}
}()
createdAt := time.Date(2026, 8, 30, 10, 0, 0, 0, time.UTC)
if _, created, err := store.CreateCatalogEntry(ctx, sessionstore.CreateCatalogEntryRequest{
TenantID: "tenant-a",
SessionID: "session-a",
AgentID: "agent-a",
RuntimeCompatibilityID: "runtime-v1",
CreatedAt: createdAt,
LastActiveAt: createdAt,
State: sessionwire.SessionStateIdle,
Residency: sessionwire.SessionResidencyCold,
DesiredPlacement: sessionwire.HostPlacementPooled,
IdempotencyKey: "create-1",
}); err != nil {
log.Fatal(err)
} else {
fmt.Println("created:", created)
}
page, err := store.ListSessions(ctx, sessionstore.ListSessionsRequest{TenantID: "tenant-a", Limit: 10})
if err != nil {
log.Fatal(err)
}
for _, summary := range page.Sessions {
fmt.Println("session:", summary.SessionID, summary.State)
}
fmt.Println("unreadable skipped:", page.UnreadableSkipped)
}
Output: created: true session: session-a idle unreadable skipped: 0
func (*Store) AcquireReconciliationClaim ¶
func (s *Store) AcquireReconciliationClaim( ctx context.Context, req AcquireReconciliationClaimRequest, ) (ReconciliationClaimEntry, error)
AcquireReconciliationClaim takes the session's claim, or extends the caller's own.
Three cases, and the middle one is the whole operation:
- No record: the claim is created. A create that finds the identity already there is a lost race, reported as a conflict so the caller re-reads and meets the live claim on the ordinary path.
- A live claim held by SOMEONE ELSE: refused with the horizon, and NOTHING IS WRITTEN. A losing replica that rewrote the row would restamp the winner's claim under its own name, which is the one way this record could take work away from the replica actually doing it.
- Anything else — a lapsed claim, or the caller's own claim, live or not: taken, in one compare-and-swap. Extending one's own live claim and taking over a crashed replica's lapsed one are the same write, because the record does not distinguish them and nothing downstream needs to.
The clock is read ONCE, before the provider read, and both the bound and the stored claim instant come from that reading. A second reading taken after the read could be later than the instant the record was evaluated at, which would start the machine erring toward taking claims rather than leaving them.
func (*Store) AcquireResidency ¶ added in v0.4.0
func (s *Store) AcquireResidency(ctx context.Context, req AcquireResidencyRequest) (*ResidencyGrant, error)
AcquireResidency validates the actual immutable catalog binding and acquires a distinct residency lease. It never opens, reads or appends a journal. A protocol-only witness without a catalog winner is insufficient authority. The caller context bounds acquisition only; the returned grant lives until Release or Store shutdown. An error always returns a nil grant; see ResidencyAcquireCleanupError for a failed rollback's retry obligation.
func (*Store) AdmitCommand ¶
func (s *Store) AdmitCommand(ctx context.Context, req AdmitCommandRequest) (InboxEntry, bool, error)
AdmitCommand makes one command durable and reports whether this call is the one that accepted it.
It is exactly one CreateOrdered call. The provider's Create is atomically idempotent by identity, so the duplicate case needs no read of its own: a duplicate arrives as the winner's canonical stored record with created false, carrying the winning runtime mapping and the immutable acceptance order.
What makes a duplicate a MISMATCH rather than a retry, and why:
- Kind, Payload, and PayloadRef must match. Reusing one command id for a DIFFERENT command must fail closed; silently returning the first command's record would tell the caller its command was accepted when nothing of the kind happened.
- The proposed RuntimeCommandID is deliberately excluded. Disagreeing about it is the normal, expected outcome of a race, and the winner's value is the answer.
- AcceptedAt and ApplyDeadline are deliberately excluded. A retry carries a fresh clock reading — a caller that computes an absolute deadline from "now" produces a different one on every attempt — so comparing them would turn every real retry into a mismatch.
- State, Claim, Result, and Rejection are deliberately excluded. By the time a retry arrives the command may already be claimed, applied, or rejected; that progress is not evidence that this retry differs, and a retry of a completed command must still receive its mapping.
func (*Store) BeginApplyingCommand ¶
func (s *Store) BeginApplyingCommand(ctx context.Context, req BeginApplyingCommandRequest) (InboxEntry, error)
BeginApplyingCommand moves a claimed command into applying, which is the statement that application is starting now rather than that capacity has been reserved. Only the holder of a live claim may make it: an epoch that is not the claim's has not claimed this command, and an expired claim is no longer a claim, so both are told the claim is lost and may claim again if the deadline still allows one.
There is no apply-deadline check, and its absence is the deadline race the spec settles in the claimer's favour: a writer holding a live claim may begin applying even past the deadline, which is precisely what stops a reconciler's clock from cancelling work that is about to commit.
func (*Store) ClaimCommand ¶
func (s *Store) ClaimCommand(ctx context.Context, req ClaimCommandRequest) (InboxEntry, error)
ClaimCommand takes a short-lived claim on a command so one writer works on it at a time.
The order of its refusals is the order in which the answers become permanent, so a caller meeting two of them at once is told the one that will still be true after it retries:
- A terminal command is settled. Nothing about it can be claimed, and the answer is to read its outcome.
- An applying command is not claimable at any epoch. See this file's header: resuming one is continuation, not a claim.
- A superseded epoch can never succeed again under that epoch.
- The apply deadline has passed, so no NEW claim may start — permanently, for this command, for every caller. It is deliberately checked before the claim is examined, because "you are too late" stays true when the live claim that would otherwise be reported expires.
- A live claim at this epoch belongs to someone else and will expire.
A claim taken over an EXPIRED claim is an ordinary claim, not a special reclaim: the record's members carry the new claim exactly as the first one did, and the reclaim horizon a reader derives from them follows. There is no second due state to file and no operation-shaped due state anywhere in this file — see inboxDue, which is the only definition there is.
A CLAIM CANNOT BE RENEWED, and a caller that needs more time has exactly one move: enter applying before its claim lapses. Re-claiming under the same epoch is refused for as long as the claim is live (that is the equal-epoch rule) and admitted only once it has lapsed — by which time any other writer at that epoch or above may take it, and the deadline may have closed new claims entirely. Renewal is deliberately absent rather than forgotten: it would let one writer hold a command indefinitely, and the state that legitimately spans a long application is applying, which the deadline cannot cancel.
func (*Store) ClearActiveContinuationPointer ¶
func (s *Store) ClearActiveContinuationPointer( ctx context.Context, req ClearSessionPointerRequest) (SessionPointerEntry, error)
func (*Store) ClearHostRegistration ¶
func (s *Store) ClearHostRegistration(ctx context.Context, req ClearHostRegistrationRequest) (HostRegistrationEntry, error)
ClearHostRegistration releases one session's route under the registration's fencing epoch, writing the tombstone rather than deleting the record.
It is idempotent under one grant: a repeat returns the stored tombstone without writing. What carries that rule is the EQUALITY in the repeat condition, not the order it is written in — an epoch equal to the tombstone's has already passed the fence, so moving the repeat check above the fence changes nothing a caller can observe, and a mutation that moves it survives. It is written after the fence anyway, because the repeat check is not an ownership test and must never become the only thing standing between a superseded lease and a success: weaken it to "is this record released" and the fence above is what still refuses epoch 3 a tombstone written at 5.
A LATER grant releasing an already-released session is not a repeat, and this is the one place that distinction has teeth: the tombstone must be rewritten so the high-water mark rises to the later epoch. Treating it as a repeat would leave the fence at the older epoch, and every lease granted in between — all of which have provably lost the session — would still be able to write.
A session with no registration at all reports NotFound. Cleanup is idempotent with respect to ITS OWN tombstone, not with respect to nothing: creating a tombstone for a session no Host ever registered would mint a fencing high-water mark out of an unverified caller-supplied epoch.
func (*Store) ClearRuntimeCheckpointPointer ¶
func (s *Store) ClearRuntimeCheckpointPointer( ctx context.Context, req ClearSessionPointerRequest) (SessionPointerEntry, error)
func (*Store) ClearWorkspaceCheckpointPointer ¶
func (s *Store) ClearWorkspaceCheckpointPointer( ctx context.Context, req ClearSessionPointerRequest) (SessionPointerEntry, error)
func (*Store) Close ¶
Close initiates shutdown exactly once. It cancels Store-owned work, waits for admitted background work, foreground operations, and returned readers, then closes an explicitly owned provider at most once with a fresh, lifecycle-owned timeout. Each caller's ctx bounds only its own wait. If a caller stops waiting, shutdown continues and a later call can observe the one stable final result.
func (*Store) CompleteCommand ¶
func (s *Store) CompleteCommand(ctx context.Context, req CompleteCommandRequest) (InboxEntry, error)
CompleteCommand records that an applying command's effect committed.
It requires the caller to be the epoch the claim was taken under and does NOT require that claim to still be live, and the asymmetry with rejection is deliberate. Completing RECORDS SOMETHING THAT ALREADY HAPPENED: the result names a journal event that is already durable, which the caller could only have committed while it held the session lease. Refusing to record it because a claim TTL lapsed in the meantime would leave a command whose effect is visible in the journal sitting in applying, waiting for a recovery pass to discover what the writer standing right there already knew. Rejecting, by contrast, DECIDES something that has not happened, so it keeps the live-claim requirement.
A SUCCESSOR LEASE finishing an application it did not start is the other half of this, and it arrives with inbox_recovery.go. It is admitted on durable evidence and on nothing else: a strictly greater epoch may complete an applying command only when the journal carries a prefix correlated with the record's own two identities AND the public event that carried its effect, and only when the result it records is that effect. Everything the same-epoch path takes on the caller's word — that an effect exists, and which event it is — the successor has to have read out of the stream.
The apply deadline takes no part in it. Finishing an application that is already durable is CONTINUATION of work that started before the deadline, not a new claim, and refusing to record it would leave a command whose effect is visible to a client sitting unfinished forever. The no-new-claim-after- deadline rule is unaffected, because a claim is a different operation and still refuses.
The claim is preserved unchanged, including its epoch, so the record keeps naming the lease that APPLIED the command rather than the one that noticed.
func (*Store) ControlShards ¶
ControlShards reports the shard count this store's backend is committed to.
It is a READ of a persisted decision, not a setting. A sweeper needs it to know how many shards to visit, and it must come from the store rather than from the sweeper's own configuration: a sweeper that visited a different number would silently never look at some of them.
func (*Store) CreateCatalogEntry ¶
func (s *Store) CreateCatalogEntry(ctx context.Context, req CreateCatalogEntryRequest) (CatalogEntry, bool, error)
CreateCatalogEntry binds the session's collision witnesses and creates its one authoritative ordered record. A duplicate identity returns the canonical stored record with created false and never overwrites it.
func (*Store) DrainHostTarget ¶
func (s *Store) DrainHostTarget(ctx context.Context, req DrainHostTargetRequest) (HostTargetEntry, error)
DrainHostTarget withdraws one Host's capacity for one target under the row's generation high-water mark.
A drain is the graceful counterpart of the reconciler: it removes the row from the placement view and from the deadline view in one compare-and-swap, at the instant a Host decides to stop rather than at the instant its promise runs out. Both write the same withdrawn SHAPE — a record with no advertisement — so there is one withdrawn state rather than two, and both reach it through the one write that moves the value and both views together. They differ only in the generation they leave behind: a drain raises the mark to the incarnation that asked, while the reconciler preserves whatever the row already carried, because a sweep speaks for no incarnation.
It is idempotent under one incarnation: a repeat returns the stored row without writing. What carries that rule is the EQUALITY in the repeat condition, not the order it is written in — a generation equal to the stored one has already passed the fence — so it is written after the fence deliberately: the repeat check is not an ownership test and must never become the only thing standing between a superseded incarnation and a success.
A LATER incarnation draining an already-withdrawn row is not a repeat, and the row is rewritten so the high-water rises to it. Treating it as a repeat would leave the mark at the older generation and let every incarnation in between — all of them provably restarted away — write again.
A (target, host) pair that has never advertised reports NotFound. A drain is idempotent with respect to ITS OWN withdrawal, not with respect to nothing: writing a withdrawal for capacity that was never offered would mint a generation high-water out of an unverified request AND leave a permanent row standing for capacity that never existed, which is the accumulation this record is built to avoid.
It deliberately does not verify the target's collision witness, which PublishHostTarget binds. A drain creates no name — it can only ever compare-and-swap a row that already exists — and the row it finds is held to the requested target by its own bytes, which is a strictly stronger check than a digest comparison. Verifying here would restate a weaker form of a check already made and would answer "there is nothing to drain" with a keyspace failure.
func (*Store) FindCommandApplication ¶
func (s *Store) FindCommandApplication(ctx context.Context, req FindCommandApplicationRequest) (CommandApplication, error)
FindCommandApplication reports what a session's journal proves about one command's application.
It reads the command's authoritative record FIRST and correlates against the identities stored there, never against identities a caller supplied. That is the point of the durable mapping: a caller 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.
It is offered publicly because a recovering Host has to DECIDE between finishing and rejecting, and because the result a finished application records is the correlated effect — which the caller has no other way to name. A journal fault is reported in the journal's own vocabulary: it is a fault of the stream rather than of the command record, and a caller separates them by type exactly as it already must.
func (*Store) GetActiveContinuationPointer ¶
func (s *Store) GetActiveContinuationPointer( ctx context.Context, req GetSessionPointerRequest) (SessionPointerEntry, error)
func (*Store) GetCatalogEntry ¶
func (s *Store) GetCatalogEntry(ctx context.Context, req GetCatalogEntryRequest) (CatalogEntry, error)
GetCatalogEntry reads one session's record directly by identity. It verifies the session's collision witnesses before any provider read, so a derived name is never trusted on its own.
func (*Store) GetCommand ¶
func (s *Store) GetCommand(ctx context.Context, req GetCommandRequest) (InboxEntry, error)
GetCommand returns one command's authoritative record, its current revision, and its immutable acceptance order.
It is the read half of every compare-and-swap in this file: a caller that loses a race, or that meets a state it has no edge out of, learns what to do next by reading the record rather than by decoding the failure. A terminal command stays readable here forever — its terminal write leaves it out of the due view but not out of the store — which is what lets a caller report an outcome it did not itself commit.
func (*Store) GetHostRegistration ¶
func (s *Store) GetHostRegistration(ctx context.Context, req GetHostRegistrationRequest) (HostRegistrationEntry, error)
GetHostRegistration returns one session's route, and returns it only while it is a route.
A released registration reports Released and an expired one reports Expired, each without the tuple: a caller cannot route to a Host this store will not vouch for, because it is never handed the endpoint. Both are the "treat an expired entry as absent" rule of the routing design, stated so that the two causes remain distinguishable to an operator while being identical to a router. See RegistryErrorCode.
It verifies the session's collision witnesses before any provider read, so a derived name is never trusted on its own.
func (*Store) GetObject ¶
func (s *Store) GetObject(ctx context.Context, req GetObjectRequest) (io.ReadCloser, error)
GetObject returns a lifecycle-held verified stream. A caller establishes integrity only by reading through terminal EOF; premature Close is an error. Open requires storage.BlobReaderLifecycle so concurrent Close bounds an active provider Read and Store shutdown can cancel outstanding streams before closing an owned provider. A shutdown-triggered reader Close error is latched on that reader; Store.Close orders the cleanup but does not aggregate an error from a reader the caller abandoned.
func (*Store) GetObjectMetadata ¶ added in v0.4.0
func (s *Store) GetObjectMetadata(ctx context.Context, req GetObjectMetadataRequest) (sessionwire.ObjectMetadata, error)
GetObjectMetadata returns immutable metadata recorded after PutObject verified the persisted blob. It does not verify current body presence or integrity and grants no authorization to consume it. GetObject still verifies through EOF.
Lookup performs at most two scope-witness reads and one exact metadata read; it never enumerates or reads blob bodies. Missing metadata is reported as ObjectErrorMetadataUnavailable, not proof that bytes are absent. Objects written before the index was introduced remain readable through GetObject with explicit metadata. A stale index may survive blob deletion, in which case GetObject preserves the provider's storage.BlobNotFoundError as a cause.
func (*Store) GetReconciliationClaim ¶
func (s *Store) GetReconciliationClaim( ctx context.Context, req GetReconciliationClaimRequest, ) (ReconciliationClaimEntry, error)
GetReconciliationClaim returns one session's claim, and returns it only while it is a claim.
A lapsed claim reports Lapsed without the record, which is the same discipline GetHostRegistration applies to an expired route: a caller is never handed state this store will not vouch for, so it cannot act on a horizon that has already passed. The holder of a lapsed claim is not withheld to protect anything — it is withheld because it is not an answer to the question this operation asks.
It verifies the session's collision witnesses before any provider read, so a derived name is never trusted on its own.
func (*Store) GetRuntimeCheckpointPointer ¶
func (s *Store) GetRuntimeCheckpointPointer( ctx context.Context, req GetSessionPointerRequest) (SessionPointerEntry, error)
func (*Store) GetWorkspaceCheckpointPointer ¶
func (s *Store) GetWorkspaceCheckpointPointer( ctx context.Context, req GetSessionPointerRequest) (SessionPointerEntry, error)
func (*Store) ListCompatibleHosts ¶
func (s *Store) ListCompatibleHosts(ctx context.Context, req ListCompatibleHostsRequest) (HostTargetPage, error)
ListCompatibleHosts returns one bounded page of the Hosts currently offering capacity for one target, most free capacity first.
The whole page is one ranked provider query. The target is the ranking scope, so the restriction and the capacity order are both inside the query and the limit applies to an already-restricted, already-ordered result. Nothing here enumerates a prefix, sorts a directory, or narrows a wider page to a target afterwards: those cost work proportional to the fleet rather than to the page, and a Factory calls this on every placement decision.
It deliberately does NOT verify the target's collision witness, which is where it differs from every write here. A write names a row and must prove the derived name before it creates one; a listing names no row, and a target nothing has ever advertised has no witness to prove, so requiring one would answer "no capacity" with a failure. Safety comes from below instead: every row the provider returns is held to the target it itself claims, so a scope two targets somehow shared would fail the page closed rather than offer one target's Hosts as the other's.
A LAPSED ROW IS NOT PUBLISHED, and the reason it is dropped here rather than excluded by the query is that no ranked query can express it: the ranked view is ordered by capacity and knows nothing about the clock. This is the same "an expired entry reads as absent" rule GetHostRegistration applies, applied per row — a caller is never handed an endpoint this store will not vouch for. It is emphatically not this package's answer to stale rows accumulating: the row is still ranked and still occupies a position in every later page, and ReconcileHostTargets is what removes it. The count says so out loud.
NO SINGLE ROW CAN FAIL A PAGE: every per-row refusal is counted and stepped over. HostTargetPage states why that is the strongest rule here rather than leniency. The consequence for this signature is what matters at the call site: a failure returned from here is always about the QUERY — a bad limit, a foreign cursor, a provider that could not answer — and never about one row.
func (*Store) ListDueCommands ¶
func (s *Store) ListDueCommands(ctx context.Context, req ListDueCommandsRequest) (DueCommandPage, error)
ListDueCommands returns one bounded page of one shard's commands whose horizon has passed.
It is a READ. It claims nothing, expires nothing, and writes nothing: what a reconciler does about an outstanding command is the reconciler's business, and every write it then performs is a compare-and-swap against the revision this page reported.
ITS COST IS THE PAGE, AND THAT IS THE WHOLE POINT. The rows come from the ordered index's due view of one namespace, so a terminal command — which inboxDue files NOT DUE — is not in the view at all, a historical session contributes nothing, and the deployment's tenant count does not appear in the cost. Nothing here reads the catalog, and nothing enumerates a session's inbox.
IT DOES NOT VERIFY EACH ROW'S SESSION WITNESSES, and that is a departure from every NAMED read here worth stating rather than leaving to be noticed. Those paths verify because they DERIVE a record name from identities and must not trust it on its own. This one derives nothing: the provider supplies the row, and the row is held to its own bytes — its filing, its scope, its shard. A collision would put two sessions in one ordering scope, and a row would still report the identities its own bytes carry; what a reconciler then DOES with it goes through a named write, which verifies the witnesses and refuses. The alternative costs one KV read per distinct session per page for a check that decides nothing this page reports.
func (*Store) ListDueGates ¶
func (s *Store) ListDueGates(ctx context.Context, req ListDueGatesRequest) (DueGatePage, error)
ListDueGates returns one bounded page of gates whose absolute deadline has passed, deployment-wide, each one validated against the durable open projection it names.
It is a READ. It takes no action, cancels nothing, suspends nothing, and schedules nothing: what a Host does about an expired gate is gate continuation, which this task deliberately does not implement. It exists because the ordering contract in this file's header creates exactly one crash remnant — an intent whose open event never committed — and something has to be the reader that validates it away rather than acting on it.
It has a continuation, and that is what stops it being starved ¶
A remnant intent — one whose gate the session's durable record does not project as open — is REPORTED rather than acted on, and it is not retired here. It cannot be: OpenGate makes an intent durable before it commits the projection, so an intent with no matching open gate is indistinguishable, in its bytes, from a gate being opened right now. Retirement is a separate call that waits out a window no single open can outlive; see RetireGateDeadlineIntent and gateIntent.RecordedAt.
Without a resume position that would be permanent head-of-line blocking: the view is ordered by deadline ASCENDING, a remnant's deadline is in the past and never changes, and a Host that re-projects its open gates wholesale through UpdateCatalogHostState — a normal path, documented as such above — produces one remnant per gate it drops. Once Limit of them accumulate ahead of the live gates, every page from the head consists entirely of them.
NextCursor is the fix, and bounding the pass would not have been: a page budget bounds what one pass costs, but nothing about it moves the row that is blocking, so the blocked rows stay blocked. The continuation steps PAST a row that reported nothing, so the sweep reaches what is behind it on the next page. A caller that pages a shard to exhaustion sees every due row in it.
The page still reports Examined and Unreadable, because they answer a different question: whether a full page reported nothing, and whether rows were skipped because they could not be read at all. See DueGatePage.
func (*Store) ListSessions ¶
func (s *Store) ListSessions(ctx context.Context, req ListSessionsRequest) (SessionPage, error)
ListSessions returns one bounded recent-first page of a tenant's sessions.
The whole page is one ranked provider query. The tenant is the ranking scope, so the restriction and the recency order are both inside the query and the limit applies to an already-restricted, already-ordered result. Nothing here enumerates a prefix, sorts a catalog, or filters a wider page afterwards: those all cost work proportional to a tenant's history rather than to the page, and a Factory calls this on every picker render.
This deliberately does NOT verify the tenant's collision witness, which is where it differs from GetCatalogEntry. A direct get names a session and must prove that session's binding before it trusts a derived name; a list names no session, and a tenant that has never created one has no binding to prove, so requiring one would answer "this tenant is empty" with a failure. Cross-tenant safety instead comes from below: every record the provider returns is held to the tenant it itself claims, so a scope two tenants somehow shared would fail the page closed rather than disclose a row.
func (*Store) OpenGate ¶
func (s *Store) OpenGate(ctx context.Context, req OpenGateRequest) (CatalogEntry, error)
OpenGate records a gate's deadline and then projects it as publicly open.
Every rejection below precedes both writes, and the two writes are ordered: see this file's header for why the intent is durable first.
func (*Store) OpenJournal ¶
func (s *Store) OpenJournal(ctx context.Context, req OpenJournalRequest) (*JournalWriter, error)
OpenJournal takes single-writer ownership of a session's journal.
It binds the session's collision witnesses, acquires the lease, reads the tip once, and commits the opening fence at that exact tip. Every failure after the lease is granted releases that grant before returning, so a caller that retries always does so under a fresh, strictly higher epoch.
The returned writer holds a Store admission until Close, so Store.Close waits for it; Store shutdown also closes an abandoned writer so it can never wedge that wait.
func (*Store) PublishHostTarget ¶
func (s *Store) PublishHostTarget(ctx context.Context, req PublishHostTargetRequest) (HostTargetEntry, error)
PublishHostTarget publishes or refreshes one Host's advertisement for one target.
The stored HostGeneration is a high-water mark, not a lock: a request naming a lower generation is refused outright and an equal one is admitted, because one incarnation heartbeats many times. The read-compare-write is closed by the revision compare-and-swap, so a request that observed a stale generation cannot land after a successor's write.
Unlike the registry's epoch, a first publish minting the high-water mark costs only the caller its own capacity. A Host naming an absurd generation fences out its own later incarnations for this one row, permanently — the mark never falls and a withdrawn row still carries it — but the row is one process's offer of capacity for one target, so the damage is a Host that cannot advertise. The registry's equivalent mistake is a session nobody can ever claim. That asymmetry is the whole practical difference between fencing capacity and fencing ownership.
func (*Store) PutHostRegistration ¶
func (s *Store) PutHostRegistration(ctx context.Context, req PutHostRegistrationRequest) (HostRegistrationEntry, error)
PutHostRegistration publishes one session's observed route under the registration's fencing epoch.
The stored LeaseEpoch is a high-water mark, not a lock: a request naming a lower epoch is refused outright and an equal one is admitted, because one lease grant heartbeats many times. The read-compare-write is closed by the revision compare-and-swap, so a request that observed a stale epoch cannot land after a successor's write.
It is also the only operation that MINTS a fence. ClearHostRegistration refuses to build one for a session no Host ever registered, because that would turn an unverified caller-supplied epoch into a high-water mark; a first registration necessarily does exactly that, and nothing here bounds the value. A first registration naming MaxUint64 therefore admits only later writers naming MaxUint64 and fences out every real lease for the life of the session, permanently — the high-water never falls and this record never expires out of existence. That is the correct fail-closed direction, and the asymmetry is deliberate rather than an oversight: whether a caller's epoch is a lease it actually holds is a question about the lease, which this record cannot answer and must not pretend to.
It reads through readHostRegistration and never through GetHostRegistration, and that separation is the single most load-bearing line in this file. The public reader reports an expired or released registration as no route at all; a writer that believed it would create a fresh record over a row it could not see, and creating a fresh record is exactly how a fencing high-water mark gets silently reset to whatever the superseded writer named.
func (*Store) PutObject ¶
func (s *Store) PutObject(ctx context.Context, req PutObjectRequest) (sessionwire.ObjectMetadata, error)
PutObject streams, verifies, persists, and re-verifies an immutable object before returning its metadata. The stages are: static validation, admission, minting an identity, writing the blob, re-reading it back, and create-only persistence of its scoped metadata index for GetObjectMetadata.
The declared SizeBytes and SHA256 are exact: the body is accepted only if it ends at that length with that digest, and neither the caller's metadata nor any reference is produced otherwise.
Orphan policy. The identity is minted before the write, and PutObject returns a reference only after the persisted bytes have been read back and verified and the immutable metadata index has committed. A failure after the blob commits can therefore leave a blob that no caller was ever told about — an orphan, whose verification may also have failed. That is deliberate: the alternative, deleting on a post-commit error, would issue a delete against a provider that has just proved unreliable, and the blob is content- and generation-addressed so it can never be mistaken for another object. Reclaiming orphans is the store operator's job, over the tenant/session blob prefix; this package's only enumeration path, listObjectReferences, is intentionally not exported, so no caller-facing GC exists yet.
func (*Store) ReadGates ¶
func (s *Store) ReadGates(ctx context.Context, req ReadGatesRequest) (sessionwire.GatePage, error)
ReadGates returns one session's open public gates as core's bounded gate page, in the record's canonical (opened_seq, gate_id) order.
It is one direct record read. The gates are already canonical when the record decodes, so nothing here re-sorts them: the comparator lives in canonicalGates and a second one here is precisely what would later disagree with the stored order.
func (*Store) ReadPublicJournal ¶
func (s *Store) ReadPublicJournal(ctx context.Context, req ReadPublicJournalRequest) (sessionwire.JournalPage, error)
ReadPublicJournal returns a bounded page of a session's public events.
Only a public event's stored canonical public body and its Core metadata are returned. Every other record — runtime control, ownership fence, application prefix — is withheld entirely: it contributes nothing to the page except an advance of CoveredThrough, the authenticated watermark that lets a client close a sequence gap without learning the kind or the bytes of what filled it. A public body held in an object is resolved through the same verified object path a caller would use; a private runtime object is never fetched.
func (*Store) ReadRuntimeJournal ¶
func (s *Store) ReadRuntimeJournal(ctx context.Context, req ReadRuntimeJournalRequest) (RuntimePage, error)
ReadRuntimeJournal returns a bounded page of every record in a session's journal, public and private alike, exactly as stored. It is the privileged replay path; product-facing readers use ReadPublicJournal.
func (*Store) ReconcileHostTargets ¶
func (s *Store) ReconcileHostTargets( ctx context.Context, req ReconcileHostTargetsRequest, ) (HostTargetReconcileResult, error)
ReconcileHostTargets withdraws the advertisements of Hosts that stopped heartbeating.
IT IS A SERVICE OPERATION, NOT A TENANT ONE. It names no tenant, no session and no target: it sweeps the whole directory's deadline view, which is exactly why a caller that is not the control plane must never be given it. It is also the ONLY thing in this package that removes a crashed Host's row from a placement page — the placement reader declines to publish a lapsed row but leaves it ranked — so a deployment that never calls this accumulates ranked capacity that no longer exists.
The due BOUND is fixed for one walk and the revalidation INSTANT is not, and the difference is deliberate rather than an oversight. The bound is fixed because the ordered index binds a due cursor to the exact bound that issued it, so a walk that recomputed it could not page at all; on a resumed call the bound therefore comes from the continuation while the clock reading is fresh.
They are allowed to differ because the bound decides only WHICH rows a page contains and the revalidation decides whether any of them may be withdrawn. A fresh reading is monotonically at or after the bound, which is the safe direction: withdrawal requires the row's own stored expiry to have lapsed at that reading, so a row judged due at the bound and heartbeated since is still refused, and a row that lapsed after the bound is simply not in the page.
A failure reading the due view returns the counts accrued so far beside the error rather than a zero result: a sweep that withdrew rows and then lost the provider did that work, and reporting nothing would make a caller's next decision — sweep again now, or wait — rest on a number it knows is false. It returns the walk's POSITION too, for the same reason it returns one on a budget exhaustion: without it a caller that lost the provider halfway pays the cost of every unreadable row ahead of it all over again.
Each row is revalidated against its OWN STORED EXPIRY before anything is written, and the compare-and-swap onto the revision the page reported is what makes that revalidation binding rather than advisory. The two together are the whole guard: a Host that heartbeated after the page was read either presents an unlapsed expiry — in which case the sweep leaves it alone — or has already advanced the revision, in which case the write loses. Trusting the page's due state alone would withdraw the capacity of a Host that is alive.
func (*Store) RejectCommand ¶
func (s *Store) RejectCommand(ctx context.Context, req RejectCommandRequest) (InboxEntry, error)
RejectCommand settles a command with a durable typed reason.
It has two callers with different authority, and one rule that serves both:
- The holder of a live claim may reject the command it is working on, from claimed or from applying. It has revalidated the command and found it cannot be applied, and that answer is as durable as an application.
- A reconciler may settle a command NOBODY is working on: pending, or claimed under a claim that has lapsed. It needs no lease epoch, and if it names one it is still held to the record's high-water mark, because a caller that asserts an epoch is asserting a view of the session that may be stale.
- A SUCCESSOR LEASE may settle an applying command its predecessor abandoned. That one is recovery rather than reconciliation and is described below.
An unexpired claim therefore wins the deadline race outright: while the claim is live the only caller who may reject is its holder, whatever the clock says. So does an applying record, which additionally cannot be reclaimed at all, so the two-step of superseding the claim and then rejecting is closed as well.
EVERY caller is additionally held to the journal, and that check is not the reconciler's alone: rejection is admitted only when the correlation establishes that NO EFFECT COMMITTED under this command. A rejection written over a durable effect is the exact overwrite the terminal states exist to prevent, and the claim holder standing on its own committed effect can commit it as easily as a late reconciler — so the rule is a property of the record's journal rather than of who is asking. Its cost is a walk of the session's stream on every rejection, paid deliberately: see inbox_recovery.go on why it is unconditional.
An APPLYING record whose claim has lapsed is the one settlement that needs an authority as well as evidence, and it needs BOTH:
- a lease epoch strictly above the one that took the claim, which is what makes this the next lease holder's move rather than an anonymous reconciler's, and
- a journal fence above that same epoch, which is what proves the applier can no longer commit the effect this rejection would orphan. The epoch the caller names cannot prove it; only the fence can.
The two arrive together in practice, because the fence is written by the successor's own OpenJournal, which is what turns the head-of-line hazard this file used to document into a bounded one: an expired applying record was settleable by nobody, forever, and is now settled when the session is next attached.
THE PERMANENT HAZARD WAS RELOCATED, NOT ELIMINATED, and a reader sizing that signal needs all three sources rather than the one this paragraph used to name:
- A live claim that outlives the apply deadline occupies a due place it cannot be settled from. Bounded by MaxCommandClaimTTL, which exists for exactly this, and it is ordinary operation rather than a crash: every command claimed close to its deadline contributes.
- An UNRESOLVED correlation in the conforming crash window — a prefix at the tip, its writer gone — is bounded by re-attachment: the successor's opening fence lands at prefix+1 and the correlation becomes abandoned.
- An UNRESOLVED correlation from a WRITER-CONTRACT VIOLATION is bounded by nothing. If the record at prefix+1 is already something that is neither the effect nor a fence — a stacked prefix, an interleaved control record — it is durable and no later event changes it, so the command is unsettleable forever. That is the same permanent row this file used to hand every crashed applier, now confined to writers that broke the adjacency rule inbox_recovery.go states. It is smaller and it is not gone; a due-command reader must still expect rows that never clear.
Nothing is starved TODAY, because this package exposes no due command reader for anything to be starved out of; the hazard arrives with the reader. ListDueGates met the same shape and answered it by reporting what a page EXAMINED alongside what it returned, so a page that is full of rows it could not act on is distinguishable from a deployment with nothing to do.
func (*Store) ReleaseReconciliationClaim ¶
func (s *Store) ReleaseReconciliationClaim( ctx context.Context, req ReleaseReconciliationClaimRequest, ) (ReconciliationClaimEntry, error)
ReleaseReconciliationClaim gives the caller's own claim back, so the next replica need not wait out the TTL.
It writes a claim whose expiry EQUALS its claim instant, which has lapsed on arrival. It does not delete the row: this package writes no provider tombstones, and a lapsed claim is already indistinguishable from no claim to every reader.
A REPEAT IS A SUCCESS THAT WRITES NOTHING. A caller cannot tell a lost reply from a failure, so a second release is the ordinary case rather than a mistake, and answering it with an error would make every retry look like a claim that had expired mid-work. The repeat condition is "the record is the caller's and is not live", which a lapsed-by-timeout claim also satisfies — so a holder that overran its TTL is told its release succeeded. That loses a signal, and it is the right trade precisely because the claim licensed nothing: nothing the holder did was authorized by the claim, so nothing it did becomes wrong when the claim runs out.
A claim that is NOT the caller's is refused either way, and the two codes are different facts rather than one fact with two names: Held means another replica is working now, so wait; Lapsed means nobody is, so there is nothing to release and no reason to wait. Telling a caller "held" for a claim that had run out would make it back off for a horizon already in the past.
func (*Store) ResolveGate ¶
func (s *Store) ResolveGate(ctx context.Context, req ResolveGateRequest) (CatalogEntry, error)
ResolveGate clears one gate from the open projection and then retires its deadline intent.
Retiring is a tombstone rather than an erasure: the intent's bytes remain readable for audit, its identity can never be reused to reopen the same gate, and a tombstone is unranked and not due, so it leaves the due pages by the provider's own contract rather than by a flag this package would have to maintain.
func (*Store) RetireGateDeadlineIntent ¶
func (s *Store) RetireGateDeadlineIntent(ctx context.Context, req RetireGateDeadlineIntentRequest) error
RetireGateDeadlineIntent removes one remnant gate deadline intent.
A REMNANT IS THE ONLY THING IT REMOVES, and "remnant" is decided here, from durable state, rather than accepted from the caller. ListDueGates reports candidates; this operation independently re-reads the session's projection and the row, at its own clock reading, and refuses everything else.
WHAT EACH ABSENT ANSWER LICENSES, enumerated, because this is a path where a value that reads as absent removes work:
- The INTENT ROW IS ABSENT: refused, NotFound. Absence is not "already retired" — this package never erases, so a retired intent has a durable spelling and it is a tombstone. An absent row means the caller is retiring something this store has never held, and answering success would tell a sweeper it had handled a row it never touched.
- The INTENT ROW IS A TOMBSTONE: success, and nothing is written. This is the repeat case, and a caller cannot tell a lost reply from a failure, so a second retirement is ordinary rather than mistaken. The tombstone is this store's own record that the work is done.
- The SESSION HAS NO DURABLE EXISTENCE — no catalog record, a tombstoned one, or an unbound session witness: retirement is PERMITTED. A session that does not durably exist cannot durably project an open gate, so every intent it carries is a remnant. This is exactly noSuchSession's set and deliberately not one entry wider: a collision, an unreadable record, or a provider fault says nothing about whether the gate is open, and each of those STOPS the operation instead.
- The GATE IS NOT IN AN EXISTING RECORD'S OpenGates: retirement is permitted, subject to the age below. This is the ordinary remnant.
- The GATE IS OPEN: refused as a conflict. Retiring a live gate's deadline is the one outcome this operation must never produce.
And the age: an intent younger than MinGateIntentRemnantAge is refused with TooSoon, because inside that window "remnant" and "in flight" are the same bytes. TooSoon is its own code rather than a conflict for a reason a caller acts on — it means retry later, whereas a conflict means re-read.
func (*Store) SetActiveContinuationPointer ¶
func (s *Store) SetActiveContinuationPointer( ctx context.Context, req SetSessionPointerRequest) (SessionPointerEntry, error)
func (*Store) SetRuntimeCheckpointPointer ¶
func (s *Store) SetRuntimeCheckpointPointer( ctx context.Context, req SetSessionPointerRequest) (SessionPointerEntry, error)
func (*Store) SetWorkspaceCheckpointPointer ¶
func (s *Store) SetWorkspaceCheckpointPointer( ctx context.Context, req SetSessionPointerRequest) (SessionPointerEntry, error)
func (*Store) UpdateCatalogDesiredState ¶
func (s *Store) UpdateCatalogDesiredState(ctx context.Context, req UpdateCatalogDesiredStateRequest) (CatalogEntry, error)
UpdateCatalogDesiredState applies Factory-authored desired state.
IdempotencyKey is checked before the revision, and the order is the contract: a retry of an already-applied write carries an expected revision that its own success invalidated, so comparing the revision first would reject exactly the requests idempotency exists to absorb.
func (*Store) UpdateCatalogHostState ¶
func (s *Store) UpdateCatalogHostState(ctx context.Context, req UpdateCatalogHostStateRequest) (CatalogEntry, error)
UpdateCatalogHostState applies Host-owned fields under the record's fencing epoch.
The stored LeaseEpoch is a high-water mark, not a lock: a request naming a lower epoch is refused outright, and an equal one is admitted because one grant legitimately writes many times. The read-compare-write is closed by the revision compare-and-swap below, so a request that observed a stale epoch cannot land after a successor's write — it loses the CAS and, on re-read, meets the successor's epoch.
type StoreClosedError ¶
type StoreClosedError struct{}
StoreClosedError reports an attempt to admit work after shutdown started.
func (*StoreClosedError) Error ¶
func (*StoreClosedError) Error() string
type UpdateCatalogDesiredStateRequest ¶
type UpdateCatalogDesiredStateRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
ExpectedRevision uint64
IdempotencyKey string
DesiredPlacement sessionwire.HostPlacement
RuntimeCompatibilityID string
DesiredWorkload DesiredWorkload
}
UpdateCatalogDesiredStateRequest writes Factory-authored desired state. It deliberately has no lease epoch member: desired state is guarded by revision compare-and-swap plus a retry-stable idempotency key, because Factory does not hold the Host's lease and must not be able to spell a claim on it.
IdempotencyKey names the INTENT, and exactly one key is retained. A request whose key equals the retained one is treated as a replay of that intent and returns the stored record unchanged with a nil error — including when the rest of the request differs. Reusing a key for a NEW intent therefore succeeds without applying anything, so a caller must mint a fresh key per distinct desired state rather than per retry batch.
type UpdateCatalogHostStateRequest ¶
type UpdateCatalogHostStateRequest struct {
TenantID sessionwire.TenantID
SessionID sessionwire.SessionID
LeaseEpoch uint64
State sessionwire.SessionState
Residency sessionwire.SessionResidency
LastActiveAt time.Time
LastJournalSeq uint64
LastEventID sessionwire.EventID
Checkpoint CheckpointSummary
OpenGates []sessionwire.GateProjection
}
UpdateCatalogHostStateRequest writes the fields owned by the Host holding the session's journal lease. LeaseEpoch is that grant's epoch and is compared against the record's committed high-water mark.
Every field here REPLACES its stored counterpart; nothing is merged. A write that omits Checkpoint zeroes the stored checkpoint summary, and a write that omits OpenGates clears the stored gate projections. That is deliberate and is why the catalog holds a *summary*: the authoritative, retained high-water checkpoint pointer is a separate epoch-fenced record, so clearing a summary here loses no durable state. Callers therefore send the complete current projection on every write rather than a delta.
LastJournalSeq is the one exception, and its asymmetry is intentional: the journal is append-only and a successor fence commits above its predecessor's tip, so a durable sequence never moves backwards and a regressing one is refused rather than stored.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
modfiles
Package modfiles enumerates Go source files owned by this module while excluding structural directories and nested repository or module boundaries.
|
Package modfiles enumerates Go source files owned by this module while excluding structural directories and nested repository or module boundaries. |
|
modfiles/cmd/modfiles
command
|
|
|
pathutil
Package pathutil provides filesystem-path normalization for SessionStore composition helpers.
|
Package pathutil provides filesystem-path normalization for SessionStore composition helpers. |