Skip to content

.NET Topology And Host Monitoring Public Interface

.NET per-language interface table of contents · Runtime Monitoring · Host Relocate, Shutdown & Handoff

1. Scope

This document fixes the public interface a .NET application uses to request host shutdown and confirm the operational status of RouteMesh/ ClientServer/Fanout. The status and observation stream only include the values the application needs to judge state and choose a response.

Descriptor revision, lifecycle generation, endpoint, admission/claim/ reservation stage, and Location Store record — used when the framework coordinates topology — aren't included in the public interface. These values can't be changed by the application and are only used by the framework to judge stale state and ownership.

2. Host Lifecycle

Relocating, Relocated, and Draining are provided as separate states because they affect the application differently. In Relocating, new placement and application admission aren't accepted, and current objects are moved to a different node. In Relocated, the move is complete but host infrastructure is kept. In Draining, remaining application processing and resources are cleaned up, without relocation.

public enum ZLinkFrameworkRuntimeState
{
    Preparing = 0,
    Serving = 1,
    Relocating = 2,
    Relocated = 3,
    Draining = 4,
    Stopped = 5,
    Error = 6
}

public enum ZLinkFrameworkRelocationOutcome
{
    Relocated = 0,
    Blocked = 1
}

public enum ZLinkFrameworkRelocationMode
{
    PlannedMaintenance = 0,
    RollingUpdate = 1
}

public enum ZLinkFrameworkRelocationReason
{
    None = 0,
    TargetUnavailable = 1,
    StoreUnavailable = 2,
    RelocationDisabled = 3,
    StateIncompatible = 4,
    DeadlineExceeded = 5,
    RelocationFailed = 6,
    RuntimeNotReady = 7,
    ManualTopologyUnsupported = 8,
    ShutdownRequested = 9,
    OperationInProgress = 10
}

public sealed record ZLinkFrameworkRelocationOptions
{
    public required ZLinkFrameworkRelocationMode Mode { get; init; }
    public long? TargetApplicationVersion { get; init; }
    public TimeSpan? Deadline { get; init; }
}

public readonly record struct ZLinkFrameworkRelocationResult(
    ZLinkFrameworkRelocationMode Mode,
    long TargetApplicationVersion,
    ZLinkFrameworkRelocationOutcome Outcome,
    ZLinkFrameworkRelocationReason Reason);

public enum ZLinkFrameworkTerminationOutcome
{
    Stopped = 0,
    ForceStopped = 1
}

public enum ZLinkFrameworkTerminationReason
{
    None = 0,
    DeadlineExceeded = 1,
    TeardownFailed = 2
}

public readonly record struct ZLinkFrameworkTerminationResult(
    ZLinkFrameworkTerminationOutcome Outcome,
    ZLinkFrameworkTerminationReason Reason);

public readonly record struct ZLinkObservationLoss(
    ulong CoalescedCount,
    ulong DiscardedTerminalCount);

public readonly record struct ZLinkObservedStatus<TStatus>(
    TStatus Status,
    ZLinkObservationLoss Loss)
    where TStatus : notnull;

public readonly record struct ZLinkCoreHwmStatus(
    ulong? ConfiguredMemoryLimitBytes,
    ulong? ConfiguredBudgetBytes,
    ZLinkCoreHwmProfile ConfiguredProfile,
    ulong EffectiveBudgetBytes,
    ulong TotalAppliedHwmBytes,
    ulong CoreQueueAccountedBytes,
    ulong ApplicationAccountedBytes,
    ulong CurrentAccountedBytes,
    ulong ProvisionalAccountedBytes,
    ulong PeakAccountedBytes,
    ulong CompletionCurrentAccountedBytes,
    ulong CompletionPeakAccountedBytes,
    ulong CompletionPendingMessageCount,
    ulong TotalMessagingAccountedBytes,
    ulong MonitorQueueAppliedHwmBytes,
    ulong MonitorQueueAccountedBytes,
    ulong TotalInstanceAppliedHwmBytes,
    ulong TotalInstanceAccountedBytes,
    ulong BlockedRatioPpm,
    ulong ActiveDirectionalQueueCount,
    ulong ActiveCompletionDirectionalQueueCount,
    ulong ActiveSendQueueCount,
    ulong ActiveReceiveQueueCount,
    ulong OutstandingApplicationLeaseCount,
    ulong RetiredQueueCount,
    ulong DeferredOriginCreditBytes);

