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

residency

package
v0.18.0 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package residency creates and restores sessions under the durable session lease, in one fixed order, and releases everything it took when any step of that order fails.

THE ORDER IS THE CONTRACT, not an implementation detail, and every step is there because the one before it must already have happened:

  1. validate AgentID/runtime compatibility and Host admission;
  2. obtain the SessionStore lease and its new epoch;
  3. construct/restore the runtime and its workspace/checkpoint;
  4. validate the required runtime capabilities and read the runtime's own journal grant;
  5. atomically install the local registry winner;
  6. write the epoch-fenced durable Host registry/residency projection;
  7. begin inbox/event/heartbeat ownership;
  8. only then report attached.

THERE USED TO BE A STEP BETWEEN 2 AND 3, and its removal is the one thing a reader of an older revision of this file must know. An opening in-stream journal fence was committed BEFORE hydration, on §10.1's reasoning that a predecessor's later appends must fail even before it observes Lease.Lost(). That reasoning is sound and it is not Host's to act on. The fence is a JOURNAL write, Host does not hold the journal grant, and the released store binds a journal writer to ProtocolModeLegacy while AcquireResidency pins ProtocolModeDisposition — so the fence conflicted on binding.protocol_mode for EVERY session a Host can hold, and no composed Host reached step 7 at all. Under Bundle B, where disposition is the only supported mode, Host never opens that writer; the journal epoch it needs is the RUNTIME'S, read at step 4 through department.LeaseEpochReporter, and the runtime's own composition root is what stakes the stream. Do not reinstate it here.

Nothing here is durable except the projection, and that is not this package's to adjudicate: the LEASE is authoritative, the local registry is an optimization, and the projection is a routing hint Factory may find stale. What this package guarantees is narrower and is the thing a caller can rely on: an Attach that returns an error took nothing that is still held.

Index

Constants

View Source
const MaxPrincipalFieldBytes = sessionwire.MaxIDBytes

MaxPrincipalFieldBytes bounds each principal field, mirroring the bound Core puts on the identities it accepts on the wire.

Variables

View Source
var ErrEpochSuperseded = errors.New("residency: a later lease epoch has already committed for this record")

ErrEpochSuperseded is what a Locations implementation returns when a LATER epoch has already committed for the record it was asked to write.

It is not a store failure and must not be retried. §10.1 makes SessionStore reject an epoch lower than the greatest committed for a record, so observing this is observing that somebody else now owns the session — the same fact Lease.Lost() reports, arriving by the other of the two paths. The loss signal is the fast guard; a rejected fenced write is the backstop, and this Host may see either first.

View Source
var ErrFenceConflict = errors.New("residency: the journal fence was refused by a later owner's committed sequence")

ErrFenceConflict is what a durable seam reports when its sequence compare-and-swap is refused.

§10.1 HAS TWO FENCING MECHANISMS AND THIS IS THE OTHER ONE. A stream is fenced by an in-stream ownership record rather than an epoch column: a successor's committed fence permanently makes the predecessor's tracked sequence stale, so "every later append from the predecessor fails with a typed conflict EVEN IF IT HAS NOT OBSERVED Lease.Lost()". That is the same meaning ErrEpochSuperseded carries on the mutable records, and this module had no sentinel for it at all.

NOTHING WIRED PRODUCES IT TODAY, and an earlier version of this paragraph said otherwise — which was a worse defect than leaving the arm unexplained. It claimed the released store answers a refused durable CURSOR compare-and-swap with the same JournalErrorFenced code. It does not. Store.SaveCursor classifies through classifyInbox, which produces ErrEpochSuperseded and nothing else; the ONLY producer of this sentinel is classifyJournal, whose only callers are the journal-writer file Host declares unwired. With the attach-time fence removed this sentinel therefore has NO production producer at all.

IT IS KEPT ANYWAY, and the reason is the honest one rather than a reachability it does not have. The arm becomes load-bearing the moment this package makes a fenced journal write again — the attempt-aware applier's correlation prefix is exactly that — and a sentinel deleted here would have to be rediscovered along with the classification rule that belongs to it.

UNREACHABLE IN PRODUCTION IS NOT UNTESTED. A test injects this sentinel directly through the location seam, so the arm that consumes it is covered and a mutant removing it dies. Do not read the paragraph above as permission to delete either one; see the measurement on epochFence.write.

View Source
var ErrInvalidRuntimeIdentity = errors.New("residency: the session's durable binding names no runtime identity a Host could launch under")

ErrInvalidRuntimeIdentity is the cause a DurableStore joins into a LoadSessionState failure when the session's durable binding names no runtime identity a Host could launch or restore under — a non-UUID, or the zero UUID.

IT IS A STATEMENT ABOUT THE SESSION AND NOT ABOUT THE STORE, which is the whole of F12. Every other LoadSessionState failure is a read that failed and may succeed on retry, so it carries no placement code. This one is permanent: the binding is immutable and every Host reads the same one. It is refused with runtime_unavailable — "this Host cannot run this session" — which is the code a restore with no durable state gets, rather than the empty code that reads as a transient store failure. Core has no class that says "and no Host ever will"; when it gains one this is the refusal that should move to it.

View Source
var ErrLeaseHeld = errors.New("residency: the session lease is held by another owner")

ErrLeaseHeld is the cause a SessionLeases implementation returns when another holder owns the lease. §15 calls this outcome LeaseHeld and says what it means: the registry Factory routed from was stale.

View Source
var ErrLeaseNotHeld = errors.New("residency: this Host no longer holds the session lease")

ErrLeaseNotHeld is the cause a release carries when it wrote nothing because this Host no longer holds the session lease. It is not a store failure and there is nothing to retry: the route expires on its own, and §15's bounded due reconciler is what cleans up after a Host that stopped owning a session.

View Source
var ErrNothingToRelease = errors.New("residency: this heartbeat's residency is gone or has been replaced")

ErrNothingToRelease reports that the residency this handle was created for is gone or has been replaced under it.

IT IS RETURNED RATHER THAN COLLAPSED TO NIL, and the distinction is §9.3's. Step 1 is a precondition for step 3: a caller that reads nil from BeginRelease and proceeds to checkpoint is checkpointing a residency whose admission this handle did not stop, because there was nothing here to stop. Success and no-op must not be indistinguishable — the argument this file makes for ReleaseError, applied to the one return that did not follow it.

View Source
var ErrTeardownOwnedElsewhere = errors.New("residency: another caller owns the teardown of this residency")

ErrTeardownOwnedElsewhere is the cause a release carries when another caller holds registry.BeginTeardown's single-owner claim on the residency. That owner performs the release; this handle writes nothing and removes nothing.

View Source
var ErrWarmReleaserStopped = errors.New("residency: this warm releaser has been stopped")

ErrWarmReleaserStopped reports a Watch on a releaser that has been stopped.

IT IS A DIFFERENT FACT FROM ErrWarmSessionWatched and a caller must be able to tell them apart: one says this session is already being watched by a running releaser, and the other says nothing on this releaser will ever run again. Both were once "an error", which is how a Stop that failed to clear its watch table reported the wrong one and passed.

View Source
var ErrWarmSessionWatched = errors.New("residency: this session is already watched for warm release")

ErrWarmSessionWatched reports a second Watch for a key already watched.

Functions

This section is empty.

Types

type ActorID

type ActorID string

ActorID identifies who an attach acts for: a Factory service identity or an end user. It is declared here because Core v0.7.0 has no such type.

type Admissions

