Documentation
¶
Overview ¶
Package admission accepts commands durably and reconciles the ones with no live owner.
The interfaces here are the ones THIS package calls. Directory and PlacementController are declared here, not in internal/routing and internal/placement, because admission is the caller: routing and placement supply implementations of a contract their consumer states.
Index ¶
- Constants
- Variables
- func IsCode(err error, code sessionwire.ErrorCode) bool
- type Authorizer
- type Catalog
- type Claims
- type Clock
- type CommandStore
- type Commands
- type Config
- type Directory
- type Disposition
- type DueCommands
- type Error
- type LegacyCreateRequest
- type LegacyCreateResult
- type OwnerDirectory
- type PlacementController
- type PublicCreateStore
- type Reconciler
- type ReconcilerConfig
- type Service
- func (s *Service) AdmitCreate(ctx context.Context, principal identity.Principal, ...) (sessionstore.DispositionInboxEntry, bool, error)
- func (s *Service) AdmitGateResponse(ctx context.Context, principal identity.Principal, ...) (sessionstore.InboxEntry, bool, error)
- func (s *Service) AdmitInput(ctx context.Context, principal identity.Principal, ...) (sessionstore.InboxEntry, bool, error)
- func (s *Service) AdmitInterrupt(ctx context.Context, principal identity.Principal, ...) (sessionstore.InboxEntry, bool, error)
- func (s *Service) AdmitLegacyCreate(ctx context.Context, principal identity.Principal, req LegacyCreateRequest) (LegacyCreateResult, error)
- func (s *Service) AdmitRestore(ctx context.Context, principal identity.Principal, ...) (sessionstore.InboxEntry, bool, error)
- type SessionBindingTemplate
- type Settlement
- type SweepResult
- type Target
- type TargetResolver
- type UUIDSource
Constants ¶
const ( CommandCreate = command.KindCreateSession CommandInput = command.KindInput CommandInterrupt = command.KindInterrupt CommandRestore = command.KindRestore CommandGateResponse = command.KindGateResponse )
The kinds this service admits.
They are ALIASES of internal/command's, not a private copy, and this package is the one where that matters most: the REST control routes and the ClientLink RPCs are two spellings of one admission contract (specification section 8.1), and both are routed here. Two private copies would satisfy every test either package could write while letting an RPC be authorized under one spelling and admitted under another. They were a private copy until A6.1's gate found it.
Variables ¶
var ErrCreateBindingUnconfigured = errors.New("admission: V1 create requires a configured session binding")
ErrCreateBindingUnconfigured reports a V1 create reached a composition that named no SessionBindingTemplate. It is NOT a protocol gap -- the released store admits a public create -- it is a deployment that did not say which storage configuration its sessions are pinned to. Guessing one would be irreversible.
var ErrInvalidReconcilerConfig = errors.New("admission: invalid reconciler configuration")
ErrInvalidReconcilerConfig reports a reconciler configuration refused before it could reach a store.
var ErrLegacyCreateUnsupported = errors.New("admission: legacy create unsupported")
ErrLegacyCreateUnsupported reports that Factory cannot create a session on the legacy protocol. No runtime this program ships can host such a session.
ErrPayloadProtocolUnavailable reports an input whose payload is too large for the inbox this module can write to.
THE MISSING PIECE IS NOT A STORE PRIMITIVE, and the earlier wording of this comment said it was, which was true at sessionstore v0.4.0 and is false at the pinned version. The disposition inbox admits an oversized body by reference and compares a retry on PayloadDigest and PayloadSize rather than on the object reference, so re-PUTting under a fresh generation is no longer a mismatch: the hazard that made this unimplementable is gone.
What is missing is the SESSION BINDING that reaches it. Every disposition entry point requires a complete immutable binding whose ProtocolMode is disposition, and the four commands this error governs are admitted into the LEGACY inbox, so there is no disposition record for such a reference to sit on. A3.1 changed which commands that covers but not the rule: a CREATE now takes the disposition path and stores an oversized payload by reference, while input, interrupt, restore and gate response remain legacy. On the legacy inbox the original hazard stands unchanged at this pin — the retry comparison still includes the object reference, and PutObject still mints a fresh generation per call — so admitting the reference there would make every legitimate retry a permanent CommandMismatch. Refusing is still correct for those four.
Functions ¶
Types ¶
type Authorizer ¶
type Authorizer interface {
AuthorizeControl(ctx context.Context, principal identity.Principal, session sessionwire.SessionID, kind sessionstore.CommandKind) error
// AuthorizeServiceSweep covers the cross-tenant due-work sweep over the
// service-control shards. It is never reachable by a tenant principal and
// its records never become a cross-tenant public response.
AuthorizeServiceSweep(ctx context.Context, principal identity.Principal) error
}
Authorizer decides both tenant command admission and the service sweep. Admission repeats the edge's control decision deliberately: it is a durable mutation boundary that can later be called by transports other than HTTP and ClientLink, and no such caller may acquire an authorization-free path.
type Catalog ¶
type Catalog interface {
GetCatalogEntry(context.Context, sessionstore.GetCatalogEntryRequest) (sessionstore.CatalogEntry, error)
CreateCatalogEntry(context.Context, sessionstore.CreateCatalogEntryRequest) (sessionstore.CatalogEntry, bool, error)
}
type Claims ¶
type Claims interface {
AcquireReconciliationClaim(context.Context, sessionstore.AcquireReconciliationClaimRequest) (sessionstore.ReconciliationClaimEntry, error)
ReleaseReconciliationClaim(context.Context, sessionstore.ReleaseReconciliationClaimRequest) (sessionstore.ReconciliationClaimEntry, error)
}
Claims is the short-lived reconciliation claim of specification section 10.
It SUPPRESSES DUPLICATE WORK AND IS NEVER A FENCE, which is a property of SessionStore's record rather than a promise made here: the record cannot name ownership, and nothing in that package reads a claim to decide a write. What makes two replicas sweeping one shard safe is the compare-and-swap on the command's own revision and the store's evidence rule; the claim only stops them both paying for it.
type Clock ¶
type Clock interface {
Now() time.Time
// AfterFunc runs f after d and returns a stop function reporting whether
// it prevented the call.
AfterFunc(d time.Duration, f func()) (stop func() bool)
}
Clock is the time seam.
It names only standard-library and builtin types on purpose. Factory's public option surface declares an identical Clock, and a shape with a named Timer type would make the two interfaces mutually unassignable across the internal/ boundary; AfterFunc's stop function is an unnamed func type, so they remain the same interface without an adapter.
type CommandStore ¶
type CommandStore interface {
AdmitCommand(context.Context, sessionstore.AdmitCommandRequest) (sessionstore.InboxEntry, bool, error)
GetCommand(context.Context, sessionstore.GetCommandRequest) (sessionstore.InboxEntry, error)
}
type Commands ¶
type Commands interface {
AdmitCommand(ctx context.Context, req sessionstore.AdmitCommandRequest) (sessionstore.InboxEntry, bool, error)
GetCommand(ctx context.Context, req sessionstore.GetCommandRequest) (sessionstore.InboxEntry, error)
RejectCommand(ctx context.Context, req sessionstore.RejectCommandRequest) (sessionstore.InboxEntry, error)
ListDueCommands(ctx context.Context, req sessionstore.ListDueCommandsRequest) (sessionstore.DueCommandPage, error)
AcquireReconciliationClaim(ctx context.Context, req sessionstore.AcquireReconciliationClaimRequest) (sessionstore.ReconciliationClaimEntry, error)
ReleaseReconciliationClaim(ctx context.Context, req sessionstore.ReleaseReconciliationClaimRequest) (sessionstore.ReconciliationClaimEntry, error)
}
Commands is the durable command plane admission uses.
type Config ¶
type Config struct {
Authorizer Authorizer
Targets TargetResolver
Catalog Catalog
Commands CommandStore
Directory OwnerDirectory
Clock Clock
IDs UUIDSource
// PublicCreates is the durable public-create plane a V1 create admits
// into. It is OPTIONAL, and its absence is a supported composition rather
// than a broken one: without it, and without Binding, AdmitCreate refuses
// and every other entry point is unaffected.
PublicCreates PublicCreateStore
// Binding is the deployment-configuration half of the immutable binding a
// V1 create pins. Optional for the same reason.
Binding SessionBindingTemplate
ApplyDeadline time.Duration
}
type Directory ¶
type Directory interface {
// Owner reports the observed registration for a session. The boolean is
// false when no owner is observed, which is not an error.
Owner(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID) (sessionwire.HostLinkRegistryObservation, bool, error)
// Candidates pages compatible accepting Hosts. Capacity is not authority:
// a candidate confers nothing until the session lease is acquired.
Candidates(ctx context.Context, req sessionstore.ListCompatibleHostsRequest) (sessionstore.HostTargetPage, error)
}
Directory is the observed target directory: which Host currently owns a session, and which Hosts could accept one.
type Disposition ¶
type Disposition uint8
Disposition is what one sweep did about one due row, or why it did nothing.
It is a closed vocabulary with a rendering for every member including an unrecognized one, for the reason placement.Outcome is: a row's fate is operational data, and "" is not a fate.
const ( // DispositionSettleable is the predicate's positive answer and never a // recorded outcome: a row that reaches it goes on to be settled, deferred // or refused, and the recorded disposition is one of those. DispositionSettleable Disposition = iota // DispositionRejected means this sweep settled the command as rejected. DispositionRejected // DispositionUnexpired means the apply deadline has not passed. A Host may // still claim and apply the command, so nothing here may settle it. DispositionUnexpired // DispositionClaimLive means a live command claim holds the row: a Host is // working on it now. DispositionClaimLive // DispositionApplying means an application is in flight. Only the NEXT // lease holder can settle it, because the safety of that settlement rests // on a journal fence written at an epoch above the applying one -- and this // reconciler holds no lease and names no epoch. DispositionApplying // DispositionTerminal means the command had already reached applied or // rejected. inboxDue files a terminal command not-due, so this is a row // that settled between the page and the write. DispositionTerminal // DispositionEvidence means the store refused the settlement because the // session's journal does not prove the command had no effect. That is the // prefix winning, and it is an ordinary outcome rather than a fault. DispositionEvidence // DispositionRaceLost means the record moved between the page and the // compare-and-swap. DispositionRaceLost // DispositionDeferred means another replica holds this session's // reconciliation claim, so this one did nothing about the row. DispositionDeferred )
func (Disposition) String ¶
func (d Disposition) String() string
type DueCommands ¶
type DueCommands interface {
ControlShards() int
ListDueCommands(context.Context, sessionstore.ListDueCommandsRequest) (sessionstore.DueCommandPage, error)
}
DueCommands is the service-plane due-work query.
It is TWO methods because a sweeper needs both halves and neither can come from anywhere else. ControlShards is a READ of the count the backend is committed to, not a setting: the shard is a pure function of (TenantID, SessionID) computed identically by the writer and the sweeper, so a sweeper configured with its own count would silently never visit some shards. ListDueCommands answers one shard at a time, deliberately -- a store that swept every shard in one call would hold the whole deployment's reconciliation in one request's latency.
type Error ¶
type Error struct {
Code sessionwire.ErrorCode
Cause error
}
type LegacyCreateRequest ¶
type LegacyCreateRequest struct {
AgentID sessionwire.AgentID
Blocks json.RawMessage
}
type LegacyCreateResult ¶
type LegacyCreateResult struct {
SessionID sessionwire.SessionID
Entry sessionstore.InboxEntry
}
type OwnerDirectory ¶
type OwnerDirectory interface {
Owner(context.Context, sessionwire.TenantID, sessionwire.SessionID) (sessionwire.HostLinkRegistryObservation, bool, error)
}
type PlacementController ¶
type PlacementController interface {
EnsurePlacement(ctx context.Context, desired sessionstore.DesiredWorkload) error
ReleasePlacement(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID) error
}
PlacementController drives desired placement. It is idempotent in both directions: a repeated Ensure for the same desired workload creates nothing twice, and a repeated Release is not an error.
type PublicCreateStore ¶
type PublicCreateStore interface {
PreparePublicCreate(context.Context, sessionstore.PreparePublicCreateRequest) (sessionstore.PublicCreatePreparation, error)
PutCommandPayload(context.Context, sessionstore.PutCommandPayloadRequest) (sessionwire.ObjectMetadata, error)
AdmitPublicCreate(context.Context, sessionstore.AdmitPublicCreateRequest) (sessionstore.DispositionInboxEntry, bool, error)
}
PublicCreateStore is the durable public-create plane.
It is the disposition family, not the legacy inbox, and that is forced rather than chosen: a Host takes residency through AcquireResidency, which pins ProtocolModeDisposition, so a legacy-bound session is one no Host can ever run.
type Reconciler ¶
type Reconciler struct {
// contains filtered or unexported fields
}
Reconciler sweeps due commands in fixed control shards, round-robin.
It holds exactly one piece of state, the rotor, and that state is advice rather than position: a restarted replica resumes at shard zero and re-derives everything else from the store within the call.
func NewReconciler ¶
func NewReconciler(cfg ReconcilerConfig) (*Reconciler, error)
NewReconciler validates a configuration before it can reach a store.
func (*Reconciler) Sweep ¶
func (r *Reconciler) Sweep(ctx context.Context, principal identity.Principal) (SweepResult, error)
Sweep reconciles the next control shard, round-robin.
STEP ONE'S TWO HALVES ARE ONE CALL. This is a cross-tenant control-plane query -- a shard holds whichever tenants hash into it, so a page can mix them -- and AuthorizeServiceSweep is the decision that a tenant principal can never reach one. AuthorizeControl is deliberately NOT called here, and not because it would be redundant: a command arrives at the httpapi or clientlink edge already authorized, admission repeats that decision as the durable mutation boundary, and a THIRD authorizing caller is how an unauthorized caller acquires a path. What this sweep authorizes is the sweep, once, and the rows it then settles belong to no caller at all.
type ReconcilerConfig ¶
type ReconcilerConfig struct {
// Authorizer decides the sweep. The method called is AuthorizeServiceSweep
// and never AuthorizeControl: see Reconciler.Sweep.
Authorizer Authorizer
Due DueCommands
Settlement Settlement
Claims Claims
Clock Clock
// HolderID names this replica in a reconciliation claim. It is not
// authority -- the claim licenses nothing -- but it must be stable for a
// process, because extending one's own claim and taking over a crashed
// replica's lapsed one are the same store write and the holder is what
// tells them apart.
HolderID string
// ClaimTTL is how long this replica claims a session's reconciliation work
// for.
ClaimTTL time.Duration
// PageLimit bounds one due page.
PageLimit int
// MaxPages bounds ONE sweep of ONE shard.
//
// It is the periodic bound, and it is a bound on a PASS rather than on the
// work: a shard with more due rows than MaxPages*PageLimit is reported
// Truncated and finished by the next pass over that shard. That is chosen
// over draining the shard because the sweep is periodic and the alternative
// lets one busy shard hold the rotor -- and therefore every other shard's
// reconciliation -- for as long as its backlog lasts.
MaxPages int
}
ReconcilerConfig is one replica's due-command sweep configuration.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
func NewService ¶
func (*Service) AdmitCreate ¶
func (s *Service) AdmitCreate(ctx context.Context, principal identity.Principal, req sessionwire.CreateRequest) (sessionstore.DispositionInboxEntry, bool, error)
AdmitCreate admits a V1 create: runbook A3.1 steps 2 and 5.
It returns a DISPOSITION entry, not a legacy InboxEntry, and the difference is the whole point rather than a type detail. A create is the one command that CHOOSES a session's protocol, and the only choice that produces a session a Host can ever take residency on is disposition.
The payload ceiling is deliberately NOT checked here the way it is for the four existing commands. admissiblePayload refuses an oversized body because the legacy inbox cannot retain its identity; this path can, so an oversized create is stored by reference instead of refused. That is step 5.
func (*Service) AdmitGateResponse ¶
func (s *Service) AdmitGateResponse(ctx context.Context, principal identity.Principal, req sessionwire.GateResponseRequest) (sessionstore.InboxEntry, bool, error)
func (*Service) AdmitInput ¶
func (s *Service) AdmitInput(ctx context.Context, principal identity.Principal, req sessionwire.InputRequest) (sessionstore.InboxEntry, bool, error)
func (*Service) AdmitInterrupt ¶
func (s *Service) AdmitInterrupt(ctx context.Context, principal identity.Principal, req sessionwire.InterruptRequest) (sessionstore.InboxEntry, bool, error)
func (*Service) AdmitLegacyCreate ¶
func (s *Service) AdmitLegacyCreate(ctx context.Context, principal identity.Principal, req LegacyCreateRequest) (LegacyCreateResult, error)
func (*Service) AdmitRestore ¶
func (s *Service) AdmitRestore(ctx context.Context, principal identity.Principal, req sessionwire.RestoreRequest) (sessionstore.InboxEntry, bool, error)
type SessionBindingTemplate ¶
SessionBindingTemplate is the deployment-configuration half of the immutable SessionBinding a V1 create pins into its session, forever.
It carries only the two members a DEPLOYMENT owns. The other two are not configuration and are deliberately absent: RuntimeSessionID is derived per create (see deriveRuntimeSessionID) and ProtocolMode is this module's own choice, not a knob -- see bind.
Why these two are exactly the deployment's: they are the two members ObjectStoreResolver keys on. A resolver "refuses any configuration this deployment does not know", so a create that pinned a StorageBindingID the resolver cannot resolve would write a session whose objects are permanently unreadable -- and SessionBinding is immutable after create, so there is no repair. The composition therefore refuses this template without a resolver, which is what makes this configuration rather than a guess.
type Settlement ¶
type Settlement interface {
RejectCommand(context.Context, sessionstore.RejectCommandRequest) (sessionstore.InboxEntry, error)
}
Settlement is the one durable write this reconciler makes.
It is exactly one method, and the narrowness is the point rather than tidiness: the settlements a due row could invite are reject, claim, begin applying and complete, and three of those are a LEASE HOLDER's. A reconciler holds no session lease and names no epoch, so a seam offering them would be authority this package cannot legitimately exercise.
type SweepResult ¶
type SweepResult struct {
// Shard is the control shard this sweep visited.
Shard int
// Pages, Examined and Unreadable are the store's own cost report,
// accumulated across the pages this sweep read. Examined is rows the
// provider returned and Unreadable is rows the store could not vouch for;
// both are reported rather than folded, because Examined == Pages*PageLimit
// with nothing returned is a different state from "nothing is due".
Pages int
Examined int
Unreadable int
// Due is the number of readable due rows this sweep considered.
Due int
// Rejected counts the commands this sweep settled.
Rejected int
// Claimed and Released count the reconciliation claims this sweep took and
// gave back. They are separate counters rather than one, because a
// difference between them is the thing worth seeing.
Claimed int
Released int
// Dispositions counts every recorded outcome by kind. DispositionSettleable
// never appears in it.
Dispositions map[Disposition]int
// Queries counts the store calls this sweep made that reach a provider:
// due pages, claim acquisitions, claim releases and settlements.
// ControlShards is deliberately not counted -- it is a read of a decision
// the store already holds in memory and makes no provider request.
Queries int
// ReleaseFailures counts releases that failed.
//
// A release failure is deliberately SWALLOWED -- replacing a correct
// settlement with a release error would report the wrong thing about the
// command, and a claim licenses nothing, so a claim left to lapse costs a
// delayed takeover of work that was already safe to do concurrently. This
// counter is what keeps "swallowed" from meaning "invisible", which is the
// shape a destructive call site with no observable takes.
ReleaseFailures int
// Truncated reports that MaxPages was reached with a continuation still
// outstanding. The remaining rows are still due and are met by the next
// pass over this shard.
Truncated bool
}
SweepResult is what one sweep of one shard did, and what producing it cost.
type Target ¶
type Target struct {
Key sessionstore.HostTargetKey
Workload sessionstore.DesiredWorkload
}
Target is the configured immutable launch identity selected for a create. It is configuration, not current Host capacity and not an ownership claim.
type TargetResolver ¶
type TargetResolver interface {
ResolveAgent(context.Context, sessionwire.AgentID) (Target, bool, error)
IsKnown(context.Context, sessionstore.HostTargetKey) (bool, error)
}
type UUIDSource ¶
UUIDSource supplies the random identifiers Factory proposes, notably the SessionID it allocates before a Host exists to allocate one.