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

placement

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 placement decides where a session should run, and reconciles the durable desired state that decision implies.

It is split into a PURE half and an I/O half, and the split is the package's main structural claim. policy.go is a total function of a catalog record, an optional registry observation and a page of capacity reports; it reads no clock of its own, performs no store call, and cannot create anything. Every rule a reviewer has to check for correctness lives there, where a test drives it directly rather than through a store. reconciler.go holds the claim, the durable desired-state write and the workload controller call, and its only decision is which of policy.go's answers it acted on.

A CANDIDATE IS A HINT AND AN OWNER IS AN OBSERVATION. Nothing here is authority over a session: specification section 15 keeps the Host lease and its epoch fence as the correctness fence, and a placement claim suppresses duplicate scaling only. Every type this package hands out is therefore a REQUEST or a HINT — sessionstore.PlacementIntent for the controller, a capacity report for the caller that will attach — and none of them carries a lease epoch this package could pretend to hold.

Index

Constants

This section is empty.

Variables

View Source
var ErrAttachFailed = errors.New("placement: host answered the attach with a failure")

ErrAttachFailed classifies a candidate that ANSWERED the attach with a failure carrying no Core code -- a transport error reply such as centrifuge's ErrorInternal. A HostLinks implementation wraps it.

It moves placement on to the next candidate, like a coded refusal and unlike the ambiguous failures that abort. It is NOT a placement outcome and not a promise that the Host rolled back: host v0.2.1 sends it after a failed launch, but also after a rollback that could not complete, and after an attach that succeeded but whose observation could not be published -- the session may be RESIDENT there. What makes moving on safe is the session lease: a Host that still holds it makes the next candidate refuse epoch_mismatch, so no second residency can form, and the re-read converges on the owner. Aborting on it instead let one Host whose launches always fail -- and which therefore never loses capacity and is ranked first on every pass -- block placement for every session of its agent and runtime (B5 quality gate Q1).

View Source
var ErrAttachUnsupported = errors.New("placement: host did not advertise hostlink.attach")

ErrAttachUnsupported classifies a candidate that did not advertise hostlink.attach in its connect reply. A HostLinks implementation wraps it.

It is a CAPABILITY fact, not a refusal, and it excludes the candidate rather than failing the placement: a mixed fleet with some Hosts that predate attach must still place onto the ones that do. A Host that predates the method would resolve it as a channel and answer runtime_unavailable, which is why a candidate is never sent one it did not advertise -- the local refusal is the only way to tell "cannot" from "will not".

View Source
var ErrBindAfterAttach = errors.New("placement: the host attached the session but the bind that followed failed")

ErrBindAfterAttach reports an attach the Host accepted followed by a bind that failed -- refused by the Host, or refused LOCALLY by this replica's pool because it still routes the session to a different Host (a binding conflict). The session IS resident; what failed is this replica's route to it. It is reported rather than retried here because the attach is done and the next thing that needs a route -- a viewer, a delivery, the next sweep -- asks the registry, which now names the owner.

View Source
var ErrHostUnreachable = errors.New("placement: host could not be reached")

ErrHostUnreachable classifies a candidate this replica could not ask at all: the dial failed, the link is between connections, or this replica already holds as many links as it may. A HostLinks implementation wraps it.

It is the one transport failure that moves placement on to the next candidate, and it is reserved for failures BEFORE the request left this process. A failure after the request may have reached the Host is not this: the Host may have attached, and moving on would put a second attach in flight for no reason. That failure aborts the attempt instead, and the next sweep retries under the same idempotency key.

View Source
var ErrInvalidConfig = errors.New("placement: invalid reconciler configuration")

ErrInvalidConfig reports a Reconciler that cannot keep its own bounds. Configuration is checked by NewReconciler before any store call.

View Source
var ErrInvalidPendingSweeperConfig = errors.New("placement: invalid pending-work sweeper configuration")

ErrInvalidPendingSweeperConfig reports a pending sweeper that cannot keep its own bounds. It is a third sentinel beside ErrInvalidConfig and ErrInvalidSweeperConfig for the reason records.go gives for the second.

View Source
var ErrInvalidSweeperConfig = errors.New("placement: invalid record sweeper configuration")

