Skip to content

ClientServer Channel

Channel·Transport topic table of contents · Spec table of contents · Previous: 02. Channel Messaging · Next: 04. Network Listener Identity

A ClientServer Channel is a one-directional service boundary where the Client starts a send or request and the Server runs a handler and replies to the request. This document defines role registration, endpoint discovery, weight- based target selection, and the drain and restart contract.

1. ClientServer Channel Overview

Role Business calls it can start Handling a received message
Client Selects one Ready server (finished initialization and discovery so it can receive application messages) and starts a send or request. Doesn't receive a message sent first by the Server without a Client request. It receives only a reply matching a request the Client started, as that request's result.
Server Can't start a new business send or request targeting a connected Client. Runs the handler for a send or request the Client sent. A request handler replies using the received reply token.

In other words, on a ClientServer Channel, the only thing a Server can send a Client is the reply to a request the Client started first. If the Server needs to send a notification or event first, it must call through a separately registered RouteMesh, not a ClientServer connection.

A ClientServer Channel isn't a RouteMesh option. It doesn't provide the following functionality.

  • Node direct (where a caller targets one specific MeshNode — a runtime node that sends or receives messages within a RouteMesh — by giving both the MeshName and the target RID) between MeshNodes
  • Spot and Actor messaging
  • Logical Multicast (delivering one message to multiple Spots of the same Channel by ChannelName and topic)
  • Connecting a different RouteMesh on its behalf or relaying messages

This restriction means ClientServer transport doesn't automatically substitute for the functionality above. An application in the same process can register ClientServer and RouteMesh separately under different ChannelName (the name that identifies the Channel scope a message is sent to) values and start separate calls on the two send paths. A ClientServer handler can also start a new call, at the application's discretion, targeting a registered RouteMesh, a different ClientServer Channel, a Spot, or an Actor. This call is a separate operation the application chose — it isn't ClientServer automatically relaying the message.

The logical address a Channel caller specifies and the meaning of call completion are defined by Channel Messaging.

This document's C# code is a reference showing how the common contract appears in the .NET public API. It doesn't require the same signature in other languages. The precise .NET signature is defined by .NET Topology Public Interface and .NET Channel Messaging Public Interface.

2. Client and Server Role Registration

One process can register Client, Server, or both roles on the same ClientServer ChannelName. The registration key is (ChannelName, Role), and Client and Server are each registered at most once per role.

A ClientServer send or request can start only when the local process registered the Client role for that ChannelName. Even if the ChannelName and a Server role exist, absence of the Client role ends the call with a NotConfigured framework error and never directly invokes a local handler. Target selection and waiting results follow the Framework API channel selection result.

public interface IZLinkFrameworkOptions
{
    // configures a Client and Server role for one ChannelName.
    IZLinkClientServerChannelRoleBuilder AddClientServerChannel(
        string channelName);
}

public interface IZLinkClientServerChannelRoleBuilder
{
    // chooses the role that starts business sends and requests.
    IZLinkClientServerChannelClientBuilder Client();

    // chooses the role that handles Client messages and replies to requests.
    IZLinkClientServerChannelServerBuilder Server();
}

The following code shows registering both roles for the same ChannelName in the same process. Each role's configuration starts separately on the same builder, and the two registrations merge into one ClientServer topology.

var billing = options.AddClientServerChannel("billing");

var client = billing.Client(); // registers the role that starts billing calls, once.

var server = billing
    .Server()                   // also registers the Server role once in the same process.
    .Listen()
    .SetAdvertiseHost("billing-1")
    .SetWeight(100)
    .AddRequestHandler<ChargeHandler, Charge, ChargeResult>();

The Client's endpoint and the Server's listener/handler configuration are explained in §3 and §4.

A reply to a request the Server received isn't the Server starting a new business call. It's a reverse-direction transport message completing the same request the Client started.

If a message arrives at the Server but doesn't match the identity of a currently pending Client request, the framework can't determine which request's reply this is. So it isn't run as an application handler or used as a different request's reply — it's recorded as a protocol error.

