Help us shape the future of Go! Take the 2026 Go Developer Survey

admission

package
v0.3.0 Latest Latest
Warning

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

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

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

View Source
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

View Source
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.

View Source
var ErrInvalidReconcilerConfig = errors.New("admission: invalid reconciler configuration")

ErrInvalidReconcilerConfig reports a reconciler configuration refused before it could reach a store.

View Source
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.

View Source
var ErrLegacySessionUnsupported = errors.New("admission: the session is bound to the legacy protocol, which no Host can take residency on")

ErrLegacySessionUnsupported reports a command for a session whose immutable binding names the LEGACY protocol.

Every command this service admits is admitted into the DISPOSITION family, because that is the only family a Host can reach: a Host takes residency through AcquireResidency, which pins ProtocolModeDisposition, so a command admitted into the legacy inbox is one no Host will ever apply. A legacy-bound session cannot be created by this module at all (ErrLegacyCreateUnsupported), so the refusal is reached only for a session some other writer created. It is runtime_unavailable for the reason every other "no runtime this deployment runs can serve this" is: nothing about the request can change the answer.

Functions

func IsCode

func IsCode(err error, code sessionwire.ErrorCode) bool

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 Claims

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

CommandStore is the durable command plane every command but a create is admitted into. It is the DISPOSITION family and only that family: see ErrLegacySessionUnsupported for why a legacy inbox write would be a command no Host could ever apply.

PutCommandPayload is here, and not borrowed from PublicCreateStore, because an oversized input is stored by reference exactly as an oversized create is (runbook A3.1 step 5), and PublicCreates is an OPTIONAL seam: a composition serving no creates must still be able to admit a large input.

type Commands

Commands is the durable command plane admission uses.

Admission writes and reads the DISPOSITION family only. The two legacy methods left here, RejectCommand and ListDueCommands, belong to the legacy deadline sweep (Reconciler), which still settles legacy rows a store may already hold; nothing in this package admits into that family any more.

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

	// DispositionUnrecognized means the record is in a state this build does
	// not know. It is refused rather than guessed at, so a newer store's state
	// cannot be rejected by an older sweeper. Only the disposition sweep
	// reports it: the legacy predicate's arms are total over its own states.
	DispositionUnrecognized
)

func (Disposition) String

func (d Disposition) String() string

type DispositionDue added in v0.3.0

type DispositionDue interface {
	ControlShards() int
	ListDueDispositionCommands(context.Context, sessionstore.ListDueDispositionCommandsRequest) (sessionstore.DispositionDueCommandPage, error)
}

DispositionDue is the disposition family's service-plane due query. It is the same two methods as DueCommands for the same reasons, over the other family's index.

type DispositionReconciler added in v0.3.0

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

DispositionReconciler sweeps expired disposition commands in fixed control shards, round-robin, one shard per pass and at most MaxPages*PageLimit rows.

It REMEMBERS WHERE EACH SHARD'S PASS STOPPED, and that is what keeps it from starving. The due view is deadline-ordered from the head, and the head is exactly where the rows this sweep may NOT reject accumulate: applying commands (a successor's business) and claimed ones whose claim is still live, each filed at its long-past deadline. A sweep that re-read from the head every pass would, once a shard held more than MaxPages*PageLimit of those, never again reach a rejectable row behind them. So a truncated pass keeps the store's continuation for its shard and the next pass over that shard resumes from it; only a pass that reaches the end re-arms at the head against a fresh bound. The position is advice, like the rotor: a restarted replica starts every shard at the head and loses nothing but time.

func NewDispositionReconciler added in v0.3.0

func NewDispositionReconciler(cfg DispositionReconcilerConfig) (*DispositionReconciler, error)

NewDispositionReconciler validates a configuration before it can reach a store.

func (*DispositionReconciler) Sweep added in v0.3.0

Sweep reconciles the next control shard's expired disposition commands.

It is authorized once, as the cross-tenant sweep it is, for Reconciler.Sweep's reasons, and it releases every claim it took on every path.

type DispositionReconcilerConfig added in v0.3.0

type DispositionReconcilerConfig struct {
	Authorizer Authorizer
	Due        DispositionDue
	Settlement DispositionSettlement
	Claims     Claims
	Clock      Clock
	HolderID   string
	ClaimTTL   time.Duration
	PageLimit  int
	MaxPages   int
}

DispositionReconcilerConfig is one replica's disposition deadline sweep. Its members mean exactly what ReconcilerConfig's do.

type DispositionSettlement added in v0.3.0

type DispositionSettlement interface {
	RejectDispositionCommand(context.Context, sessionstore.RejectDispositionCommandRequest) (sessionstore.DispositionInboxEntry, bool, error)
}

DispositionSettlement is the one durable write the disposition sweep makes. It is the pre-dispatch rejection and nothing else, for Settlement's reason: every other disposition transition belongs to a residency holder.

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
}

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type LegacyCreateRequest

type LegacyCreateRequest struct {
	AgentID sessionwire.AgentID
	Blocks  json.RawMessage
}

type LegacyCreateResult

type LegacyCreateResult struct {
	SessionID sessionwire.SessionID
	Entry     sessionstore.InboxEntry
}

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

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 NewService(cfg Config) (*Service, error)

func (*Service) AdmitCreate

AdmitCreate admits a V1 create: runbook A3.1 steps 2 and 5.

It returns a DISPOSITION entry, and the difference from the legacy inbox 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. Every other command is then admitted into the same family (see admit), so there is no mixed-family session.

An oversized create is stored by reference instead of refused. That is step 5, and admit applies the same rule to the other four kinds.

func (*Service) AdmitGateResponse

AdmitGateResponse is runbook A3.1 step 4: a gate response needs the matching durable gate projection AND a fresh resident, accepting owner, or it is gate_not_resumable before anything is written. Moving the command into the disposition family changed where it is written and nothing about that rule.

func (*Service) AdmitLegacyCreate

func (s *Service) AdmitLegacyCreate(ctx context.Context, principal identity.Principal, req LegacyCreateRequest) (LegacyCreateResult, error)

type SessionBindingTemplate

type SessionBindingTemplate struct {
	StorageBindingID string
	BindingVersion   string
}

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

	// Resumed reports that this pass continued from the position an earlier
	// truncated pass over the same shard kept, rather than from the head.
	// Only the disposition sweep keeps positions; the legacy one always reads
	// from the head.
	Resumed 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

type UUIDSource interface {
	NewUUID() (string, error)
}

UUIDSource supplies the random identifiers Factory proposes, notably the SessionID it allocates before a Host exists to allocate one.

Jump to

Keyboard shortcuts

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