public readonly record struct ZLinkApplicationJobQueueStatus(
    ZLinkApplicationJobQueueProfile ConfiguredProfile,
    ulong? ConfiguredManualMax,
    uint ConfiguredPauseThresholdPercent,
    uint ConfiguredResumeThresholdPercent,
    ulong EffectiveProcessorCount,
    ulong EffectiveMaxQueuedApplicationJobs,
    ulong PausePermitCount,
    ulong ResumePermitCount,
    ulong ReservedSupplyPermits,
    ulong QueuedApplicationJobs,
    ulong PermitsInUse,
    ulong PeakPermitsInUse,
    ulong CapacityWaiters,
    ulong CapacityWaitCount,
    TimeSpan CapacityWaitDuration,
    ZLinkApplicationJobQueuePressureState PressureState,
    TimeSpan CurrentPauseDuration);

public readonly record struct ZLinkHostCapacityStatus(
    ulong MeasurementEpoch,
    ZLinkCoreHwmStatus CoreHwm,
    ZLinkApplicationJobQueueStatus ApplicationJobQueue);

public sealed record ZLinkFrameworkRuntimeStatus(
    ZLinkFrameworkRuntimeState State,
    bool IsReady,
    bool AcceptingWork,
    DateTimeOffset? Deadline,
    ZLinkFrameworkRelocationResult? RelocationResult,
    ZLinkFrameworkTerminationResult? TerminationResult,
    ulong Sequence,
    DateTimeOffset ObservedAt,
    ZLinkHostCapacityStatus Capacity = default,
    bool SafeToShutdown = true);

public interface IZLinkFrameworkRuntime
{
    ZLinkFrameworkRuntimeStatus Status { get; }
    void ResetCapacityMetrics();

    IAsyncEnumerable<ZLinkObservedStatus<ZLinkFrameworkRuntimeStatus>> ObserveAsync(
        CancellationToken cancellationToken = default);

    ValueTask<ZLinkFrameworkRelocationResult> RelocateAsync(
        ZLinkFrameworkRelocationOptions options,
        CancellationToken cancellationToken = default);

    ValueTask<ZLinkFrameworkTerminationResult> ShutdownAsync(
        TimeSpan? deadline = null,
        CancellationToken cancellationToken = default);
}

In ZLinkCoreHwmStatus, ApplicationAccountedBytes, OutstandingApplicationLeaseCount, RetiredQueueCount, and DeferredOriginCreditBytes are ABI-reserved compatibility fields and are always 0 since 0.13.1. The framework projects them unchanged and does not reinterpret them as Application Job Queue pressure.

IsReady is true only when State == Serving. AcceptingWork indicates whether the current host is accepting new application operations. The two values let readiness and admission be judged without exposing relocation unit count or internal queue state to the application.

RelocateAsync(...) moves the current stateful objects to a target of the application version the mode determines. PlannedMaintenance doesn't specify TargetApplicationVersion — the framework fixes the source host's ApplicationVersion as the effective target version. RollingUpdate must specify a TargetApplicationVersion greater than source. A different value combination is rejected with ArgumentException before starting the operation.

Target candidates are narrowed in the following order.

  1. PlannedMaintenance keeps only a target whose application version matches source. RollingUpdate keeps only a target that exactly matches the caller-specified application version, excluding a different higher or lower version.
  2. It keeps only a Serving Object Server on the same Mesh that isn't the source.
  3. It keeps only a target compatible in stable type, factory, relocation policy, and state adapter.
  4. It confirms population capacity and reservation availability and excludes the same MaintenanceWave as source.
  5. It keeps only a target whose RID and lifecycle generation match in the same descriptor snapshot and Core peer table, and whose peer is Admitted and Ready.
  6. If multiple candidates remain, it applies the existing node-wide placement weight.

