Documentation
¶
Overview ¶
Package gates publishes a resident session's open gates to the durable gate projection, so a Factory can show every AskUser or permission question to a user and admit the answer.
THE RUNTIME'S JOURNAL IS THE AUTHORITY AND THE PROJECTION IS A COPY. A gate is open exactly when the runtime journaled its GateOpened and no GateResolved for it; harness has no other gate-closing event. So the Publisher folds the journal into the set of open gates and converges the projection on that set: it opens what the journal holds open and the projection lacks, and resolves what the projection holds and the journal has closed. It never decides a gate is open or closed on any other evidence.
THE ORDER IS SUBSCRIBE, FENCE, FOLD, CONVERGE (the Host recipe's R3):
- SUBSCRIBE to the runtime's committed stream FIRST. A delivery is only a hint that the journal moved; what moved is read back out of the journal. Subscribing before the fold means no append can fall between the two.
- FENCE: one gate write under this Host's residency grant before anything else, a resolve of a gate identity no runtime mints. sessionstore v0.12.0 raises the projection's residency mark to the writer's grant on every successful gate write, so after it a predecessor's late write is refused. It also narrows sessionstore's booked F2 window, whose mitigation is exactly "one fencing write before opening any new gate".
- FOLD the whole journal once, then only what was appended since (positioned by the ledger sequence the fold last reached).
- CONVERGE the projection on the fold.
A LOST SUBSCRIPTION IS A GAP IN HINTS AND NOT IN STATE. The fold is positioned by sequence, so resubscribing and folding from the last sequence reached recovers everything the lost subscription would have hinted at. This is why the committed-output pump's stop-on-overflow does not matter here.
A SUPERSEDED GRANT STOPS THE PUBLISHER FOR GOOD. A mark above this Host's grant means a successor has written; every later write would be refused, and retrying would only spend the store.
Index ¶
Constants ¶
const DefaultStopBound = 5 * time.Second
DefaultStopBound is Stop's wait when Options.StopBound is zero.
const FenceGateID coresessionwire.GateID = "host.residency-fence"
FenceGateID is the gate identity the fencing write resolves.
NO RUNTIME CAN MINT IT. Harness gate identities are UUIDs and this is not one, so a resolve of it can never close a real gate; it exists only so the store has a gate write to raise the residency mark on.
Variables ¶
var ErrProjectionFull = errors.New("gates: the session's gate projection is full")
ErrProjectionFull is what a Session returns for a gate the projection has no room for: sessionstore holds at most sessionstore.MaxCatalogOpenGates (16) open gates per session. The gate is NOT visible to Factory until another closes. The publisher logs it once per gate and tries it again only when the fold changes, never on a retry timer.
var ErrUnpublishable = errors.New("gates: this gate can never be projected")
ErrUnpublishable is what a Session returns for a gate it can never project, such as one whose deadline intent a stale resolve tombstoned (sessionstore's booked F2 residue). The publisher records it and stops trying that gate.
Functions ¶
func Deadline ¶
func Deadline(opened event.GateOpened) time.Time
Deadline is the effective deadline of one journaled gate: its durable creation time plus its own response timeout when it carries one, and otherwise plus the TTL compiled for its kind.
func Project ¶
func Project(scope Scope, opened event.GateOpened, seq uint64) (coresessionwire.GateProjection, error)
Project builds the durable projection of one open gate.
EVERYTHING IN IT IS A FUNCTION OF THE JOURNALED GateOpened AND ITS SEQUENCE, so a successor that folds the same journal projects the same bytes (the recipe's R1). The deadline is the event's durable creation time plus the gate's own response timeout, or a per-kind TTL compiled into Host when the gate has none; answerability is always resident, because this Host's runtime holds the gate.
func TTL ¶
TTL is the deadline Host gives a gate whose journaled envelope carries no response timeout.
IT IS COMPILED IN AND NEVER CONFIGURED, so two Hosts folding one journal project one deadline. A permission gate normally carries harness's own five-minute default in its journaled policy and never reaches this table; the entry covers a record written without one. An ask_user or form gate waits for a human by default, and a day is how long this Host will advertise that it is still waiting. An open-url gate is bound to a live out-of-band exchange and cannot outlive it for long.
CHANGING A VALUE IS SAFE ACROSS VERSIONS for a gate already projected: the publisher never re-opens a projected gate, so a successor's different TTL is used only for a gate its predecessor never published.
Types ¶
type InvalidOptionsError ¶
type InvalidOptionsError struct {
Field string
}
InvalidOptionsError reports a Publisher that may not run.
func (*InvalidOptionsError) Error ¶
func (e *InvalidOptionsError) Error() string
type Options ¶
type Options struct {
// Session is the session's durable gate surface. Required.
Session Session
// Hints is the runtime's committed stream. Required. Its deliveries are
// hints only; see the package documentation.
Hints department.PublicationSubscriber
// After is the clock the retry waits on. Required.
After func(time.Duration) <-chan time.Time
// Retry is how long the publisher waits after a failed pass, or after
// its subscription ended, before trying again. Required and positive.
Retry time.Duration
// OnConverged is called after each pass that converged the projection,
// so a caller waiting on this Host's ownership of a gate can re-check.
// Optional.
OnConverged func()
// Logger receives WARN records for failed passes. Optional.
Logger *slog.Logger
// StopBound is how long Stop waits for the publisher to return after
// cancelling it. Zero means DefaultStopBound.
//
// N5, RECORDED AND NOT FIXED: NO COMPOSED HOST CAN SET IT. host.Composition
// declares no field for it and internal/compose passes none, so every Host
// built through host.Compose runs at DefaultStopBound and this field is
// reachable only from a test. That is acceptable rather than merely
// tolerated -- the released stores honour cancellation, so the bound is a
// backstop against a provider that does not, and a deployment has no
// information with which to choose a better number than five seconds. It is
// written down because a configurable-looking option nobody can configure
// is the kind of thing a reader assumes is wired.
StopBound time.Duration
}
Options configures a Publisher.
type Publisher ¶
type Publisher struct {
// contains filtered or unexported fields
}
Publisher converges one session's durable gate projection on its runtime's journal, until it is stopped or superseded.
func Start ¶
Start validates the options and starts publishing. The returned Publisher runs until Stop, until ctx ends, or until its grant is superseded.
func (*Publisher) Activity ¶ added in v0.7.0
Activity reports what the publisher's last successful fold of the runtime journal saw: how many gates the journal holds open, and the journal position the fold reached. folded is false until the first fold has completed, and a caller must then treat both numbers as unknown rather than as zero.
IT IS THE HOST'S ONLY LOCAL READER OF A RUNTIME'S GATE STATE. The fold is the same one the projection is converged on, so "open" here means exactly what a Factory will be shown. The position moves on every journaled event, so a caller comparing two readings learns that the runtime did something between them even when it was idle at both.
func (*Publisher) Converged ¶
Converged reports how many passes have converged the projection. It is a monotonic count a caller can wait on.
func (*Publisher) Stop ¶
Stop ends publication and waits, BOUNDED, for the publisher to return.
THE WAIT IS BOUNDED (quality gate F8). A store call that ignores its context would otherwise hold Stop — and with it the session's release, which runs Stop before the grace-bounded runtime release — forever. Past the bound Stop returns and the goroutine is abandoned to finish its call; the released stores honour cancellation, so this is a guard against a provider that does not. It reports whether the publisher returned in time.
func (*Publisher) Superseded ¶
Superseded reports whether the publisher stopped because a successor's grant fenced it.
type Scope ¶
type Scope struct {
TenantID coresessionwire.TenantID
SessionID coresessionwire.SessionID
AgentID coresessionwire.AgentID
RuntimeSessionID uuid.UUID
}
Scope is one session's identities as the projection needs them, read from ONE read of the session's durable binding.
THE PAIRING IS NOT CHECKED DOWNSTREAM. harness's ReadScope scopes events by RuntimeSessionID and answers under SessionID, and it says in terms that a mismatched pair projects another session's events under this one's identity. Taking both from one binding read is the only guard there is.
type Session ¶
type Session interface {
// Scope reads the session's durable binding once and returns its
// identities. It must be called before Replay.
Scope(context.Context) (Scope, error)
// Projected returns the gates the durable projection holds open.
Projected(context.Context) ([]coresessionwire.GateProjection, error)
// Open projects one open gate under the bound grant. Replaying an
// identical open is idempotent.
Open(context.Context, coresessionwire.GateProjection) error
// Resolve removes one gate from the projection under the bound grant. A
// gate the projection does not hold is not an error; the write still
// raises the residency mark.
Resolve(context.Context, coresessionwire.GateID) error
// Replay visits the runtime journal's events in sequence order from the
// inclusive sequence from, with each event's ledger sequence.
Replay(ctx context.Context, from uint64, visit func(event.Event, uint64) error) error
}
Session is one resident session's durable gate surface, bound to this Host's residency grant over it by the implementation. No method takes an epoch: the grant is the implementation's, so no caller can name one.