Documentation
¶
Overview ¶
Package lifecycle provides the per-binding client lifecycle state machine: the legal states, the legal transitions between them, a thread-safe current state, and change notification. It owns nothing else — transports, catalogs and session wiring live elsewhere. Illegal transitions report *TransitionError; callers wrap it into their own error taxonomy (this package deliberately does not import pkg/client or internal/protocol).
The modelled lifecycle is:
configured
|
v
starting -> authenticating -> discovering -> ready
| | |
+-------------+--------------+-> failed
|
v
reconnecting
ready -> degraded -> reconnecting -> ready
|
v
closing -> closed
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CanTransition ¶
CanTransition reports whether the transition from -> to is legal. It is fail-closed: a state outside the declared range — including the zero value — is never a legal source or destination, and no state may transition to itself.
Types ¶
type Machine ¶
type Machine struct {
// contains filtered or unexported fields
}
Machine is a binding's current lifecycle state, guarded for concurrent use, with change notification for registered watchers. The zero value is not usable; call NewMachine.
A Machine has no notion of who owns a transition: any goroutine may move it, and every To is judged against the state at that moment. Because shutdown is reachable from every non-terminal state, a multi-step sequence such as startup can be overtaken at any step by a concurrent close. Callers must therefore treat a *TransitionError as a legitimate race outcome and unwind; see To.
func (*Machine) To ¶
To transitions the machine to next, returning *TransitionError if the transition is illegal from the current state — including a self-transition or a state outside the declared range — in which case the current state is unchanged.
The legality check is against the CURRENT state, not against an expected source state: To is not a compare-and-swap. A caller cannot assert that the state it last observed still held, because another goroutine may have moved the machine in between. Callers needing that guarantee must serialize their own transitions.
Consequently a *TransitionError from a step of an otherwise-correct sequence (say discovering -> ready during startup) does not imply a bug in the caller: it usually means someone else legally moved the machine first, and since shutdown is reachable from every non-terminal state, that someone is most often a concurrent close. The contract for such a caller is to treat the error as "my sequence has been overtaken", abandon the remaining steps, and unwind — not to retry, force the state, or panic.
On success the new state is committed before any watcher is notified, and notifications are delivered without holding the machine's lock, so a callback may call back into the machine.
Notification is serialized: transitions are delivered to watchers in the order they were committed, never concurrently. The committing goroutine normally performs the delivery itself and returns once its own notification has been delivered. If another goroutine is already delivering, the committing goroutine enqueues its change and returns immediately, leaving delivery to that goroutine — no change is ever dropped. This is also what makes a To call from inside a callback safe: it commits and returns, and the callback's own delivery loop notifies afterwards.
If a watcher panics, the panic is re-raised to the caller of To once every watcher and pending change has been delivered, so the machine is left consistent and usable. When several watchers panic on one drain, only the first value propagates.
func (*Machine) Watch ¶
Watch registers fn to be called on every successful transition, and returns a cancel func that deregisters it.
Callbacks are invoked in registration order, after the new state has been committed, and never while the machine's lock is held — fn may safely call State (or To; see that method) on the machine. fn runs on the goroutine performing delivery and blocks further notification, so it must not block. A panic in fn is isolated: the other watchers still receive the change, and the value is re-raised to the caller of To (see that method).
Registration is NOT a snapshot of the current state. Watchers are resolved when a change's delivery BEGINS, not when it was committed, so a watcher registered after a transition committed but before its delivery began still receives it — possible whenever registration races an in-flight drain. A caller that seeds its own view from State and then calls Watch may therefore be told about a change it has already accounted for, and must tolerate the repeat rather than assume every delivery is news. (Registering during a change's delivery — from another watcher's callback — is the other side of the same rule: that watcher misses the in-flight change and starts at the next one.)
cancel is idempotent and safe to call concurrently. Once it returns, fn will not be invoked for any notification whose delivery starts afterwards; a notification already in flight may still complete.
A nil fn is a programmer error and panics.
func (*Machine) WatcherCount ¶
WatcherCount reports how many watchers are currently registered.
It exists so that a caller which registers a watcher can prove it deregistered it. Nothing else can: a leaked watcher spawns no goroutine (see Watch — delivery runs on the committing goroutine), and once a machine is in a terminal state no transition can be made to reveal one. What a leak costs is the callback and everything it captured, held for the machine's lifetime, which is invisible until it matters.
type State ¶
type State uint8
State is a binding's lifecycle state. The zero value is not a valid state.
const ( // StateConfigured is a binding that has been validated but not started. StateConfigured State = iota + 1 // StateStarting is a binding whose transport is being established. StateStarting // StateAuthenticating is a binding performing authentication. Startup may // skip this state entirely for servers that require no auth. StateAuthenticating // StateDiscovering is a binding fetching its catalog from the server. StateDiscovering // StateReady is a binding serving calls normally. StateReady // StateDegraded is a binding still serving calls but with reduced // capability or a known fault. StateDegraded // StateReconnecting is a binding re-establishing its transport, which // re-runs authentication and discovery as needed. StateReconnecting // StateFailed is a binding that is not serving calls; it may be retried // via StateReconnecting or shut down. StateFailed // StateClosing is a binding shutting down. Shutdown always wins: it is // reachable from every non-terminal state. StateClosing // StateClosed is the terminal, absorbing state. StateClosed )
The declared lifecycle states. stateSentinel must remain the final entry: tests derive the declared range from it, so a state appended before it is automatically covered.
type TransitionError ¶
type TransitionError struct {
From, To State
}
TransitionError reports an attempted transition the state machine forbids.
func (*TransitionError) Error ¶
func (e *TransitionError) Error() string
Error renders "illegal lifecycle transition <from> -> <to>".