A Server for the same ChannelName can be registered in multiple processes. Registering Client and Server once each for the same ClientServer ChannelName in the same process is normal and merges into one topology. The following duplications are startup configuration errors.

  • Registering the Client role twice for the same ClientServer ChannelName
  • Registering the Server role twice for the same ClientServer ChannelName
  • Registering the same ChannelName on both RouteMesh and ClientServer at once

Several different ClientServer ChannelNames can be registered in the same process. The already-defined ChannelName conflict rule between RouteMesh and fanout doesn't change.

3. How to Find a Server Endpoint and Make It Ready

The Client obtains a Server endpoint from one or more of the following sources.

Discovery method Endpoint source Location Store (a store that keeps each Spot's and descriptor's current state where multiple nodes can check it together)
Manual The application registers it via Connect(endpoint). Not needed if only manual endpoints are used.
Automatic Queries the ClientServer Server descriptor of the same ChannelName. Location Store is required.

If the manual and automatic sources point to the same Server RID and lifecycle generation, the framework merges them into one connection candidate.

From endpoint discovery through target selection, three parties participate in sequence: the Client, the discovery source (a manual setting or an automatic descriptor), and the Server.

%%{init: {'sequence': {'actorFontSize': '18px', 'messageFontSize': '18px', 'noteFontSize': '18px', 'boxMargin': 8, 'width': 140}, 'themeVariables': {'fontSize': '18px'}}}%%
sequenceDiagram
    participant Client as Client role
    participant Discovery as Discovery source<br/>(manual setting or automatic descriptor)
    participant Server as Server role

    Client->>Discovery: query for endpoint (manual Connect or automatic query)
    Discovery-->>Client: Server RID, lifecycle generation, and endpoint
    Client->>Server: starts the transport connection (only the Client starts a connection)
    Server-->>Client: confirms identity and generation, then becomes ready
    Note over Client: only a ready, weight>0, non-draining Server is a candidate
    Client->>Server: send or request to the Server picked by weight ratio

In this flow, discovery only produces a candidate endpoint. It becomes a ready target (a target that finished the listener/transport connection, identity check, and required handler registration, and so can receive new messages) only once identity and generation are re-verified on the actual transport connection (§3.2). The weight rule that picks the one Server that actually receives a call among ready targets is defined by §4.

3.1 Only the Client Starts a Connection to the Server

In both manual and automatic discovery, the Client starts the connection to the Server endpoint. The Server doesn't look up a Client endpoint or start an outbound connection toward the Client.

In automatic discovery, one connection intent, distinguished by Server RID and lifecycle generation, is created per valid Server descriptor for the same ChannelName. If multiple Servers are discovered, each Server's ready connection is kept independently, and one is selected by §4's weight rule when starting a business call.

So a ClientServer Channel is an asymmetric topology with a fixed connection direction. Unlike RouteMesh, it doesn't decide which side starts the connection by comparing the two MeshNodes' RIDs.

public interface IZLinkClientServerChannelClientBuilder
{
    // adds a manual endpoint. If omitted, automatic discovery can be used.
    IZLinkClientServerChannelClientBuilder Connect(string endpoint);
}

public interface IZLinkClientServerChannelServerBuilder
{
    // port 0 has automatic discovery publish the actual bound port.
    IZLinkClientServerChannelServerBuilder Listen(int port = 0);
    IZLinkClientServerChannelServerBuilder SetBindHost(string bindHost);
    IZLinkClientServerChannelServerBuilder SetAdvertiseHost(
        string advertiseHost);

    // 0 excludes it from new Client call selection.
    IZLinkClientServerChannelServerBuilder SetWeight(int weight);

    IZLinkClientServerChannelServerBuilder
        AddSendHandler<THandler, TMessage>(
            string? packetName = null)
        where THandler : class, IZLinkSendHandler<TMessage>;

    IZLinkClientServerChannelServerBuilder
        AddRequestHandler<THandler, TRequest, TReply>(
            string? packetName = null)
        where THandler : class, IZLinkRequestHandler<TRequest, TReply>;
}

The following example configures a manual Client and an automatic-discovery Server in different processes.

