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

hostlink

package
v0.18.1 Latest Latest
Warning

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

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

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

MethodBind and MethodUnbind are two of the five reserved RPC method names; MethodAttach in attach.go and 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.

THE NAMES ARE CORE'S. See ChannelPrefix for why this package no longer holds its own spelling of any framing string.

View Source
const (
	DefaultTransientRateBytesPerSecond = ephemeralRateBytesPerSecond
	DefaultTransientBurstBytes         = ephemeralBurstBytes
	MaxTransientFrameBytes             = ephemeralMaxFrameBytes
	ClientQueueMaxBytes                = clientQueueMaxBytes
)

Transient admission limits are shared with public Composition validation.

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.

THE NAMES ARE CORE'S, as every framing string in this package is since core v0.8.0; see ChannelPrefix in bindings.go.

View Source
const CapabilityAttributionPrincipal = sessionwire.HostLinkCapabilityAttributionPrincipal

CapabilityAttributionPrincipal is Core's token for a Host that strictly reads command principal and message metadata before the dispatch attempt. It is unconditional: the pinned harness runtime can carry both members.

View Source
const CapabilityGateResponse = sessionwire.HostLinkCapabilityGateResponse

CapabilityGateResponse is the capability token a Host advertises when it applies the gate_response runtime command through the disposition path. It is Core's name. A Factory admits, and wakes, a gate response only for a Host whose connect reply Supports it.

View Source
const CapabilityPayloadReference = sessionwire.HostLinkCapabilityPayloadReference

CapabilityPayloadReference signals that the applier resolves scoped command bodies.

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.

IT IS CORE'S NAME AND NOT A LOCAL SPELLING OF IT, since core v0.8.0. Factory and Host each used to hold their own copy of the framing, and the two had already disagreed once — Factory delivered commands under a method name Host treated as a channel (B6). Core's hostlink_framing.go is the one home for these strings now, and TestFramingNamesAndChannelBytesArePinned holds the bytes themselves so the move here could be shown to change nothing.

MethodAttach is the reserved RPC name that carries a HostLinkAttachRequest. It is Core's, as every framing name here is.

Variables

View Source
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.39.3 node.go:149-151) and refuses a longer channel with ErrorBadRequest at subscribe (client.go:3958-3961), 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

func ChannelFor(key registry.Key) string

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.

THE ENCODING IS CORE'S, through sessionwire.HostLinkChannel; this is the registry.Key spelling of it and nothing more. Core's goldens for that function were derived by running this function on a Host pinned to core v0.7.0, so the two are byte-identical by construction and by test.

This says nothing about characters centrifuge@v0.39.3 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 AttachRefusal added in v0.2.0

type AttachRefusal struct {
	Code                   sessionwire.HostLinkErrorCode
	CurrentLeaseEpoch      uint64
	RuntimeCompatibilityID string
	Reason                 string
	Cause                  error
}

AttachRefusal is the typed error an Attacher returns.

Code is the HostLink class a Factory branches on and is EMPTY when the failure is not a placement outcome — a store that failed, a runtime that refused to launch. Core's own contract says such a failure carries no code at all, because re-placing a session onto the next Host running the same broken build is a stampede and not a recovery; this package therefore answers an empty Code at the TRANSPORT rather than with a HostLinkError.

CurrentLeaseEpoch is the OTHER holder's epoch and is read only for epoch_mismatch. Core refuses an epoch_mismatch carrying a zero epoch, so an Attacher that knows the lease is held elsewhere but not by whom must still set the Code: this package substitutes a publishable class rather than put an invalid body on the wire. See RefusalHolderEpochUnknown.

RuntimeCompatibilityID is the build THIS Host launches or runs for the refused request, and is read only for runtime_mismatch, which Core refuses to encode without it. A runtime_mismatch whose id is absent or not a value Core accepts is published as runtime_unavailable instead; see RefusalMismatchBuildUnknown. Neither member is ever published under a class Core does not attach it to.