type Admissions interface {
	// Admit charges one session's admission weight. It is idempotent for a
	// repeated (key, agent).
	Admit(registry.Key, sessionwire.AgentID) error

	// Release credits an admitted session's weight back, whoever owns it. Only
	// an attach's own rollback uses it: the charge is that attach's.
	Release(registry.Key) bool

	// Own binds the session's charge to the residency generation that
	// installed it, and reports whether it is now that generation's. A
	// re-admission of the session (a successor's) claims the charge back for
	// the attach in flight; see ReleaseOwned.
	Own(registry.Key, uint64) bool

	// ReleaseOwned credits the charge back only while it still belongs to the
	// releasing generation (booked finding B2). Every path that ends a
	// residency credits through it: a successor that admitted the session
	// between the old residency's registry removal and its credit holds the
	// charge, and a credit keyed by session alone uncharged it.
	ReleaseOwned(registry.Key, uint64) bool

	// Draining reports whether this Host has begun graceful drain.
	//
	// IT IS ON THE LEDGER RATHER THAN BESIDE IT because they are the same
	// object and the same rule: internal/service owns exactly one of each, and
	// two sources of "draining" is a Host that stops accepting in one place
	// while advertising Accepting from the other. This Manager WAS that second
	// place — step 8 published accepting as a literal true — so an attach
	// admitted a moment before a drain began went on to advertise an accepting
	// route on a draining Host. The window is not narrow: Admit refuses new
	// sessions once draining, but an already-admitted attach spans the lease,
	// and the whole of hydration.
	Draining() bool
}

Admissions is the Host's admission ledger.

IT IS DELIBERATELY NOT A SECOND LEDGER. internal/service already owns the only one, and its 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 interface as written, and a test in this package asserts that rather than leaving it to a comment.

type AttachError

type AttachError struct {
	Step   Step
	Code   sessionwire.HostLinkErrorCode
	Key    registry.Key
	Reason string
	Cause  error

	// RuntimeCompatibilityID is the build THIS Host runs or launches for the
	// refused request — the resident runtime's on the warm path, the launch
	// target's on the cold one — and is set exactly when Code is
	// runtime_mismatch. Core refuses to encode that class without it, so a
	// runtime_mismatch without one could not reach a Factory as a refusal at
	// all (D3.1 F1). It is never the caller's value and never the durable
	// state's: a Factory reading it learns what this Host offers.
	RuntimeCompatibilityID department.CompatibilityID

	// Unreleased names every compensation that itself failed, so a caller can
	// tell "the attach failed and took nothing" from "the attach failed and
	// something is still held". The second is an operator's problem — a live
	// route with no owner, a lease nobody will renew — and it must not be
	// indistinguishable from the first.
	Unreleased []string

	// UnreleasedCauses are the errors behind Unreleased, one per compensation
	// that itself failed, for a caller that must classify them (an expected
	// lost-grant refusal is not a leak). A resource Unreleased names only
	// because a registry loser leaves it to the winner has no cause here.
	UnreleasedCauses []error
}

AttachError reports a refused or failed attach.

Step names WHERE it failed, which is what a Host operator needs and what no wrapped cause carries. Code is the HostLink refusal class Factory branches on and is EMPTY when the failure is not a placement outcome — a store that failed is not a reason to re-run placement, and giving it a code would tell Factory to move a session that has nowhere better to go.

func (*AttachError) Error

func (e *AttachError) Error() string

func (*AttachError) HostLinkCode

func (e *AttachError) HostLinkCode() (sessionwire.HostLinkErrorCode, bool)

HostLinkCode returns the refusal class Factory branches on, and whether this failure has one at all. It is a two-value accessor rather than an ErrorCode method because the empty code is a real state and a bare accessor would hand a caller a value Core refuses.

func (*AttachError) Unwrap

func (e *AttachError) Unwrap() error

Unwrap returns the typed error a lower layer produced, if any.

type Consumption

type Consumption interface {
	// Wake resumes consumption for a session. It is a wake and not a delivery,
	// so repeating it is harmless.
	Wake(registry.Key)
}

Consumption is the durable command consumer for a session, narrowed to the wake an aborted release owes it.

THE WAKE IS NOT OPTIONAL AND IT IS EASY TO OMIT. A consumer that went quiet because the session was idle has no reason to look again on its own, so an abort that merely returned would leave accepted durable work sitting in the inbox of a session that is resident, accepting, and doing nothing. The releaser is the only thing that knows the session stopped being a release candidate.

type ConsumptionHalter added in v0.7.0

type ConsumptionHalter interface {
	// Halt stops the session's consumer claiming anything and waits, bounded
	// by ctx, for a pass in flight to finish.
	Halt(context.Context, registry.Key) error

	// Resume undoes Halt, for a release that aborts.
	Resume(registry.Key)
}

ConsumptionHalter is the optional half of Consumption a release uses to keep this Host from claiming or applying a command while it releases.

WITHOUT IT THE CONSUMER RUNS UNTIL STEP 4. Between the inbox re-read and the runtime release it can be woken — by a hint delivered before admission stopped, by the gate publisher, by its own timer — and claim a command admitted after the re-read, starting a turn in a session that is being released, or leaving an attempt in flight under a lease about to be handed back. With it, the consumer is halted BEFORE the re-read and a pass already in flight is waited out, so what the re-read saw is what this Host will ever have applied; anything admitted later stays pending for the successor.

type DurableInbox

type DurableInbox interface {
	// AcceptedWork reports whether the durable inbox holds work accepted for
	// this session and not yet terminal.
	AcceptedWork(context.Context, registry.Key) (bool, error)
}

DurableInbox is the durable command inbox, narrowed to the re-read step 1 makes.

IT IS A DURABLE READ AND NOT A LOCAL ONE. The point of step 1 is that a command accepted by ANOTHER component since this session went idle is visible; a local queue depth would report only what this process has already seen, which is exactly the work a warm release does not need to worry about.

type DurableStore

type DurableStore interface {
	// LoadSessionState returns the durable state for a session.
	LoadSessionState(context.Context, sessionwire.TenantID, sessionwire.SessionID) (SessionState, error)
}

DurableStore reads the durable session state a hydration needs.

type Guard

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

Guard is the attach's own epoch fence, exported so the composition root can hand the SAME fence to the command consumer.

It exists because commands.Fence asks for it by name: that interface is "the SHAPE residency.epochFence already has, declared there because that type's methods are unexported and this is a different package", and it records that an adapter is owed at the composition root. An adapter outside this package cannot reach the unexported methods, so the hand-off has to start here.

IT IS A VIEW AND NOT A SECOND FENCE. The three methods delegate to the one epochFence an attach built over its one grant, which is the property OwnershipRequest.Fence's own documentation calls "one grant, one fence": a supersession recorded through this view is visible to the heartbeat, and the reverse, because there is one piece of state and not two.

THE ZERO VALUE FAILS CLOSED ON THE TWO METHODS THAT DECIDE, AND OPEN ON THE ONE THAT NOTIFIES. Held and Write are the authorizing pair: a Guard holding no fence reports ownership gone and runs no write, rather than dereferencing nil at the first write. Lost cannot answer that way — it returns a channel, and the only "already lost" channel is a closed one, which would make every select watching it spin. It returns nil instead, which blocks forever and therefore reads as "not lost". A caller that watched Lost ALONE would therefore learn nothing from a zero Guard; it must ask Held, or let Write refuse. This is stated in the heading because a heading that said only "fails closed" would be the wider-than-true sentence. That state is not reachable through BeginOwnership, which refuses a nil fence before any of this; it is reachable because Guard is a method on an exported struct, and the safe answer for a guard with nothing to guard is the refusing one.

func (Guard) Held

func (g Guard) Held() error

Held reports the typed reason ownership is gone, or nil.

func (Guard) Lost

func (g Guard) Lost() <-chan struct{}

Lost closes when ownership is gone. With no fence it is a nil channel, which blocks forever in a select and is therefore "not lost" rather than "lost".

func (Guard) Write