If there's no eligible target for the requested version, it waits until the deadline for descriptor and Core ready state to converge, and then returns Blocked/TargetUnavailable.

Once every object has finished moving, it returns Relocated, and the host becomes Relocated. In this state, new application operations aren't accepted, but infrastructure and connections are kept. If the move can't be safely started or completed, it returns Blocked. The framework cleans up not-yet-committed changes, and returns to Serving if the host still has a local object to keep processing.

ShutdownAsync(...) doesn't start relocation. Calling it from Serving cleans up remaining application processing and resources; calling it from Relocated only cleans up infrastructure and connections. In both cases, it becomes Stopped once shutdown finishes. If deadline == null, the default for each operation is 30 seconds.

If ShutdownAsync(...) is called during Relocating, only the current atomic relocation unit's terminal result is confirmed, and the rest of relocation isn't started. The relocation waiter receives Blocked/ShutdownRequested, and the shutdown operation cleans up the remaining objects and resources on the source.

The CancellationToken the caller passes only ends that waiter. An already-started shared lifecycle operation keeps running and doesn't affect other waiters or host lifecycle. A waiter that repeatedly calls the same operation shares the in-progress operation and terminal result. Only a call with the same Mode and effective target application version joins; the later call's deadline doesn't change the shared operation deadline. A call with a different Mode or effective target application version doesn't change the existing operation and returns Blocked/OperationInProgress.

3. Common Topology State

Host state represents the whole process's lifecycle. ZLinkTopologyState represents the availability of one topology registered by MeshName or ChannelName. Even if the host is Serving, only that specific topology can be Degraded if it has no ready peer or target.

Topology status only provides closed states from which the user can judge readiness and failure scope. ZLinkTopologyReason is used by the application to decide whether to check configuration or observe again later. Detailed transport or Store errors are recorded in .NET logging and tracing.

public enum ZLinkTopologyState
{
    Starting = 0,
    Ready = 1,
    Degraded = 2,
    Stopping = 3,
    Stopped = 4,
    Failed = 5
}

public enum ZLinkTopologyReason
{
    RuntimeNotReady = 0,
    NoReadyPeer = 1,
    NoReadyTarget = 2,
    LocationUnavailable = 3,
    CapacityExceeded = 4,
    Draining = 5,
    InternalFailure = 6
}

public enum ZLinkPeerState
{
    Connecting = 0,
    Ready = 1,
    Draining = 2,
    NotConnected = 3,
    NotRequired = 4
}

public sealed record ZLinkChannelStatus(
    string ChannelName,
    bool IsReady,
    int ReadyTargetCount);

public sealed record ZLinkPeerStatus(
    RoutingId NodeRid,
    ZLinkPeerState State,
    ZLinkTopologyReason? UnavailableReason);

NodeRid is the MeshNode's transport identity, used to correlate a peer with log and deployment information. A separate operational node identity isn't added. Endpoint and connection generation aren't provided in public status.

4. RouteMesh

RouteMesh status shows, at once, peer connections of the same MeshName, channel readiness, and object placement availability. Placement count only aggregates active objects that exist in this process.

public sealed record ZLinkPlacementStatus(
    bool IsAvailable,
    int ActiveActorCount,
    int ActiveSpotCount,
    ZLinkTopologyReason? UnavailableReason);

public sealed record ZLinkRouteMeshStatus(
    string MeshName,
    ZLinkTopologyState State,
    bool IsReady,
    int ReadyPeerCount,
    IReadOnlyList<ZLinkChannelStatus> Channels,
    IReadOnlyList<ZLinkPeerStatus> Peers,
    ZLinkPlacementStatus Placement,
    ulong Sequence,
    DateTimeOffset ObservedAt);

public interface IZLinkRouteMeshRuntime
{
    ZLinkRouteMeshStatus GetStatus(string meshName);

