placement

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package placement wires the Sessions layer to the Runtime compute SPI. The Placer owns the incarnation lifecycle for a session: it drives Runtime.Create, mints the fence from the log (the single fence authority), stamps it on the incarnation, and binds a controller to that fence. Keeping this seam out of session.Service keeps the gRPC layer thin and the SPI logic unit-testable, and lets cmd/agentnode reuse it.

Index

Constants

This section is empty.

Variables

View Source
var ErrAdmissionInterrupted = errors.New("placement: admission interrupted")

ErrAdmissionInterrupted is returned when the caller's own deadline or cancellation ended a Describe check before the harness answered. It also wraps the context's error, so errors.Is reports context.DeadlineExceeded or context.Canceled. Like ErrHarnessUnavailable it is only raised before the harness runs or journal records are appended; session.Service surfaces it as codes.DeadlineExceeded or codes.Canceled. An interruption during Run does not wrap it.

View Source
var ErrCheckpointing = errors.New("placement: session is being checkpointed")

ErrCheckpointing is returned when another operation holds the session's checkpoint mark, or a checkpoint ended the harness connection a turn was using. It is retryable once that operation returns; session.Service maps it to codes.Aborted.

View Source
var ErrDescriptorMismatch = errors.New("placement: harness is not the expected one")

ErrDescriptorMismatch is returned when the harness reports a descriptor id other than the one the Placer expects (WithDescriptorID): the address reaches a different harness than the one that was configured. Like ErrUnplaceable it is raised by the admission check, before any compute is provisioned or anything is written to the log, and again on the turn's own connection for a LiveDescriber backend. session.Service surfaces it as codes.FailedPrecondition.

View Source
var ErrHarnessExists = errors.New("placement: harness name already in use")

ErrHarnessExists is returned by Add and MarkUnavailable for a name already in use or reserved for a static harness. This includes unservable names, which remain in use but do not resolve.

View Source
var ErrHarnessUnavailable = errors.New("placement: harness unavailable")

ErrHarnessUnavailable is returned when the harness could not be reached to describe itself during admission or the controller's connected-harness identity check. Neither check runs the harness or appends journal records; the connected check may follow compute allocation and fence minting. session.Service surfaces it as codes.Unavailable. If the caller's own deadline or cancellation ended the check, it is ErrAdmissionInterrupted instead: the same expired call cannot succeed.

View Source
var ErrHarnessUnservable = errors.New("placement: registered harness is unservable on this host")

ErrHarnessUnservable means a registered name is known to this host but cannot run here. Unlike ErrHarnessUnavailable (a transport/backend outage on a usable Placer), this is a registration-load failure: the name remains reserved and visible to operators.

View Source
var ErrRecordedHarnessNotServed = errors.New("placement: recorded harness is not served")

ErrRecordedHarnessNotServed refuses recovery when the pending invocation names a registry entry this host no longer serves. Unlike a caller's unknown name, it is a recovery precondition.

View Source
var ErrSessionBusy = errors.New("placement: another operation is in progress for this session")

ErrSessionBusy is returned when Exec, Suspend, Resume, Fork, or a guarded callback overlaps another operation on the same session through this Placer or its Registry. No compute or journal transition is attempted; session.Service surfaces it as codes.Aborted, independently of any backend transport status.

View Source
var ErrUnknownHarness = errors.New("placement: unknown harness")

ErrUnknownHarness is returned for a name unknown to this host. session.Service surfaces it as InvalidArgument: naming a harness that does not exist is a caller mistake, not a host fault, and failing is the point. Silently substituting a different harness would run the session on something other than what was asked for, and the log would record that as if it had been intended.

View Source
var ErrUnplaceable = errors.New("placement: harness cannot be placed on this runtime")

ErrUnplaceable is returned when a harness requires a capability the chosen backend cannot provide (e.g. REQUIRES_MEMORY_SNAPSHOT on a filesystem-only pod). The gate runs BEFORE Create, so an unplaceable harness never provisions compute or writes to the log; for a LiveDescriber backend it runs again on the turn's own connection, still before anything is written to the log. session.Service surfaces it as codes.FailedPrecondition.

Functions

func DefaultDial

func DefaultDial(inc api.Incarnation) (api.Harness, func() error, error)