ErrInvalidSweeperConfig reports a RecordSweeper configuration refused before it could reach a store.

It is a SECOND error in this package rather than a reuse of ErrInvalidConfig, and the duplication is deliberate. ErrInvalidConfig belongs to Reconciler, which places one session; a caller that composes both would otherwise be unable to tell which of the two it misconfigured from the sentinel alone, and the two have no configuration member in common.

View Source
var ErrNoWorkloadController = errors.New("placement: this replica reconciles no dedicated workloads")

ErrNoWorkloadController reports a dedicated session reaching a replica that creates no workloads.

It is H5's two-binary split made a runtime rule rather than a convention. The decision of 2026-09-04 puts the Kubernetes adapter in internal/placement/kubernetes and links it into cmd/controller ALONE: cmd/factory ships with a different ServiceAccount holding no workload create/delete RBAC, so it composes no controller and its import graph reaches no Kubernetes client package. A dedicated session that reaches that binary has therefore arrived somewhere that cannot serve it, and saying so by name is the only honest answer -- reporting it as "no capacity" would send a caller into a retry loop waiting for an autoscaler that will never run, and reporting success would claim a workload nobody created.

View Source
var ErrRegistryStale = errors.New("placement: the session lease is held elsewhere and the registry did not show the holder")

ErrRegistryStale reports that every re-placement this call was allowed ended in epoch_mismatch: the session's lease is held elsewhere and the registry has not caught up. The work is left for the next sweep, which re-reads the registry from the start.

Functions

func ReusableOwner

ReusableOwner reports whether a registry observation is this session's own live, admitting owner.

It is THE statement of that rule for the whole module: internal/admission asks the same question before delivering a gate response to a resident Host, and a second copy of the comparison would be free to admit an owner one caller routes to and the other does not.

Placement is compared along with agent and runtime because a record's desired placement is what its target key is built from; an observation under the other admission model is a Host the directory would never have offered. Migration between the two is deferred by specification section 14, so this reports false rather than arranging one — the caller re-places a session that is not resident anywhere, and never moves a running one.

The expiry is read against the caller's clock even though the directory already refuses an expired registration with the STORE's. The two answers differ only under skew, and the conservative one is right here: the cost of treating a live owner as absent is one re-placement that the Host lease then refuses, and the cost of the opposite is routing a command to a process that has lost its lease.

func WorkloadName added in v0.2.0

func WorkloadName(intent sessionstore.PlacementIntent) string

WorkloadName returns only the platform-safe name derived from intent.

Types

type AttachRefusal added in v0.3.0

type AttachRefusal struct {
	sessionwire.HostLinkError
}

AttachRefusal is a Host's own coded answer to an attach. A HostLinks implementation returns it for a Host's HostLinkError.

It embeds Core's record for the reason hostlink.HostRefusal does: Core already states which detail travels with which code, and a flat copy would be a second authority for that rule.

func (*AttachRefusal) Error added in v0.3.0

func (e *AttachRefusal) Error() string

type CandidateRefusal added in v0.3.0

type CandidateRefusal struct {
	HostID         sessionwire.HostID
	HostGeneration uint64
	Code           sessionwire.HostLinkErrorCode
}

CandidateRefusal is one candidate a placement asked and was refused by.

type Catalog

type Catalog interface {
	GetCatalogEntry(ctx context.Context, req sessionstore.GetCatalogEntryRequest) (sessionstore.CatalogEntry, error)
	UpdateCatalogDesiredState(ctx context.Context, req sessionstore.UpdateCatalogDesiredStateRequest) (sessionstore.CatalogEntry, error)
}

Catalog is the durable session record: the desired state a Factory authors and the record every decision here is made against.

type ClaimSweep

type ClaimSweep interface {
	ReleaseReconciliationClaim(context.Context, sessionstore.ReleaseReconciliationClaimRequest) (sessionstore.ReconciliationClaimEntry, error)
}

ClaimSweep is the one durable write the claim half makes.

