Documentation
¶
Overview ¶
Package compose is Host's composition root: it builds the Department runtime, the residency manager, the command consumers, the live event relay, the per-tenant HostLink servers, the metrics and the drain into one running service, with an explicit start and stop order.
WHY IT IS NOT internal/service, WHICH 04-host.md NAMES. That package's own guard, TestPublishIsTheOnlyPublishingSurface, asserts its production file set is exactly [capacity.go events.go] and enumerates every exported declaration in it, because the property it holds is "Host publishes one advertisement per LaunchTarget and nothing else". Putting a composition root there would force that enumeration open for every type a composition exports, which trades a guard for a filename. The composition consumes internal/service instead, and the two halves of §15 stay on opposite sides of a package boundary.
WHAT IT OWNS AND WHAT IT DOES NOT. It owns wiring and lifetime: which object is handed to which, in what order they start, and in what order they stop. It decides no policy — admission is internal/service's ledger, residency is internal/residency's manager, the drain sequence is internal/lifecycle's state machine — and the adapters here are mechanical joins between seams that were declared to fit. Where two seams do NOT fit, the divergence is named at the adapter rather than smoothed over; see releaseSession for the one case.
Index ¶
- Constants
- Variables
- func LeaseHolderEpoch(err error) (uint64, bool)
- type Clock
- type CompatibilityOutcome
- type DispositionWriters
- type GateSessions
- type InvalidOptionsError
- type LiveTextOptions
- type Options
- type Service
- func (s *Service) AcceptedWork(ctx context.Context, key registry.Key) (bool, error)
- func (s *Service) Attach(ctx context.Context, request residency.Request) (residency.Residency, error)
- func (s *Service) AwaitSessionIdle(ctx context.Context, key registry.Key) (CompatibilityOutcome, error)
- func (s *Service) BlockedSessions() uint64
- func (s *Service) CloseUnstarted(ctx context.Context, closeOwnedStore func() error) error
- func (s *Service) ConfirmIdle(key registry.Key) bool
- func (s *Service) ConsumerFor(key registry.Key) (hostlink.CommandConsumer, bool)
- func (s *Service) Department() *department.Department
- func (s *Service) GateWaiting() uint64
- func (s *Service) Halt(ctx context.Context, key registry.Key) error
- func (s *Service) Handler() http.Handler
- func (s *Service) Live() bool
- func (s *Service) Metrics() *lifecycle.Metrics
- func (s *Service) Ready() bool
- func (s *Service) ResidencyLost(ctx context.Context, lost residency.LostResidency)
- func (s *Service) ResidentSessions() []lifecycle.Session
- func (s *Service) Resume(key registry.Key)
- func (s *Service) Start(ctx context.Context) error
- func (s *Service) StartDrain(scope hostlink.DrainScope) (hostlink.DrainStatus, error)
- func (s *Service) Stop(ctx context.Context) (lifecycle.Report, error)
- func (s *Service) Wake(key registry.Key)
- func (s *Service) WarmRelease(outcome residency.WarmOutcome)
- type ServiceLifecycleError
- type SystemClock
- type TargetDirectory
- type WorkStates
Constants ¶
const DefaultGateRetry = time.Second
DefaultGateRetry is the gate publisher's retry wait when GateRetry is zero.
const HostLinkPathPrefix = sessionwire.HostLinkPathPrefix
HostLinkPathPrefix is the path every HostLink connection arrives under. The segment after it is the tenant the connection claims, which the server then AUTHENTICATES; a path segment is a claim and never a credential.
IT IS CORE'S CONSTANT, NOT A COPY. As of core v0.10.0 a Host advertises a BASE and every Factory derives a tenant's address with sessionwire.HostLinkEndpoint, which appends exactly sessionwire.HostLinkPathPrefix. A private spelling here could drift from that and make this Host unreachable from every Factory while every local test stayed green; the root package's TestRoutesResolve… guards hold the real router to the derivation.
Variables ¶
var ( ErrServiceDisposed = &ServiceLifecycleError{Reason: "service has been disposed"} ErrServiceActive = &ServiceLifecycleError{Reason: "service has started, is starting, or holds an attachment"} ErrServiceNotStarted = &ServiceLifecycleError{Reason: "service has not started"} )
var ErrNotResident = errors.New("compose: this Host holds no residency for that session")
ErrNotResident is what a compatibility wait reports for a session this Host does not hold.
Functions ¶
func LeaseHolderEpoch ¶ added in v0.2.0
LeaseHolderEpoch lifts the OTHER holder's epoch out of a refused attach.
It is the one mechanism for that lift, shared by the HostLink attach path and the exported composition's Attach, so the two cannot read the chain differently. The epoch is storage.LeaseHeldError.HolderEpoch, which the store adapter joins into residency.ErrLeaseHeld's refusal and the manager carries as AttachError.Cause; false means no provider error is in the chain, which a SessionLeases other than the released adapter may legitimately produce.
Types ¶
type Clock ¶
type Clock interface {
// Now is the instant every derived record is stamped with.
Now() time.Time
// After is the bound every wait in the composition and the drain is held
// to.
After(time.Duration) <-chan time.Time
// NewWarmTimer is one session's warm countdown.
NewWarmTimer(time.Duration) residency.WarmTimer
}
Clock is the composition's single time source.
IT IS ONE INTERFACE OVER THREE SEAMS THAT ALREADY EXIST, and merging them is the point rather than a convenience. host.Clock, lifecycle.Clock and residency.WarmClock each describe the time a different package needs, and a composition free to satisfy them independently could hand the drain a real clock and the warm releaser a fake one — so a test could pass while the two bounds it was comparing came from different timelines. Naming one type here is what makes them the same clock by construction.
type CompatibilityOutcome ¶
type CompatibilityOutcome string
CompatibilityOutcome is why a synchronous compatibility wait returned.
IT IS FIVE VALUES BECAUSE THE WAIT HAS FIVE ENDINGS and a caller acts differently on each. A boolean, or an error-or-nil, would fold "the session finished its work" together with "the session is waiting for a human at a gate" — and a legacy caller that resubmitted on the second would be answering a question the session is still asking.
const ( // CompatibilityIdle is durable whole-session idle: no work in flight. CompatibilityIdle CompatibilityOutcome = "idle" // CompatibilityGateBoundary is a resident gate wait. The session has not // finished; it is blocked on an answer this Host is not the source of. CompatibilityGateBoundary CompatibilityOutcome = "gate_boundary" // CompatibilityFailed is the runtime stopping answering, or this Host's // residency grant being lost. CompatibilityFailed CompatibilityOutcome = "failed" // CompatibilityStopped is this Host being stopped underneath the wait. CompatibilityStopped CompatibilityOutcome = "stopped" // CompatibilityTimedOut is the configured bound expiring. CompatibilityTimedOut CompatibilityOutcome = "timed_out" )
type DispositionWriters ¶
type DispositionWriters interface {
// DispositionWriterFor returns the writer bound to this lease's grant.
//
// The lease is the one this composition recorded when the grant was taken;
// an implementation that cannot recognise it must REFUSE rather than return
// a writer with no grant, because such a writer would fail at the first
// command of a session this Host had already taken residency of.
DispositionWriterFor(residency.Lease) (commands.DispositionWrites, error)
}
DispositionWriters binds one session's disposition transitions to the residency GRANT this Host holds for it.
IT IS DECLARED HERE RATHER THAN IN internal/commands BECAUSE IT IS A COMPOSITION CONCERN. commands consumes one session's writer and knows nothing about how a Host got it; what this expresses is the step between holding a residency.Lease and having something that may write under it, and that step exists only because sessionstore's claim edge takes a grant rather than an epoch — "the epoch comes off the grant and from nowhere else, so no number a caller chose can reach the record's mark".
A REFUSAL HERE FAILS THE ATTACH, and that is the right place for it. A Host that took residency of a session and then discovered it had no writer for it would be holding a session it can consume from and never apply in, which is the shape the whole dispatch boundary existed to prevent.
type GateSessions ¶ added in v0.4.0
type GateSessions interface {
GateSessionFor(residency.Lease, sessionwire.TenantID, sessionwire.SessionID) (gates.Session, error)
}
GateSessions binds one resident session's durable gate surface to the residency grant this Host holds for it, for the gate publisher.
IT IS A FACTORY FOR DispositionWriters' REASON: sessionstore v0.12.0's gate writes take the store-issued *ResidencyGrant and refuse a bare epoch, so the grant is bound once per session rather than passed per call. An implementation that cannot recognise the lease must refuse.
type InvalidOptionsError ¶
InvalidOptionsError reports a composition that may not run.
func (*InvalidOptionsError) Error ¶
func (e *InvalidOptionsError) Error() string
type LiveTextOptions ¶ added in v0.12.0
type LiveTextOptions struct {
RateBytesPerSecond int
BurstBytes int
FlushInterval time.Duration
IncludeReasoning bool
IncludeToolSteps bool
}
LiveTextOptions configures the opt-in transient relay and shared transport budget. Reasoning requires a runtime implementing department.LiveOptionsSubscriber, or both department.LivePublicationSubscriber and department.ReasoningPublicationSubscriber. Separate reasoning blocks within a turn are concatenated without a separator in live previews. Tool steps require department.LiveOptionsSubscriber and share the same budget.
type Options ¶
type Options struct {
// LiveText enables transient text when non-nil, and visible reasoning when requested.
LiveText *LiveTextOptions
// Host is the validated Host this composition runs.
Host *hostconfig.Host
// HostGeneration is this process's incarnation identity. Core refuses a
// zero generation on every HostLink record, and the value belongs to the
// RUN rather than the configuration — a Host restarted on identical options
// is a new generation — which is why host.Options does not carry it.
HostGeneration uint64
// Clock is the composition's one time source. See Clock.
Clock Clock
// Durable seams, each already declared by the package that consumes it.
//
// THE APPLICATION SEAMS ARE ABSENT AND THAT IS THE BOUNDARY. An earlier
// composition took Records, Applications, Gates, InboxWrites and a
// SessionOpener, and wired them into commands.Applier. All five are over
// sessionstore's LEGACY command family, which a Host cannot reach —
// AcquireResidency pins ProtocolModeDisposition — and the opener's journal
// writer was refused outright for every session this Host can hold. They are
// not retained as unread options: an option a composition validates and never
// consults is a configuration a deployment can get wrong with no consequence
// and no signal. Records and Writers below say what replaced them.
Leases residency.SessionLeases
Durable residency.DurableStore
Locations residency.Locations
Workspaces residency.Workspaces
Inbox commands.Inbox
Cursors commands.Cursors
Targets TargetDirectory
// Records and Writers are the DISPOSITION family's application seams, and
// they are what replaced the dispatch refusal that stood in their place.
//
// THE PARAGRAPH ABOVE USED TO SAY THE APPLICATION SEAMS WERE ABSENT AND THAT
// THE ABSENCE WAS THE BOUNDARY. That was true of the LEGACY five — Records,
// Applications, Gates, InboxWrites and a SessionOpener, all over a family a
// Host cannot reach — and it stays true of them: none is retained here.
// These two are the disposition family's, over edges this Host can actually
// use, and they arrived with sessionstore v0.9.0's settlement and harness
// v0.34.0's evidence writer.
//
// Writers IS A FACTORY AND NOT A WRITER, and that is the residency grant's
// doing rather than a style choice. The claim edge takes a *ResidencyGrant
// and derives the epoch from it, so no number a caller chose can reach the
// record's high-water mark; a single shared writer would therefore have to
// take a grant per call, handing that choice straight back. One writer is
// bound per session, to the grant this Host holds for it.
Records commands.DispositionRecords
Writers DispositionWriters
// Objects is the control SessionStore's scoped command-payload reader.
Objects commands.CommandPayloadObjects
// MaxCommandBodyBytes bounds a referenced runtime body; zero uses 8 MiB.
MaxCommandBodyBytes int64
// Checkpointer is the product's release checkpoint. It is REQUIRED; see
// host.Checkpointer for why an absent one may not become a no-op.
Checkpointer hostconfig.Checkpointer
// Auth verifies the service credential on every HostLink connection.
Auth hostlink.Authenticator
// PingInterval and PongTimeout are the HostLink heartbeat. They are
// optional and paired; see hostlink.Config.
PingInterval time.Duration
PongTimeout time.Duration
// MaxBindingsPerLink and MaxBindings bound what one link and one tenant's
// routing table may hold.
MaxBindingsPerLink int
MaxBindings int
// MaxTenantLinks bounds how many per-tenant transports this Host will
// allocate at once.
//
// IT IS NOT A TENANT ALLOWLIST AND MUST NOT BECOME ONE. H8 made this Host's
// admission rule the isolation class, derived from the admitted ledger, so
// a bound here is a statement about memory and file descriptors and about
// nothing else: the composition refuses a new ALLOCATION at the limit, and
// refuses it to whichever tenant asks next rather than to the ones that did
// not connect first. A link holding no bindings is reclaimed before the
// refusal.
MaxTenantLinks int
// Grace, IdleBoundary and PublishBound are the drain's bounds. See
// lifecycle.Options.
Grace time.Duration
IdleBoundary time.Duration
PublishBound time.Duration
// CompatibilityTimeout bounds the synchronous compatibility wait. See
// Service.AwaitSessionIdle.
CompatibilityTimeout time.Duration
// WorkPoll is how often this Host asks what its resident sessions are doing.
//
// A CADENCE IS REQUIRED BECAUSE WorkStates IS A PULL. That seam answers a
// question and pushes nothing, so a session that went idle — or reached a
// gate — after the last question is visible only when somebody asks again.
//
// ONE CADENCE SERVES BOTH READERS, and that is deliberate rather than
// economical: the warm releaser and the compatibility wait are asking the
// same question of the same source, and two intervals would let a
// deployment configure a Host that evicts a session the compatibility wait
// still believes is working. It is required even with no WorkStates source,
// so a deployment that later supplies one does not discover it has no
// cadence configured.
WorkPoll time.Duration
// WorkStates is the optional source of what resident sessions are doing.
// See WorkStates for what a composition without one loses.
WorkStates WorkStates
// DeriveWorkStates makes the composition its own work-state source,
// derived from each resident runtime's idle probe and its gate
// publisher's fold; see derivedWorkStates. It requires Gates, because a
// session whose gates this Host cannot see may have a human waiting at
// one, and it excludes WorkStates, because two sources would answer one
// question twice. A POOLED Host so composed warm-releases idle sessions
// after the Host's WarmTTL; a DEDICATED one does not (see warmSamples).
DeriveWorkStates bool
// Budget is the weighted memory limit reported by metrics. Its zero value
// is UNKNOWN, which is deliberately not a default; see
// lifecycle.MemoryBudget.
Budget lifecycle.MemoryBudget
// Gates binds each resident session's gate publisher to its grant. It is
// OPTIONAL here and the production composition always supplies it: a
// composition without one publishes no gate, so a Factory can show no
// AskUser or permission question from this Host and every answer is
// refused as resolved. Internal fixtures that exercise nothing about gates
// leave it nil.
Gates GateSessions
// GateReads is the durable gate projection a gate_response is checked
// against before its attempt. OPTIONAL here, supplied by the production
// composition: without it every gate_response blocks its session's pass
// (commands.RefusalNoGateReader).
GateReads commands.Gates
// GateRetry is how long a gate publisher waits after a failed pass before
// retrying. Zero means DefaultGateRetry.
GateRetry time.Duration
// Logger receives this Host's operator diagnostics. It is OPTIONAL and nil
// discards: logging is never a precondition of running. What it says is
// stated where each record is written; see logAttach.
Logger *slog.Logger
}
Options configures the composition.
Every field is required unless its documentation says otherwise. Nothing here restates a value host.New already checked: the Host is taken whole and every identity, endpoint, placement, capacity and timing is read from it.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is one composed, runnable Host.
func New ¶
New validates the options and builds every collaborator, wired to each other.
IT BUILDS AND DOES NOT START. A constructor that opened goroutines would make a refusal after the first one a partially started Host, and there is no such thing here: New either returns a Service with nothing running or returns an error and nothing at all.
func (*Service) AcceptedWork ¶
AcceptedWork reports whether the durable inbox holds work for this session that is accepted and not yet terminal.
IT IS A DURABLE READ and it is the whole point of the warm release's re-read: a command accepted by another component since this session went idle is visible here and would not be in any local queue depth.
func (*Service) Attach ¶
func (s *Service) Attach(ctx context.Context, request residency.Request) (residency.Residency, error)
Attach makes a session resident on this Host.
It is the composition's ONE attach entry point. HostLink cannot be a second one: a bind validates current ownership and never grants it, and the routing table is handed a read-only view of the registry precisely so that it cannot.
func (*Service) AwaitSessionIdle ¶
func (s *Service) AwaitSessionIdle(ctx context.Context, key registry.Key) (CompatibilityOutcome, error)
AwaitSessionIdle is the synchronous compatibility wait of 04-host.md's step 3.
IT IS NOT ON THE HOSTLINK COMMAND PATH AND MUST NOT REACH IT. A HostLink command delivery acknowledges PICKUP AND STATUS — the RPC's whole contract is that the command was accepted for application — and a delivery that blocked until the session went idle would make the acknowledgement a statement about a different event, hold a transport RPC open for the length of a model turn, and reintroduce the request-scoped session lifetime §9 exists to remove. This exists for a caller that has not yet been ported off a synchronous API, and TestTheHostLinkCommandPathDoesNotWaitForIdle is what keeps the two apart.
THE FIVE ENDINGS ARE RACED, NOT ORDERED. Whichever lands first wins, and no ending is preferred: preferring one would mean a session that went idle and was then lost could report either, depending on how a select was written rather than on what happened.
THE GATE BOUNDARY NEEDS A WorkStates SOURCE AND NOTHING IN THIS MODULE HAS ONE. A resident gate wait lives inside the runtime and department.Runtime exposes no reader for it, so a composition built without that optional seam can never return CompatibilityGateBoundary. That is asserted rather than left as a sentence.
func (*Service) BlockedSessions ¶
BlockedSessions counts the resident sessions whose durable command consumer has stopped at a command.
IT IS THE FIRST PRODUCTION READER OF Consumer.LastPass. That method's own doc calls itself "the loop's only reader … without this a blocked predecessor is reported to nobody", and until this existed a composed binary was the nobody: a blocked pass returns a nil error, so Failures() never moves, and a Host wedged in its designed steady state read healthy on every exported signal.
IT COUNTS SESSIONS AND NOT COMMANDS, deliberately. A pass reports the ONE command it stopped at, so a per-command count would be a count of ones; how many sessions are not advancing is the question an operator is actually asking, and it is the one this can answer honestly.
func (*Service) CloseUnstarted ¶ added in v0.6.0
CloseUnstarted disposes a composed Host which never started or held a residency. Its cleanup is independent of the waiter's context and is shared by subsequent callers. A refused call leaves the Host available.
func (*Service) ConfirmIdle ¶ added in v0.7.0
ConfirmIdle is the warm releaser's re-confirmation at expiry (residency.WarmConfirmer).
A COUNTDOWN IS ARMED BY A SAMPLE AND FIRES BETWEEN SAMPLES, so the sample that armed it says nothing about the instant it fires: a user's reply that arrived after the last sample would otherwise be taken into a release. The session must be idle NOW by the same source that armed it, and — where the source reports a journal position — the position must be the one recorded by the last sample. The countdown survives a sample only if that sample saw no movement, so "unchanged since the last sample" is "unchanged since the countdown was armed". Anything this cannot establish is false.
func (*Service) ConsumerFor ¶
ConsumerFor resolves the durable inbox consumer that owns a session, for the HostLink command delivery path.
func (*Service) Department ¶
func (s *Service) Department() *department.Department
Department is the immutable set of launch targets this Host serves.
func (*Service) GateWaiting ¶ added in v0.8.1
GateWaiting counts the resident sessions parked at a gate, from each session's gate publisher fold (lifecycle.GateWaits). A session with no publisher, or one that has not folded yet, is not counted: this gauge reports gates this Host can see, and the derived work-state source already treats the unknown case as work.
func (*Service) Halt ¶ added in v0.7.0
Halt stops a resident session's command consumer and waits, bounded by ctx, for its pass in flight (residency.ConsumptionHalter). A session with no consumer has nothing to halt.
func (*Service) Live ¶
Live reports whether this process should keep running.
IT REMAINS TRUE THROUGHOUT A DRAIN, and that is the half a platform gets wrong. A liveness probe that failed when a drain began would have the platform kill the process it had just asked to shut down gracefully — killing it in the middle of the checkpoints the drain exists to take.
IT ALSO REMAINS TRUE AFTER THE DRAIN HAS FINISHED, and the previous sentence here said otherwise — it said liveness turns false "when this Host has finished releasing everything it owned, or has been stopped". The code has only ever read `started && !stopped`, which Stop alone sets, so the doc was a sentence wider than the probe. THE CODE IS THE CORRECT ONE and the doc was fixed to match it, not the reverse: a Host that has completed a link-initiated drain stays up ANSWERING `drained` over HostLink, because that terminal answer is what a placement controller deletes the workload on. A liveness probe that failed the moment release finished would have the platform kill the process before it could deliver it — reintroducing, as a SIGKILL, exactly the disconnect-instead-of-an-answer this design removed.
So liveness turns false when, and only when, Stop has run.
func (*Service) Ready ¶
Ready reports whether this Host is ACCEPTING.
READY AND LIVE ARE NOT THE SAME QUESTION and this pair is the whole of 04-host.md's step 2. Ready is the readiness probe's answer and it is about admission: a Host that has begun a drain is not ready, so a platform stops routing new placements to it, and that is true from the instant the ledger flips rather than from the instant the last session is released.
func (*Service) ResidencyLost ¶
func (s *Service) ResidencyLost(ctx context.Context, lost residency.LostResidency)
ResidencyLost is handed a residency whose ownership is gone.
THE RUNTIME ARRIVES UNRELEASED, by that seam's contract, and this is where the composition gives it up. It does NOT run the release protocol's durable half: the grant is gone, so every fenced write in that protocol would be refused or, worse, accepted under an epoch a successor has superseded. What is owed is the LOCAL half, all of it — see releaseLost.
func (*Service) ResidentSessions ¶
ResidentSessions returns the sessions this Host holds, as the drain's seam.
func (*Service) Resume ¶ added in v0.7.0
Resume undoes Halt for a warm release that aborted.
IT REFUSES, SILENTLY, TO UNDO A DRAIN'S HALT. A warm release runs concurrently with a drain until Stop stops the releaser, and every warm abort path resumes the consumer it halted. The halt is one flag, not a count, so without this a warm abort racing the drain would restart consumption on a session the drain is releasing — the defect the drain's halt exists to close.
THE CHECK AND THE RESUME HAPPEN UNDER s.mu, the lock haltForDrain latches under, so the two cannot interleave: a Resume that runs before the latch completes before the drain's Halt sets the consumer halted, and one that runs after it sees the latch. Consumer.Resume only flips a flag and sends a non-blocking hint, so holding s.mu across it costs nothing.
func (*Service) Start ¶
Start publishes this Host's advertisements and begins its heartbeat.
THE ORDER IS PUBLISH-THEN-BEAT AND THE FIRST PUBLICATION IS SYNCHRONOUS. A Start that returned before anything was durable would report a Host ready while a Factory paging candidates still could not see it, and a Host whose very first publication is refused — an expiry the directory rejects, a clock Core refuses, a store that is not there — must fail the way a configuration fails, at startup, rather than by being quietly unreachable.
NOTHING ELSE IS STARTED HERE and the absences are deliberate. The residency manager, the consumers and the relays start per session, at attach. The HTTP listener is the caller's: Handler returns a handler and this package opens no socket, so a binary can mount HostLink beside its own probes on one server.
func (*Service) StartDrain ¶ added in v0.6.0
func (s *Service) StartDrain(scope hostlink.DrainScope) (hostlink.DrainStatus, error)
StartDrain is the shared admission point for process and HostLink drains. A drain attempt may already have changed the durable admission ledger when its publication reports an error, so it permanently excludes disposal.
func (*Service) Stop ¶
Stop drains this Host and waits for the drain to finish.
THE ORDER IS THE REVERSE OF START, WITH THE DRAIN IN THE MIDDLE, and each step is here because something downstream depends on the one before it:
- the drain stops admission, publishes nonaccepting durably, and only then acknowledges — internal/lifecycle owns that sequence and this does not restate it;
- every resident session is released through HaltConsumption, BeginRelease, WaitIdle, Checkpoint, ReleaseResidency and FinishRelease, and each step that fails is logged with the session's residency epoch;
- the advertisement heartbeat stops, AFTER the drain rather than before, because a heartbeat stopped first would leave the last derivation this Host published standing until it expired;
- the warm releaser stops, so no countdown fires against a session the drain has already released;
- the residency manager closes;
- every tenant transport closes, LAST, because Factory reads the bounded drain-status observation through it.
STEP 6 IS THIS FUNCTION'S, AND IT USED TO BE THE DRAIN'S. internal/lifecycle held a Link seam and closed it as the drain's last step, which meant a HostLink-INITIATED drain destroyed the transport carrying the terminal `drained` answer before that answer existed — and the disconnect a Factory was left with is the same Centrifuge 3001 whether release finished or FinishRelease refused, so it is ambiguous between "delete the workload" and "this Host still holds the lease". The drain now closes nothing; the PROCESS stopping closes it. "LAST" is unchanged and still asserted: this runs after Wait has returned, so it is after every release step.
A LINK-DRAINED HOST THEREFORE STAYS UP AND KEEPS ANSWERING, which is not a new cost: Run already only leaves on ctx.Done, so such a Host lingered before this change too — it just lingered refusing connections instead of reporting `drained` with Ready false and Live true.
THE CLOSE FAILURE IS RECORDED RATHER THAN RETURNED, under the same step name the drain used to book it under, because a transport that will not shut down is an operator's problem and not a reason to fail a Stop whose releases all succeeded.
func (*Service) Wake ¶
Wake resumes durable command consumption for a session whose release was aborted.
func (*Service) WarmRelease ¶
func (s *Service) WarmRelease(outcome residency.WarmOutcome)
WarmRelease records one warm release attempt's outcome.
It forwards to the metrics collector and, on a release that completed, drops the composition's own handle on the session. A composition that kept it would go on offering a released session to the drain, which would then take a second session through a release protocol that has already run.
A RESUMED OR HELD SESSION IS KEPT, deliberately: its runtime did not release, so it is still live under this Host's grant. A resumed one is an ordinary resident again; a held one is the drain's to release (or to end the process over), which it can do only if this handle is still offered to it. Both are logged, because nothing else reports them.
type ServiceLifecycleError ¶ added in v0.6.0
type ServiceLifecycleError struct{ Reason string }
ServiceLifecycleError reports an operation refused by the service lifecycle.
func (*ServiceLifecycleError) Error ¶ added in v0.6.0
func (e *ServiceLifecycleError) Error() string
type SystemClock ¶
type SystemClock struct{}
SystemClock is the production Clock: the real one.
IT IS THE FIRST PRODUCTION residency.WarmClock IN THE MODULE. Until this task the warm releaser had a fake and no real timer at all, and residency.WarmTimer documents why it could not simply reuse host.Clock: NewTimer there returns a CONCRETE *time.Timer, which a test cannot expire without sleeping. The three methods below are time.Timer's own, so this is a forwarding wrapper and not a reimplementation — which is what keeps the fake and the real timer honest about each other.
func (SystemClock) After ¶
func (SystemClock) After(d time.Duration) <-chan time.Time
After is time.After.
func (SystemClock) NewTimer ¶
func (SystemClock) NewTimer(d time.Duration) *time.Timer
NewTimer satisfies host.Clock, so one value is the Host's clock and the composition's.
func (SystemClock) NewWarmTimer ¶
func (SystemClock) NewWarmTimer(d time.Duration) residency.WarmTimer
NewWarmTimer arms a real timer.
func (SystemClock) Now ¶
func (SystemClock) Now() time.Time
Now is the wall clock, in UTC.
UTC IS NOT COSMETIC HERE. Every record this Host publishes carries an instant Core validates and a Factory compares against its own, and a Location-carrying time round-trips through JSON as an offset. Fixing the zone at the source is one decision instead of one per derivation.
type TargetDirectory ¶
type TargetDirectory interface {
// MaxAdvertisementTTL is the longest expiry promise the directory stores.
// The composition refuses a Host whose registry expiry exceeds it, because
// such a Host is constructible and could never publish a single row.
MaxAdvertisementTTL() time.Duration
// PublishTarget writes one derived advertisement. It is also the heartbeat.
PublishTarget(context.Context, service.Advertisement) error
// WithdrawTarget removes this Host's offer for one target. It is NOT a
// publication carrying accepting=false: a row that still carried an
// advertisement would still be ranked and still be due.
WithdrawTarget(context.Context, service.Advertisement) error
}
TargetDirectory is the durable §15 target directory this Host advertises into.
internal/service derives an Advertisement and its own package documentation says nothing there writes; this is the write. It is declared here rather than there because a writer taking an Advertisement is a second surface through which one reaches a caller, and that package's enumeration exists to keep there being exactly one.
type WorkStates ¶
type WorkStates interface {
// WorkState returns the session's current state and whether it is known.
WorkState(registry.Key) (residency.WorkState, bool)
}
WorkStates reports what one resident session is currently doing.
IT IS OPTIONAL. A resident gate wait is §9.4 state that lives inside the runtime, and department.Runtime exposes no reader for it; the composition derives one from its gate publisher when DeriveWorkStates is set (see derivedWorkStates), and the metrics' GateWaits reads the same fold. The seam is here because two consumers need the same answer and must not develop two — the warm releaser, which must not evict a session with a human waiting at a gate, and the compatibility wait, whose gate boundary is one of its five outcomes.
A COMPOSITION WITHOUT ONE IS EXPLICIT ABOUT THE CONSEQUENCE. The warm releaser receives no work observations, so it never arms a countdown and never evicts; the compatibility wait can never return at a gate boundary. Both are absences a test asserts rather than behaviours a comment claims.