func (g Guard) Write(run func() error) error

Write performs one fenced write through the attach's own fence.

type Heartbeat

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

Heartbeat maintains one residency's epoch-fenced Host location.

func (*Heartbeat) AbortRelease added in v0.7.0

func (h *Heartbeat) AbortRelease(ctx context.Context) error

AbortRelease undoes BeginRelease for a release that could not finish because the runtime refused to release: the entry returns to resident and accepting under the same generation, the `resident, accepting` observation is written under the held epoch, and the begin latch is cleared so a later release can begin again.

It REFUSES after FinishRelease (the entry is gone), under a lost grant, while the Host is draining (the drain owns every session then), and for a residency claimed for teardown. A refusal leaves the release where it was.

A FAILED OBSERVATION WRITE IS NOT A REFUSAL. The entry is already resident locally, and the next beat republishes it; only a fence that ended is reported, because then this Host no longer owns the session at all.

func (*Heartbeat) Beats

func (h *Heartbeat) Beats() int

Beats reports how many observations this heartbeat has published.

IT AND ConsecutiveFailures HAVE NO PRODUCTION CALLER TODAY. They are the only observable of the retry contract — that an unreachable store is a run of failures and not a loss of ownership — so the tests that pin that contract are their readers, and O3.2's metrics will be. That is a weaker justification than a consumer and is written down rather than left to be assumed.

func (*Heartbeat) BeginRelease

func (h *Heartbeat) BeginRelease(ctx context.Context) error

BeginRelease is §9.3 step 1: mark the entry releasing, stop admission, and write that state under the held epoch.

IT IS HALF OF A RELEASE ON PURPOSE, and the split is what O6.1 needs rather than a refinement of it. §9.3 and 04-host.md put the mark-releasing and the durable `releasing` observation BEFORE checkpoints and ReleaseResidency, and the tombstone and removal AFTER them. A single fused call leaves a caller two bad options: checkpoint with admission still open, or drive MarkReleasing and a durable write itself and become a SECOND durable-location writer — the exact defect this package refuses for the drain flag. There is one writer, and it offers the two halves the protocol actually has.

THE LOOP KEEPS RUNNING between the halves, deliberately. A checkpoint can take longer than the registry expiry, and a residency that stopped beating at step 1 would have its route expire mid-release and read as `cold` while this Host still holds the lease and the runtime. The beats in between carry `releasing` and Accepting false, because that is what the entry now says.

func (*Heartbeat) ConsecutiveFailures

func (h *Heartbeat) ConsecutiveFailures() int

ConsecutiveFailures reports how many publishes in a row the store has refused for a reason that is not a superseded epoch.

func (*Heartbeat) FinishRelease

func (h *Heartbeat) FinishRelease(ctx context.Context) error

FinishRelease is §9.3 steps 4 and 5' location half: stop the loop, write the expired epoch-fenced tombstone, and drop the local entry.

IT STOPS THE LOOP FIRST AND WAITS FOR IT. A beat racing the tombstone would re-publish a route after it was removed, and the last writer wins on a record whose whole purpose is telling a router what is true now. Stopping first costs nothing: the loop selects on the stop signal, so nothing waits for a timer.

It CONTINUES past a failed write, for the reason the attach rollback does: abandoning the rest turns one stale record into two. What could not be written is named on the returned *ReleaseError, because a release that reports success while leaving a live route with no owner is the worst outcome available and must not be indistinguishable from a clean one.

func (*Heartbeat) Stop

func (h *Heartbeat) Stop(context.Context) error

Stop ends the heartbeat loop and waits for it to leave. It WRITES NOTHING: the caller that stops a heartbeat is either rolling back an attach, which writes a tombstone on its own path, or releasing residency, which is Release.

type HeartbeatOptions

type HeartbeatOptions struct {
	// Host supplies the identity, endpoint, placement, clock, heartbeat
	// interval and registry expiry every observation is derived from. Nothing
	// here restates a value host.New already checked.
	Host *hostconfig.Host

	// HostGeneration is this Host process's incarnation identity, which Core
	// requires to be non-zero on every HostLink record.
	HostGeneration uint64

	Registry  HeartbeatRegistry
	Locations Locations

	// Admissions is the Host's ONE ledger, and its drain flag with it.
	//
	// IT IS THE WHOLE LEDGER RATHER THAN A NARROWER DrainState, which this
	// package used to declare. That interface was a strict subset, so a
	// composition could satisfy the two independently and hand the Manager one
	// object and the heartbeat another — moving "two sources of draining" from
	// a code possibility to a COMPOSITION possibility, which is the defect the
	// Workspaces note in manager.go describes. Naming the same type in both
	// places is what makes them the same object by construction.
	Admissions Admissions

	// Teardown receives a residency whose ownership is gone, once.
	Teardown TeardownObserver
}

HeartbeatOptions configures the ownership factory.

type HeartbeatOwnership

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

HeartbeatOwnership begins the durable-location half of a residency's ownership. It is the Ownership the residency Manager consumes.

func NewHeartbeatOwnership

func NewHeartbeatOwnership(options HeartbeatOptions) (*HeartbeatOwnership, error)

NewHeartbeatOwnership validates options and returns the factory.

func (*HeartbeatOwnership) BeginOwnership

func (o *HeartbeatOwnership) BeginOwnership(ctx context.Context, request OwnershipRequest) (OwnershipHandle, error)

BeginOwnership starts one residency's heartbeat under the SESSION context.

type HeartbeatRegistry

type HeartbeatRegistry interface {
	Get(registry.Key) (registry.Entry, bool)
	MarkReleasing(registry.Key, uint64) (registry.Entry, bool)
	ResumeResident(registry.Key, uint64) (registry.Entry, bool)
	BeginTeardown(registry.Key, uint64) (registry.Entry, bool)
	RemoveByGeneration(registry.Key, uint64) bool
}

HeartbeatRegistry is the local index a heartbeat reads and, on loss or release, claims.

BeginTeardown is the whole reason this is not just a reader. It is single-owner and its ownership is RECORDED rather than inferred from state, because StateDraining is re-enterable and a second caller finding the state already draining must be told it does not own the teardown. That is exactly the "one teardown owner" this task needs, already built and already tested, so nothing here invents a second notion of ownership.

type InvalidManagerOptionsError

type InvalidManagerOptionsError struct {
	Field  string
	Reason string
}

InvalidManagerOptionsError reports an option a Manager may not run with.

func (*InvalidManagerOptionsError) Error

type JournalEpoch

type JournalEpoch uint64

JournalEpoch is THE RUNTIME'S grant over its own agent journal, surfaced to Host as a value it reads and never as one it chooses.

HOST DOES NOT HOLD THIS LEASE. It is minted by the storage lease the runtime's composition root acquired, and the only route out of a live runtime is department.LeaseEpochReporter. Host reads it in order to STAMP work the runtime will fence — harness compares an admitted command's epoch for equality against the lease it holds, and a disposition attempt records the journal and residency epochs as separate members whose settlement fence orders the journal one. A residency epoch copied into either slot produces work that is refused, or an attempt no evidence can ever match.

type Lease

type Lease interface {
	// Epoch is the RESIDENCY epoch this grant owns. It is never zero: Core
	// rejects a zero lease_epoch on the registry observation, so a store that
	// grants one has granted something Host cannot publish under.
	Epoch() ResidencyEpoch

	// Lost closes when the grant is no longer held, whether by renewal
	// failure, expiry, or observation of a later epoch. It is the FAST guard
	// only; the non-rebasing journal CAS is the hard backstop.
	Lost() <-chan struct{}

	// Release drops the grant. It is not termination: the session remains
	// durable and resumable.
	Release(context.Context) error
}

Lease is HOST'S OWN residency grant, and nothing else.