It is Release and nothing else. Acquire is deliberately absent: a sweeper that could acquire would be a second placement reconciler, and the work a claim suppresses is Reconciler's. Get is absent for a sharper reason — GetReconciliationClaim refuses a lapsed claim rather than returning it (sessionstore@v0.8.0/reconcile.go:450), so it cannot answer the question this sweep asks, and a caller that read it first would still have to attempt the release to learn anything.

type ClaimSweepResult

type ClaimSweepResult struct {
	Examined int

	// Released counts the sessions whose claim is this replica's and is no
	// longer live afterwards. It deliberately does NOT separate "a write
	// happened" from "the record was already ours and lapsed": the store
	// answers both with success and no error, because a repeat release is the
	// ordinary case rather than a mistake, and a sweeper that reported them
	// differently would be reporting the reply it happened to race.
	Released int

	// Held counts the sessions another replica is reconciling right now.
	Held int

	// Lapsed counts another replica's claim that has run out. Nothing is
	// written for one and nothing needs to be: a lapsed claim is already
	// indistinguishable from no claim to every reader of this record.
	Lapsed int

	// Absent counts the sessions with no claim record at all.
	Absent int
}

ClaimSweepResult accounts for every session one SweepClaims call examined.

type Claims

Claims is the short-lived reconciliation claim of specification section 10.

type Clock

type Clock interface {
	Now() time.Time
}

Clock is the time seam. It declares only Now: this package sets a claim horizon and reads an owner's expiry, and schedules nothing.

type Config

type Config struct {
	Directory Directory
	Catalog   Catalog
	Claims    Claims

	// Workloads is OPTIONAL, and its absence is a supported deployment rather
	// than a degraded one: H5's cmd/factory composes exactly this. See
	// ErrNoWorkloadController.
	Workloads WorkloadController

	Clock Clock

	// HolderID names this replica in a 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 scaling work for.
	ClaimTTL time.Duration

	// CandidateLimit bounds one capacity page.
	CandidateLimit int

	// Links is OPTIONAL. With it, a pooled session with no live owner is
	// ATTACHED to the first ranked admissible candidate that accepts it and
	// bound with the epoch that Host answered (B5). Without it, the reconciler
	// only NAMES the candidate in Result.Decision, which is what it did before
	// any caller existed; a composition with no HostLink pool uses that.
	Links HostLinks

	// ActorID is the requesting SERVICE identity an attach carries. Required
	// when Links is set. It names who asked for residency, never whose
	// authority a later command carries: placement runs from a sweeper with no
	// live user behind it.
	ActorID string

	// ReplaceAttempts bounds how many times one call re-runs placement after an
	// epoch_mismatch, counting the first attempt. Zero takes a small default.
	ReplaceAttempts int

	// ReplaceBackoff is the wait before the first re-placement, doubled for
	// each further one. Zero takes a small default.
	ReplaceBackoff time.Duration

	// Wait sleeps for a backoff or until the context ends. Nil uses a timer. It
	// is a seam so a re-placement's backoff is measured rather than waited for.
	Wait func(context.Context, time.Duration) error

	// Logger receives the one diagnostic this package emits: a pooled
	// candidate excluded because it does not advertise hostlink.attach, named
	// by Host, so a mixed fleet is diagnosable. Nil discards.
	Logger *slog.Logger
}

Config is one replica's placement configuration.

type Decision

type Decision struct {
	Outcome Outcome
	Owner   sessionwire.HostLinkRegistryObservation
	Target  sessionwire.HostLinkCapacityReport
}

Decision is one placement answer together with exactly the record the outcome licenses acting on.

Owner is populated only for OutcomeReuseOwner and Target only for OutcomeAttachPooled; the other member stays zero. That is deliberate rather than tidy: a decision carrying both would let a caller act on the one its switch reached rather than the one the policy chose.

func Decide

Decide chooses where a session should run.

ownerObserved is separate from owner, and the separation is load-bearing: an absent registration is the ZERO observation, and a policy that read the value alone would compare an empty tenant and an empty agent against a record whose members a caller had not filled either, and reuse a Host that does not exist.

candidates arrive in the store's capacity rank, most free capacity first, and nothing here re-ranks them. The first ADMISSIBLE entry is selected, so two Factory replicas reading the same page choose the same Host with no leader and no shared state — which is what H5's "replicas are safe and no leader is introduced" rests on for the pooled half.