DefaultDial reaches a harnesswire server by address form: runtime/local passes a unix-socket address (unix://…); substrate passes the atenet-router's host:port. Both ride the same harnesswire gRPC client with the incarnation's CallMetadata attached to every call; only the transport differs. One dial path, two address forms.

Types

type Backend

type Backend interface {
	api.Runtime
	Describe(ctx context.Context) (api.Descriptor, error)
}

Backend is the compute Runtime the Placer drives. It exposes the harness DESCRIPTOR so the Placer can gate CanPlace before Create (placement must not provision compute to learn a harness's needs); the harness itself is reached by dialing Incarnation.Address with Incarnation.CallMetadata on every call (Harness.Connect) — the one dial path both runtime/local and substrate use. runtime/local satisfies this.

type Dialer

type Dialer func(inc api.Incarnation) (api.Harness, func() error, error)

Dialer opens a Harness.Connect client to the harness an incarnation names and returns a closer for the connection. runtime/local passes a unix-socket address (unix://…); substrate passes the atenet-router's host:port, dialed over h2c, plus CallMetadata naming the actor, which the client must attach to every call. The default dialer handles both forms; WithDialer overrides it (tests). This is the one transport seam the harness rides unchanged.

type ExecOption

type ExecOption func(*execConfig)

ExecOption configures a single execution.

func WithDeadline

func WithDeadline(t time.Time) ExecOption

WithDeadline bounds the execution. It applies to the whole turn, including the model call, which is what makes it enforceable at all: the host owns that call, so cancelling the turn cancels the request in flight rather than leaving it to finish unobserved.

func WithHarness

func WithHarness(name string) ExecOption

WithHarness records the resolved registry name for this execution. Without a name, the controller uses the connected harness's descriptor ID. Names are per-call so registry aliases can share a Placer without changing one another's identity.

func WithObserver

func WithObserver(o controller.Observer) ExecOption

WithObserver relays a turn's records and streaming chunks as they happen, instead of leaving the caller to re-read the log once the turn is over.

func WithStart

func WithStart(config []byte, resumeFromSeq int64) ExecOption

WithStart carries the caller's opaque per-execution config and resume cursor to the harness.

type ForkChild

type ForkChild struct {
	UID string
	Log eventlog.Store
}

ForkChild is one child of a fan-out fork: the child's session UID and its (empty) log.

type LiveDescriber

type LiveDescriber interface {
	DescribesLiveHarness() bool
}

LiveDescriber is implemented by a Backend whose Describe asks whichever harness is answering at an address, rather than returning a declaration fixed when the backend was built (runtime/remote).

The Placer runs a turn over its own connection to Incarnation.Address, and that connection can reach a different process than the one Describe asked: another replica behind a load balancer or a Kubernetes Service, or a harness restarted in between. When DescribesLiveHarness reports true, the Placer therefore asks again on the connection that will run the turn and applies the same CanPlace gate before writing records. Exec and unchecked Resume do this before minting a fence; cursor-checked Resume has minted an early fence and retakes its writer fence after this gate. This narrows the window but does not close it; see recheck for the limit.

A backend that reports true must provision nothing in Create and Restore: a refusal by the second check only closes the dial and never calls Stop, so anything provisioned would leak.

type Option

type Option func(*Placer)

Option configures a Placer.

func WithDescriptorID

func WithDescriptorID(id string) Option

WithDescriptorID makes the Placer refuse a harness whose Describe reports a HarnessDescriptor.id other than id, with ErrDescriptorMismatch, wherever it applies the CanPlace gate: Exec, Resume and Fork, and the turn's own connection for a LiveDescriber backend. Empty, the default, means the id is not checked.

The id is what the harness reports about itself, so the check catches an address that reaches the wrong harness, not a harness that lies about what it is. It is not authentication.

func WithDialer

func WithDialer(d Dialer) Option

WithDialer overrides how the Placer reaches a harness (default: unix-socket dial for runtime/local).

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger enables structured operational logs. Message contents and fence tokens are never logged.

func WithStreamingModel

func WithStreamingModel(fn controller.StreamFunc) Option

WithStreamingModel supplies a model that reports partial output, which the controller relays as ephemeral deltas. Without it a turn still runs; the caller just sees the finalized output only.

func WithToolExecutor

func WithToolExecutor(fn controller.ToolFunc) Option

WithToolExecutor supplies the host executor for controller-mediated tools in Exec and Resume. It receives the session UID bound by the Placer. Without one, tool calls fail closed. The host owns session-scoped authorization and durable deduplication by session UID plus idempotency key.

type Placer

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

Placer owns the incarnation lifecycle: Create the compute, mint+bind the fence, drive the controller.

func New

func New(backend Backend, model controller.ModelFunc, opts ...Option) *Placer

New builds a Placer over a compute backend and the live model.

func (*Placer) Describe

func (p *Placer) Describe(ctx context.Context) (api.Descriptor, error)

Describe asks the placed harness for its descriptor, the same call Exec gates placement on. It provisions nothing.

func (*Placer) DescriptorID

func (p *Placer) DescriptorID() string

DescriptorID returns the id set with WithDescriptorID, or "" when the Placer does not check it.

func (*Placer) Exec

func (p *Placer) Exec(ctx context.Context, log eventlog.Store, sessionUID string, inputs []api.Message, expectedLastSeq int64, opts ...ExecOption) (inc api.Incarnation, err error)

Exec rejects an owned unreceipted approval before runtime admission; otherwise it creates an incarnation and drives the harness under a log-minted fence stamped on the returned incarnation. A new approval gate closes the dial, snapshots and commits SUSPEND under that same fence before returning *api.ApprovalParkedError. Handoff failures return the actual operational cause.

func (*Placer) Fork

func (p *Placer) Fork(ctx context.Context, parent eventlog.Store, parentUID string, children []ForkChild, atSeq int64) (err error)

Fork branches a session into one or more children: it provisions each child's compute through the Runtime SPI and copies the parent's log prefix up to atSeq so every child shares the parent's hash chain.

The compute half depends on the harness's resumability. A STATELESS_REPLAY harness replay-forks — each child cold-boots and the copied journal reconstructs it, leaving the parent untouched. A REQUIRES_MEMORY_SNAPSHOT harness holds live state the journal cannot reconstruct (I4), so the parent is checkpointed first and every child is cloned from that snapshot.

The whole fan-out shares ONE parent checkpoint: N children cost one snapshot and N clones, and all of them branch from the identical state. The parent's checkpoint mark is held until the last child is cloned, so a turn on the parent is refused (ErrCheckpointing) for the whole fan-out instead of waking the parent and replacing the snapshot the remaining children clone from.

func (*Placer) Resume

func (p *Placer) Resume(ctx context.Context, log eventlog.Store, sessionUID string, opts ...ResumeOption) error

Resume returns *api.ApprovalParkedError for an owned pending request without runtime IO or a new fence. A call-only cut mints a fence and repairs one request at the captured head, also without runtime IO. Neither park proves cold compute: only a committed SUSPEND records that. Otherwise Resume restores the recorded SnapshotRef (or the session handle when no owned SUSPEND exists), mints a new fence, re-drives the turn and records RESUME on ordinary success. A new gate instead closes the dial, snapshots and commits SUSPEND under that recovery fence, returning a typed park without RESUME. Repair/handoff failures return their actual cause.

func (*Placer) RuntimeCapabilities

func (p *Placer) RuntimeCapabilities() api.RuntimeCapabilities

RuntimeCapabilities reports the backend's compute capabilities without contacting the harness.

func (*Placer) Suspend

func (p *Placer) Suspend(ctx context.Context, log eventlog.Store, sessionUID string) (ref api.SnapshotRef, err error)

Suspend transitions the incarnation to cold via SnapshotExternal, then records the SnapshotRef in a SUSPEND lifecycle event so Resume can recover it from the tamper-evident chain (§5.1). Snapshot owns dedicated compute release and retains the handles Restore needs; Stop would destructively tear them down. Snapshot keys on the session id, so a minimal incarnation suffices.

Suspend does not interrupt a running turn. While an Exec or Resume of the session runs through this Placer or its Registry, Suspend fails with ErrSessionBusy without touching compute or the journal, and the caller retries once the turn has returned. So the checkpoint never runs under an open harness stream of a turn this Registry drives.

Suspend also holds the session's checkpoint mark, the one a stateful Fork holds, from before it snapshots until it has recorded the SUSPEND event. While a fork of the session holds the mark, from its checkpoint until its last child is cloned, Suspend fails with ErrCheckpointing without touching compute or the journal; a fork that arrives during a Suspend is refused the same way. So a Suspend can neither fail the fork's SUSPEND record nor replace the snapshot its children clone from.

type Registry

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

Registry resolves a harness name to the Placer that runs it.

A Placer owns exactly one backend and one model, so a harness and the compute it runs on are chosen together rather than separately. That is why the registry sits above the Placer instead of inside it: which harness to run is a routing decision, while placing one harness on one runtime is what a Placer already does.

The entries given to NewRegistry are static: built into the host or configured when it starts. Add puts a registered harness in later, on a running host; MarkUnavailable reserves one that cannot be loaded. An entry is never replaced or removed once added, because a session's turns, streams and fences are coordinated through the Placer it resolves to, and a second Placer for the same name would split that coordination.

func NewRegistry

func NewRegistry(defaultHarness string, placers map[string]*Placer, opts ...RegistryOption) (*Registry, error)

NewRegistry builds a registry over the named placers. defaultHarness names the entry used when a caller does not specify one, and must itself be registered: a default that resolves to nothing would turn every unqualified request into an error at call time rather than at startup.

The placers are switched to the Registry's shared session guard and its shared set of per-session connections and checkpoint marks; Add does the same for a Placer added later. session.Service routes a turn by ExecRequest.harness but a Suspend or Fork by the session's recorded harness, so the two can land on different Placers; sharing them is what lets Suspend refuse a turn (ErrSessionBusy), and a stateful fork end and refuse one, whichever Placer runs it. Separate Registries do not fence each other.

NewRegistry rejects a Placer that already belongs to a registry, and one with an operation in progress: an Exec, Suspend or Resume holding the session guard, an open harness connection, or a checkpoint mark. Replacing its guard and set would orphan them, so a later Suspend would not be refused by that operation and a later checkpoint would neither end nor refuse its turn. A Placer that has run operations and is now idle holds nothing and is accepted. The check cannot see an operation that starts while NewRegistry runs, and the guard and set are replaced without synchronization, so NewRegistry must not run concurrently with any method of the placers it is given: build the registry before serving.

func (*Registry) Add

func (r *Registry) Add(name string, p *Placer) error

Add makes a registered harness resolvable. It refuses a reserved name and a name already in use, static or not: the caller decides whether a repeat registration is the same harness before it builds a Placer, and an existing entry is never replaced.

On success p shares the Registry's session guard and its per-session connections and checkpoint marks, exactly as the Placers given to NewRegistry do: an operation on a session through p and one through any other Placer in this Registry refuse to overlap with ErrSessionBusy, and a stateful fork through either ends and refuses a turn through the other. Add refuses a p that belongs to another Registry or has an operation in progress, as NewRegistry does; the check cannot see an operation that starts while Add runs, so p must not be in use until Add returns. Adding a Placer already in this Registry under another name shares nothing new. A refused p is left untouched.

func (*Registry) Approve

func (r *Registry) Approve(ctx context.Context, log eventlog.Store, sessionUID string, decision api.ApprovalDecision) (eventlog.Record, error)

Approve journals only a decision under the shared session guard. The decision does not need a served/available harness or any compute, so routing and host recovery checks do not apply. Approve returns ErrCheckpointing while a stateful Fork holds the session's checkpoint mark.

func (*Registry) Default

func (r *Registry) Default() string

Default is the harness used when a caller names none. session.Service records it on a session created without one, so a stored harness always refers to something the host can actually run.

func (*Registry) For

func (r *Registry) For(harness string) (*Placer, error)

For returns the Placer for a harness name. An empty name selects the default, which is how a caller that does not care about harness selection keeps working unchanged.

func (*Registry) Has

func (r *Registry) Has(name string) bool

Has reports whether this host knows the name, including registrations it cannot serve. For, not Has, determines whether a usable Placer exists.

func (*Registry) MarkUnavailable

func (r *Registry) MarkUnavailable(name string, cause error) error

MarkUnavailable reserves a registered name whose stored spec or local placement cannot be loaded. This records a permanent host-local registration failure, not a transient backend outage (ErrHarnessUnavailable). It never replaces a live Placer or a previously recorded failure.

func (*Registry) Names

func (r *Registry) Names() []string

Names lists the served and unservable known harnesses in sorted order, for startup logs and error messages. Has/Names include unavailable names, while For alone resolves a usable Placer.

func (*Registry) Reserved

func (r *Registry) Reserved(name string) bool

Reserved reports whether name belongs to the host: a static harness, or a name reserved with WithReservedNames. A reserved name can never be added.

func (*Registry) ReservedNames

func (r *Registry) ReservedNames() []string

ReservedNames lists every reserved name, static harnesses included, in sorted order.

func (*Registry) Resume

func (r *Registry) Resume(ctx context.Context, log eventlog.Store, sessionUID, sessionDefaultName string, opts ...RegistryResumeOption) error

Resume resolves and recovers an invocation under the Registry's shared session guard, running the resolved-harness check before runtime IO or call-only repair. Pending/repaired requests return *api.ApprovalParkedError without runtime IO; that alone is not cold-compute proof. Decided recovery delegates to the Placer: a new gate commits SUSPEND and returns park, while ordinary success records RESUME. Repair/handoff failures retain their actual cause. Contention through this Registry wins over stale invocation errors. Legacy markerless and completed journals retain sessionDefaultName (empty = registry default).

func (*Registry) Static

func (r *Registry) Static(name string) bool

Static reports whether name is a harness given to NewRegistry rather than one added later.

func (*Registry) StaticNames

func (r *Registry) StaticNames() []string

StaticNames lists the static harnesses in sorted order.

func (*Registry) Suspend

func (r *Registry) Suspend(ctx context.Context, log eventlog.Store, sessionUID, sessionDefaultName string, opts ...RegistryResumeOption) (api.SnapshotRef, error)

Suspend routes an owned unreceipted approval to its recorded harness, including retries after an automatic snapshot/append failure. Inherited approvals and ordinary sessions retain their stored-default route.

func (*Registry) WithSessionGuard

func (r *Registry) WithSessionGuard(sessionUID string, operation func() error) error

WithSessionGuard runs operation synchronously under the same reclaimable guard as every Placer in this Registry. Contention returns ErrSessionBusy without calling operation. The callback must not call guarded Registry or Placer methods: the guard is not reentrant.

func (*Registry) WithSessionRead

func (r *Registry) WithSessionRead(sessionUID string, operation func() error) error

WithSessionRead protects a routing read phase from deletion without serializing it with running turns or Fork rollback. Contention refuses the callback before it can wait for SQL. The callback must not call WithSessionGuard: shared admission cannot upgrade to exclusive.

type RegistryOption

type RegistryOption func(*Registry)

RegistryOption configures a Registry.

func WithReservedNames

func WithReservedNames(names ...string) RegistryOption

WithReservedNames reserves harness names for static harnesses that this host can be configured with but is not serving now, such as a built-in that needs a flag. Reserving them keeps a registration from taking a name that would collide the next time the host starts with that configuration.

type RegistryResumeOption

type RegistryResumeOption func(*registryResumeConfig)

RegistryResumeOption configures one Registry.Resume call.

func WithRegistryResumeObserver

func WithRegistryResumeObserver(o controller.Observer) RegistryResumeOption

WithRegistryResumeObserver reports newly committed recovery records and streaming chunks. This is a RegistryResumeOption, distinct from the Placer's WithResumeObserver option.

func WithResolvedHarnessCheck

func WithResolvedHarnessCheck(check func(name string) error) RegistryResumeOption

WithResolvedHarnessCheck runs check on the harness name Registry.Resume or Registry.Suspend resolved, under the session guard and before runtime IO. A non-nil error is returned unchanged. The host uses it to apply routing rules that live above the Registry.

func WithResumeExpectedLastSeq

func WithResumeExpectedLastSeq(seq int64) RegistryResumeOption

WithResumeExpectedLastSeq makes Registry.Resume refuse a different journal head with eventlog.ErrConflict, including both values. Checks run under the shared session guard before inspection, after an early fence before Restore/repair, and after a restored turn's regular writer fence before Run. A late refusal may already have restored compute. Either fence can supersede another writer even on refusal; pending approval queries need no fence. Without this option behavior is unchanged.

type ResumeOption

type ResumeOption func(*resumeConfig)

ResumeOption configures one recovery attempt.

func WithResumeHarness

func WithResumeHarness(name string) ResumeOption

WithResumeHarness identifies the registry entry selected for this recovery attempt. The controller checks it against the pending invocation before running the harness.

func WithResumeObserver

func WithResumeObserver(o controller.Observer) ResumeOption

WithResumeObserver reports newly committed recovery records and streaming chunks, including SUSPEND on a second approval park. It does not re-emit previously recorded effects.

Jump to

Keyboard shortcuts

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