clientOptions
    .AddClientServerChannel("billing")
    .Client()
    .Connect("tcp://billing-1:7200"); // registers a manual server endpoint directly.

serverOptions
    .AddClientServerChannel("billing")
    .Server()
    .Listen()                        // binds on port 0, then publishes the actual port.
    .SetAdvertiseHost("billing-2")
    .SetWeight(100)                  // becomes a selection candidate for new billing calls.
    .AddRequestHandler<ChargeHandler, Charge, ChargeResult>(); // registers the request handler

3.2 Finding the Server Descriptor Alone Does Not Make It Ready

An automatic-discovery Server publishes a dedicated ClientServer ClientServer Server descriptor and owner lease holding the following information. An owner lease is information proving, by renewing on a fixed schedule, that this Server still has the right to keep using the ClientServer Server descriptor.

  • ChannelName
  • Server identity and lifecycle generation
  • Endpoint
  • Weight and drain state. Drain state represents a state where new-call selection is stopped while preparing for a safe shutdown or exclusion from targets, finishing only already-received calls.
  • Descriptor revision (an increasing number marking the version of mutable descriptor information, such as weight or drain state, within the same lifecycle)

The Client only uses a valid ClientServer Server descriptor for the same ChannelName. After finding the endpoint from this registration information, it must re-verify that Server identity and lifecycle generation match on the actual transport connection before using it as a ready target.

The ClientServer Server descriptor doesn't include the following information.

  • MeshName or RouteMesh membership
  • Spot or Actor location
  • Information used to decide whether to accept a MeshNode peer connection

A MeshNode descriptor isn't used for ClientServer discovery, and a ClientServer Server descriptor isn't used for a RouteMesh peer connection.

3.3 When the Location Store Is Needed

If only manual endpoints are used, a Location Store isn't needed. If automatic discovery is enabled but there's no Location Store, startup fails before the Server listener binds.

A manual connection also verifies the following information on the actual transport connection.

  • ChannelName
  • Server RID and lifecycle generation
  • Weight and drain state
  • Security identity

This information only controls the ClientServer connection — it isn't converted into a MeshNode descriptor or RouteMesh peer information.

4. Weight and Target Selection

Server weight is a relative share that determines how often new sends and requests are assigned among several selectable Servers. The range is 0..10000, with a default of 100. A value outside the range is a configuration error during startup configuration or a runtime change.

This weight doesn't mean the number of concurrent requests a Server can handle or its physical performance. For example, if Server A's weight is 100 and Server B's is 50, with other conditions equal, repeated target selection gives A twice B's assignment share. It doesn't mean A is necessarily chosen for every individual request. The selection order is defined by Channel Messaging §3.

ChannelName ready/drain eligibility and weighted select-one are defined by Channel Messaging §3.

Drain is the process of first blocking selection for new sends and requests, to safely shut down a Server or exclude it from service targets, then finishing work the Server already received within a set time. Weight 0 and drain have different meanings. Weight 0 is a setting that zeroes only the selection share while keeping the Server running; drain is a termination procedure that cleans up existing work and then closes the descriptor and listener.

Weight 0 does not turn the Server role or an existing connection into a Client role.

4.1 A Server in the Same Process Is Also a Selection Candidate

If Client and Server for the same ChannelName are registered in the same process, the local Server joins the same candidate set as remote Servers after listener bind and service admission. Eligibility and ordering are defined by Channel Messaging §3.

%%{init: {'flowchart': {'nodeSpacing': 32, 'rankSpacing': 40, 'padding': 8, 'wrappingWidth': 180}, 'themeVariables': {'fontSize': '18px'}}}%%
flowchart LR
    C["Client role in the same process"]
    S1["Ready Server in the same process"]
    S2["Ready Server in a different process"]
    Pick["picks one, reflecting relative assignment share and safe-shutdown status"]

    C -->|starts a call| Pick
    Pick -->|selectable| S1
    Pick -->|selectable| S2
  • Even if the local Server is selected, its handler isn't called directly. The actual transport message is delivered from the Client DEALER to the Server ROUTER. This is so that codec, HWM, timeout, cancellation, the identifying information linking request and reply, and the terminal-completion rule aren't bypassed.