The page's liveness is the store's answer, not this function's: routing's Directory already drops lapsed advertisements against the store's clock, and re-deciding expiry here against a Factory clock would take a live Host out of service on skew. The owner's expiry IS re-read here, for the opposite reason ReusableOwner gives.

type Desired

type Desired struct {
	Placement              sessionwire.HostPlacement
	RuntimeCompatibilityID string
	Workload               sessionstore.DesiredWorkload
}

Desired is the desired state a caller wants stored for a session.

It is an INPUT rather than something this package derives, and that boundary is where A4.2 stops. What a dedicated session's workload payload should contain is a launch-template question owned by composition (A9.1); this package's job is that whatever the caller names is stored idempotently and handed to the controller with a generation.

type Directory

Directory is the observed target directory: which Host currently owns a session, and which Hosts could accept one.

type HostLinks interface {
	Attach(ctx context.Context, endpoint sessionwire.InternalEndpoint, req sessionwire.HostLinkAttachRequest) (sessionwire.HostLinkRegistryObservation, error)
	Bind(ctx context.Context, endpoint sessionwire.InternalEndpoint, req sessionwire.HostLinkBindRequest) error
	Unbind(ctx context.Context, req sessionwire.HostLinkUnbindRequest) error
	DeliverCommand(ctx context.Context, tenant sessionwire.TenantID, session sessionwire.SessionID, delivery sessionwire.HostLinkCommandDelivery) error
	// RouteFor reports whether this replica already routes the session, and
	// to which Host. Placement leaves a route it did not create in place.
	RouteFor(tenant sessionwire.TenantID, session sessionwire.SessionID) (sessionwire.HostID, bool)
}

HostLinks is the Host-facing half of pooled placement, stated in Core's vocabulary. The HostLink pool implements it through one adapter in the composition, which is also where transport errors are classified onto ErrAttachUnsupported, ErrHostUnreachable and *AttachRefusal: this package names no transport type, for the reason internal/routing's Binder gives.

type Outcome

type Outcome uint8

Outcome is the closed set of answers Decide gives.

It is an enum rather than a pair of booleans so a caller's handling is exhaustive by construction, and Undecided is a member rather than a zero to fall through: a record naming a desired placement this build does not understand must reach a caller as "no decision", not as the first arm of a switch that happens to be pooled.

const (
	// OutcomeUndecided means no rule here applies to the record, which today
	// means its desired placement is neither of the two admission models.
	OutcomeUndecided Outcome = iota

	// OutcomeReuseOwner means the session already has a live owner and no
	// placement is needed. Specification section 15 step 1.
	OutcomeReuseOwner

	// OutcomeAttachPooled names one ranked pooled candidate the caller should
	// ask to take the session. It is not a reservation: the candidate may
	// refuse, and only its lease acquisition decides ownership.
	OutcomeAttachPooled

	// OutcomeReconcileDedicated means one workload should exist for this one
	// session. Specification section 13.
	OutcomeReconcileDedicated

	// OutcomeNoCapacity means the target has no admissible seat right now.
	// It is not an error: pooled capacity is scaled by an autoscaler outside
	// this module, and the caller retries.
	OutcomeNoCapacity
)

func (Outcome) String

func (o Outcome) String() string

String renders the outcome for a log line. Every member renders distinctly, including an unrecognized one, so a decision is never reported as the empty string.

type PendingCommands added in v0.3.0

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

PendingCommands is the disposition inbox's service-plane due query.

type PendingSweepResult added in v0.3.0

type PendingSweepResult struct {
	Shard      int
	Pages      int
	Examined   int
	Unreadable int
	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.
	Resumed bool

	// Sessions is the number of distinct sessions this pass reconciled.
	Sessions int
	// Outcomes counts the decisions those reconciliations reported.
	Outcomes map[Outcome]int
	// Attached counts the sessions this pass attached to a Host.
	Attached int
	// Deferred counts the sessions another replica held the claim for.
	Deferred int
	// NoController counts dedicated sessions this replica cannot place because
	// it composes no workload controller (ErrNoWorkloadController).
	NoController int
	// Failures counts the sessions whose reconciliation returned an error. A
	// failure is per session and never stops the pass: one session whose
	// registry is stale must not keep every other session in the shard cold.
	Failures int
}