    IAsyncEnumerable<ZLinkObservedStatus<ZLinkRouteMeshStatus>> ObserveAsync(
        string meshName,
        CancellationToken cancellationToken = default);
}

IsReady is true when the host is Serving and that RouteMesh can process application traffic. ReadyPeerCount is the number of remote MeshNodes in ready state. Since a local channel can also be used normally, having 0 peers alone doesn't make every RouteMesh judged unavailable.

Between two Object Clients, a peer connection isn't made only when neither side has RouteMesh Channel Server membership. The same applies when only Channel Client membership exists. Each other's RID appears in Peers as NotRequired, but isn't included in ReadyPeerCount. This state is excluded from liveness probe/reconnect/health failure aggregation, and doesn't change topology to Degraded. If either side has Channel Server membership, including weight 0, a connection is needed, and the unconnected state is NotConnected. A peer that needs a connection but has no ready connection is shown as NotConnected and reflected in failure aggregation. Peer observation for a Channel-only topology whose Object role is None keeps the existing rule.

Placement.IsAvailable is true when this node is Object Server role and can accept new Actors/Spots. Both Actor and Spot capacity, and activation concurrency, must have room. Population reservation, the current value of activation concurrency, the activation barrier, and per-stable-type internal counts aren't included in public status.

5. ClientServer

A Server registered on the same process is also a normal target subject to the same weight rule as a remote Server. Status provides the total selectable target count and per-target operational status, and doesn't provide endpoint or discovery revision.

public enum ZLinkClientServerRole
{
    Client = 1,
    Server = 2,
    ClientAndServer = 3
}

public sealed record ZLinkClientServerTargetStatus(
    RoutingId NodeRid,
    int Weight,
    ZLinkPeerState State,
    ZLinkTopologyReason? UnavailableReason);

public sealed record ZLinkClientServerStatus(
    string ChannelName,
    ZLinkClientServerRole LocalRole,
    ZLinkTopologyState State,
    bool IsReady,
    int ReadyTargetCount,
    IReadOnlyList<ZLinkClientServerTargetStatus> Targets,
    ulong Sequence,
    DateTimeOffset ObservedAt);

public interface IZLinkClientServerRuntime
{
    ZLinkClientServerStatus GetStatus(string channelName);

    IAsyncEnumerable<ZLinkObservedStatus<ZLinkClientServerStatus>> ObserveAsync(
        string channelName,
        CancellationToken cancellationToken = default);
}

ReadyTargetCount includes every Ready Server with positive weight that isn't draining, without distinguishing local/remote. Targets is a read-only value for diagnostics. This list isn't used to select a specific Server or change target weight.

6. Fanout

Fanout runtime status shows the publisher connections currently available to an automatic subscriber. An individual publisher's endpoint, discovery source, and generation are managed by the framework.

public sealed record ZLinkFanoutStatus(
    string ChannelName,
    ZLinkTopologyState State,
    bool IsReady,
    int ReadyPublisherCount,
    IReadOnlyList<ZLinkPeerStatus> Publishers,
    ulong Sequence,
    DateTimeOffset ObservedAt);

public interface IZLinkFanoutRuntime
{
    ZLinkFanoutStatus GetStatus(string channelName);

    IAsyncEnumerable<ZLinkObservedStatus<ZLinkFanoutStatus>> ObserveAsync(
        string channelName,
        CancellationToken cancellationToken = default);
}

The connection list of a manual subscriber is owned by the manual connection API. Querying a manual ChannelName through IZLinkFanoutRuntime raises ZLinkConfigurationException.

7. Observation Stream

The unit each ObserveAsync(...) delivers is ZLinkObservedStatus<TStatus>. Status is a completed immutable status at the moment state changed meaningfully, shared across observers. Loss is a loss accumulator specific to this one enumeration, so it isn't put inside status. If a consumer can't keep up with the rate of change, intermediate statuses are coalesced and the latest status is delivered. The status stream isn't an event log auditing every transition.