IT USED TO BE BOTH GRANTS AT ONCE. Epoch() was documented as the value published as the registry observation's lease_epoch AND as the value stamped into the in-stream journal fence, which is two grants in two epoch domains behind one uint64. Against the LEGACY shared-backend store that was accurate — Store.OpenJournal hands out one number that really does play every role — but legacy is not a supported deployment target, and a disposition-mode session refuses the fusion outright: Store.AcquireResidency rejects a legacy session with catalog invalid and Store.OpenJournal rejects a disposition session with catalog conflict on binding.protocol_mode. The journal epoch is now JournalEpoch, read from the runtime; see finding F15 in internal/sessionstoreadapter.

*sessionstore.ResidencyGrant satisfies this shape with the wrap in internal/sessionstoreadapter, which is also what finally gives Lost() a real provider signal rather than an echo of Host's own writes (finding F3).

type LeaseCleanupError

type LeaseCleanupError struct {
	// Cause is the refusal, joined with the cleanup failure by an
	// implementation that has both.
	Cause error

	// Cleanup retries the release. It may be nil only in a value nobody built
	// through a store; Release treats that as nothing owed.
	Cleanup func(context.Context) error
}

LeaseCleanupError is a refused acquisition that still holds a provider lease and, against sessionstore, a Store admission. Until a release succeeds the admission is retained and Store.Close may time out.

IT CARRIES A RELEASE AND NOT A LEASE, deliberately. sessionstore's ResidencyAcquireCleanupError "exposes no ownership capability" and neither does this: the caller owns a cleanup obligation, not a grant, and handing back something with Epoch() and Lost() would invite a caller to attach under a lease the store has already refused it.

func (*LeaseCleanupError) Error

func (e *LeaseCleanupError) Error() string

func (*LeaseCleanupError) Release

func (e *LeaseCleanupError) Release(ctx context.Context) error

Release retries the cleanup this refusal owes. It is safe on a nil Cleanup so that an unwinder can own the obligation without first interrogating it.

func (*LeaseCleanupError) Unwrap

func (e *LeaseCleanupError) Unwrap() error

type LocalRegistry

type LocalRegistry interface {
	Get(registry.Key) (registry.Entry, bool)
	Insert(registry.Key, registry.Admission) (registry.Entry, bool)
	RemoveByGeneration(registry.Key, uint64) bool
}

LocalRegistry is the local residency index this manager installs into.

It is an interface over the concrete *registry.Registry for ONE reason worth stating, because "for testing" would be the wrong one: the registry race is the substance of this task, and the losing branch is reachable only when two runtimes have already been constructed. A concurrent test can produce that but cannot GUARANTEE it, and a coin-flip guard over the one bug this task exists to fix is not a guard. The seam lets a test interleave the competing insert deterministically; the concurrent test then runs anyway, over the same code, under -race.

type Locations

type Locations interface {
	// PublishResidency writes the observation under its lease epoch. The
	// implementation rejects an epoch lower than the greatest already
	// committed for that record.
	PublishResidency(context.Context, sessionwire.HostLinkRegistryObservation) error

	// TombstoneResidency writes the EXPIRED epoch-fenced tombstone of §10.1.
	// It removes the route without erasing the fencing high-water mark, which
	// is why a rollback calls this rather than deleting the record.
	TombstoneResidency(context.Context, sessionwire.TenantID, sessionwire.SessionID, uint64) error
}

Locations writes the epoch-fenced durable Host registry projection of §15.

type LossReason

type LossReason string

LossReason names how ownership of a session was found to be gone.

const (
	// LossReasonLeaseLost reports Lease.Lost() closing: renewal failure,
	// expiry, or the leaser observing a later epoch.
	LossReasonLeaseLost LossReason = "lease_lost"

	// LossReasonEpochSuperseded reports a fenced write refused because a later
	// epoch has committed. It is the same fact by the other path.
	LossReasonEpochSuperseded LossReason = "epoch_superseded"

	// LossReasonFenceConflict reports a journal append refused by a successor's
	// committed sequence, which is §10.1's OTHER fencing mechanism. It is a
	// third name for the same fact and is kept distinct because an operator
	// diagnosing "a later epoch committed" looks in a different place from one
	// diagnosing "my journal sequence is stale".
	LossReasonFenceConflict LossReason = "fence_conflict"
)

type LostResidency

type LostResidency struct {
	Key             registry.Key
	AgentID         sessionwire.AgentID
	CompatibilityID department.CompatibilityID
	LeaseEpoch      ResidencyEpoch
	Generation      uint64
	Runtime         department.Runtime
	Reason          LossReason
}

LostResidency describes a residency this Host no longer owns.

type Manager

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

Manager creates and restores sessions under the durable session lease.

func NewManager

func NewManager(options Options) (*Manager, error)

NewManager validates options and returns a Manager.

func (*Manager) Attach

func (m *Manager) Attach(requestCtx context.Context, request Request) (Residency, error)

Attach creates or restores a session under its lease and reports the residency. It is the state machine this package's doc comment describes.

THE REQUEST CONTEXT IS NOT PROPAGATED, and that is the point rather than an oversight. Spec §8.3 separates request lifetime from session residency: a runtime launched on the caller's context dies when the caller disconnects, and a rollback run on it releases nothing once the caller has gone — which is the failure mode a rolled-back attach can least afford. Every collaborator below is called on the SESSION context, derived from this Manager's root, and every rollback on the root itself.

TWO COSTS, and they COMPOUND, which is why they are written together rather than one each in their own place. A caller cannot cancel an attach; and attaches of one key are serialized, so one hung Create or Restore parks every later attacher for that key on the slot with no timeout and no way for any of them to abandon. The bound is the collaborators' own deadlines, and that bound now covers the QUEUE and not just the one call. If it stops being enough the answer is a deadline owned by this package — a bound on the whole sequence, applied to the session context — and not a return to the request's lifetime, which would reintroduce the failure the paragraph above describes.

THE SLOT ALSO BARGES: acquireKey deletes the map entry before closing the channel it woke everyone on, so a fresh arrival can take the slot ahead of a waiter that has been parked longer. There is no correctness consequence — every waiter re-reads the residency and either finds it or attaches — but there is no fairness bound either, so a hot key under sustained arrivals can starve one waiter indefinitely. A queue would fix it and is not worth its complexity until an attach is slow enough for the starvation to be visible.

func (*Manager) CancelAttaches added in v0.15.1

func (m *Manager) CancelAttaches()

CancelAttaches cancels the session context of every attach that has not yet committed, so each stops waiting on its collaborators and rolls back. It is the drain's last resort for an attach that did not finish within the grace.

A COMMITTED ATTACH IS NOT CANCELLED. Commit and this run under the same mutex, so an attach is either cancelled before it commits — and its Commit then answers false — or committed and out of reach: a residency already in the drain's resident set is the drain's to release, and cancelling its context would cancel a live runtime beneath that release. The rollback itself runs on a context this cannot cancel; see attach.

func (*Manager) Close

func (m *Manager) Close()

Close cancels this Manager's root session context, which cancels every session derived from it and therefore every ownership started under one.

IT IS NOT RESIDENCY RELEASE, and the difference is deliberate rather than an omission. Release is a PROTOCOL — mark releasing, checkpoint, remove the registry entry, tombstone the projection, release the lease — and it belongs to O3.2 and O6.1. Doing half of it here would create a second release path that writes no tombstone and releases no lease, which is worse than doing none of it: the durable projection would outlive the process that wrote it and Factory would route to a Host that had stopped listening. Close is for process shutdown after that protocol has run, or for a test. An attach in flight when it arrives still rolls back completely: see the rollback context in attach, which Close deliberately cannot cancel.

A residency's record is pruned by EndResidency, which every path that ends a residency calls; Close is only the process-wide backstop.

