Documentation
¶
Overview ¶
Package service derives what a Host publishes about itself, and relays what its resident sessions have already committed.
The first is the TARGET DIRECTORY of spec §15: one advertisement per Department LaunchTarget, describing what this Host can launch and how much room it has left. Factory pages these to choose a placement candidate and aggregates them into `/v1/agents`.
The second is the LIVE TAIL of one resident session (events.go): the events its runtime has durably appended, relayed byte-for-byte onto that session's HostLink channel. It is the opposite kind of thing to an advertisement — it is DERIVED FROM NOTHING, because nothing here decides what a public event is or what its body says. Every whitelist below is about the advertisement half and says so; a Tail cannot express an Advertisement, which is what keeps the two halves from becoming one surface.
WHAT THIS PACKAGE DOES NOT DO, because §15 says Factory must not find it here: it publishes NO SECOND CATALOGUE and COPIES NO RIG SUBAGENT ENTRY.
Those two halves are ONE property, because a copied Rig subagent entry and an invented static row are the same thing seen from two sides: a published row whose agent this Department does not register. One pair of whitelists holds it. TestPublishedAgentsAreExactlyTheDepartmentsRegisteredAgents holds the DATA — the rows Publish returns are in bijection with Department.AgentIDs, in all three of initial publish, post-admission and drain. TestPublishIsTheOnlyPublishingSurface holds the API: this package's production files are enumerated, every exported top-level DECLARATION in them is enumerated, every exported STRUCT's fields are enumerated, and exactly one declaration lets an Advertisement reach a caller.
EACH OF THOSE WORDS REPLACED A NARROWER ONE THAT A PROBE DEFEATED, and the four failures were one mistake: the guard named a proxy for its subject. Scoped to one file name, a second publishing surface in a new FILE escaped. Walking only *ast.FuncDecl, one declared as a func-typed VAR escaped. Collecting only named fields, an EMBEDDED struct added published fields invisibly. Checking only a function's RESULTS, a second catalogue delivered into a caller-supplied sink PARAMETER escaped, because "results" is a proxy for "reaches a caller". All four are closed and all four are re-probed.
THE DATA GUARD AND THE API GUARD ARE BOTH WHITELISTS: each enumerates what may appear and reports everything else. The alternative shape — searching for a second catalogue — cannot establish an absence, which is why neither is written that way.
WHAT THOSE TWO DO NOT REACH, measured rather than assumed. They are scoped to this package: a second catalogue published from a DIFFERENT package is invisible to both, and so is anything a caller appends after Publish has returned. A probe confirmed the file scope was the live gap — a second exported publishing surface added in a new file of this package survived both guards until the FILE SET became part of the enumeration; it now fails. And note what the API guard does not claim: "nothing here reads a Rig" is true of the code as written and is not itself asserted anywhere. What is asserted is the consequence — that no row reaches the output whose agent the Department does not register — which is what a copied subagent listing would have to do to matter.
NOTHING HERE WRITES. An Advertisement is a value describing a record; the SessionStore OrderedIndex write, its CAS, and the due reconciler that reaps a crashed Host's rows are later tasks. "Heartbeats move rank and expiry atomically" is held here only in the form this layer can hold it — both are fields of one derived record, so a writer CASes them together or not at all.
Index ¶
- Constants
- Variables
- type AdmissionConflictError
- type AdmissionRefusedError
- type Advertisement
- type AdvertisementError
- type CapacityOptions
- type CapacityPublisher
- func (p *CapacityPublisher) Admit(key registry.Key, agent sessionwire.AgentID) error
- func (p *CapacityPublisher) BeginDrain()
- func (p *CapacityPublisher) ConsumedWeight() uint64
- func (p *CapacityPublisher) Draining() bool
- func (p *CapacityPublisher) Own(key registry.Key, generation uint64) bool
- func (p *CapacityPublisher) Publish() ([]Advertisement, error)
- func (p *CapacityPublisher) Release(key registry.Key) bool
- func (p *CapacityPublisher) ReleaseOwned(key registry.Key, generation uint64) bool
- type FlushTimer
- type InvalidCapacityOptionsError
- type InvalidTailOptionsError
- type Projector
- type Publications
- type Routes
- type Tail
- type TailEnd
- type TailOptions
- type Tails
Constants ¶
const AdvertisementNamespace = "host/target-directory"
AdvertisementNamespace is the OrderedIndex namespace target advertisements live in. It obeys the storage name grammar — lower-case segments joined by '/' — because a namespace that did not could never be written.
Variables ¶
var ErrForeignPublication = errors.New("service: the tail delivered a publication naming another session")
ErrForeignPublication is the refusal for a publication naming a session other than the one the tail was started for.
It is fail-closed rather than a filter. The identities are stamped by the runtime adapter from the session it bound, so a mismatch is not a stray delivery to skip past — it means this tail's stream and its key disagree about whose events these are, and publishing either interpretation onto a channel derived from the key would put one session's events on another's route.
var ErrTargetGenerationSuperseded = errors.New("service: a newer generation of this HostID owns the target row, so this incarnation can no longer write it")
ErrTargetGenerationSuperseded reports a target-directory write refused because a NEWER incarnation of this same HostID — a higher host generation — has already written the row.
IT IS PERMANENT FOR THIS PROCESS, which is what separates it from every other directory failure. The row's generation mark only rises, so no retry by this incarnation can ever succeed, and the row this Host would withdraw is no longer this Host's: it says whatever the newer incarnation last published. A drain that treated it as an ordinary failure aborted before releasing a single session, and the residency leases it held were then never handed back (the v0.3.0 same-HostID overlap).
Functions ¶
This section is empty.
Types ¶
type AdmissionConflictError ¶
type AdmissionConflictError struct {
Key registry.Key
Admitted sessionwire.AgentID
Requested sessionwire.AgentID
}
AdmissionConflictError reports one ledger key admitted under two agents.
It is a DISTINCT type from AdmissionRefusedError and deliberately carries no HostLinkErrorCode: it is not a placement outcome Factory should retry elsewhere but a Host-side bookkeeping fault, and giving it a wire code would invite a caller to treat it as one.
func (*AdmissionConflictError) Error ¶
func (e *AdmissionConflictError) Error() string
Error describes the conflict.
type AdmissionRefusedError ¶
type AdmissionRefusedError struct {
Code sessionwire.HostLinkErrorCode
AgentID sessionwire.AgentID
Reason string
Cause error
}
AdmissionRefusedError reports an admission this Host may not accept.
Code is one of Core's HostLinkErrorCode values rather than a locally invented vocabulary, because §15's routing algorithm tells Factory to discard its binding and re-run placement on exactly these: not_admitting, no_capacity, runtime_unavailable, runtime_mismatch. A local code would have to be translated at the wire edge, and a translation table is a second place for the meaning to drift.
func (*AdmissionRefusedError) Error ¶
func (e *AdmissionRefusedError) Error() string
Error describes the refusal.
func (*AdmissionRefusedError) Unwrap ¶
func (e *AdmissionRefusedError) Unwrap() error
Unwrap returns the typed error a lower layer produced, if any.
type Advertisement ¶
type Advertisement struct {
// Namespace is the OrderedIndex namespace. It is AdvertisementNamespace for
// every record this package derives, and is carried on the value rather
// than left implicit so a writer needs nothing but the Advertisement.
Namespace string
// RankingScope is the LaunchTarget scope this record is ranked within. It
// is the stable key's tuple MINUS the host, because ranking exists to
// compare Hosts offering the same target against each other.
RankingScope string
// StableKey identifies this record: (agent, runtime compatibility,
// placement, host). It is a digest, not the tuple — see stableKey.
StableKey string
// Rank is available admission capacity, which is what §15 ranks by. It is
// zero on a tombstone.
Rank int64
// Ranked reports whether this record appears in a ranked page at all. It is
// false for a drained Host AND for a target this Host cannot place, and the
// second half is a DECISION beyond what §15 spells out — the spec names
// drain as un-ranking and does not say drain is the only thing that may.
//
// The distinction it rests on is between a row that is not a candidate NOW
// and one that can never be a candidate HERE. A full but accepting Host has
// zero available capacity and stays ranked, because the next heartbeat may
// find room; a target whose declared placement this Host does not offer is
// unplaceable for the life of the process, so ranking it only puts a row in
// the page Factory must then discard. Tombstone, not Ranked, is what
// distinguishes drain from either.
//
// It therefore coincides with Report.Accepting in every state this package
// can derive. They are separate fields because they are read by different
// parties — Accepting is record CONTENT a reader filters on, Ranked is an
// INDEX operation the writer performs — and the coincidence is asserted by
// test rather than assumed, in all three states.
Ranked bool
// DueAt is when a bounded due reconciler must look at this record. It is
// ExpiresAt, so a Host that stops heartbeating is reaped by its own expiry.
DueAt time.Time
// Tombstone reports a record graceful drain has retired. It is a record and
// not a deletion, so a reader that has already seen the offer learns it is
// gone rather than waiting out the expiry.
Tombstone bool
// Report is the sessionwire observation itself, validated before it is
// returned.
Report sessionwire.HostLinkCapacityReport
}
Advertisement is one derived target-directory record.
It is a VALUE and carries no method that writes. Report is the wire record a reader consumes; the seven fields around it are the OrderedIndex placement of that record — where it is stored (Namespace, StableKey), how it is ordered against other Hosts offering the same LaunchTarget (RankingScope, Rank, Ranked), when a due reconciler sees it (DueAt), and whether it is a gravestone rather than an offer (Tombstone).
func (Advertisement) Expired ¶
func (a Advertisement) Expired(now time.Time) bool
Expired reports whether a reader must treat this record as absent at now.
The boundary is INCLUSIVE of the expiry instant: a record whose ExpiresAt has arrived is gone. The alternative reading keeps a lapsed advertisement alive for one instant, which buys nothing and makes "expired" disagree with Core's validateHostLinkTimes, where ExpiresAt must be strictly after ObservedAt.
type AdvertisementError ¶
type AdvertisementError struct {
AgentID sessionwire.AgentID
Reason string
Cause error
}
AdvertisementError reports a derived record no reader would accept.
func (*AdvertisementError) Error ¶
func (e *AdvertisementError) Error() string
Error describes the underivable advertisement.
The cause is optional in the TEXT even though every construction site in this file supplies one, because the type is exported: a caller building one without a cause would otherwise crash inside the logging call, which is the worst place to crash. The other three error types here already guard this.
func (*AdvertisementError) Unwrap ¶
func (e *AdvertisementError) Unwrap() error
Unwrap returns the typed error a lower layer produced, so a caller reaches Core's *RequestValidationError and the field it names.
type CapacityOptions ¶
type CapacityOptions struct {
// Host is the validated Host whose Department is advertised. Everything
// else the derivation needs — identity, endpoint, isolation class,
// placement, capacity, registry expiry, clock — is read from it, so there
// is no second copy of 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. It is NOT on
// host.Options: a Host that restarts on the same configuration is a new
// generation, so the value belongs to the run and not to the configuration.
HostGeneration uint64
}
CapacityOptions configures a CapacityPublisher.
type CapacityPublisher ¶
type CapacityPublisher struct {
// contains filtered or unexported fields
}
CapacityPublisher derives the Department's target advertisements.
It holds the ONLY mutable state in this package: the admission ledger and the drain flag. Everything else is either read from the immutable Host or taken from the target snapshot below, so no value it derives from can drift from the one that was validated.
THAT SENTENCE WAS FALSE WHEN IT WAS FIRST WRITTEN, which is why it now names the snapshot. Capabilities() and CompatibilityID() were re-read on every heartbeat, and they are caller code free to answer differently each time; the claim covered only the Host and read as though it covered everything. See launchable for the panic that cost.
FOR O6.1 AND O6.3, WHICH ARRIVE LATER AND ELSEWHERE. 04-host.md puts weighted memory/admission limits in internal/residency and internal/lifecycle, and drain in internal/lifecycle/drain.go. Those must CONSUME this ledger and this flag, not keep their own. Two sources of "consumed" is an over-admission bug that neither package's tests could see — each would be internally consistent and the Host would admit past its capacity — and two sources of "draining" is a Host that stops accepting in one place while still advertising Accepting from the other. Whatever the final ownership, there must be exactly one of each, and this is the one that already exists.
func NewCapacityPublisher ¶
func NewCapacityPublisher(options CapacityOptions) (*CapacityPublisher, error)
NewCapacityPublisher validates options and returns a publisher.
It SNAPSHOTS every LaunchTarget's description, validates it, and then derives the whole publication once, refusing if any record would be one Core rejects.
The snapshot is what makes everything after this total: see launchable. The trial derivation is separate and is not belt-and-braces — HostGeneration is a required non-zero wire field that host.Options does not carry, and Clock is an injected interface that may return an instant Core refuses, so a publisher can be constructed that could never publish anything. Failing here hands the operator the configuration; failing at the first heartbeat hands them a Host that started and then went silent.
func (*CapacityPublisher) Admit ¶
func (p *CapacityPublisher) Admit(key registry.Key, agent sessionwire.AgentID) error
Admit charges one session's admission weight against this Host's capacity.
It is IDEMPOTENT for a repeated (key, agent): a redelivered command must not charge twice, and Host's command delivery is explicitly redeliverable. The same key under a DIFFERENT agent is refused instead, because the ledger would then hold a weight attributable to neither.
func (*CapacityPublisher) BeginDrain ¶
func (p *CapacityPublisher) BeginDrain()
BeginDrain stops admission and turns every advertisement into an unranked tombstone. It does NOT evict: the sessions already admitted stay charged until their own Release, which is the difference between draining and being emptied.
func (*CapacityPublisher) ConsumedWeight ¶
func (p *CapacityPublisher) ConsumedWeight() uint64
ConsumedWeight reports the admission weight currently charged.
func (*CapacityPublisher) Draining ¶
func (p *CapacityPublisher) Draining() bool
Draining reports whether graceful drain has begun.
func (*CapacityPublisher) Own ¶ added in v0.9.0
func (p *CapacityPublisher) Own(key registry.Key, generation uint64) bool
Own binds a session's charge to the residency generation that installed it, and reports whether the charge is now that generation's.
A charge that belongs to no residency yet (a fresh Admit, or one a successor's Admit claimed) is bound; a charge the generation already owns is reported owned; a charge another generation owns, or no charge, is refused.
func (*CapacityPublisher) Publish ¶
func (p *CapacityPublisher) Publish() ([]Advertisement, error)
Publish derives the current advertisement for every Department LaunchTarget.
It is BOTH the initial publish and the heartbeat; there is no separate refresh path, because a heartbeat that derived its record differently from the first publish is exactly the drift that makes a stale advertisement hard to explain. The instant comes from the Host's Clock and the expiry from its validated RegistryExpiry, so the margin host.MinHeartbeatsBeforeExpiry enforces is the margin the record actually carries.
A record Core would reject fails the whole call and the result is nil, so a caller cannot write the rows that happened to be derived first. The probe for that returns the rows derived so far alongside the error and is killed — but by the NIL, not by a genuinely partial slice: the only refusal reachable here is the observation instant, which is derived once and applies to every row at once, so no input this package accepts produces a partial publication to begin with.
func (*CapacityPublisher) Release ¶
func (p *CapacityPublisher) Release(key registry.Key) bool
Release returns one admitted session's weight to this Host's capacity, and reports whether this call was the one that released it.
A second release of the same key, or a release of a key never admitted, credits nothing and reports false. Either crediting would let the Host admit beyond its capacity, which is the one failure the ledger exists to prevent.
func (*CapacityPublisher) ReleaseOwned ¶ added in v0.9.0
func (p *CapacityPublisher) ReleaseOwned(key registry.Key, generation uint64) bool
ReleaseOwned credits a session's charge back ONLY while it still belongs to the residency generation releasing it, and reports whether it did.
It is the credit every path that ends a residency uses (booked finding B2). Release, keyed by session alone, credited whatever charge the key held — and between the old residency's registry removal and its credit a successor may already have claimed that charge with an idempotent Admit, so the credit uncharged a resident session and the Host over-admitted by one.
type FlushTimer ¶ added in v0.12.0
FlushTimer is the small timer contract used by the live-text relay.
type InvalidCapacityOptionsError ¶
InvalidCapacityOptionsError reports an option a publisher may not run with.
func (*InvalidCapacityOptionsError) Error ¶
func (e *InvalidCapacityOptionsError) Error() string
Error names the option and why it was refused.
func (*InvalidCapacityOptionsError) Unwrap ¶
func (e *InvalidCapacityOptionsError) Unwrap() error
Unwrap returns the typed error a lower layer produced, if any.
type InvalidTailOptionsError ¶
InvalidTailOptionsError reports an option a Tails may not run with.
func (*InvalidTailOptionsError) Error ¶
func (e *InvalidTailOptionsError) Error() string
type Projector ¶ added in v0.10.0
type Projector func(ctx context.Context, body json.RawMessage, journalSeq uint64) (json.RawMessage, error)
Projector rewrites one committed public body before it is published, and nothing else about the publication. A nil Projector relays bodies verbatim.
IT IS THE ONE EXCEPTION TO "THE BYTES ARE CARRIED", and it is one on purpose (finding W1): a runtime's body names the RUNTIME session and command ids, which are private, and every consumer of this stream is a client's. The exception is a DETERMINISTIC function of the stored body and immutable identities, applied identically to the durable read (see host.PublicJournal), so the live body still equals the one /journal serves for the same event. A projection that fails refuses the publication (TailEndRefused): relaying the unprojected body would leak, and skipping it would leave a hole.
It is handed the body and the event's journal sequence, and nothing it could use to move the event: identity, sequence and coverage are relayed unchanged.
type Publications ¶
type Publications interface {
// Publish delivers one payload to every subscriber of one channel.
Publish(channel string, payload []byte) error
}
Publications is the transport one session's committed tail is published through.
IT TAKES A CHANNEL AND A PAYLOAD AND NOTHING ELSE, which is how "Host sends one message per event and fans out nowhere" is a property of the TYPE rather than of this package's restraint: there is no parameter a link, a replica or a subscriber set could ride in, so no future caller can address one even by accident. The fan-out belongs to the transport, which owns a queue per physical connection; Host owns the decision to publish once.
The shape is centrifuge's own — a channel, bytes, no context — because the implementation is a call straight through to it, and a seam carrying a context nobody could honour would be describing a cancellation that does not exist.
type Routes ¶
type Routes interface {
// InvalidateSession drops every link's route to one session and returns
// them.
InvalidateSession(registry.Key) []hostlink.Binding
}
Routes is the routing table a lost tail invalidates, narrowed to the single method a tail may reach.
It is ONE method and that method is session-scoped, for the reason Residencies is one read: a publisher that could bind, deliver or close a link would be able to repair its own loss by re-routing it, and the whole point of invalidating is that the repair is Factory's — a durable reset from its own cursor, not a hidden retry here. *hostlink.Multiplexer satisfies it; the conformance assertion lives in the test so this package does not need it at build time.
type Tail ¶
type Tail struct {
// contains filtered or unexported fields
}
Tail is one running publisher for one session.
func (*Tail) Channel ¶
Channel is the HostLink channel this tail publishes on. It is minted by hostlink.ChannelFor and never parsed here.
func (*Tail) Done ¶
func (t *Tail) Done() <-chan struct{}
Done closes when this tail has stopped publishing.
func (*Tail) End ¶
End reports why this tail stopped, and the underlying cause when there is one. It is TailEndRunning until Done closes.
func (*Tail) EphemeralDrops ¶ added in v0.12.0
EphemeralDrops reports transient frames discarded at the relay. The counter carries no body or text.
func (*Tail) EphemeralPublished ¶ added in v0.12.0
EphemeralPublished reports admitted transient frames separately from the durable count.
type TailEnd ¶
type TailEnd string
TailEnd is why one tail stopped.
const ( // TailEndRunning reports a tail that has not ended. TailEndRunning TailEnd = "running" // TailEndStopped reports a tail this Host ended: the caller stopped it or // the context it was started under is done. It is the ONE ending that does // not invalidate a route, because the route did not fail — Host chose to // stop feeding it, and whoever chose that owns what happens to the route. TailEndStopped TailEnd = "stopped" // TailEndLost reports a tail that ended without Host asking: the runtime // stopped, the hub failed the subscription for an enduring overflow, or // Host's own egress overflowed. Every one of them means this Host can no // longer feed the live stream, so the routes are invalidated. // // A mixed subscription carries a typed cause when its adapter sees an // enduring delivery without committed fields; Tail.End surfaces that cause. // A bare channel close still carries no cause, so Host cannot distinguish // hub backpressure from all upstream invariant failures. It invalidates // routes in either case and never resubscribes on its own. TailEndLost TailEnd = "lost" // TailEndRefused reports a publication this Host would not put on the wire: // one naming another session, one Core's own encoder rejects, or one the // transport refused. It invalidates for the same reason a loss does — the // stream has a hole in it — and it is a separate value because the repair // is different and an operator must not read one as the other. TailEndRefused TailEnd = "refused" )
type TailOptions ¶
type TailOptions struct {
Publications Publications
Routes Routes
FlushInterval time.Duration
IncludeReasoning bool
// IncludeToolSteps relays the runtime's public ToolCallStarted and
// ToolCallCompleted bodies. It needs a runtime implementing
// department.LiveOptionsSubscriber.
IncludeToolSteps bool
// NewFlushTimer allows a controlled clock for interval tests.
NewFlushTimer func(time.Duration) FlushTimer
Logger *slog.Logger
}
TailOptions configures a Tails.
type Tails ¶
type Tails struct {
// contains filtered or unexported fields
}
Tails publishes committed session tails and admitted transient text onto HostLink channels.
WHAT IT DOES NOT DO IS THE SUBSTANCE. It does not read a journal, does not project an event, does not re-encode a body, and does not decide what a public body IS. Harness's committed-public-event capability supplies the exact canonical body the durable append stored, together with the EventID, JournalSeq and CoveredThrough that append committed under; this type relays all five unchanged. A second projection would be a second answer: a consumer joining its durable tail to this live stream dedupes on (sequence, event id) and compares bodies, and if the live body were re-derived it could differ from the stored one in key order or in a field a later projector added, with no error anywhere.
Core's own EnduringPublication says the same from the other side: its MarshalJSON "keeps the canonical public body opaque … a Host relays that committed body rather than decoding and synthesizing it anew" (core@v0.7.0 sessionwire/v1/publications.go:152-154). Body is a json.RawMessage on both sides of this package, so the bytes are carried, not parsed.
THE ONE EXCEPTION IS A Projector (finding W1). A runtime's canonical body names its PRIVATE runtime session and command ids, and every reader of this stream is a client's, so a composed Host rewrites exactly those identities before publishing. That is not a second projection in the sense above: it is a deterministic function of the stored body that the durable read applies too (host.PublicJournal), so live and durable bodies stay byte-identical.
func NewTails ¶
func NewTails(options TailOptions) (*Tails, error)
NewTails validates the options and returns a publisher.
func (*Tails) Publish ¶
func (t *Tails) Publish( ctx context.Context, key registry.Key, subscriber department.PublicationSubscriber, ) (*Tail, error)
Publish subscribes to a session's committed tail and relays it onto that session's HostLink channel until the tail ends.
The subscription is opened from the LIVE TAIL and cannot be positioned; the runtime adapter refuses a non-empty resume point, and this call never supplies one. A Factory that needs earlier events reads them durably.
func (*Tails) PublishProjected ¶ added in v0.10.0
func (t *Tails) PublishProjected( ctx context.Context, key registry.Key, subscriber department.PublicationSubscriber, rewrite Projector, ephemeralRewrite ...Projector, ) (*Tail, error)
PublishProjected is Publish with each body rewritten by rewrite first.