PendingSweepResult is what one pass over one shard did.

type PendingSweeper added in v0.3.0

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

PendingSweeper places the sessions with open commands, one control shard per pass, round-robin.

It REMEMBERS WHERE EACH SHARD'S PASS STOPPED, and without that it starves. The due view is deadline-ordered from the head, and the head is where rows this sweep skips pile up: an applying command past its deadline, a claimed one whose claim outlived it, and -- until the disposition deadline sweep retires them -- expired pending ones. A pass that always read from the head would, once a shard held more than MaxPages*PageLimit of those, never again reach a live create behind them (the B5 spec gate's F3: 2 expired rows ahead of a live create at 2 rows per pass, never attached across 5 passes). So a truncated pass keeps the store's continuation for its shard, the next pass over that shard resumes from it, and only a pass that reaches the end re-arms at the head against a fresh bound. Retirement keeps the head short; this is what makes the sweep correct even when it does not.

The cost is stated: a resumed pass carries the bound its cycle STARTED with, which is the store's rule for a continuation, so a command admitted after a long cycle began is met when the next cycle starts rather than immediately. The position is advice: a restarted replica starts every shard at the head.

func NewPendingSweeper added in v0.3.0

func NewPendingSweeper(cfg PendingSweeperConfig) (*PendingSweeper, error)

NewPendingSweeper validates a configuration before it can reach a store.

func (*PendingSweeper) Horizon added in v0.3.0

func (s *PendingSweeper) Horizon() time.Duration

Horizon and Logger report what this sweeper was composed with, for the composition's own tests (B5 spec gate S3): the horizon must be the apply deadline admission gives every command, or a command accepted up to now falls outside the due read.

func (*PendingSweeper) Logger added in v0.3.0

func (s *PendingSweeper) Logger() *slog.Logger

Logger reports the configured logger, nil when none was composed.

func (*PendingSweeper) Sweep added in v0.3.0

Sweep places every session with open work in the next control shard.

type PendingSweeperConfig added in v0.3.0

type PendingSweeperConfig struct {
	Authorizer SweepAuthorizer
	Pending    PendingCommands
	Placer     Placer
	Clock      Clock

	// Horizon is how far past now the due read reaches. It is the apply
	// deadline admission gives every command, so every command accepted up to
	// now is inside it.
	Horizon time.Duration

	// PageLimit bounds one due page and MaxPages one pass over one shard, with
	// admission.Reconciler's meaning: a shard with more open commands than
	// PageLimit*MaxPages is reported Truncated and finished by the next pass.
	PageLimit int
	MaxPages  int

	// Logger receives one line per session whose placement failed. Nil
	// discards.
	Logger *slog.Logger
}

PendingSweeperConfig is one replica's pending-work sweep.

type Placer added in v0.3.0

type Placer interface {
	Reconcile(ctx context.Context, req Request) (Result, error)
}

Placer is the per-session reconciliation a sweep drives. *Reconciler implements it.

type Reconciler

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

Reconciler places one session at a time.

It holds no per-session state. Everything it decides from is read within the call: the catalog record, the registry observation and one capacity page. A replica that restarts mid-placement leaves only a claim, which lapses.

func NewReconciler

func NewReconciler(cfg Config) (*Reconciler, error)

NewReconciler validates a configuration before it can reach a store.

func (*Reconciler) ActorID added in v0.3.0

func (r *Reconciler) ActorID() string

ActorID and Logger report what this reconciler was composed with. They exist for the composition's own tests: the actor an attach names and the logger an exclusion is written to are both decided by the root package, and neither is observable from outside a real Host exchange otherwise (B5 spec gate X4, Q2).

func (*Reconciler) Logger added in v0.3.0

func (r *Reconciler) Logger() *slog.Logger

Logger reports the configured logger, nil when none was composed.

func (*Reconciler) Reconcile

func (r *Reconciler) Reconcile(ctx context.Context, req Request) (Result, error)