func (*Manager) EndResidency added in v0.9.0

func (m *Manager) EndResidency(key registry.Key, generation uint64, runtimeReleased bool) bool

EndResidency prunes the record of a residency that has ENDED — released, abandoned, lost or drained, with its registry entry already removed — and reports whether it pruned one.

IT IS FENCED BY GENERATION, so a late end of an old residency cannot prune the residency that replaced it under the same key.

Before it existed nothing pruned these records (booked finding B1): every session that ever left a long-lived pooled Host kept an uncancelled session context and an ownership handle whose heartbeat reaches the runtime, so the Host grew by one record per distinct session it had held.

runtimeReleased says whether the session context may be CANCELLED. The runtime was launched on it, so it may be cancelled only once the runtime was released or abandoned. A runtime whose release was refused — a drain of a session parked at a gate — is left PARKED: cancelled, it would interrupt its turn and abandon the gate after this Host stopped publishing it. Its record is still pruned; its context stays a child of the root, which Close cancels.

AN OVERTAKEN RESIDENCY IS STILL FOUND (review finding R1). When a successor of the same key reached step 8 first, the older record was moved aside rather than overwritten, and it is pruned — and cancelled on the same rule — here.

func (*Manager) Observe added in v0.2.0

Observe reports the §15 projection of a residency this Manager currently holds, as it stands NOW, and whether it holds one at all.

IT IS THE ATTACH RPC'S ACCEPTED REPLY BODY. Core requires an accepted hostlink.attach to answer with the HostLinkRegistryObservation the following bind needs, and this is the ONE derivation of that record — the same `observation` the sequence publishes durably at its publish step — read from the registry rather than rebuilt from the attach result. The difference matters on the warm path: an attach that found the session already resident took nothing, and the entry it found may have moved on (a warm release may have closed admission, or begun), so the reply says what the registry says and not what the establishing call once saw. A router reading this and a router reading the durable projection are reading one derivation.

The second result is false for a session this Manager does not hold, which a caller reaches only in the window between an attach returning and the residency being released; it is a fact and not a refusal, and the caller decides what to do with it.

func (*Manager) PruneResidency added in v0.17.0

func (m *Manager) PruneResidency(key registry.Key, generation uint64) (context.CancelFunc, bool)

PruneResidency is EndResidency that hands the session context's cancellation to the caller instead of deciding it now. A caller whose runtime may still finish tearing down LATER keeps it and cancels then; the record itself is pruned at once, fenced by generation exactly as EndResidency is.

func (*Manager) Records added in v0.9.0

func (m *Manager) Records() int

Records reports how many residency records this Manager holds: one per attached residency that has not ended. It is what shows a long-lived Host does not grow per session it has ever held (booked finding B1).

func (*Manager) SessionContext

func (m *Manager) SessionContext(key registry.Key) (context.Context, bool)

SessionContext returns the root context of a resident session.

type Mode

type Mode string

Mode selects whether an attach creates a session or restores one.

const (
	// ModeCreate launches a new session.
	ModeCreate Mode = "create"

	// ModeRestore relaunches one over existing durable state.
	ModeRestore Mode = "restore"
)

type Options

type Options struct {
	// Host is the validated Host whose identity, endpoint, placement, tenant,
	// registry expiry and clock the projection is derived from. Nothing here
	// re-states a value host.New already checked.
	Host *hostconfig.Host

	// HostGeneration is this Host process's incarnation identity. Core
	// requires it to be non-zero on every HostLink record, and it belongs to
	// the run rather than to the configuration, which is why it is not on
	// host.Options.
	HostGeneration uint64

	// Registry is the local residency index. It is an optimization; see
	// LocalRegistry and the registry package comment.
	Registry LocalRegistry

	// Admissions is the Host's ONE admission ledger. See Admissions.
	Admissions Admissions

	Leases     SessionLeases
	Durable    DurableStore
	Workspaces Workspaces
	Locations  Locations
	Ownership  Ownership
}

Options configures a Manager.

type Ownership

type Ownership interface {
	// BeginOwnership starts ownership under the SESSION context, not the
	// request's, and returns the handle that ends it.
	BeginOwnership(context.Context, OwnershipRequest) (OwnershipHandle, error)
}

Ownership begins the durable inbox consumption, event fan-out and heartbeat a resident session owns. O3.2 and O4.1 implement it; this package only sequences it, and sequences it LAST, because every one of those three writes under the lease epoch and none of them may start before the epoch is fenced and the runtime exists.

type OwnershipHandle

type OwnershipHandle interface {
	// Stop ends the ownership this handle started.
	Stop(context.Context) error
}

OwnershipHandle is one session's running inbox/event/heartbeat ownership.

type OwnershipRequest

type OwnershipRequest struct {
	Key             registry.Key
	AgentID         sessionwire.AgentID
	CompatibilityID department.CompatibilityID
	LeaseEpoch      ResidencyEpoch
	Generation      uint64
	Runtime         department.Runtime

	// Fence is the attach's OWN epochFence, handed on rather than rebuilt.
	//
	// IT IS THE GRANT'S FENCE AND NOT MERELY ITS LEASE, and the difference is
	// what "the one mechanism" has to mean. Building a second fence over the
	// same lease gave two objects that shared Lost() and did NOT share the
	// classification: a supersession one of them observed was invisible to the
	// other, so the Manager could know the epoch was stale while the heartbeat
	// went on publishing under it. One grant, one fence.
	//
	// IT CLOSES A REAL HOLE, and an earlier version of this comment called it
	// equivalent on a reason that was simply wrong — that a heartbeat ends its
	// own fence only after the attach has returned. It does not: the heartbeat
	// starts at step 7 and TWO fenced writes follow it inside the same attach.
	// So a beat refused with ErrEpochSuperseded ends the heartbeat's fence
	// without closing Lost(); step 8 then fails for some other reason; and the
	// unwinder's tombstone goes through a Manager fence that knows nothing, at
	// an epoch a successor has already superseded. That closure's own comment
	// calls that the worst case in the file, because a tombstone is a route
	// removal and the route may be the successor's.
	Fence *epochFence

	// Commit is the attach's point of no return, for an Ownership that keeps a
	// resident set a drain reads. It reports whether the attach may still become
	// resident — false once CancelAttaches has cancelled it — and, having
	// answered true, takes the attach out of CancelAttaches' reach: from then on
	// it is a residency the drain releases, and cancelling its session context
	// would tear a live runtime down beneath that release. An implementation
	// calls it inside the same critical section that inserts the session into
	// its resident set. Nil means there is nothing to commit.
	Commit func() bool
}

OwnershipRequest describes the residency whose ownership is beginning.

func (OwnershipRequest) Guard

func (r OwnershipRequest) Guard() Guard

Guard returns the view of this request's fence.

type Principal

type Principal struct {
	// TenantID is the tenant the attach acts within. It must equal the
	// request's tenant; a principal for one tenant may not attach another's
	// session.
	TenantID sessionwire.TenantID

	// ActorID is who the attach acts for. It is REQUIRED: an attach with no
	// actor is an attach nothing can be attributed to.
	ActorID ActorID

	// TraceID correlates the attach with its cause. It is optional.
	TraceID TraceID
}

Principal is the WHITELIST of request-scoped values a session may carry.

It is a whitelist expressed as a TYPE rather than as a list of keys to strip, and that is the whole security property. A session context is derived from this Manager's own root — never from a request context — and the only value installed on it is one of these. So an auth token, a bearer credential, a database handle or a cancellation a caller happened to attach to its request context is not "dropped": it was never reachable. There is no context.WithoutCancel anywhere in this package, and a test parses the production files to keep it that way.