func (*AttachRefusal) Error added in v0.2.0

func (e *AttachRefusal) Error() string

func (*AttachRefusal) Unwrap added in v0.2.0

func (e *AttachRefusal) Unwrap() error

Unwrap returns the cause the Attacher recorded, if any.

type AttachRequest added in v0.2.0

type AttachRequest struct {
	Key                    registry.Key
	AgentID                sessionwire.AgentID
	RuntimeCompatibilityID string
	Mode                   sessionwire.HostLinkAttachMode
	ActorID                string
	TraceID                string
	IdempotencyKey         string
}

AttachRequest is the RESOLVED form of a Core attach request: what an Attacher is handed once this package has fenced the tenant and the Host incarnation.

It carries the request's session, launch and attribution fields and NOTHING about the Host: host_id and host_generation have already been compared here and an Attacher has no use for them. The idempotency key rides along because the residency manager's per-key serialization is what makes a retry safe and a caller correlating a retry storm wants the key on the same record.

type Attacher added in v0.2.0

type Attacher interface {
	Attach(context.Context, AttachRequest) (sessionwire.HostLinkRegistryObservation, error)
}

Attacher makes one session resident on this Host, or reports why it did not.

IT IS THE ONE PLACE A HOSTLINK RPC CAN CAUSE A RESIDENCY TO EXIST, and it is a SEAM rather than a handle for the reason DrainStarter is: this package still holds no mutable registry handle and no residency manager. What it holds is an injected capability with exactly one method, and "an attach RPC grants nothing itself — the store grants the lease" is the whole of what that capability is allowed to mean. Bind is unchanged: it validates current ownership and never grants it, and the two guard tests that hold that structurally (TestProductionHoldsNoMutableRegistryHandle, TestCollaboratorInterfacesExposeOnlyReads) stay true with this seam present.

The observation an accepted attach returns is the ACCEPTED REPLY BODY. Core requires it: its host_id, host_generation and lease_epoch are what the following bind needs, so attach is, like drain, an exception to the empty accepted body. An Attacher that cannot produce one has not attached.

A REFUSAL IS AN *AttachRefusal. Any other error is a failure with no placement outcome and is answered without a Core class; see AttachRefusal.

type Authenticator

type Authenticator interface {
	VerifyTenant(context.Context, sessionwire.TenantID, string) error
}

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) Error

func (e *BindError) Error() string

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.

IT IS FALSE, TOO, FOR A RECORD CORE WOULD REFUSE TO ENCODE. Core's encoder validates — epoch_mismatch needs its epoch, runtime_mismatch its build id, every other class neither — and a record it refuses cannot reach the wire as a refusal at all. Reporting it as published would hand the transport a marshal error, which reached a Factory as 107 bad request: the caller's fault, for a record this Host built wrong (D3.1 F1). Refusing it here sends it down the no-class path, which the transport answers as this Host's own failure; see errUnpublishableRefusal.

func (*BindError) Unwrap

func (e *BindError) Unwrap() error

Unwrap returns the typed error a decoder produced, if any.

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
	// TransientRateBytesPerSecond and TransientBurstBytes default to 32 KiB/s and 8 KiB.
	TransientRateBytesPerSecond int
	TransientBurstBytes         int

	// 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.39.3 config.go:519 and config.go:525) — 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

	// Capabilities are the capability tokens this server advertises after
	// its methods. Each must be one of advertisedCapabilities; a server
	// without a Multiplexer advertises nothing, tokens included.
	Capabilities []string
}

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

type InvalidMultiplexerOptionsError struct {
	Field  string
	Reason string
}

InvalidMultiplexerOptionsError reports an option a Multiplexer may not run with.

func (*InvalidMultiplexerOptionsError) Error

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) Attach added in v0.2.0

Attach fences the request against this link's tenant and this Host's incarnation, and only then asks the Attacher to make the session resident.

