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

lifecycle

package
v0.15.0 Latest Latest
Warning

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

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

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

Constants

This section is empty.

Variables

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

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

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

type ConsumptionHalter interface {
	HaltConsumption(context.Context) error
}

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

func NewDrainer(options Options) (*Drainer, error)

NewDrainer validates the options and returns a Drainer that has not begun.

func (*Drainer) BegunScope

func (d *Drainer) BegunScope() (registry.Key, bool)

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.

func (*Drainer) Wait

func (d *Drainer) Wait() Report

Wait blocks until the drain has finished and returns its report. On a Drainer that has not begun it returns the unbegun report immediately.

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.

func (Failure) Error

func (f Failure) Error() string

func (Failure) Unwrap

func (f Failure) Unwrap() error

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

type InvalidOptionsError struct {
	Field  string
	Reason string
}

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) Snapshot

func (m *Metrics) Snapshot() Snapshot

Snapshot reports this Host's load now.

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

type Residencies interface {
	Snapshot() []registry.Entry
}

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"

	// 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"
)

Jump to

Keyboard shortcuts

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