EVERY FIELD IS STRING-SHAPED, and a test asserts it over the reflected type rather than over this sentence. A field of interface, map, pointer or channel kind would reopen exactly the hole the type closes: `any` would let a caller hand the session an arbitrary value, including the request context itself.

func PrincipalFrom

func PrincipalFrom(ctx context.Context) (Principal, bool)

PrincipalFrom returns the approved principal a session context carries.

type ReleaseAuthority added in v0.15.1

type ReleaseAuthority interface {
	ReleaseRuntime(context.Context) error
	ReleaseLease(context.Context) error
	ReleaseWorkspace(context.Context) error
	// ReleaseLocation writes the epoch-fenced tombstone and removes the local
	// registry entry, as one step with one verdict.
	ReleaseLocation(context.Context) error
	// ReleaseAdmission credits the residency's admission charge back.
	ReleaseAdmission(context.Context) error
}

ReleaseAuthority is the optional OwnershipHandle capability that takes over an attach's rollback of the resources a RESIDENCY owns once ownership has begun.

FROM STEP 7 THE RESIDENCY IS VISIBLE TO OTHER RELEASE PATHS — the drain, a lost or faulted residency's give-up — and each releases the runtime, the grant and the local workspace through the owner's once-only guards. A rollback that released them directly would bypass those guards and release a second time (review finding P2/3). So once a handle offering this exists, the attach's compensations for those three go through it, and every path shares one completion and one first error for each.

IT COVERS EVERY COMPENSATION FROM STEP 7 ON, the location and the admission charge as well: a give-up that completed first has already written the tombstone, removed the local entry and credited the charge, and a direct compensation would then report "nothing was removed" and "not charged" for a release that in fact finished (round-3 review). Through the authority it receives that release's own verdict — and still a genuine failure, such as an entry replaced under a generation nothing here released.

type ReleaseError

type ReleaseError struct {
	Key registry.Key

	// Cause carries the typed reason, so a caller can tell a lost grant from an
	// unreachable store with errors.Is instead of matching text.
	Cause error

	// Unwritten names each durable write that failed, in the order attempted,
	// for the reason AttachError.Unreleased exists: a release that reports
	// failure and a release that leaves a live route with no owner must not be
	// indistinguishable.
	Unwritten []string
}

ReleaseError reports a release that could not write everything it owed.

func (*ReleaseError) Error

func (e *ReleaseError) Error() string

func (*ReleaseError) Unwrap

func (e *ReleaseError) Unwrap() error

Unwrap returns the typed reason this release wrote nothing, if any.

type Request

type Request struct {
	TenantID  sessionwire.TenantID
	SessionID sessionwire.SessionID
	AgentID   sessionwire.AgentID
	Mode      Mode

	// CompatibilityID is the runtime build Factory placed this session on. It
	// is OPTIONAL, and when set it must equal the target's own. Checking it in
	// step 1 refuses a mis-placed session before a lease is taken; the durable
	// state's build is checked again in step 3, against a different source.
	CompatibilityID department.CompatibilityID

	// Principal is the approved subset of the caller's identity that may reach
	// the session root context. See Principal.
	Principal Principal
}

Request is one attach request.

type Residency

type Residency struct {
	Key             registry.Key
	AgentID         sessionwire.AgentID
	CompatibilityID department.CompatibilityID
	Generation      uint64
	Runtime         department.Runtime

	// LeaseEpoch is HOST'S residency grant, the value published as the registry
	// observation's lease_epoch.
	LeaseEpoch ResidencyEpoch

	// JournalEpoch is THE RUNTIME'S grant, read from the launched runtime and
	// never derived from LeaseEpoch. Its type differs from LeaseEpoch's so the
	// two cannot be assigned across without a conversion a reviewer can see.
	//
	// JournalEpochHeld is the half a consumer must branch on. A runtime that is
	// headless, has no persistence, or is simply not wired for durable commands
	// legitimately holds no journal lease, and one whose lease has been released
	// or lost reports absence rather than the number it used to hold. Zero is
	// therefore not "no epoch" — false is.
	JournalEpoch     JournalEpoch
	JournalEpochHeld bool

	// Attached reports whether THIS call established the residency. False
	// means the session was already resident and this call took nothing.
	Attached bool
}

Residency is what an attach reports.

type ResidencyEpoch

type ResidencyEpoch uint64

ResidencyEpoch is HOST'S OWN orchestration grant over a session, and it is a defined type rather than a uint64 so that the other epoch in this system cannot be put where one is wanted.

THE TWO EPOCHS ARE DIFFERENT GRANTS WITH DIFFERENT HOLDERS. This one is minted by the lease Host takes in the session's residency namespace; it authorizes Host to publish the §15 route and to fence its own registry writes, and it authorizes NO journal write and no command application. sessionstore.ResidencyEpoch states the rule in terms — it "must never be compared with, or used as, a journal epoch" — and JournalEpoch below is the other one. That the two often agree early in a session's life is an accident of two fresh counters both starting at 1; this package's fakes are seeded from different bases precisely so that accident cannot pass for a relationship.

type RuntimeJournal added in v0.3.0

type RuntimeJournal int

RuntimeJournal is what the runtime's own journal holds for a session's runtime identity.

IT EXISTS BECAUSE A CREATE CANNOT BE TRUSTED TO MEAN "NEW". Factory sends every attach as create (both inputs its attach mode is chosen from are dead on a disposition record), and harness does not verify that a session id it is asked to create under is fresh: naming a session whose journal exists re-opens THAT stream, appends a second SessionStarted and restores nothing, with no error. So a Host that launched every create fresh silently restarted every re-placed session's conversation. The journal is the authority; the attach mode is only a request.

const (
	// RuntimeJournalUnknown means nothing established whether a journal
	// exists. A create is REFUSED on it rather than launched, because
	// launching is exactly the silent restart when the answer would have been
	// "present". It is the zero value on purpose.
	RuntimeJournalUnknown RuntimeJournal = iota
	// RuntimeJournalAbsent means the runtime has no conversation under the
	// session's runtime identity, so a create launches a new one under it.
	RuntimeJournalAbsent
	// RuntimeJournalPresent means a conversation exists, so a create restores
	// it instead of starting over.
	RuntimeJournalPresent
)

type SessionLeases

type SessionLeases interface {
	// AcquireSessionLease grants the lease for one session, or reports
	// ErrLeaseHeld if another holder has it.
	//
	// A REFUSAL MAY STILL OWE A RELEASE, which is finding F16(b) and the reason
	// LeaseCleanupError exists. (Lease, error) cannot say "refused, and you
	// still owe a release", so every fake in this module read a non-nil error
	// as nothing acquired and LEAKED the case the released store actually
	// produces. An implementation in that state wraps the refusal in a
	// *LeaseCleanupError; a caller reaches it with errors.As and owns the retry.
	AcquireSessionLease(context.Context, sessionwire.TenantID, sessionwire.SessionID) (Lease, error)
}

SessionLeases grants exclusive epoch-fenced residency leases over a session.

type SessionState

