Documentation
¶
Overview ¶
Package lifecycle owns this Host's drain: the single-owner state machine that stops admission, releases every resident session in order, and reports when the Host is drained.
IT DEPENDS ON NO UNPUBLISHED CODE, which is the whole of runbook 04's O6.3 title. The top-level `drain` repository is a tier-0 leaf with no configured origin, so this package names narrow local interfaces and nothing else; if `github.com/looprig/drain` is ever released, an adapter satisfying those interfaces is a separate, additive commit and NOT a rewrite of anything here.
IT DOES NOT SELF-EXIT. A drain is something a placement controller or a platform asks this Host to do; a Host that decided on its own that it had been idle long enough and terminated would, in a pooled Deployment, be restarted by its controller and do it again — a restart loop that reads as instability rather than as the idleness that caused it.
WHAT IT DOES NOT OWN. This package holds no admission ledger and no registry of its own. internal/service's CapacityPublisher is the Host's ONE ledger and its ONE drain flag, and its own comment says so; a second "draining" would be a Host that stops accepting in one place while advertising Accepting from the other. Everything here is either injected or derived from what is injected. It reads neither half of registry.Entry's epoch/accepting seam either — see TestTheDrainReadsNeitherHalfOfTheRegistryEntrySeam for why both are deliberate abstentions rather than omissions.
WHAT IT CANNOT DO, stated so it is not mistaken for done. The runbook's 2026-09-05 hold names a Shutdown fallback for the case where graceful release REFUSES. department.Runtime declares no terminal capability — Identity, IdleWaiter, Liveness, Releaser, PublicationSubscriber, CommandApplier, LeaseEpochReporter — and department's own comment on Releaser records that residency release "is NONTERMINAL and is not Shutdown". A Shutdown method on Session here would therefore be a seam nothing in this module can satisfy. The refusal is RECORDED as its own Failure so a reconciler can find it; the fallback belongs to H4.1 and to O7.1's composition.
Index ¶
- Variables
- type Admissions
- type Advertiser
- type Attaches
- type BlockedConsumers
- type Clock
- type CommandBacklog
- type ConsumptionHalter
- type Drainer
- type Ender
- type EpochReporter
- type Failure
- type GateWaits
- type InvalidOptionsError
- type Ledger
- type MemoryBudget
- type Metrics
- type MetricsOptions
- type Options
- type ReleaseLedger
- type Report
- type Residencies
- type Residents
- type Session
- type Snapshot
- type Step
Constants ¶
This section is empty.
Variables ¶
var ErrIdleBoundary = errors.New("lifecycle: the session did not reach its safe boundary in time")
ErrIdleBoundary marks a wait ended by the configured boundary rather than by the session, so a caller tells "this session would not settle" from "this session reported an error while settling" with errors.Is.
var ErrPublishBound = errors.New("lifecycle: the nonaccepting publication did not complete within its bound")
ErrPublishBound reports a nonaccepting publication that did not answer within Options.PublishBound. The drain is REFUSED: this Host cannot say the write landed, and a drain that proceeded on that assumption would release sessions a Factory is still routing work to.
var ErrWaitAbandoned = errors.New("lifecycle: the session did not answer cancellation before the platform grace expired")
ErrWaitAbandoned marks an idle wait this drain STOPPED WAITING ON because the platform grace expired before the session answered its cancellation.
It is a strictly worse condition than ErrIdleBoundary alone and is reported alongside it, never instead of it. The session's WaitIdle goroutine may still be running: this Host has given up on hearing from it, which is what makes Grace a bound rather than a request.
Functions ¶
This section is empty.
Types ¶
type Admissions ¶
type Admissions interface {
// BeginDrain stops admission and unranks every advertisement. It does not
// evict: sessions already admitted stay charged until their own release.
BeginDrain()
// Draining reports whether graceful drain has begun.
Draining() bool
}
Admissions is the Host's admission ledger and its drain flag.
It is the SAME OBJECT internal/service owns, narrowed to two methods. service.CapacityPublisher satisfies it and the conformance assertion lives in this package's test, so production here does not name internal/service.
type Advertiser ¶
type Advertiser interface {
// PublishNonaccepting returns once every target advertisement and registry
// row this Host publishes says accepting=false, durably.
PublishNonaccepting(context.Context) error
}
Advertiser publishes this Host's stopped admission where a Factory reads it.
IT IS THE DURABILITY BOUNDARY OF THE ACKNOWLEDGEMENT. Admissions.BeginDrain flips an in-process flag; this is what makes the consequence visible outside this process, and StartDrain does not acknowledge until it has returned. A Host that acknowledged first would have told a placement controller it had stopped accepting while a Factory reading the orchestration store still saw an accepting, ranked target.
IT IS NOT registry.Entry.Accepting. The rows this publishes are the orchestration store's; the local registry index is an optimization whose accepting flag is a strict function of its own state.
type Attaches ¶ added in v0.15.1
type Attaches interface {
// AwaitAttaches blocks until every attach in flight at the moment of the
// call has finished — resident or rolled back — or the context ends, and
// then reports the context's error, naming what is still in flight. How a
// rollback went is the ReleaseLedger's to report.
AwaitAttaches(context.Context) error
// AwaitSessionAttaches is AwaitAttaches for ONE session. The drain calls it
// before it releases a session from its snapshot, because an attach that
// had already COMMITTED when the snapshot was read is in the snapshot and
// still running its last steps: released beneath it, its final
// `resident` publication landed AFTER the drain's tombstone, and it
// reported the session attached on a Host that had just released it.
AwaitSessionAttaches(context.Context, registry.Key) error
// CancelAttaches cancels every attach that has not yet become resident, so
// it stops waiting on its collaborators and rolls back. It must never
// cancel a residency that is already in the resident set: that one is the
// drain's to release.
CancelAttaches()
}
Attaches is the optional Residents capability that accounts for the attaches IN FLIGHT when the drain began.
THE RESIDENT SNAPSHOT DOES NOT COVER THEM, and that gap was a real wedge (the tests lane's C1). An attach that passed admission before Admissions.BeginDrain and was still hydrating when ResidentSessions was read is in neither set the drain acts on: the ledger admitted it, and the snapshot missed it. If it then became resident it was never released, the drain reported `drained`, and its runtime kept its journal lease for the life of the process — so every successor's hydrate was refused.
THE CONTRACT THAT CLOSES IT IS SPLIT between this seam and its implementer. The implementer refuses, atomically with the snapshot, any attach that would become resident once the drain has begun, so an attach in flight can only roll back; the drain waits for those rollbacks before it reports `drained`, because until one has finished its lease and runtime are still held.
type BlockedConsumers ¶
type BlockedConsumers interface {
// BlockedSessions returns the number of resident sessions whose last
// consumption pass ended blocked at a command.
BlockedSessions() uint64
}
BlockedConsumers reports how many resident sessions have a durable command consumer that has stopped at a command it will not process.
IT IS THE WEDGE'S ONLY OPERATIONAL SIGNATURE, and it exists because that condition had none. A blocked pass returns a NIL ERROR, so Consumer.Failures() stays at zero; nothing in internal/commands logs; and the HostLink delivery that triggered it was answered `accepted`. Without this gauge a wedged session is indistinguishable on every exported signal from a healthy Host with an empty inbox, and the first operator to meet it would have to diagnose it from source.
ITS MEANING INVERTED WHEN THE DISPATCH BOUNDARY WAS REMOVED, and the HELP string a dashboard actually renders is the one that has to say so. A composed Host once refused EVERY dispatch, so a non-zero value was the designed steady state and expected to equal the number of sessions holding work. It now dispatches and settles, so a non-zero value is a FAULT: a command reached the runtime and could not be settled, or could not be dispatched at all. An operator reading the old sentence would have treated a real wedge as normal.
IT IS OPTIONAL IN SHAPE AND WIRED IN FACT, as GateWaits now is too: the composition satisfies it, so the gauge is live in a real binary rather than reserved for a later one.
A NON-ZERO VALUE IS NOT AN ERROR. It says work is durably present and this Host is not advancing past it — which is expected while dispatch is refused, and is a fault only once an applier exists. Alert on it changing meaning, not on it being non-zero.
type Clock ¶
type Clock interface {
// After returns a channel that receives once the duration has elapsed.
After(time.Duration) <-chan time.Time
}
Clock is the time source, so every bound in this package is testable without a sleep.
type CommandBacklog ¶
type CommandBacklog interface {
// Backlog returns the durable lag and the local queue depth.
Backlog() (lag uint64, depth uint64)
}
CommandBacklog reports how far behind durable command consumption is and how deep the local queue has grown. It is OPTIONAL for the same reason GateWaits is: internal/commands owns both numbers and the composition that wires them is O7.1's.
type ConsumptionHalter ¶ added in v0.8.1
ConsumptionHalter is the optional step a Session offers to stop applying durable commands for the rest of its drain.
IT IS THE WARM RELEASE'S STEP 0, AND THE DRAIN USED TO HAVE NO EQUIVALENT. The drain stopped a session's command consumer only inside ReleaseResidency, AFTER the idle wait and the checkpoint, so a command admitted while the drain waited or checkpointed was applied by the draining Host under its own epoch while its registration said `releasing` (measured by the tests lane, I2.3). Halting first means what the drain checkpoints is the session's last state under this Host, and anything admitted later stays pending for the successor.
The halt waits for a pass already in flight and must honour cancellation; the drain bounds it by the idle boundary and the platform grace. A halt that is cut short leaves consumption halted — only the in-flight pass may still land. The drain never takes a halt back: no drain path returns a session to resident, so there is nothing to resume.
type Drainer ¶
type Drainer struct {
// contains filtered or unexported fields
}
Drainer is this Host's single drain, and is safe for concurrent use.
A HOST DRAINS ONCE. Every repeat — a retry, a reconnect after an ambiguous reply, a second placement controller, a request that arrives after the Host is already drained — is answered with the same generation and the current state rather than starting a second machine. That is why StartDrain's idempotency lives here and not in HostLink: the transport has no way to know whether the work it acknowledged is still running.
func NewDrainer ¶
NewDrainer validates the options and returns a Drainer that has not begun.
func (*Drainer) BegunScope ¶
BegunScope reports the scope the call that BEGAN this drain named.
IT EXISTS FOR ONE ATTRIBUTION AND NOT AS A GENERAL ACCESSOR. hostlink's drain resolver refuses a fixed-session drain unless the requesting tenant currently HOLDS that session — R-1's rung 8 — and that read is correct for BEGINNING a Host-wide drain. Applied unchanged to the read-only observation it makes the TERMINAL answer unreachable: the drain releases the session, so the tenant that began the drain stops holding it at exactly the moment `drained` becomes true, and is refused the one answer it is waiting for.
WHAT IT LETS THROUGH IS A STRICT SUBSET OF WHO ALREADY KNOWS. A caller matches only by naming the tenant AND session that BEGAN this drain, and only that caller ever received the acknowledgement. A tenant that never held the fixed session began nothing, matches nothing, and is refused exactly as before and indistinguishably. So this discloses nothing to anyone who did not already have it, which is why it may relax a rung whose whole purpose is non-disclosure.
THE SECOND RETURN IS NOT "a drain has begun". It is "a drain has begun AND it named a scope". The process-lifecycle drain passes the zero DrainScope, so a zero Key would otherwise match a caller that named nothing — and rungs above guarantee a real caller's key is never zero, so a zero match could only ever be an accident.
func (*Drainer) ObserveDrain ¶
func (d *Drainer) ObserveDrain(hostlink.DrainScope) (hostlink.DrainStatus, bool)
ObserveDrain reports this Host's drain and starts nothing.
func (*Drainer) StartDrain ¶
func (d *Drainer) StartDrain(scope hostlink.DrainScope) (hostlink.DrainStatus, error)
StartDrain begins this Host's drain and acknowledges the initiation.
EVERYTHING BEFORE THE GOROUTINE IS THE ACKNOWLEDGEMENT'S PRICE, and the split is the contract O5.4 step 2 states: admission is stopped and nonaccepting is durably published before this returns, and no checkpoint, no ReleaseResidency and no tombstone has run when it does.
THE TRANSITION IS NOT OBSERVABLE HALF-DONE, and what makes that true is the ORDER OF THE STATE WRITES under the lock, not the lock being held for the whole thing. The ledger flip happens under the lock before the publication starts; `begun`, `done` and the resident snapshot are written under the lock only after it has returned. So no observer can see nonaccepting published by a Host still admitting, and none can see a drain that has not been published. A concurrent StartDrain cannot begin a second machine because `starting` is set in the same critical section as the ledger flip.
The publication itself runs WITHOUT the lock, deliberately: holding it there blocked ObserveDrain and Wait for the whole publication, which made the bounded status observation unbounded. TestTheDrainTransitionIsNotObservableHalfDone probes the interleavings and carries the positive control that makes its silence mean something, and TestTheStatusObservationIsAnsweredWhileTheDrainIsStillPublishing holds the half that regressed.
THE SCOPE IS NEITHER RECORDED NOR BRANCHED ON, and the parameter is UNNAMED so that neither is expressible. An earlier sentence here said "recorded and not branched on"; the first half was never true. Every drain this type runs is the whole Host's, whatever scope it is handed.
That is sound only because HostLink refuses, above this seam, every scope this Host cannot attribute to the caller — a whole-Host drain on a tenant-authenticated link, and a fixed session the requesting tenant does not hold. A dedicated Host holds one session (its placement pins Capacity to 1), so draining that session and draining the Host are then the same work.
type Ender ¶ added in v0.15.1
type Ender interface {
Ended() bool
}
Ender is the optional Session method that reports a residency whose ATTACH ROLLED BACK after it had committed into the drain's snapshot — while the drain waited for it. That rollback went through the residency's one release authority, so there is nothing left for the drain to release; what it could not give back is on the Host's ReleaseLedger, not here.
IT IS NOT "the residency is gone". A residency another path removed — a warm release, a teardown — is not ended in this sense and answers false, so the drain's own release steps run and share that path's outcome.
type EpochReporter ¶ added in v0.8.1
type EpochReporter interface {
ResidencyEpoch() uint64
}
EpochReporter is the optional Session method that names the RESIDENCY epoch the session is held under, so each Failure records the epoch it happened at.
It is spelled ResidencyEpoch and never LeaseEpoch on purpose: this package reads neither half of registry.Entry's two-domain LeaseEpoch (see TestTheDrainReadsNeitherHalfOfTheRegistryEntrySeam), and the value here is the residency grant's — never the runtime's journal epoch.
type Failure ¶
type Failure struct {
Key registry.Key
Step Step
Err error
// ResidencyEpoch is the residency epoch the session was held under when
// the step failed, or zero when the session does not report one
// (EpochReporter). It is what lets an operator tie a forced,
// crash-equivalent release — which journals no SessionResidencyReleased —
// to the grant its successor fences. It is never a journal epoch.
ResidencyEpoch uint64
}
Failure is one step that did not succeed.
IT IS RECORDED RATHER THAN RETURNED, and that is the runbook's "record explicit failures and continue deterministic cleanup". A drain that abandoned the rest of a session on its first failure would turn one unreleased resource into several, and a drain that reported success while leaving a live route with no owner would be worse than either.
type GateWaits ¶
type GateWaits interface {
GateWaiting() uint64
}
GateWaits reports how many resident sessions are waiting on a gate.
It is OPTIONAL IN SHAPE AND WIRED IN FACT as of host v0.8.1: the composition satisfies it from each resident session's gate publisher fold, the same fold the derived work-state source reads. Before that nothing wired it and the gauge read zero while a session was parked at a gate (tests lane I2.3, finding 2). A Host composed without a source — or without gate publishing — still reports zero, which is absent rather than none.
type InvalidOptionsError ¶
InvalidOptionsError reports an option a Drainer may not run with.
func (*InvalidOptionsError) Error ¶
func (e *InvalidOptionsError) Error() string
type Ledger ¶
type Ledger interface {
ConsumedWeight() uint64
}
Ledger is the Host's ONE admission ledger, narrowed to the charge.
IT IS NOT RECOUNTED HERE. internal/service owns the only ledger and its own documentation names this task as a required consumer: two sources of "consumed" is an over-admission bug that neither package's tests could see, because each would be internally consistent while the Host admitted past its capacity. service.CapacityPublisher satisfies this as written.
type MemoryBudget ¶
type MemoryBudget struct {
// CaptureCeilingBytes is the per-fallback bound a bounded-materialized
// target enforces on its highest-output tool.
CaptureCeilingBytes uint64
// MaxParallelFallbacks is how many such materializations may be in flight
// at once.
MaxParallelFallbacks uint64
}
MemoryBudget is 04-host.md's weighted memory limit for a pooled Host: "capture ceiling × maximum parallel materialized fallbacks" for non-streaming bounded tools.
NEITHER FIELD CAN BE DERIVED FROM department.Capabilities TODAY, and that is stated rather than papered over. Capabilities carries CaptureSafety — which is the CLASS of the highest-output tool — and no ceiling and no fallback parallelism, so this Host cannot compute a budget from what a target declares. The seam is here: a composition that knows those two numbers supplies them, and a Host that does not know them reports an UNKNOWN budget rather than a default.
THE REJECTION HALF OF THE RUNBOOK'S RULE IS ALREADY HELD ELSEWHERE, and this type does not duplicate it. "Reject pooled admission for an unknown or unbounded descriptor rather than assuming preview size" is enforced by department.Capabilities.Validate, which refuses SupportsPooled alongside an unknown or unbounded CaptureSafety outright, and by service.placementSupported, which refuses admission for a target that does not support this Host's placement. What is left for this type is the SIZE of the budget, and an unknown size must not become a number.
func (MemoryBudget) Bytes ¶
func (b MemoryBudget) Bytes() (uint64, bool)
Bytes returns the budget and whether it is known.
UNKNOWN IS (0, false) AND NOT A DEFAULT. A caller that read the zero as a budget would admit without bound, which is the failure the runbook's "rather than assuming preview size" names.
The multiply SATURATES. Two plausible operational numbers — a large ceiling and a modest parallelism — multiply past MaxUint64 and wrap to a tiny budget, which is a limit that refuses nothing while reading like one that does. Saturating to MaxUint64 is honest about the same thing the operator meant: more than this process has.
type Metrics ¶
type Metrics struct {
// contains filtered or unexported fields
}
Metrics reports this Host's load, and is safe for concurrent use.
HOST DOES NOT SCALE ITSELF. 04-host.md step 4: an HPA may consume these for pooled replicas, and scaling out creates capacity for NEW sessions — it does not migrate sessions pinned in an existing process. Nothing here acts on a number it publishes.
func NewMetrics ¶
func NewMetrics(options MetricsOptions) (*Metrics, error)
NewMetrics validates the options and returns the collector.
func (*Metrics) Collect ¶
func (m *Metrics) Collect(out chan<- prometheus.Metric)
Collect publishes this instant's snapshot.
func (*Metrics) Describe ¶
func (m *Metrics) Describe(out chan<- *prometheus.Desc)
Describe sends every descriptor this collector can publish.
THE MEMORY BUDGET IS DESCRIBED EVEN WHEN IT IS UNKNOWN, because Describe is about the collector's shape and Collect is about this instant. A descriptor withheld here would make registration's consistency checking depend on whether a descriptor happened to be composed.
func (*Metrics) WarmRelease ¶
func (m *Metrics) WarmRelease(outcome residency.WarmOutcome)
WarmRelease records one warm release attempt.
AN ABORT IS NOT A FAILED RELEASE. It stopped at step 1, before anything was taken, and the session stays resident and is released on a later idle observation. Counting its recorded inbox failure would make an unreachable store look like a Host that cannot release.
type MetricsOptions ¶
type MetricsOptions struct {
Residencies Residencies
Ledger Ledger
// Capacity is the Host's configured admission capacity, supplied by the
// composition rather than read from *host.Host, so this package keeps the
// narrow-interface posture the rest of the module has.
Capacity uint64
// GateWaits, Backlog and Blocked are optional.
GateWaits GateWaits
Backlog CommandBacklog
Blocked BlockedConsumers
// Budget is the weighted memory limit. Its zero value is UNKNOWN.
Budget MemoryBudget
}
MetricsOptions configures this Host's metrics.
type Options ¶
type Options struct {
// Generation is this drain's stable identity, supplied by the composition
// so it can carry the Host's incarnation. Core refuses a zero generation.
Generation uint64
Clock Clock
Admissions Admissions
Advertiser Advertiser
Residents Residents
// Grace is the whole drain's bound — the platform's termination grace, less
// whatever margin the composition keeps for its own shutdown. When it
// expires, every outstanding idle wait is cancelled, any wait whose session
// has not answered that cancellation is ABANDONED with ErrWaitAbandoned,
// and cleanup continues.
//
// The abandonment is the part that makes this a bound. Cancelling a context
// only asks; a session that ignores it would otherwise hold this drain past
// any grace at all.
//
// An attach still in flight when the drain began (see Attaches) is waited
// for until one IdleBoundary before the grace — floored at half the grace —
// then cancelled, and abandoned at the grace itself. So the whole drain,
// attaches included, stays inside Grace.
Grace time.Duration
// IdleBoundary is one session's bound on reaching idle. It must not exceed
// Grace: a per-session wait longer than the whole drain's is a bound that
// can never be the one that fires, which reads as a policy and is not one.
IdleBoundary time.Duration
// PublishBound is how long StartDrain will wait for the nonaccepting
// publication before refusing the drain.
//
// IT BOUNDS THE ACKNOWLEDGEMENT, which is a synchronous RPC. DrainStarter
// takes no context by design — the RPC acknowledges initiation, so a
// caller-supplied deadline would be a deadline on the wrong event — and
// that left the publication with no bound at all: an Advertiser that never
// returned hung the drain RPC forever. The context handed to
// PublishNonaccepting is cancelled when this expires AND the wait for its
// answer ends, so the bound holds whether or not the Advertiser honours
// cancellation.
//
// EXPIRY REFUSES THE DRAIN rather than proceeding. This Host cannot say
// nonaccepting was published, so it must not act as though it were; the
// ledger stays stopped and a retry may find the write landed after all,
// which is safe because the publication is idempotent at the store.
//
// It must not exceed Grace: an acknowledgement that could outlive the whole
// drain's bound is not an acknowledgement of anything.
PublishBound time.Duration
}
Options configures one Host's drain.
type ReleaseLedger ¶ added in v0.15.1
type ReleaseLedger interface {
Unreleased() error
}
ReleaseLedger is the optional Residents capability that reports every release on this Host — an attach rollback, a warm release, a give-up — that ended with resources still held, whether or not the drain's snapshot ever named its session. The drain reads it after its own releases and withholds `drained` while it reports anything: a grant this Host still holds is a release that did not finish, whichever path left it.
type Report ¶
type Report struct {
Generation uint64
State sessionwire.HostLinkDrainState
// Failures are every step that did not succeed, in completion order. A
// drained Host with failures is drained: the cleanup ran to the end and
// these are what an operator must reconcile.
//
// CORE'S BOUNDED STATE CANNOT EXPRESS MOST OF THEM. HostLinkDrainState has
// exactly two values, so a Factory polling the drain-status RPC cannot see
// a failed checkpoint, a missed idle boundary or a refused ReleaseResidency
// at all; these Failures are how a process-local caller and an operator
// tell them apart. That is a limit of the published wire contract.
//
// THE ONE IT CAN EXPRESS IS THE ONE THAT DESTROYS WORK. A refused
// FinishRelease means release did not finish, so State stays `draining` and
// a Factory does not delete the workload. See settledState.
Failures []Failure
}
Report is one drain's outcome.
type Residencies ¶
Residencies is the local registry, narrowed to the snapshot a count reads.
*registry.Registry satisfies it; the conformance assertion lives in this package's test so production here holds no mutable registry handle. What this interface can express is a READ of a slice of copies, which is what makes "metrics adjudicate nothing" structural rather than promised.
type Residents ¶
type Residents interface {
// ResidentSessions returns the sessions held at the moment of the call.
ResidentSessions() []Session
}
Residents enumerates the sessions this Host holds.
type Session ¶
type Session interface {
// BeginRelease marks the residency releasing and stops admission for this
// session, durably, without terminating it.
BeginRelease(context.Context) error
// Checkpoint commits the runtime and workspace checkpoints the release
// requires.
Checkpoint(context.Context) error
// FinishRelease writes the epoch-fenced tombstone and releases the lease.
FinishRelease(context.Context) error
// Key identifies the session, for the failure record.
Key() registry.Key
// ReleaseResidency closes process-local resources. It is NONTERMINAL: the
// session stays durable and resumable and no terminal event is appended.
ReleaseResidency(context.Context) error
// WaitIdle blocks until the session has no work in flight. The drain
// bounds it with the configured idle boundary and cancels the context when
// that expires, so an implementation must honour cancellation.
WaitIdle(context.Context) error
}
Session is one resident session, narrowed to the ordered steps a drain takes it through.
THE ORDER IS §9.3'S AND IT IS THE REASON THIS IS FIVE METHODS RATHER THAN ONE. A single Release(ctx) would hide exactly the property O6.3 exists to establish — that the durable `releasing` mark precedes the checkpoint, and that the tombstone and lease release follow it — behind an implementation this package could not test. residency.Heartbeat already offers the two halves this shape needs, BeginRelease and FinishRelease, and its own doc records that it was split for this caller rather than fused.
BeginRelease PRECEDES WaitIdle, which the runbook's prose does not order. A session that is still admitting work has no reason to become idle, so waiting first is waiting on a condition the drain has not yet made reachable; marking it releasing first is what bounds the wait.
type Snapshot ¶
type Snapshot struct {
Resident uint64
Releasing uint64
Draining uint64
// GateWaiting is resident sessions parked on a gate. Zero when no source
// is composed, which is not the same as none; see GateWaits.
GateWaiting uint64
// BlockedSessions is resident sessions whose consumer has stopped at a
// command. See BlockedConsumers for why a non-zero value is expected today.
BlockedSessions uint64
AdmissionWeightConsumed uint64
AdmissionWeightCapacity uint64
CommandLag uint64
QueueDepth uint64
// ReleaseFailures counts failed release STEPS and not failed releases. A
// warm release recorded as released may carry failures — after its step 2
// the release is committed and a failed step is recorded and continued past
// — so counting outcomes would report zero for exactly the case an operator
// must reconcile.
ReleaseFailures uint64
// MemoryBudgetBytes is meaningful only when MemoryBudgetKnown. See
// MemoryBudget: unknown is not zero.
MemoryBudgetBytes uint64
MemoryBudgetKnown bool
}
Snapshot is this Host's load at one instant.
THERE IS NO Attaching COUNT, and that is a gap rather than an omission. internal/registry has three states — resident, releasing and draining — and Insert installs a residency directly as resident, so there is no attaching row to count. An always-zero attaching gauge would be worse than none: an operator watching an attach storm would read a flat line as its absence. sessionwire.SessionResidencyAttaching exists on the wire and has no local counterpart; closing that is a registry change, not a metrics one.
type Step ¶
type Step string
Step names one thing the drain does to one session, so a failure says which.
const ( StepHaltConsumption Step = "halt_consumption" StepBeginRelease Step = "begin_release" StepWaitIdle Step = "wait_idle" StepCheckpoint Step = "checkpoint" StepReleaseResidency Step = "release_residency" StepFinishRelease Step = "finish_release" // StepSettleAttaches is the wait for the attaches in flight when the drain // began (see Attaches). The Host-wide wait's failure carries no session key // — it names the attaches in its error — while a wait for one session's own // attach carries that session's. Either WITHHOLDS `drained`: an attach that // has not finished may still hold a lease and a runtime, or still publish // over the drain's tombstone. StepSettleAttaches Step = "settle_attaches" // StepUnreleased is the Host's ReleaseLedger reporting a release — by any path — // that ended with resources still held. It carries no session key (the // error names them) and it WITHHOLDS `drained`. StepUnreleased Step = "unreleased" // StepCloseLink is the transport shutdown, and THIS PACKAGE NEVER RECORDS // IT. The drain does not close a transport at all; the process-lifecycle // caller closes one after Wait returns and books its failure under this // name, so an operator reading a Report sees the same vocabulary wherever // the step ran. It stays here because Report is this package's type. StepCloseLink Step = "close_link" )