ZLinkObservedStatus<TStatus>.Loss delivered by ObserveAsync(...) has type ZLinkObservationLoss. CoalescedCount and DiscardedTerminalCount have type ulong. Runtime monitoring §7.2 owns the delivery unit, loss-counter semantics, range and saturation, and subscription lifetime contract.

A general-purpose event DTO whose nullable field meaning changes based on an identifier isn't used. A consumer can use the entire received status as the current state, without interpreting a field combination per event kind.

Sequence is a value for comparing status order within the same runtime instance. It can restart from 0 when the process restarts, and doesn't guarantee persistence or a global order.

SafeToShutdown is the source's own observation that every relocation unit it started has reached the Message Follow route removal point (S4) and that its cutover retransmission window has ended. It is never a completion ACK from a target, and it is true when this source has not started a relocation.

CancellationToken only ends that asynchronous enumeration. Once cancellation is recognized, no new status is delivered, and it doesn't affect other observers, topology connections, or host lifecycle.

8. Dispatch Policy And Diagnostics

The unhandled-message policy and diagnostics configuration are handled by separate child interfaces under ConfigureDispatch(). Core HWM and application-job-queue settings are owned by the independent ConfigureInboundDispatch() surface. Tracing mode is owned by the diagnostics child; observer, error sink, and file output aren't provided.

public enum ZLinkUnhandledDispatchAction
{
    ReplyError = 0,
    LogAndDrop = 1,
    Drop = 2,
    Throw = 3
}

public interface IZLinkUnhandledDispatchOptions
{
    ZLinkUnhandledDispatchAction Request { get; set; }
    ZLinkUnhandledDispatchAction Send { get; set; }
    ZLinkUnhandledDispatchAction Publish { get; set; }
}

public enum ZLinkDiagnosticsLevel
{
    Off = 0,
    Errors = 1,
    Normal = 2,
    Detailed = 3
}

public interface IZLinkDiagnosticsOptions
{
    IZLinkDiagnosticsOptions SetLevel(ZLinkDiagnosticsLevel level);
    IZLinkDiagnosticsOptions SetSampleRate(double rate);
    IZLinkDiagnosticsOptions IncludeMessageSizes(bool include);
}

public interface IZLinkDispatchOptions
{
    IZLinkUnhandledDispatchOptions Unhandled { get; }
    IZLinkDiagnosticsOptions Diagnostics { get; }
}

public interface IZLinkInboundDispatchOptions
{
    ulong? CoreHwmMemoryLimitBytes { get; set; }
    ulong? CoreHwmBudgetBytes { get; set; }
    ZLinkCoreHwmProfile CoreHwmProfile { get; set; }
    ZLinkApplicationJobQueueProfile ApplicationJobQueueProfile { get; set; }
    ulong? MaxQueuedApplicationJobs { get; set; }
    uint ApplicationJobQueuePauseThresholdPercent { get; set; }
    uint ApplicationJobQueueResumeThresholdPercent { get; set; }
}

public interface IZLinkDiagnosticsRuntime
{
    ZLinkDiagnosticsLevel Level { get; set; }
    Task SetLevelAsync(ZLinkDiagnosticsLevel level);
}

SetSampleRate(...) only allows 0.0 to 1.0 inclusive. Out of range raises ArgumentOutOfRangeException. Recording message size adds the payload size distribution to telemetry, without recording payload content.

IZLinkDiagnosticsRuntime is a process singleton obtained from DI. SetLevelAsync is the canonical asynchronous control; the Level setter is the synchronous bridge over it. Use SetLevelAsync from framework execution contexts such as handlers and callbacks. Reading Level returns the level currently applied to the process. Changing the value applies the new level starting from message processing that begins afterward. The change is an atomic state change that doesn't wait for message processing. A record already in the telemetry queue can be delivered or dropped, and turning it back on doesn't retroactively create records for earlier processing.

The .NET runtime provides trace as ActivitySource, metric as System.Diagnostics.Metrics.Meter, and log as Microsoft.Extensions.Logging.ILogger. The export destination and log storage location are decided by the application's telemetry and logging configuration. The framework doesn't take a file path or provide its own exporter lifetime as a public API.