Documentation
¶
Overview ¶
Package hostlink owns the authenticated realtime server used by Factory to reach one Host. Its boundary is expressed in Looprig and standard-library types; the transport engine is private to this package.
Index ¶
- Constants
- Variables
- func ChannelFor(key registry.Key) string
- type Admission
- type Authenticator
- type BindError
- type Binding
- type CommandConsumer
- type CommandConsumers
- type Config
- type DrainObserver
- type DrainScope
- type DrainScopeReporter
- type DrainStarter
- type DrainStatus
- type InvalidMultiplexerOptionsError
- type LinkID
- type Multiplexer
- func (m *Multiplexer) Bind(link LinkID, request sessionwire.HostLinkBindRequest) (Binding, error)
- func (m *Multiplexer) CloseLink(link LinkID) []Binding
- func (m *Multiplexer) Deliver(link LinkID, channel string, delivery sessionwire.HostLinkCommandDelivery) error
- func (m *Multiplexer) InvalidateSession(key registry.Key) []Binding
- func (m *Multiplexer) Len() int
- func (m *Multiplexer) LinkBindings(link LinkID) []Binding
- func (m *Multiplexer) MaySubscribe(link LinkID, channel string) bool
- func (m *Multiplexer) ObserveDrain(request sessionwire.HostLinkDrainRequest) (sessionwire.HostLinkDrainObservation, error)
- func (m *Multiplexer) Replicas(key registry.Key) []LinkID
- func (m *Multiplexer) StartDrain(request sessionwire.HostLinkDrainRequest) (sessionwire.HostLinkDrainObservation, error)
- func (m *Multiplexer) Unbind(link LinkID, request sessionwire.HostLinkUnbindRequest) error
- type MultiplexerOptions
- type Refusal
- type Residencies
- type Server
Constants ¶
const ( MethodBind = "hostlink.bind" MethodUnbind = "hostlink.unbind" )
MethodBind and MethodUnbind are two of the four reserved RPC method names; MethodDrain and MethodDrainStatus in drain.go are the others. Any other method is treated as a channel and resolved by exact lookup against the routes this link holds, so a command delivery names its binding and carries only a CommandID in its body.
const ( MethodDrain = "hostlink.drain" MethodDrainStatus = "hostlink.drain_status" )
MethodDrain and MethodDrainStatus are the two reserved drain RPC names.
They are SEPARATE METHODS rather than one method with a mode flag, because the difference between them is exactly the difference a Factory must not get wrong: one initiates and the other only looks. A mode flag would put that distinction in a body a retry could corrupt.
const ChannelPrefix = "hostlink.v1."
ChannelPrefix is the leading segment of every channel this package mints.
Every reserved RPC method name is a constant that does not begin with it, so dispatch can tell a method from a channel without parsing either.
Variables ¶
var MaxChannelBytes = len(ChannelPrefix) + 1 + 2*base64.RawURLEncoding.EncodedLen(sessionwire.MaxIDBytes)
MaxChannelBytes is the longest channel ChannelFor can mint.
It is DERIVED from the encoding and Core's own identity ceiling rather than measured from a sample, so a Core release that widens sessionwire.MaxIDBytes widens this with it. Centrifuge defaults ChannelMaxLength to 255 (centrifuge@v0.38.0 node.go:138-140) and refuses a longer channel with ErrorBadRequest at subscribe (client.go:2804-2807), which two maximum-length Core identifiers exceed comfortably, so NewCentrifugeServer raises the node's ceiling to this value instead of leaving a legal session unroutable.
Functions ¶
func ChannelFor ¶
ChannelFor returns the channel a session's publications travel on.
The identifiers are base64url-encoded WITHOUT PADDING, and the property that matters is INJECTIVITY: two distinct keys cannot mint one channel however many separators their opaque identifiers contain. Core mints no SessionID — they are client-supplied strings — so one containing a '.' is an ordinary event, and a channel built by plain concatenation would let two tenants collide onto one route. The encoding is what makes that unrepresentable rather than unlikely. Its alphabet is A-Za-z0-9-_ and the prefix, so a channel also needs no escaping wherever it is later logged or compared.
This says nothing about characters centrifuge@v0.38.0 treats specially, because it treats none specially: its own channel handling is exact string comparison and map keys, and the ':' and '#' conventions belong to Centrifugo, which this Host does not run. The one channel rule the library does enforce is a length ceiling, and MaxChannelBytes is what answers it.
Types ¶
type Admission ¶
type Admission interface {
// Draining reports that this Host has begun a drain and is publishing
// accepting=false, so a new binding must not be established.
Draining() bool
}
Admission reports whether this Host is accepting new work AT ALL, which is a Host-wide fact and not a session's.
It is separate from the residency row on purpose, and the separation is what keeps not_admitting and releasing from masking each other. There are now THREE such facts, not two, and each is read from the place that owns it: this interface for the Host-wide drain, Entry.State for a residency whose release has been published, and Entry.Accepting for a resident session whose admission a warm release has just closed.
The third became reachable at O6.1. Until then Entry.Accepting was a strict function of Entry.State — Insert set resident/true, MarkReleasing and BeginTeardown both set false — so a bind reading both fields would have had one decide every reachable case and the other decide none, which is an identical refusal standing in for two checks; the field consequently had no reader here. registry.StopAdmitting is what separated them, and Bind reads State FIRST so that the releasing ground is not swallowed by the admission one, which MarkReleasing also satisfies.
type Authenticator ¶
Authenticator verifies the service credential presented by a Factory when it opens a HostLink. Authentication happens for every physical connection, including reconnects.
type BindError ¶
type BindError struct {
// Refusal is which check fired, and is the field an operator reads.
Refusal Refusal
// Key is the session the request named, as far as it was parsed.
Key registry.Key
// Reason is human-readable detail. It never carries a credential, a
// payload or another tenant's identifiers.
Reason string
// Cause is the typed error a decoder produced, if any.
Cause error
// contains filtered or unexported fields
}
BindError reports a declined bind, unbind or command delivery.
func (*BindError) HostLinkError ¶
func (e *BindError) HostLinkError() (sessionwire.HostLinkError, bool)
HostLinkError returns the Core record this refusal is published as, and whether it has one at all.
It is a two-value accessor for the reason residency.AttachError.HostLinkCode is: the absent code is a real state, and a bare accessor would hand a caller a record Core's own Validate refuses.
type Binding ¶
type Binding struct {
Link LinkID
Key registry.Key
Channel string
// LeaseEpoch is the epoch the bind VALIDATED against. It is a record of
// what was true at bind time and confers nothing; the lease itself is the
// only thing that grants, and this package never touches it.
LeaseEpoch uint64
// IdempotencyKey is the most recent retry-stable key the Factory supplied.
//
// HOSTLINK DOES NOT DE-DUPLICATE ON IT, and that is a decision rather than
// an omission: a repeated bind RE-VALIDATES current ownership instead of
// replaying a cached acceptance, so a retry that arrives after the epoch
// moved is refused rather than confirmed. Replaying would make the bind a
// statement about the past, which is the one thing a routing validation
// must not be. The key is recorded so an operator can correlate a retry
// storm with one Factory attempt.
IdempotencyKey string
}
Binding is a COPY of one route: one link, one session, one channel.
type CommandConsumer ¶
type CommandConsumer interface {
// Hint wakes consumption of an already accepted command. It is a wake and
// not a delivery, so repeating it is harmless.
Hint(sessionwire.CommandID)
}
CommandConsumer is the durable inbox consumer for one session, narrowed to the single method a HostLink command delivery may reach.
Hint takes a CommandID AND NOTHING ELSE, which is the whole of §3's rule expressed as a type: there is no parameter a payload could ride in even if a future caller wanted to pass one. commands.Consumer satisfies it exactly; the conformance assertion lives in the test so this package does not name internal/commands in production.
type CommandConsumers ¶
type CommandConsumers interface {
// ConsumerFor returns the consumer for a resident session, and whether one
// is running.
ConsumerFor(registry.Key) (CommandConsumer, bool)
}
CommandConsumers resolves the durable inbox consumer that owns a session.
type Config ¶
type Config struct {
TenantID sessionwire.TenantID
Authenticator Authenticator
// PingInterval and PongTimeout are optional but must be supplied together:
// setting one without the other is rejected. When both are zero HostLink
// sends no PingPongConfig at all and the heartbeat falls to Centrifuge's
// own application-level defaults of 25s ping and 10s pong
// (centrifuge@v0.38.0 config.go:218 and config.go:224) — zero does not mean
// "no heartbeat". That route held one exception until O5.2 removed it:
// cf_ws_frame_ping_pong would have moved a zero-configured link onto
// WebSocket frame ping instead, and rejectedTransportQueryKeys now refuses
// the key before the upgrade. When supplied, PingInterval must be at least
// one second and PongTimeout must be strictly shorter than PingInterval.
PingInterval time.Duration
PongTimeout time.Duration
// Multiplexer is the routing table this server's links bind through. It is
// optional: a server built without one still authenticates, negotiates and
// heartbeats, and answers every RPC and subscribe with Centrifuge's own
// unhandled-command refusal.
Multiplexer *Multiplexer
}
Config contains the Looprig-owned inputs to the embedded server.
type DrainObserver ¶
type DrainObserver interface {
// ObserveDrain returns the drain covering scope and whether one has begun
// at all. It never starts one.
ObserveDrain(DrainScope) (DrainStatus, bool)
}
DrainObserver reports a drain already begun, and is a pure read.
type DrainScope ¶
type DrainScope struct {
// Key is the fixed session of a dedicated Host. Its zero value means the
// whole Host, and a partially populated Key is not reachable: the resolver
// sets both fields or neither.
Key registry.Key
}
DrainScope is what one drain covers.
Core spells the two scopes as an OPTIONAL tenant/session pair on the request, which is a shape a caller can get half right; this is the RESOLVED form, and the only thing that can produce one FROM A REQUEST is drainScope below. A scope therefore either names the session this Host is configured to hold or names nothing at all — "a session this Host does not hold" is not a value a state machine can be handed.
THE WHOLE-HOST SCOPE IS NOT ONE drainScope CAN PRODUCE, since R-1. A request naming no tenant is refused rather than resolved, because every link is authenticated for one tenant and the Drainer behind them is the Host's. The zero scope is still a legitimate VALUE — the process lifecycle constructs it and hands it to the Drainer directly — so nothing here may read a zero Key as impossible; what it may not do is accept one off the wire.
IT CARRIES NO IDEMPOTENCY KEY, deliberately. See drainScope.
func (DrainScope) WholeHost ¶
func (s DrainScope) WholeHost() bool
WholeHost reports whether this scope covers the Host rather than one session.
type DrainScopeReporter ¶
type DrainScopeReporter interface {
// BegunScope reports the scope the drain was begun with, and whether a
// drain has begun that named one at all.
BegunScope() (registry.Key, bool)
}
DrainScopeReporter reports the scope the call that BEGAN the drain named.
IT IS A SEGREGATED CAPABILITY DISCOVERED BY ASSERTION and deliberately NOT a method on DrainObserver: a state machine that cannot answer it is a state machine this package still observes, and widening the observer interface would break every implementation for one rung's benefit.
WHY IT EXISTS. rung 8 of drainScope refuses a fixed-session drain unless the requesting tenant currently HOLDS that session. That is right for BEGINNING a Host-wide drain and wrong for OBSERVING one that has finished: release drops the residency, so the terminal `drained` answer becomes unreachable to the only caller entitled to it at the instant it becomes true. The observation path therefore accepts EITHER a current holder OR the caller that began this very drain.
A REPORTER THAT IS ABSENT CHANGES NOTHING. Without one the observation path is exactly rung 8 as R-1 wrote it.
type DrainStarter ¶
type DrainStarter interface {
// StartDrain begins the drain covering scope and reports its stable
// generation and current state. It is IDEMPOTENT: a call for a drain
// already begun returns that same generation rather than starting a
// second one, and it returns promptly rather than waiting for release.
StartDrain(DrainScope) (DrainStatus, error)
}
DrainStarter begins, or re-reports, the single drain this Host is running.
IT IS THE ONLY MUTATOR THIS PACKAGE HOLDS besides its own routing table, and it deliberately takes NO CONTEXT and returns NO COMPLETION: the RPC acknowledges initiation plus generation, so a StartDrain that blocked until release would make the acknowledgement a statement about the wrong event.
type DrainStatus ¶
type DrainStatus struct {
// Generation identifies this drain across retries, reconnects and
// ambiguous replies. Core refuses a zero generation.
Generation uint64
// State is Core's bounded drain state. A value outside Core's two is not
// published; see drainObservation.
State sessionwire.HostLinkDrainState
}
DrainStatus is one drain's stable identity and current state.
GENERATION IS THE FIELD THAT MATTERS AND IT IS THE STATE MACHINE'S, not the handler's. Every request is answered by asking the machine again rather than from any memory this package keeps, which is why a Host that somehow restarted its drain would report a different generation instead of a cached one that lies.
type InvalidMultiplexerOptionsError ¶
InvalidMultiplexerOptionsError reports an option a Multiplexer may not run with.
func (*InvalidMultiplexerOptionsError) Error ¶
func (e *InvalidMultiplexerOptionsError) Error() string
type LinkID ¶
type LinkID string
LinkID identifies one PHYSICAL HostLink connection, and it is the unit of binding ownership rather than the Factory replica behind it.
Two replicas of one Factory hold two links and therefore two independent binding sets over the same session, which is what makes losing one of them survivable for the other. A replica that reconnects gets a NEW LinkID and must bind again: nothing about a binding survives the connection that made it, because a binding is a route over that connection and nothing else.
type Multiplexer ¶
type Multiplexer struct {
// contains filtered or unexported fields
}
Multiplexer holds every Factory route this Host currently serves, keyed by physical link, and is safe for concurrent use.
func NewMultiplexer ¶
func NewMultiplexer(options MultiplexerOptions) (*Multiplexer, error)
NewMultiplexer validates the options and returns an empty Multiplexer.
func (*Multiplexer) Bind ¶
func (m *Multiplexer) Bind(link LinkID, request sessionwire.HostLinkBindRequest) (Binding, error)
Bind validates the ownership tuple a Factory presented and, if every part of it is currently true, records a route from this link to that session.
IT GRANTS NOTHING. Every value it consults is read through Residencies, which has no mutator, and the only state it writes is this package's own routing table. A session that is not resident is not made resident by binding to it; an epoch that does not match is not adopted; a runtime that does not match is not relaunched. The whole method is a comparison followed by a map write.
func (*Multiplexer) CloseLink ¶
func (m *Multiplexer) CloseLink(link LinkID) []Binding
CloseLink drops every route one link held, and returns them.
IT TOUCHES NOTHING ELSE. Not the runtime, which this package cannot reach; not the residency, which it can only read; and not another link's routes, including another replica's route over the same session. A link is a route and losing it loses routes.
func (*Multiplexer) Deliver ¶
func (m *Multiplexer) Deliver(link LinkID, channel string, delivery sessionwire.HostLinkCommandDelivery) error
Deliver wakes the durable consumer for the session THIS LINK is bound to on the named channel.
The channel is never parsed. It is looked up in the set of channels this Host itself minted for this link, so a well-formed channel naming a session that is resident and bound on some OTHER link is refused here exactly as a nonsense string is: the route is the authority, not the shape of its name.
The delivery record carries a CommandID and nothing else, and repeating one is harmless — Hint is a wake, so the second one either finds the pass already running or starts another that finds the durable record already terminal.
HOLDING THE ROUTE IS NECESSARY AND NOT SUFFICIENT. The residency is re-read here on every delivery, because §9.3 lets a session binding outlive the admission it was granted under: "A session binding may remain through the warm TTL". A binding is therefore a statement about routing and never a standing permission to wake a runtime.
func (*Multiplexer) InvalidateSession ¶
func (m *Multiplexer) InvalidateSession(key registry.Key) []Binding
InvalidateSession drops every link's route to ONE session and returns them, ordered by link.
IT IS THE SESSION-SCOPED SIBLING OF CloseLink AND TOUCHES AS LITTLE. The residency is not released, the runtime is not stopped, no other session's route on any of these links is affected, and the physical connections stay up: what disappears is the routing this Host can no longer serve a live tail over, which is exactly what obliges a Factory to reset durably rather than keep advancing a cursor it is no longer being fed.
THE SESSION SCOPE IS WHY IT IS NOT CloseLink. A tail lost for one session says nothing about the other sessions a Factory replica routes over the same physical link, and closing the link would invalidate all of them — turning a one-session durable reset into a whole-replica one. The transport has its own per-connection failure (a slow client's queue), and that one IS link-scoped because it is the connection that failed; this one is not.
func (*Multiplexer) Len ¶
func (m *Multiplexer) Len() int
Len reports how many routes this Host holds across every link.
func (*Multiplexer) LinkBindings ¶
func (m *Multiplexer) LinkBindings(link LinkID) []Binding
LinkBindings returns the routes one link holds, ordered by channel.
func (*Multiplexer) MaySubscribe ¶
func (m *Multiplexer) MaySubscribe(link LinkID, channel string) bool
MaySubscribe reports whether this link may subscribe to a channel, which it may only when it holds the binding that minted it.
func (*Multiplexer) ObserveDrain ¶
func (m *Multiplexer) ObserveDrain(request sessionwire.HostLinkDrainRequest) (sessionwire.HostLinkDrainObservation, error)
ObserveDrain reports the drain covering the scope the request names, and STARTS NOTHING.
The absent drain is a REFUSAL rather than a drained-looking observation, which is the whole point of the separate method: a Factory polling a Host that never received the drain request must not read "no drain here" as "the drain finished" and delete the workload.
func (*Multiplexer) Replicas ¶
func (m *Multiplexer) Replicas(key registry.Key) []LinkID
Replicas returns every link bound to one session, ordered, which is how many Factory replicas currently route to it.
func (*Multiplexer) StartDrain ¶
func (m *Multiplexer) StartDrain(request sessionwire.HostLinkDrainRequest) (sessionwire.HostLinkDrainObservation, error)
StartDrain begins this Host's drain for the scope the request names and acknowledges the INITIATION.
IT ACKNOWLEDGES INITIATION AND NOT RELEASE. The returned state is whatever the state machine reports at this instant — draining while it works, drained once it has finished — and the handler neither waits for nor infers the second from the first.
func (*Multiplexer) Unbind ¶
func (m *Multiplexer) Unbind(link LinkID, request sessionwire.HostLinkUnbindRequest) error
Unbind removes one route. Repeating it is safe and repeating it for a route that was never held is safe, because an unbind is a statement about what this link no longer wants and never about what the Host owns.
It validates the tenant and the addressed Host for the same reason Bind does — an unbind is still authenticated control-plane input — but it deliberately does NOT validate the lease epoch or the residency: a Factory that has just been told its epoch is stale must still be able to drop its route.
type MultiplexerOptions ¶
type MultiplexerOptions struct {
// TenantID is the tenant every link on this Multiplexer authenticated as.
//
// IT IS THE LINK'S TENANT AND NO LONGER THE HOST'S. Human gate H8, answered
// 2026-09-04, dropped the fixed tenant from host.Options: a pooled Host
// advertising cross_tenant_isolated admits several tenants, and one
// advertising tenant_exclusive is exclusive by PLACEMENT rather than by
// construction. A composition therefore supplies this from the
// authenticated service identity of the connection, not from the Host, and
// a Host serving several tenants runs one Multiplexer per authenticated
// tenant. The refusal below still says "this link is authenticated for
// another tenant", which was always the accurate sentence.
TenantID sessionwire.TenantID
// HostID and HostGeneration are this Host process's identity and
// incarnation, compared against every bind.
HostID sessionwire.HostID
HostGeneration uint64
Residencies Residencies
Admission Admission
Consumers CommandConsumers
// DrainStarter and DrainObserver are the drain state machine this Host's
// HostLink may begin and observe. They are OPTIONAL AND PAIRED: supplying
// one without the other is refused, because a Host that can be told to
// drain and cannot be asked how it is going, or the reverse, is a Factory
// bug discovered in production instead of at construction.
//
// Their ABSENCE IS A REFUSAL AND NOT A DEFAULT. A Multiplexer built
// without them answers both drain RPCs with RefusalDrainUnsupported, so a
// Host with no drain to run cannot be made to look as though it started
// one. That is the same shape as Config.Multiplexer's own absence.
DrainStarter DrainStarter
DrainObserver DrainObserver
// FixedSessionID is the sole session a DEDICATED Host holds, and its zero
// value means this Host is pooled.
//
// It is read by the drain path only. It is NOT a second admission rule:
// nothing on the bind path consults it, and the dedicated-attach refusal
// lives in the residency manager where the durable state is. Putting a
// second copy of "which sessions may this Host hold" on the routing table
// would be two answers to one question.
FixedSessionID sessionwire.SessionID
// MaxBindingsPerLink and MaxBindings bound what one link and the whole
// Host may hold. A binding is not free — it is a subscription and an
// enduring-event queue — so an unbounded one is an unbounded Host.
MaxBindingsPerLink int
MaxBindings int
}
MultiplexerOptions configures a Multiplexer.
type Refusal ¶
type Refusal string
Refusal is why a bind, unbind or command delivery was declined.
IT IS FINER THAN sessionwire.HostLinkErrorCode AND DELIBERATELY SO. Core publishes six refusal classes and this package refuses on twelve grounds, so five of them answer runtime_unavailable on the wire: a foreign tenant, an unheld session, a foreign Host, a stale Host generation and an unbound channel are indistinguishable to a Factory ON PURPOSE, because telling them apart is telling a caller whether somebody else's session exists. The two capacity grounds likewise share no_capacity. What must NOT be indistinguishable is which check fired: that is the difference between a misconfigured tenant, a stale placement route and a Host that has restarted, which are three different repairs. The wire stays narrow and the local value stays exact, and an identical wire class is exactly how a refusal path acquires no test of its own.
const ( // RefusalForeignTenant reports a bind naming a tenant other than the one // this link authenticated as. Since H8 that is the whole of the rule: the // Host itself serves whatever tenants its isolation class permits. RefusalForeignTenant Refusal = "foreign_tenant" // RefusalUnknownSession reports a session this Host does not hold. RefusalUnknownSession Refusal = "unknown_session" // RefusalForeignHost reports a bind addressed to a different HostID, which // means Factory routed from a registry row that is not this Host's. RefusalForeignHost Refusal = "foreign_host" // RefusalStaleHostGeneration reports the right HostID at an earlier // incarnation. The route survived a restart the residency did not. RefusalStaleHostGeneration Refusal = "stale_host_generation" // RefusalEpochMismatch reports a lease epoch other than the one this // residency was established under. RefusalEpochMismatch Refusal = "epoch_mismatch" // RefusalRuntimeMismatch reports a runtime compatibility ID other than the // build actually launched under this residency. RefusalRuntimeMismatch Refusal = "runtime_mismatch" // RefusalReleasing reports a residency already on its way out. The session // stays durable and resumable; this route is about to disappear. RefusalReleasing Refusal = "releasing" // RefusalHostNotAdmitting reports a Host that has begun draining. The // session is still resident and still accepting through existing // bindings; what is refused is establishing a new one. RefusalHostNotAdmitting Refusal = "host_not_admitting" // RefusalSessionNotAdmitting reports a residency that is still RESIDENT and // has stopped admitting new work. It is the third fact, added by O6.1, and // it is not either of its neighbours: RefusalHostNotAdmitting is about this // whole Host and RefusalReleasing is about a residency whose state has // already moved. This one is a warm release that has closed admission and // not yet written its durable `releasing` observation. // // It publishes not_admitting, which is the class §9.3 names for a command // racing release, and which it shares with the Host-wide ground. The // repairs differ: a Factory re-places this session and leaves the Host // alone, where a draining Host must be avoided entirely. // // IT IS READ ON BOTH PATHS. Bind refuses a NEW binding, and Deliver refuses // a command over a RETAINED one — which §9.3 makes the likely path, since a // binding may remain through the warm TTL. The first round of O6.1 shipped // only the bind half, and a retained binding went on delivering into a // session that had stopped admitting. RefusalSessionNotAdmitting Refusal = "session_not_admitting" // RefusalResidencyReleased reports a delivery over a RETAINED binding whose // session this Host no longer holds resident. // // IT IS SEPARATE FROM RefusalUnknownSession, which answers the same // question on the bind path, and the difference is disclosure. A bind is // refused with runtime_unavailable precisely so that it cannot tell a // caller whether somebody else's session exists; a DELIVERY arrives over a // binding this Host itself granted, so the caller has already proved it // knew, and there is nothing left to withhold. That is what lets this one // publish the class §9.3 names for a command racing release — // not_admitting, on which a Factory invalidates the binding and re-runs // placement — instead of the deliberately uninformative one. RefusalResidencyReleased Refusal = "residency_released" // RefusalNoLinkCapacity reports one link's binding budget already spent. RefusalNoLinkCapacity Refusal = "no_link_capacity" // RefusalNoHostCapacity reports the Host-wide binding budget already spent. // // It is a separate value from the per-link one because they publish the // same Core class and the repairs differ: one Factory replica holding too // many routes is that replica's bug, and a Host at its ceiling is a // placement decision. One code for both would make each an equally good // explanation of the other. RefusalNoHostCapacity Refusal = "no_host_capacity" // RefusalNotBound reports a command delivery on a channel THIS LINK does // not hold a binding for, whether or not some other link does. RefusalNotBound Refusal = "not_bound" // RefusalMalformedRequest reports a body Core's own decoder refused. It is // the one refusal with no wire class, because Core's six classes are all // statements about ownership and a malformed body is a statement about the // caller. RefusalMalformedRequest Refusal = "malformed_request" // RefusalWrongDrainScope reports a session-scoped drain this Host cannot // answer: either it holds no fixed session and is drained as a whole, or // it holds one and the request names a different session. // // The two grounds share a value ON PURPOSE, unlike every other pair here. // Telling them apart would tell a caller whether this Host is dedicated // and to which session, which is the same disclosure the unheld-session // refusal exists to prevent. The Reason string carries the difference for // the operator; the value does not. RefusalWrongDrainScope Refusal = "wrong_drain_scope" // RefusalDrainUnsupported reports a Host composed without a drain state // machine. It is a DEPLOYMENT defect: this Host can never be drained // gracefully, and no retry will change that. RefusalDrainUnsupported Refusal = "drain_unsupported" // begin, or that answered with a status Core refuses to publish. It is a // RUNNING-HOST condition — a process already shutting down is the ordinary // case — and it is separate from RefusalDrainUnsupported because a retry // or a different Host is the repair for one and a redeploy is the repair // for the other. RefusalDrainUnavailable Refusal = "drain_unavailable" // RefusalNoDrainInProgress reports a status observation on a Host that has // begun no drain covering that scope. It is NOT a drained observation, and // the distinction is what stops a Factory whose drain request never // arrived from reading silence as completion. RefusalNoDrainInProgress Refusal = "no_drain_in_progress" )
type Residencies ¶
type Residencies interface {
// Get returns a COPY of the registry row for a key, and whether one is
// held. registry.Entry is a copy by that package's contract, so a caller
// mutating the result changes nothing.
Get(registry.Key) (registry.Entry, bool)
}
Residencies is the read-only view of the sessions this Host holds resident.
IT HAS EXACTLY ONE METHOD AND THAT METHOD IS A READ, which is how "a bind validates current ownership and never grants it" is structural here rather than promised in prose. The Multiplexer holds no other handle on residency: it cannot insert, cannot mark, cannot touch and cannot remove, because the only value it is given cannot express any of those. Two derived guards hold that: TestProductionHoldsNoMutableRegistryHandle takes the package's production files from the directory and the used-symbol set from the type checker, and TestCollaboratorInterfacesExposeOnlyReads takes the method set from this type. Neither can be satisfied by a method nobody thought to forbid.
type Server ¶
type Server interface {
// Handler serves only clients that select the JSON protocol explicitly,
// by a sole Sec-WebSocket-Protocol field of centrifuge-json tokens or by a
// format/cf_protocol query value of json. Anything else, including a
// request that names no protocol at all, is answered with HTTP 400 before
// the upgrade; see selectsJSONProtocol for the rule and its provenance.
Handler() http.Handler
// Publish puts one payload on one channel.
//
// IT REPORTS TRANSPORT ACCEPTANCE AND NOT DELIVERY. A channel nobody has
// subscribed is not an error: a Factory link that has dropped and not yet
// reconnected is an ordinary state, and a transport that refused it would
// make an idle route indistinguishable from a broken one to the caller that
// decides whether to invalidate it.
//
// It is the production side of the seam the live event relay declares. That
// seam takes a channel and a payload and nothing else, so this signature
// carries no link, no replica set and no context — the fan-out belongs to
// the transport, which owns a queue per physical connection, and Host owns
// the decision to publish once.
Publish(channel string, payload []byte) error
Close(context.Context) error
}
Server is an embedded HostLink transport, mounted at the Host's private service endpoint; Close gracefully releases active connections.
func NewCentrifugeServer ¶
NewCentrifugeServer constructs and starts the embedded JSON/WebSocket transport. The node uses Centrifuge's in-process broker only; application durability remains in SessionStore rather than transport history.