Target selection and submit are one operation. The framework doesn't return the selected Server identity to the application as an intermediate result.

  • Even if a connection closure, timeout, or cancellation occurs after submit, the same request isn't automatically resent to a different Server. This is because the first Server may have already run the request, with only the reply not delivered.

4.2 When Target-Selection Information Changes at Runtime

If a Server changes its weight or starts draining while running, the target-selection information the Client uses changes. The Server records this change in the ClientServer Server descriptor and increments the descriptor revision. The Client only applies a larger revision within the same lifecycle generation. So a late-arriving previous descriptor doesn't make a draining Server a selection candidate again, or revert to a pre-change weight.

Changing a local Server's weight at runtime specifies the target by ChannelName. Server RID and endpoint are values distinguishing remote targets in monitoring — the application doesn't specify them as the target for a local weight change.

5. Send, Request, and Reply

Send submits a one-way message to one ready Server and doesn't create a reply token.

Reply correlation is an identifying value created when sending a request to link the request with a reply that arrives later. The Server includes the same value in the reply, and the Client uses it to confirm which request the reply is the result of.

A request selects one ready Server and then builds reply correlation. It completes with whichever of reply, error, timeout, cancellation, or shutdown is confirmed first.

If the Client DEALER doesn't receive earlier one-way DATA from the Server ROUTER in Core, or keeps receive flow PAUSED, a reply later on the same connection is also delayed. The ClientServer request timeout can therefore be decided before the reply, and the late reply after timeout is discarded under the existing first-terminal rule. A disconnect, allocation failure, or timeout can terminate first, so the reply is not guaranteed to arrive eventually.

%%{init: {'sequence': {'actorFontSize': '18px', 'messageFontSize': '18px', 'noteFontSize': '18px', 'boxMargin': 8, 'width': 140}, 'themeVariables': {'fontSize': '18px'}}}%%
sequenceDiagram
    participant Caller
    participant Client as Client runtime
    participant Selector as Ready server selector
    participant Server as Selected server
    participant Handler as Request handler

    Caller->>Client: submit ChannelName and request
    Client->>Selector: request a ready Server selection
    Selector-->>Client: return one Server reflecting weight and drain
    Client->>Server: send the request including its identifying information
    Server->>Handler: pass the request and reply token
    Handler-->>Server: return the reply payload
    Server-->>Client: send the reply with the same identifying information
    Client-->>Caller: return the request result once

5.1 Reply Token

The reply token a Server request handler receives can only be used for the current request. Once the final reply is made once, it can't be reused.

If a reply route can be restored after any of the following failures, the request completes with a structured error reply.

  • No handler found
  • Payload couldn't be interpreted
  • Handler exception

If the same failure occurs on a one-way send, no reply is built. The message isn't delivered to a handler — it's recorded in runtime observability information.

5.2 When a Handler Calls Another Target

A ClientServer handler can send a request to a different RouteMesh, ClientServer Channel, Spot, or Actor. This downstream request uses separate request-reply correlation information from the original ClientServer request.

The original request only completes once, via the reply the ClientServer handler returned. The downstream reply's correlation information doesn't replace the original ClientServer request's value.

5.3 Application Record Contract

The internal wire contract for a ClientServer application record carries application metadata in the JSON header's metadata field as a string-to-string object. A missing field is interpreted as empty metadata. Character, size-limit, error, and reply rules follow channel messaging §9. The total size in that section is the UTF-8 JSON encoding length of the entire metadata object, including keys, values, and structural overhead. The header's metadata field name and other fields are excluded from this size. This length is measured on the minimal-escape form — in key and value strings only ", \, and U+0001–U+001F are escaped (control characters use \b \f \n \r \t where one exists, otherwise lowercase \u00xx), every other character is written as raw UTF-8, and no whitespace appears between structural characters. A sender writes this form, and a receiver judges by the length of the received value re-measured in this form. The business payload frame carries no metadata.

6. Drain