Reconcile routes or places one session.

The order is specification section 15's routing algorithm and the deviations from a naive reading are all deliberate:

  1. The record and the owner are read FIRST, and a live owner returns without taking a claim at all. An owned session is the common case, and putting a durable compare-and-swap in front of it would serialize routing behind a record that exists to coordinate SCALING.
  2. Only then is the claim taken. Losing it is not a failure: step 4 of the algorithm says a replica that observes an existing claim re-reads desired and observed state rather than scaling again, so the deferral path can report an owner the winner has just produced for the generation it wrote.
  3. Desired state is written before the decision, because the write can change the desired placement the decision branches on.

With Links composed, the pooled arm ATTACHES (B5): see placePooled for the sequence and attachAnswer for what each Host answer means. The attach is gated on the candidate having advertised hostlink.attach, which the HostLink transport enforces locally, so a Host that predates the method is excluded rather than asked. Without Links, OutcomeAttachPooled only names the candidate for the caller, as it did before any caller existed.

type RecordSweeper

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

RecordSweeper sweeps the two CONTROL RECORDS placement leaves behind: a crashed Host's target advertisement and this replica's own placement claim.

IT IS NOT Reconciler AND DOES NOT EXTEND IT, which is worth stating because both live in this package and both are called "reconciliation" upstream. Reconciler's subject is a SESSION: it reads a catalog record, takes that session's claim and drives a workload controller. This one's subject is a RECORD, it names no session of its own in the target half, and it makes no placement decision at all. They were kept separate rather than fused because a fused type would have to hold both a per-session request and a directory-wide continuation, and the continuation is the one piece of state this module carries between calls — putting it on the type that is invoked once per session would make its lifetime a function of traffic.

func NewRecordSweeper

func NewRecordSweeper(cfg SweeperConfig) (*RecordSweeper, error)

NewRecordSweeper validates a configuration before it can reach a store.

func (*RecordSweeper) SweepClaims

func (s *RecordSweeper) SweepClaims(ctx context.Context, refs []SessionRef) (ClaimSweepResult, error)

SweepClaims releases this replica's own placement claims for the named sessions.

ITS REACH IS THIS REPLICA'S OWN CLAIMS, and that is a limit of the pinned store rather than a choice. A reconciliation claim is filed with an EMPTY due state (sessionstore@v0.8.0/reconcile.go:708 reconciliationClaimDue), so there is no deadline view to walk and no operation that enumerates claims at all: the sessions have to come from the caller. And a release is refused for any holder but the claim's own, so another replica's lapsed claim can be reported but not written back. Nothing is lost by that -- a lapsed claim is already indistinguishable from no claim to every reader of the record -- but the counts say which happened, because "swept" and "was already harmless" are different facts to an operator.

A FAILURE ENDS THE PASS rather than skipping the session. The four dispositions below are outcomes; anything else is the store declining to answer, and walking the rest of the list would spend one request per session against a store that has already said it cannot serve one.

func (*RecordSweeper) SweepTargets

func (s *RecordSweeper) SweepTargets(ctx context.Context) (TargetSweepResult, error)

SweepTargets runs one bounded pass of the directory's deadline view.

THE CONTINUATION IS THE ONLY STATE THIS TYPE CARRIES BETWEEN CALLS, and it is carried rather than dropped because a page budget alone leaves this sweep able to make no progress at all. A row the store steps over -- one a heartbeat saved, one whose compare-and-swap lost, one it could not decode -- stays in the deadline view, so a sweeper that restarted at the head on every pass would meet that row again with its whole budget and never reach the rows behind it.

What is NOT retained is a judgement. This call returns what the store did; it does not remember which Hosts were due, and a later placement decision reads the directory rather than anything recorded here.

type Request

type Request struct {
	TenantID  sessionwire.TenantID
	SessionID sessionwire.SessionID
	Desired   *Desired

	// Wake is the already accepted commands to deliver once the session has a
	// route: after an attach this call made, or to a live owner it found. It
	// is a HINT -- delivery wakes a Host's consumption of its durable inbox and
	// carries only the retry-stable public CommandID -- so an empty Wake still
	// places, and a failed delivery is counted, not returned.
	Wake []sessionwire.CommandID
}