THE ORDER IS THE MECHANISM, as it is for Bind and drainScope. Core's attach record says a Host that is not the incarnation the caller placed on "must refuse the attach before it acquires any lease": without that fence a replacement Host serving the same endpoint would take the session lease and return an observation the caller cannot bind to, stranding the session with a lease and no route. So the tenant is compared first — a foreign tenant learns nothing, not even whether this Host is the one it named — then the Host identity and generation, and the Attacher is reached only by a request that passed all three. TestTheAttachRefusalLadderAnswersWithItsFirstFailingCheck holds each rung as the FIRST failure and asserts the Attacher was not called.

THE IDEMPOTENCY KEY IS CARRIED AND NOT CONSULTED HERE, for the reason Binding.IdempotencyKey gives: this package caches no acceptance. The residency manager serializes attaches per session and answers a repeat with the existing residency, which is a statement about NOW rather than a replay.

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 (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

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

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

	// Attacher is the composition's one attach entry point, reached by the
	// hostlink.attach RPC. It is OPTIONAL, and its absence is a refusal and not
	// a default: a Multiplexer built without one answers every attach with
	// RefusalAttachUnsupported, which is the same shape as the drain seams'
	// absence. It is a seam and not a handle — see Attacher for what that
	// keeps true about Bind.
	Attacher Attacher

	// 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"

	// RefusalDrainUnavailable reports a drain state machine that could not
	// 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"

	// RefusalAttachUnsupported reports a Host composed without an Attacher. It
	// is a DEPLOYMENT defect, as RefusalDrainUnsupported is: nothing can make a
	// session resident on this Host over HostLink, and no retry changes that.
	RefusalAttachUnsupported Refusal = "attach_unsupported"

	// RefusalAttachRefused reports an attach the Attacher declined WITH a Core
	// class: the class is the one it named, and the Factory branches on it.
	// The local value is one because the classes are the Attacher's — the
	// residency manager decides runtime_mismatch from no_capacity, and a second
	// vocabulary for the same decisions here would be two answers to one
	// question.
	RefusalAttachRefused Refusal = "attach_refused"

	// RefusalAttachFailed reports an attach that failed with NO placement
	// outcome — a store that was down, a launch that failed outright, an error
	// that was not a refusal at all. It has no wire class, by Core's own rule
	// for such failures, and reaches the transport as this Host's error rather
	// than as a HostLinkError. The attach is idempotent, so a retry is safe.
	RefusalAttachFailed Refusal = "attach_failed"

	// RefusalHolderEpochUnknown reports a session lease held by another owner
	// whose epoch this Host could not read. Core requires epoch_mismatch to
	// carry the holder's epoch and refuses a zero, so the class is downgraded
	// to runtime_unavailable on the wire; the local value keeps the real
	// ground, which is that the registry the caller placed from is stale.
	RefusalHolderEpochUnknown Refusal = "holder_epoch_unknown"

	// RefusalMismatchBuildUnknown reports an attach the Attacher refused as
	// runtime_mismatch without naming a build Core will publish. Core requires
	// runtime_mismatch to carry runtime_compatibility_id and refuses to encode
	// it otherwise, so the class is downgraded to runtime_unavailable on the
	// wire, as RefusalHolderEpochUnknown downgrades an epoch_mismatch with no
	// holder epoch; the local value keeps the real ground.
	RefusalMismatchBuildUnknown Refusal = "mismatch_build_unknown"

	// RefusalUnpublishableAttach reports an Attacher that attached and then
	// produced an observation Core refuses to publish. The session may be
	// resident, so no Core class is sent — re-placement would be wrong — and
	// the failure leaves at the transport as this Host's.
	RefusalUnpublishableAttach Refusal = "unpublishable_attach"
)

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

func NewCentrifugeServer(config Config) (Server, error)

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.

Jump to

Keyboard shortcuts

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