Server drain is a procedure that blocks new requests and finishes already- received ones, processed in the following order.

  1. Closes local ready status and stops accepting new business messages.
  2. Publishes draining state and a larger revision to the ClientServer Server descriptor.
  3. Continues processing already-accepted handlers and request replies up to the deadline.
  4. Once every final result is confirmed, releases the ClientServer Server descriptor and owner lease and closes the listener.

A manual Client is notified of the same drain state via a connection control message.

  • The Client can submit a request right before confirming the drain state. If the Server rejects this request, it doesn't wait indefinitely — it completes with a finite rejected result. This is so that, even in the race between the submit point and the confirmation point, the caller doesn't wait indefinitely for a result.

7. Server Restart

When the same Server identity restarts, it issues a lifecycle generation different from the previous value.

  • This value's order isn't judged by numeric magnitude. Even at the same endpoint, a previous generation's connection and ClientServer Server descriptor aren't used as a new target. This is because the generation is a fence that only compares whether values are equal.

The Client replaces it in the following order.

  1. Finds the new generation's ClientServer Server descriptor.
  2. Re-verifies identity and generation on the transport connection.
  3. Makes the new generation a ready target.
  4. Removes the previous generation's connection.

The Client compares the reply correlation included in a reply against the value of the currently pending request.

  • Only if the values match is it treated as that request's result. So even a reply sent from a previous generation can become the result of the original request, if that request is still pending — because it's judged only by reply correlation, not by which generation it came from.

If the original request's reply correlation has disappeared due to timeout, cancellation, or Client restart, a late-arriving reply is discarded. It isn't used as the result of a different request started afterward.

8. Location Store Failure

If the Location Store becomes unavailable, the Client keeps the last successfully obtained automatic connection candidates. During the failure, it stops computing additions and removals of new ClientServer Server descriptors.

  • An already-ready connection and an already-accepted request aren't canceled merely due to a Store failure. This is because discovery updates stopping and the validity of an already-verified transport connection are separate concerns.

If a Server fails to renew its owner lease and the allowed time elapses, it stops accepting new business messages. This final time point is called the fencing deadline. Once the Store recovers, the target list is re-aligned based on the latest descriptor revision and lifecycle generation.

9. Verification Requirements

Verify the following using only the role registration builder, the send/request results and replies observed by Client and Server, ClientServer Server descriptor queries and owner lease state, and the connection snapshot provided by monitoring.

Role and Registration

  • The Server has no public API to start a new business call targeting the Client.
  • A Server message that doesn't match a Client request isn't delivered to an application handler.
  • Client and Server roles can each be registered once for the same ClientServer ChannelName in the same process.
  • Duplicate registration of the same role for the same ClientServer ChannelName, and a ChannelName conflict with RouteMesh, are startup configuration errors.

Endpoint Discovery and Target Selection

  • In both manual and automatic discovery, only the Client starts the connection to the Server.
  • If multiple Servers are discovered, each ready connection is kept independently.
  • RouteMesh's RID-comparison rule isn't used to decide the connection-start direction on a ClientServer Channel.
  • Automatic discovery uses a ClientServer Server descriptor, and it isn't used interchangeably with a MeshNode descriptor.
  • Server weight allows 0, default 100, and cap 10000; -1 and 10001 are rejected at startup configuration and runtime change.
  • ChannelName Server selection verifies Channel Messaging §3.
  • A selected local Server handler also runs through actual transport.

Restart and Failure

  • When the same identity restarts, only the new lifecycle generation becomes a ready target.
  • A late reply from a previous generation doesn't complete a new request.
  • Even if a handler calls a different send path, the original request only completes once.
  • A Store failure doesn't immediately drop an already-ready connection.
  • After Store recovery, the target list is re-aligned with the latest revision and generation.

Request Completion and the Single FIFO

  • If a Server sends a request reply after one-way DATA and the Client stops receiving DATA or remains PAUSED, the configured timeout can end the request. A late reply that arrives after DATA drains doesn't create a second terminal.
  • After the reply reaches the Core physical head and Core identifies it as a completion, it acquires no Application Job Queue permit, but it doesn't bypass earlier DATA, Core HWM, or PAUSED.

Channel·Transport topic table of contents · Spec table of contents · Previous: 02. Channel Messaging · Next: 04. Network Listener Identity