Request is one reconciliation.

Desired is optional. Nil means "reconcile what the record already says", which is the ordinary path for a session whose desired state was written at creation and has not changed since; no desired-state write is attempted at all in that case, so the generation cannot move.

type Result

type Result struct {
	// Decision is the placement answer. It is the zero Decision when Deferred.
	Decision Decision

	// Deferred reports that another replica holds this session's claim and
	// this one did nothing. It is not an error: it is the claim working.
	Deferred bool

	// ClaimExpiresAt is the holder's horizon, populated only when Deferred. It
	// is how long a caller can wait before the work is anyone's again.
	ClaimExpiresAt time.Time

	// DesiredWrites counts the desired-state write calls this reconciliation
	// made: zero when the record already carried the intent, one for an
	// ordinary application, and two when a racer's different intent forced one
	// re-read and retry.
	//
	// It counts CALLS rather than reporting "this replica applied it", and the
	// difference is not a hedge. Two replicas deriving one intent derive one
	// idempotency key, so the loser's write is absorbed as a replay of the
	// winner's and the two are indistinguishable in the record they leave --
	// which is the whole point of the derived key. What is observable, and what
	// the caller can act on, is how much work this call did.
	DesiredWrites int

	// Intent is the stored desired state handed to the controller, populated
	// only for OutcomeReconcileDedicated.
	Intent sessionstore.PlacementIntent

	// Attached is the Host's observation of the residency an attach made by
	// THIS call produced. Its lease epoch is the one the bind named. Zero when
	// this call attached nothing.
	Attached sessionwire.HostLinkRegistryObservation

	// Bound reports that this call bound a route: to the Host it attached, or
	// to a live owner it woke.
	Bound bool

	// Delivered and DeliveryFailures count the Wake deliveries.
	Delivered        int
	DeliveryFailures int

	// Excluded names the admissible candidates skipped because they do not
	// advertise hostlink.attach; Unreachable those this replica could not ask;
	// Refused those that answered with a HostLinkError; Failed those that
	// answered with a failure carrying no code (ErrAttachFailed). Each is in
	// the order asked.
	Excluded    []sessionwire.HostID
	Unreachable []sessionwire.HostID
	Refused     []CandidateRefusal
	Failed      []sessionwire.HostID

	// Replacements counts the times placement re-ran after an epoch_mismatch.
	Replacements int
}

Result is what one reconciliation did.

type SessionRef

type SessionRef struct {
	TenantID  sessionwire.TenantID
	SessionID sessionwire.SessionID
}

SessionRef names one session for the claim half.

type SweepAuthorizer added in v0.3.0

type SweepAuthorizer interface {
	AuthorizeServiceSweep(ctx context.Context, principal identity.Principal) error
}

SweepAuthorizer decides the sweep, as it does for every other cross-tenant sweep this module runs.

type SweeperConfig

type SweeperConfig struct {
	Targets TargetSweep
	Claims  ClaimSweep

	// HolderID names this replica in a claim release. It must be the SAME
	// string this replica's Reconciler puts in Config.HolderID, because a
	// release is refused for any other holder and the whole reach of this
	// sweep is the claims this replica itself left behind.
	HolderID string

	// PageLimit bounds one due page.
	PageLimit int

	// MaxPages bounds one SweepTargets call.
	MaxPages int
}

SweeperConfig is one replica's record-sweep configuration.

type TargetSweep

type TargetSweep interface {
	ReconcileHostTargets(context.Context, sessionstore.ReconcileHostTargetsRequest) (sessionstore.HostTargetReconcileResult, error)
}

TargetSweep is the service-owned directory sweep.

It is ONE method and it is the store's, not this package's, which is the whole structural claim of the target half. SessionStore revalidates every due row against that row's OWN STORED EXPIRY at a fresh clock reading and closes the write with a compare-and-swap onto the revision the due page reported (sessionstore@v0.8.0/host_targets.go:1631 reconcileHostTargetRow).