type SessionState struct {
	// Exists reports whether the session has durable state at all.
	Exists bool

	// Namespace is the tenant- and session-scoped object prefix every launched
	// runtime writes under. It is read from the store rather than derived here
	// because the layout is SessionStore's, not Host's.
	//
	// IT IS REQUIRED WHETHER OR NOT THE SESSION EXISTS. The prefix is a
	// property of the identities, not of the durable state, so a store that
	// answers Exists=false must still answer with the namespace that session's
	// objects will live under. An empty one is refused before the launch: a
	// runtime handed the empty prefix scopes every object it writes to nothing,
	// and it does so having reported success.
	Namespace string

	// CompatibilityID is the runtime build that wrote the durable state. A
	// restore onto a different build is refused.
	CompatibilityID department.CompatibilityID

	// RigSessionID is Harness's identity for the session: the runtime session
	// id the session's immutable durable binding names (Factory derives it
	// at create; it is NOT the Core session id). A restore that does not carry
	// it has nothing to restore from, and a create launches under it so the
	// journal the runtime writes is the one the binding names. Zero arises
	// only for a session with NO catalog record (Exists=false), and a create
	// then lets the rig mint one. A record without a binding (legacy) gets the
	// deprecated RigSessionIDs collaborator's answer, or an error.
	RigSessionID uuid.UUID

	// RuntimeJournal reports whether the runtime's own journal already holds
	// a conversation under RigSessionID. It is what decides a create: see
	// RuntimeJournal. The zero value is RuntimeJournalUnknown, which refuses a
	// create, so a store that forgot to answer fails closed. A legacy record
	// (no binding) is never probed, so it stays Unknown and every create over
	// one is refused; v0.2.1 launched it. No shipped path creates a legacy
	// record a Host could hold (residency is disposition-only).
	RuntimeJournal RuntimeJournal

	// HasCheckpoint and CheckpointSequence report the active workspace
	// checkpoint. A target declaring RequiresCheckpoint may not be restored
	// without one.
	HasCheckpoint      bool
	CheckpointSequence uint64
}

SessionState is the durable state a hydration reads. It is deliberately not host.SessionStore's opaque []byte: this package needs the fields, and decoding them here would put a wire format in a state machine.

type Step

type Step string

Step names the sequence step an attach failed at.

const (
	StepValidate     Step = "validate"
	StepLease        Step = "lease"
	StepHydrate      Step = "hydrate"
	StepCapabilities Step = "capabilities"
	StepInstall      Step = "install"
	StepPublish      Step = "publish"
	StepOwnership    Step = "ownership"
	StepAttached     Step = "attached"
)

type TeardownObserver

type TeardownObserver interface {
	ResidencyLost(context.Context, LostResidency)
}

TeardownObserver is handed a residency whose ownership is gone, AT MOST ONCE per residency, by whichever caller won registry.BeginTeardown.

What it then does is O6.1's: this package guarantees the handoff happens once and does not invent the release protocol behind it. In particular the runtime is handed over UNRELEASED — releasing it here would be a second release path with no checkpoint, no lease release and no tombstone.

type TraceID

type TraceID string

TraceID correlates an attach with the request that caused it.

type WarmClock

type WarmClock interface {
	NewWarmTimer(time.Duration) WarmTimer
}

WarmClock hands out warm timers.

type WarmConfirmer added in v0.7.0

type WarmConfirmer interface {
	// ConfirmIdle reports whether the session is idle NOW and has done no work
	// since the observation that armed its countdown. Anything it cannot
	// establish must be false.
	ConfirmIdle(registry.Key) bool
}

WarmConfirmer re-confirms, at expiry, that a session is still idle.

IT CLOSES THE WINDOW A SAMPLED IDLE LEAVES OPEN. The countdown is armed by an idle SAMPLE and is deliberately not re-armed by later idle samples, so a turn that starts after the last sample and before the TTL fires is invisible to the releaser unless something asks again at the instant it fires. The inbox re-read cannot stand in for this: a command already applied has advanced the cursor, and a turn started any other way was never in the inbox.

It is OPTIONAL: a releaser without one releases on the arming alone, which is what every releaser did before it existed.

type WarmFailure

type WarmFailure struct {
	Step WarmStep
	Err  error
}

WarmFailure is one step that did not succeed.

func (WarmFailure) Error

func (f WarmFailure) Error() string

func (WarmFailure) Unwrap

func (f WarmFailure) Unwrap() error

type WarmObserver

type WarmObserver interface {
	WarmRelease(WarmOutcome)
}

WarmObserver is handed the outcome of every warm release attempt.

It is OPTIONAL. A Host composed without one still releases; what it loses is the only record of a recorded failure, which is why internal/lifecycle's metrics consume this.

type WarmOptions

type WarmOptions struct {
	Clock WarmClock

	// TTL is how long a session stays warm after durable whole-session idle.
	//
	// IT MUST BE POSITIVE. host.Options already refuses a non-positive WarmTTL,
	// so accepting zero here would define a behaviour for a configuration no
	// Host can be constructed with. A deployment that wants no warm window
	// does not watch the session.
	TTL time.Duration

	Registry    WarmRegistry
	Inbox       DurableInbox
	Consumption Consumption

	// Admissions is the Host's ONE admission ledger. The warm release credits a
	// released session's weight back through it; keeping a second count here
	// would be the over-admission bug internal/service's own documentation
	// names this task as a required consumer to avoid.
	Admissions Admissions

	// Observer receives every attempt's outcome. It is optional.
	Observer WarmObserver

	// Confirm re-confirms idle at expiry. It is optional; see WarmConfirmer.
	Confirm WarmConfirmer

	// ReleaseBound bounds the two steps that wait on the session rather than
	// on a store: halting its consumption and releasing its runtime. Zero
	// leaves them unbounded. See release.
	ReleaseBound time.Duration
}

WarmOptions configures this Host's warm release.

type WarmOutcome

type WarmOutcome struct {
	Key        registry.Key
	Generation uint64
	Kind       WarmOutcomeKind

	// Reason is free text for an operator. A caller branches on Kind and on
	// the Failures' Steps, never on this.
	Reason string

	// Failures are every step that did not succeed, in the order they ran.
	Failures []WarmFailure
}

WarmOutcome is one warm release attempt's result.

type WarmOutcomeKind

type WarmOutcomeKind string

WarmOutcomeKind is how one warm release attempt ended.

const (
	// WarmOutcomeReleased reports a release that ran to the end. It may still
	// carry Failures: see WarmReleaser.release for why a failure after step 2
	// is recorded rather than returned.
	WarmOutcomeReleased WarmOutcomeKind = "released"

	// WarmOutcomeAborted reports an attempt that stopped at step 1, before
	// anything was taken. The session stays resident and accepting and is
	// released on a later idle observation.
	WarmOutcomeAborted WarmOutcomeKind = "aborted"

	// WarmOutcomeResumed reports a release whose runtime did not release
	// within the bound (step 4) and that was then TAKEN BACK: the session is
	// an ordinary resident, admitting session again under the same
	// generation, its consumption resumed, and the watch goes on exactly as
	// after an abort. It carries the step-4 failure, so it is counted.
	WarmOutcomeResumed WarmOutcomeKind = "resumed"

	// WarmOutcomeHeld reports a release that stopped at step 4 because the
	// runtime refused or did not finish its nonterminal release within the
	// bound. Nothing after it ran: the tombstone is not written, the grant is
	// still held and the session's state is not dropped, so the runtime — which
	// is still live — keeps a valid owner and no successor can race it. The
	// session stays with this Host, not admitting, until its drain.
	WarmOutcomeHeld WarmOutcomeKind = "held"

	// WarmOutcomeAbandoned reports a session this releaser has stopped
	// watching without releasing it, because it no longer owns it: the grant
	// is gone, or the residency has been replaced under the same key. Somebody
	// else owns what happens next — Heartbeat's teardown handoff, or the
	// replacement's own releaser.
	WarmOutcomeAbandoned WarmOutcomeKind = "abandoned"
)

type WarmRegistry

type WarmRegistry interface {
	StopAdmitting(registry.Key, uint64) (registry.Entry, bool)
}

WarmRegistry is the local residency index, narrowed to the one write a warm release makes to it directly.

Everything else this package does to the registry goes through the session's own release halves, which is what keeps ONE writer per durable record. This one is separate because it is the per-session admission stop, and it must happen BEFORE the residency state moves: see StopAdmitting's own doc, and WarmReleaser.release step 2.

type WarmReleaser

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

