12. Operations — Runtime Metrics · Graceful Drain · Readiness¶
Guide Home | Previous: How Session Binding Works | Next: Options and Defaults
View in another language — C++ · C#/.NET · Java · Kotlin · Node/TypeScript
What you get from this chapter
You can collect runtime metrics and connect readiness and drain to deployment procedures. Each language code tab shows the public runtime surface used in operations.
1. What It Provides¶
Once a service is in production, you need the following in addition to the event observation
covered in the 11. Monitoring chapter.
- Metrics — see numbers like CCU, queue depth, and request latency on a dashboard.
- Graceful drain — clean up a node being taken down for a deployment or scale-in without dropping connected users.
- Readiness — tell the deployment infrastructure "is this node allowed to accept new requests."
The framework provides the metric instruments and the drain procedure at host shutdown. The app wires the meter name into its collection pipeline, and composes the readiness endpoint the deployment environment calls out of the public runtime query API.
Terms that appear for the first time:
| Term | One-line explanation |
|---|---|
| Meter / instrument | A language-standard metric emission unit. counter, gauge, and histogram are instruments |
| OpenTelemetry (OTel) | The standard for collecting metrics/traces. Exported via Prometheus and other exporters |
| Relocate | The operation that moves a stateful object to a compatible target and puts the host into Relocated state |
| Shutdown | The operation that performs bounded cleanup of local resources with no new relocation |
| Readiness probe | The deployment infrastructure's status check asking "is it OK to accept new requests" |
2. Runtime Metrics¶
The framework emits every instrument through one System.Diagnostics.Metrics.Meter named
"zlink.framework". The app registers this canonical meter name in its collection pipeline.
// Wire the zlink meter into the OpenTelemetry Node SDK.
// "zlink.framework" is the canonical meter name the Framework emits instruments through.
const meterProvider = new MeterProvider({ readers: [prometheusExporter] });
meterProvider.getMeter('zlink.framework');
- There's no zlink-specific metrics API. The framework uses each language's standard metrics
API directly. To collect without OTel, subscribe directly to the meter name
"zlink.framework"from aMeterListener. - If no listener is attached at all, an instrument update ends on a minimal-cost inactive path. Registering an instrument without turning it on has no effect on messaging performance.
- Choosing a dashboard and exporter is the app's job. The framework doesn't bundle a scrape server.
The catalog below lists the measurements and units for MeshNode, object/STREAM, and location/fanout instruments. For detailed labels, units, and kinds, see Runtime Metrics and Complete Host Relocation Flow.
| Instrument | What it measures |
|---|---|
zlink.stream.connections.active |
Active STREAM connection count (CCU) |
zlink.stream.connections.opened |
Cumulative STREAM connections started |
zlink.stream.connections.closed |
Cumulative STREAM connections closed |
zlink.spot.count |
Active spot count |
zlink.actor.count |
Active Actor count |
zlink.relocation.started |
Cumulative Actor/User/Instance Spot relocations started |
zlink.relocation.completed |
Cumulative relocation terminal results |
zlink.relocation.duration |
Time from prepare to the terminal phase |
zlink.relocation.bytes |
Encoded size of the moved relocation payload |
zlink.instance_spot.activations |
Cumulative Instance Spot activation results |
zlink.instance_spot.activation.duration |
Time from first address resolution to Ready or terminal failure |
zlink.instance_spot.pending.messages |
Number of messages waiting at the activation barrier |
zlink.instance_spot.pending.bytes |
Payload bytes reserved at the activation barrier |
zlink.instance_spot.claim.conflicts |
Cumulative Instance location claim conflicts |
zlink.mesh_node.peers.configured |
Peer count present in the descriptor |
zlink.mesh_node.peers.connected |
Peer count with transport connected |
zlink.mesh_node.peers.ready |
Peer count that passed admission and handler readiness |
zlink.mesh_node.channels.ready_members |
Member count available for ChannelName select-one |
zlink.mesh_node.channel.selection_failures |
Count of times no member was available for select-one |
zlink.mesh_node.requests.inflight |
Requests currently waiting for a reply |
zlink.mesh_node.request.duration |
Time from request submit to terminal completion |
zlink.mesh_node.request.timeouts |
Cumulative request timeouts |
zlink.mesh_node.messages.dropped |
Cumulative one-way drops with a cause confirmed by the Framework |
zlink.fanout.published |
Cumulative classic fanout publishes |
zlink.fanout.received |
Cumulative classic fanout receives |
zlink.fanout.dropped |
Cumulative classic fanout drops with a cause confirmed by the Framework |
zlink.location.store.errors |
Cumulative Redis read/write/lease failures |
zlink.location.owner_lease.renew.failures |
Cumulative owner lease renewal failures |
zlink.location.owner_lease.renew.lateness |
Owner lease renewal delay versus the scheduled time |
zlink.observability.events.overflow |
Cumulative monitoring/trace observer queue overflows |
zlink.host.state |
The current host Framework runtime state |
zlink.host.core_hwm.effective_budget |
Effective Core HWM byte budget fixed at startup |
zlink.host.core_hwm.applied |
Sum of ordinary directional queue HWMs, excluding the completion lane |
zlink.host.core_hwm.accounted |
Current or epoch-peak Core-accounted bytes (state=current|peak) |
zlink.host.core_hwm.completion_accounted |
Current or epoch-peak completion-accounted bytes (state=current|peak) |
zlink.host.core_hwm.blocked_ratio |
Core blocked ratio in parts per million |
zlink.host.application_job_queue.limit |
Effective Application Job Queue permit limit fixed at startup |
zlink.host.application_job_queue.jobs |
Reserved, queued, in-use, or peak permits (state=reserved|queued|in_use|peak) |
zlink.host.application_job_queue.capacity_waiters |
Current number of permit-capacity waiters |
zlink.host.application_job_queue.capacity_waits |
Permit-capacity waits in the current measurement epoch |
zlink.host.application_job_queue.capacity_wait_duration |
Cumulative permit-capacity wait duration in the current epoch |
zlink.host.application_job_queue.pressure_state |
Current running · paused state (state) |
zlink.host.application_job_queue.pressure_transitions |
Transitions by destination state (state=running · paused) |
zlink.host.application_job_queue.pause_duration |
Current/cumulative pause seconds (state=current · cumulative) |
zlink.host.application_job_queue.flow_state_config_failures |
Failures to apply an absolute Core flow state |
zlink.host.relocation.duration |
Time from starting Host relocate to the terminal result |
zlink.host.relocation.blocked |
Count of host relocate calls that ended in Blocked |
zlink.host.shutdown.duration |
Time from starting Host shutdown to the terminal result |
zlink.host.shutdown.forced |
Count of host shutdown calls that ended via bounded teardown |
2.1 Capacity Snapshot and Measurement Reset¶
The host runtime's capacity snapshot exposes the Core HWM and Application Job Queue status together. Use it to correlate the fixed startup configuration and effective limits with current/peak accounted bytes, reserved/queued/in-use permits, and capacity waits. Read the exact type and member names from the relevant per-language monitoring contract.
Resetting measurements starts a new epoch without changing capacity. Current gauges and configuration stay unchanged, including pressure state and current pause duration. Each peak is rebased to its current value, and epoch waits, pressure transitions, cumulative pause duration, and flow-state configuration failures become zero. An event concurrent with reset belongs to exactly one epoch. Always-on metrics deliberately do not timestamp every job or create a per-job queue-wait histogram; record such distributions only inside a bounded performance fixture. The exact snapshot and reset rules are in Runtime state query and operational diagnostics and the metric names, units, and labels are in Runtime metrics.
3. Relocate — Moving to Another Host While Keeping State¶
relocate(...) moves every User Spot, Instance Spot, and Actor alive on this host to another
Serving node. It's an operation that targets the whole host, and the call itself doesn't
terminate the host.
After the move the logical ids, the work not yet run and the timers remain, and only the application state is carried over by the adapter registered on the factory. What survives, the order the procedure runs in and how far it can be rolled back are covered by Relocation.
This section covers the operator's side — when it can be called, what is configured, and when it is safe to terminate.
When it cannot be called. With no eligible destination it ends as blocked without changing this host's state. While a move or a shutdown is already under way, a new call is not accepted.
3.1 The Unit That Moves¶
What is bundled into one unit is decided by the Spot kind and the execution mode — Relocation covers that split. For operations it matters for one reason. The larger the bundle, the wider the range that pauses at once.
A User Spot whose Actors move individually is a shell that carries no state itself, so its factory can only use the policy that creates it fresh on the destination. Each member Actor's factory decides its own policy separately.
3.2 Moving-State Transfer Settings¶
The moving state travels over the source–target mesh connection in chunks, and four server settings keep it from crowding out ordinary messages on the same connection. The defaults are fine to start with; the runtime never adjusts them on its own, so change them based on what you observe in your deployment.
| Setting | Default | Purpose and tuning criterion |
|---|---|---|
RelocationPayloadChunkLimit |
256 KiB | Size of one chunk. Lower it if chunk transfers intrude on the latency target of ordinary messages sharing the connection |
RelocationInFlightPayloadBudget |
16 MiB | Total relocation bytes that may be in flight at once on one peer connection. 0 disables it. Lower it if relocation transfers eat into ordinary message bandwidth; raise it if host move throughput is capped by this budget |
RelocationNodeInFlightPayloadBudget |
0 (disabled) | Node-wide cap on concurrent transfers. Set it only when a node with many peer connections needs its total occupancy bounded |
RelocationCutoverWaitTimeout |
1,000ms | How long the target waits for a cutover retransmission. If the cutover_timeout counter is nonzero, adjust it to your deployment's round-trip time |
When the budget is full, a new relocation unit waits before its seal, and the Actor/Spot keeps processing messages normally while it waits — no payload size is ever too large to start because of the budget. Internal protocol details such as the chunk format and verification rules are covered by Relocation Flow.
3.3 SafeToShutdown — When It's Safe to Terminate¶
Relocated means the source finished sending its cutovers, not that every caller caching
the old route now points at the new owner. The source runtime publishes the
SafeToShutdown state in its own runtime status once every unit's Message Follow can end
and the cutover retransmission window has closed. A deployment orchestrator should confirm
Relocated, then observe this state before calling shutdown — terminating
before it's published is allowed, but the remaining follow routes disappear and requests
from callers still caching the old route can end in Unavailable. State queries and change
observation follow
Runtime Status Queries And Operational Diagnostics.
The relocation window is observed as three separate segments — source stop (from seal to
cutover submission), target resume (from the target's owner confirmation to dispatch
opening), and route convergence (from cutover submission until the Message Follow route can
be removed). The number of fallbacks that proceeded unverified after waiting for a cutover
is published as the cutover_timeout counter. Metric names, units, and labels are owned by
Runtime Metrics.
4. Shutdown — Terminating Without Moving¶
shutdown(...) terminates this host. Unlike §2, it doesn't move state to another node.
Calling it starts no new relocation; work already in progress either finishes within the
given deadline or is settled as a failure. It then notifies the Entry, User, and Instance
Spots' onClosing with the HostShutdown reason, and once that callback finishes, cleans up
scope, authority, session, and topology resources. If no deadline is given, it's 30 seconds.
The state of any Spot cleaned up here doesn't survive. If your deployment automation needs to
keep state alive while taking a host down, call relocate(...) before shutting down,
confirm the result is Relocated, and only then move on to this call (the example in §4).
When possible, also confirm the SafeToShutdown publication (§2.3).
A Spot's lifetime is independent of any request. A User/Instance Spot isn't closed just because an ordinary request finished. Likewise, preparing a nonexistent Instance Spot never starts from a separate address or manager create — only from attaching Instance intent to a SpotId direct call (Spot §5).
5. Wiring Operational Calls and Readiness¶
The two operations above don't happen automatically. The application calls them directly on the framework runtime. This interface is a DI singleton that owns host maintenance.
The order used in deployment is "move first, and shut down only if it succeeded." Confirming
Relocated and then observing the SafeToShutdown publication (§2.3) before terminating
avoids failures for callers still caching the old route.
const result = await runtime.relocate({
mode: ZLinkFrameworkRelocationMode.RollingUpdate,
// Uses only eligible nodes on the specified new version.
targetApplicationVersion: 12n,
deadlineMs: 25_000
});
if (result.outcome === ZLinkFrameworkRelocationOutcome.Relocated) {
await runtime.shutdown({ deadlineMs: 10_000 });
} else {
logger.error(`host relocation blocked: ${result.reason}`);
}
PlannedMaintenance uses only a target on the same application version as the source.
RollingUpdate requires a targetApplicationVersion greater than the source's, and uses
only a target exactly at that version. If there's no eligible target, it waits until the
deadline and then returns Blocked/TargetUnavailable. Cancellation ends only that waiter —
a shared lifecycle operation that's already started keeps running.
A readiness endpoint combines the readiness of the host framework runtime with that of any component runtime required by the application.
// The readiness endpoint looks only at the host runtime's status.
const ready = runtime.status.isReady;
// Respond 503 if ready is false.
In a Kubernetes deployment, the setup looks like this.
# readiness probe → /healthz/ready — excluded from new-traffic targets the moment Draining starts
# preStop hook + terminationGracePeriodSeconds >= drain deadline — allows time for auto-drain to finish
5.1 Calling It Again or Overlapping Calls¶
Deployment automation retries on failure. So what happens when you make the same call twice is defined by contract.
| Situation | Result |
|---|---|
relocate called again with the same mode while one is in flight |
Shares the deadline with the first operation. The later call doesn't extend the deadline |
relocate called again with a different mode while one is in flight |
Blocked with no wait — meaning an operation is already in progress |
relocate called again after Blocked |
Blocked isn't stored, so it re-checks the host's condition from scratch. This is the only result where retrying is meaningful |
relocate called again after Relocated |
Returns the original success result as-is. Doesn't move again |
shutdown called again while one is in flight |
Shares the same operation and stores the terminal result |
shutdown called again after Stopped |
Returns the stored result |
relocate called while starting up, or in an error/stopped state |
Blocked without touching admission |
A caller's cancellation ends only that call. The shared operation itself isn't cancelled.
shutdown is never blocked. It proceeds even with no target, insufficient capacity, or
no Relocation Store. So if shutdown gets confirmed while something is waiting on
relocate, the waiter ends in Blocked — this is why you must keep to the order "move
first, confirm success, then shut down."
If shutdown doesn't finish within its deadline, it performs only bounded cleanup and ends
in a forced-termination result. A deadline overrun and a callback failure are distinguished
by different result values.
5.2 What Stays Alive During a Transition¶
Relocating, Relocated, and Draining aren't "accepting nothing" states. Only starting
something new is blocked — what's already accepted is processed through to completion.
Relocating |
Relocated |
Draining |
|
|---|---|---|---|
| Selection by channel name | Excluded from new selection. The existing owner path is kept | Excluded from new selection | New admission closed |
| A request that directly names a node | Accepted until the unit seals | Not accepted | Ends with the shutdown result |
| Spot/Actor creation and join | Rejected | Rejected | Rejected |
| STREAM | New binding excluded. An existing session is handled via a barrier | New binding excluded | New session not accepted |
| An already-accepted request | Ends exactly once, via reply, error, timeout, or shutdown | Same | Same |
Monitoring or observer callbacks never hold up termination. Even if code observing status runs for a long time, maintenance never waits for it.
6. Location Readiness and Operational Queries¶
Operational code uses the location readiness API to check whether a needed peer is Ready. It uses the location runtime query for overall status and paged topology.
const status = await query.getStatus();
const page = await query.listTopology({ meshName: 'play' }, { pageSize: 100 });
const objectPeerReady = await readiness.isPeerReady('play', ZLinkLocationRole.Spot);
// Assemble status.storeHealthy · status.ownerLeaseHealthy · objectPeerReady · page.items
// into the operational endpoint's response.
The operational query returns only health and topology intended for human inspection. Store keys,
authority versions, owner tokens, and relocation records are internal Framework information
and aren't returned. NodeRid is used only to map operational info back to the actual
transport node.
7. MeshNode Runtime Control and Observation¶
A MeshNode registered with addRouteMesh is operated through two DI singletons.
Runtime options. Values that can be changed while serving are below. The remaining
socket options (HWM, timeout) are exclusive to configureRouterSocket() before startup.
// Excludes it from new object placement
meshOptions.mesh('game.room').placementWeight = 0;
// Excludes it from new channel select-one
meshOptions.channel('game.room').weight = 0;
The two weights are independent and take effect on new selections while running. Placement weight is used only for Actor/Spot create and relocation target selection. Channel weight is used only for selecting new select-one targets for that server membership. Looking up an unregistered mesh or membership is a configuration error.
Status query — RouteMesh runtime. It provides one consistent snapshot and an ordered component event stream for one mesh. Host termination is owned by the framework runtime.
// The immutable current status of nodes/peers/channels
const snapshot = meshRuntime.snapshot('game.room');
const ready = meshRuntime.isReady('game.room');
for await (const observed of meshRuntime.observe('game.room', 64, signal)) {
// observed.status carries the transition, observed.loss the missed count —
// see the shared rule in the `11. Monitoring` chapter §2.
}
8. Host Lifecycle¶
The Framework runtime is tied to the host's start/stop as its lifecycle service. The channel/SPOT/STREAM runtime is created based on the roles registered at startup, and cleaned up at shutdown.
- Configuration phase — finish every declaration before
app.Run(). A bad configuration is rejected as an exception at host startup. - Stopping — once the host shutdown signal arrives, it goes down in the order hosted
service
stop()→ channel/SPOT/STREAM runtime cleanup. - Fold background work into the same lifecycle using the host's standard lifecycle service.
8.1 Observing Status¶
Host relocate/shutdown state transitions are observed through the framework runtime's
bounded status stream. The per-MeshName runtime provides a component snapshot, but doesn't
create a separate termination authority or partial-drain operation.
for await (const observed of runtime.observe(signal)) {
// Records the whole-host state, effective intent, and terminal outcome in sequence order.
const status = observed.status;
logger.log(
`host lifecycle: ${status.state} ${status.relocationResult} ${status.terminationResult}`
);
}
Observe the host lifecycle states as-is (preparing, serving, relocating, relocated,
draining, stopped, error). The notation follows the language. The status's relocation/
termination results must match that operation's terminal result. To view it as numbers, use
the zlink.host.* instruments in Runtime Metrics.
8.2 Restarting with the same RoutingId¶
A server restarted immediately after forced termination with a fixed RoutingId can encounter the
previous process's active owner lease. Publishing the descriptor for the same MeshName and RoutingId
returns REJECTED_CONFLICT and fails host startup, so the host start call of every language ends
with a failure. Check the public runtime status after successful startup instead of treating an open
port as readiness.
Deployment should confirm the previous server's normal termination result before starting its replacement. After forced termination, start a new process once the previous owner lease has expired. The default owner lease TTL is 15 seconds and can be configured. Framework does not retry the conflict or select another RoutingId within the same startup operation. Also check that two processes are not using the same fixed RoutingId concurrently.
Normal shutdown cleans up the host's descriptors and owner lease. Forced termination may prevent that cleanup, leaving the active claim until lease renewal stops and the lease expires. Check the termination result to confirm that cleanup finished within the shutdown deadline. Process exit or a shutdown call alone does not guarantee that an immediate restart can claim the same RoutingId.
9. Related Documents¶
- Runnable verification examples for this chapter's contract:
13. Interface Catalogchapter §7 — the verification classFrameworkRuntimeContracts - The formal contract: Complete Host Relocation Flow · Runtime Metrics
- Status observation and diagnostics: the
11. Monitoringchapter - The Spot where the application decides the relocation boundary: 06-spot §7