THE TWO MECHANISMS CATCH DIFFERENT HEARTBEATS, and the distinction was stated wrongly here before. reconcileHostTargetRow takes the due page's FROZEN BYTES (stored storage.OrderedRecord) and NEVER RE-READS THE ROW; only the clock it compares against is fresh. So a heartbeat that landed AFTER the page was read cannot present an unlapsed expiry to the revalidation at all -- its only catcher is the compare-and-swap, which loses against the revision that heartbeat advanced. The expiry revalidation catches the EARLIER landing: one already carried in the page's bytes when they were captured, which a weakly consistent due view can still name as due.

The conclusion is unchanged and rests on the pair: Factory must not re-derive that judgement, because a due observation this side of the seam is a hint about which rows to LOOK at and is never evidence that a Host is gone.

type TargetSweepResult

type TargetSweepResult struct {
	// Scanned is every row the store looked at. It is the sum of the five
	// outcomes below, which the store asserts for itself.
	Scanned int

	// Unranked is the store's Withdrawn: rows this sweep removed from the
	// placement rank.
	Unranked int

	// Retained is the store's StillLive: rows the revalidation refused to
	// withdraw. A nonzero value is the heartbeat race being won by the Host,
	// which is the correct outcome and not a failure.
	Retained int

	Contended  int
	Unreadable int
	Unverified int

	// Exhausted reports that the sweep reached the end of the deadline view.
	Exhausted bool

	// Resumed reports that this call presented a continuation retained from an
	// earlier one. It is observable state rather than a log detail: a sweeper
	// that never resumes makes no progress past its first page budget, and
	// nothing else about the result would say so.
	Resumed bool
}

TargetSweepResult is what one SweepTargets call did.

Unranked and Retained are RENAMED rather than passed through, and the names are the point of the type. The store's Withdrawn is the count of rows whose own stored expiry had lapsed at the revalidation instant and whose compare-and-swap then won -- those, and only those, left the placement rank. StillLive is the count the revalidation SAVED: rows a due page named and a heartbeat rescued. Anything that reported the due page's own count as "removed" would be claiming a Host is gone on the strength of an observation that is explicitly weakly consistent.

type WorkloadController

WorkloadController is the consumer-owned domain seam for a dedicated workload's lifecycle.

Every argument and result is a Core or SessionStore record. In particular, the platform adapter receives the Factory-authored PlacementIntent and returns the Host-owned registry/drain observations; no Kubernetes, Nomad or other platform type can cross this boundary. The adapter may use the intent to derive its own platform object name, but the placement package never sees that object.

EnsureWorkload is idempotent for an unchanged intent. ObserveWorkload reports whether a Host registry observation exists for that intent. RequestDrain is the only lifecycle operation that initiates Host draining, and DeleteWorkload is called only after the caller has observed a completed drain. Keeping the ordering in the caller makes a platform adapter unable to treat a transport close or a platform deletion as graceful release.

type WorkloadIdentity added in v0.2.0

type WorkloadIdentity struct {
	Name                   string
	TenantID               sessionwire.TenantID
	SessionID              sessionwire.SessionID
	AgentID                sessionwire.AgentID
	RuntimeCompatibilityID string
	Generation             uint64
}

WorkloadIdentity is the stable identity of one desired workload revision.

Name is deliberately a digest rather than a concatenation of user-controlled identifiers. TenantID and SessionID may contain characters that are legal in Core but unsafe in platform names, and leaking either into a workload name also exposes tenant data to platform operators. The source fields remain available to a domain caller for correlation; only Name is intended for use as a platform object name.

func DeriveWorkloadIdentity added in v0.2.0

func DeriveWorkloadIdentity(intent sessionstore.PlacementIntent) WorkloadIdentity

DeriveWorkloadIdentity deterministically derives one platform-safe workload identity from a desired placement intent.

The framing is length-prefixed and ordered, so delimiters or UTF-8 bytes in tenant/session/agent/runtime identifiers cannot create an alternate preimage. Desired generation is included as a fixed-width integer: a changed desired revision receives a distinct workload identity, while a restart deriving the same revision receives the exact same name. The 56 hexadecimal digest bytes plus the prefix fit Kubernetes' 63-character DNS-label limit and remain valid for stricter platform name rules as well.

Jump to

Keyboard shortcuts

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