WarmReleaser releases idle sessions after their warm TTL, and is safe for concurrent use.

ONE WATCH PER KEY, ONE GOROUTINE PER WATCH. Two timers for one session is a release racing itself, so a second Watch is refused rather than replacing the first silently.

func NewWarmReleaser

func NewWarmReleaser(options WarmOptions) (*WarmReleaser, error)

NewWarmReleaser validates the options and returns a releaser watching nothing.

func (*WarmReleaser) Forget

func (w *WarmReleaser) Forget(key registry.Key)

Forget stops watching a session WITHOUT releasing it. It is what a caller uses when the residency has gone by another path — a teardown, a drain, an attach that replaced it — and repeating it is safe.

func (*WarmReleaser) Observe

func (w *WarmReleaser) Observe(key registry.Key, state WorkState)

Observe records what a session is doing, arming or cancelling its warm countdown.

IT DOES THE WORK SYNCHRONOUSLY rather than posting to the watch goroutine. An asynchronous signal would make "the timer was cancelled" a fact a caller cannot rely on having taken effect when Observe returns, which is exactly the guarantee an activity reset needs: work has arrived, and the release must not fire.

func (*WarmReleaser) Stop

func (w *WarmReleaser) Stop()

Stop ends every watch and waits for the goroutines. A release already past step 1 is allowed to finish: stopping halfway is the one thing worse than not starting.

func (*WarmReleaser) Watch

func (w *WarmReleaser) Watch(session WarmSession) error

Watch begins watching a resident session. It arms NOTHING: only a durable whole-session idle observation starts a countdown.

func (*WarmReleaser) Watching added in v0.10.0

func (w *WarmReleaser) Watching(key registry.Key) bool

Watching reports whether a watch is held for key. It is a snapshot: a lost grant ends a watch on its own goroutine, so a caller waiting for that end polls it.

type WarmSession

type WarmSession interface {
	// Key identifies the session.
	Key() registry.Key

	// Generation is the registry generation this residency was installed under.
	// Every step is fenced by it, so a warm timer armed for one residency
	// cannot act on the residency that REPLACED it under the same key.
	Generation() uint64

	// Lost closes when this Host's residency grant is gone. The warm path then
	// abandons the session: releasing under a lost grant would write fenced
	// records under an epoch a successor has superseded, and Heartbeat.surrender
	// already owns the handoff for that case.
	Lost() <-chan struct{}

	// BeginRelease marks the residency releasing and publishes that state
	// durably, without terminating it.
	BeginRelease(context.Context) error

	// Checkpoint commits the runtime and workspace checkpoints the release
	// requires.
	Checkpoint(context.Context) error

	// ReleaseResidency closes process-local resources. It is NONTERMINAL: no
	// terminal event is appended and the session stays durable and resumable.
	ReleaseResidency(context.Context) error

	// FinishRelease writes the epoch-fenced tombstone and removes the visible
	// Host registry entry.
	FinishRelease(context.Context) error

	// ReleaseLease drops the residency grant.
	ReleaseLease(context.Context) error

	// DropState drops in-memory state and disposable local materializations.
	DropState(context.Context) error

	// AbortRelease returns a session whose runtime refused to release to an
	// ordinary resident, admitting session under the same generation: the
	// registry and the durable observation go back to `resident, accepting`
	// and BeginRelease and Checkpoint may run again. It must refuse when it
	// cannot deliver that — the session's work already stopped, the Host is
	// draining, the grant is gone — and the release is then left held.
	AbortRelease(context.Context) error
}

WarmSession is one resident session, narrowed to the ordered steps a warm release takes it through.

THE ORDER IS §9.3'S AND IT IS THE REASON THIS IS SIX METHODS RATHER THAN ONE. A single Release(ctx) would hide the properties O6.1 exists to establish — that the durable `releasing` mark precedes the checkpoint, that the tombstone precedes the lease release, and that local state is dropped last — behind an implementation this package could not test. residency.Heartbeat already offers BeginRelease and FinishRelease, split for exactly this caller.

THE PRODUCTION ADAPTER IS NOT THIS TASK'S, and the seam is stated rather than invented: no production lifecycle.Session or Advertiser adapter exists in this module — both are O7.1's — and department.Runtime identifies a runtime with sessionwire identities while Harness identifies a session with a UUID. O7.1 composes a Heartbeat, a Lease and a department.Runtime into this shape.

type WarmStep

type WarmStep string

WarmStep names one thing a warm release does, so a failure says which.

const (
	WarmStepHaltConsumption  WarmStep = "halt_consumption"
	WarmStepInbox            WarmStep = "inbox_reread"
	WarmStepStopAdmitting    WarmStep = "stop_admitting"
	WarmStepBeginRelease     WarmStep = "begin_release"
	WarmStepCheckpoint       WarmStep = "checkpoint"
	WarmStepReleaseResidency WarmStep = "release_residency"
	WarmStepFinishRelease    WarmStep = "finish_release"
	WarmStepReleaseLease     WarmStep = "release_lease"
	WarmStepDropState        WarmStep = "drop_state"
	WarmStepAbortRelease     WarmStep = "abort_release"
)

type WarmTimer

type WarmTimer interface {
	// C is the expiry channel.
	C() <-chan time.Time

	// Stop cancels an arming.
	Stop() bool

	// Reset arms the timer for a fresh duration.
	Reset(time.Duration) bool
}

WarmTimer is one session's warm countdown, narrowed to what an arming needs.

IT IS DECLARED HERE RATHER THAN REUSING host.Clock, whose NewTimer returns a CONCRETE *time.Timer. A concrete timer cannot be made to expire by a test without sleeping, and the runbook's step 1 for this task is "use an injected fake clock; no test sleeps". The three methods are time.Timer's own, so a production adapter is a two-line wrapper rather than a reimplementation.

type WorkState

type WorkState string

WorkState is what an observer reports one session to be doing, and it is a THREE-VALUED type rather than a boolean on purpose.

04-host.md: "A waiting resident gate is not whole-session idle and therefore is not warm-evicted by this plan." A boolean idle signal makes that a rule about every caller, and the one caller that reports a gate wait as idle releases a session with a human waiting on it. Naming the gate wait puts the rule in the releaser, where it is enforced once.

const (
	// WorkStateWorking reports work in flight. It cancels any warm arming.
	WorkStateWorking WorkState = "working"

	// WorkStateGateWaiting reports a resident gate waiting on a response. It is
	// NOT idle: §9.4's phase-one implementation pins the session for a bounded
	// TTL and the continuation that would let it be released is a later plan.
	WorkStateGateWaiting WorkState = "gate_waiting"

	// WorkStateIdle reports durable whole-session idle, which is the only
	// observation that arms a warm release.
	WorkStateIdle WorkState = "idle"
)

type Workspaces

type Workspaces interface {
	// EnsureWorkspace makes a workspace available and returns its root.
	EnsureWorkspace(context.Context, sessionwire.TenantID, sessionwire.SessionID) (string, error)

	// ReleaseWorkspace drops the local materialization. It never deletes
	// durable state.
	ReleaseWorkspace(context.Context, sessionwire.TenantID, sessionwire.SessionID) error
}

Workspaces materializes and un-materializes session workspaces.

It is WIDER THAN host.WorkspaceProvider by exactly one method, and the extra one is why the interface is declared here rather than reused. That type has only EnsureWorkspace, which is the half a successful attach needs; a FAILED attach needs the other half, or every rolled-back attach leaves a materialized workspace behind and a Host that refuses a hundred attaches fills its disk. ReleaseWorkspace drops the LOCAL MATERIALIZATION ONLY: the durable session, its objects and its checkpoints are untouched, which is what makes calling it on a rollback safe.

Jump to

Keyboard shortcuts

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