02. Topology discovery¶
This category covers the topology registration entry points zlink_framework_options_t provides,
and the entry points that query RouteMesh/ClientServer/Fanout operational status. The exact
signatures are owned by the
Channel messaging exact interface
and the
Configuration and host exact interface
(Korean-only). Every registration entry point is a call made at host configuration time.
add_route_mesh (configuration time)¶
Registers one physical MeshNode. The starting point for RouteMesh-based topology.
auto play = options.add_route_mesh("play")
.listen(5501)
.set_automatic_routing_id_prefix("play")
.set_placement_weight(100);
Options. Commonly used modifiers are as follows.
| Modifier | Default | Meaning |
|---|---|---|
.listen(endpoint) / .listen(port) |
Does not bind if omitted | The receiving endpoint for this MeshNode |
.set_bind_host(string) / .set_advertise_host(string) |
Follows configure_network()'s root default |
The bind/advertise host that applies only to this MeshNode |
.set_routing_id(routing_id) / .set_automatic_routing_id_prefix(prefix) |
Issued by the Framework | A fixed RID, or the prefix of an issued RID (1..64 characters of [A-Za-z0-9._-]) |
.set_object_role(object_role_t) |
none |
One of client/server/none. server includes client capability, and both client/server require a Location Store |
.set_placement_weight(int) |
100 (range 0..10000) |
The relative weight for placing new Actors/Spots on this node |
.set_actor_limit(int32_t) / .set_spot_limit(int32_t) |
0 (unlimited) |
The Actor/Spot capacity this node accepts |
.set_activation_concurrency(int32_t) |
128 (positive) | The concurrent-execution cap for activation admission |
.set_default_request_timeout(milliseconds) |
This MeshNode's default request timeout | The value request_to_node/request_to_channel (messaging-execution category) uses when .timeout(...) is omitted |
.set_instance_spot_idle_timeout(milliseconds) |
0 (never reclaims) |
The Instance Spot idle reclaim time |
.configure_router_socket() |
mesh_node_socket_config_t default |
This MeshNode's ROUTER socket HWM/buffer/timeout (max_message_size, send_high_water_mark, etc.) |
.channel(channel_name) |
— | Enters this MeshNode's RouteMesh Channel role registration. See the RouteMesh Channel registration entry |
.peer_connections() |
— | See the Manual peer connections entry |
.add_route_send_handler<THandler, TMessage>(packet_name = {}) |
The packet name is determined from the message type | Registers a Node-direct one-way handler. The target send_to_node (messaging-execution category) calls |
.add_route_request_handler<THandler, TRequest, TReply>(packet_name = {}) |
The packet name is determined from the message type | Registers a Node-direct request handler. The target request_to_node calls |
Completion result. Registers synchronously with no return value. An invalid combination (a
duplicate MeshName, a missing listener setting, etc.) surfaces as a configuration error in
app.run(...)'s validation before the socket bind.
When to use. Every host that uses RouteMesh registers at least one MeshNode. A node that only uses manual peers and needs no distributed discovery can start without a Location Store.
Object role registration (configuration time)¶
Registers how a MeshNode treats Actors/Spots (whether it only acts as a Client, or hosts them as a Server).
play.add_entry_spot<game_entry_spot_t>();
play.add_spot_factory<room_spot_t>(
"room",
[](zlink::framework::spot_context_t context) {
return std::make_shared<room_spot_t>(std::move(context));
},
[](auto &factory) {
factory.set_execution_mode(
zlink::framework::user_spot_execution_mode_t::spot_wide);
factory.template preserve_state_with<room_relocation_adapter_t>();
});
play.add_actor_factory<player_actor_t, player_actor_factory_t>(
"player",
std::make_shared<player_actor_factory_t>(),
[](auto &factory) {
factory.template preserve_state_with<player_relocation_adapter_t>();
});
Options. Commonly used modifiers are as follows.
| Modifier | Default | Meaning |
|---|---|---|
.add_entry_spot<TEntrySpot>() / .add_entry_spot<TEntrySpot>(factory) |
None | Registers an Entry Spot type dedicated to external entry. Omitting the factory uses the TEntrySpot(entry_spot_context_t) constructor |
.add_spot_factory<TSpot>(stable_type, factory, configure) |
None | Registers a stable User Spot type. configure receives exactly one of preserve_state_with<TAdapter>/recreate_on_relocation/disable_relocation, in addition to set_stable_type_limit/set_execution_mode/set_relocation_coordination_mode |
.add_instance_spot_factory<TSpot>(stable_type, factory, configure) |
None | Registers a cold-activation Instance Spot type. configure receives exactly one relocation policy, in addition to set_stable_type_limit |
.add_actor_factory<TActor, TFactory>(stable_type, factory, configure) |
None | Registers a stable Actor type. configure receives exactly one relocation policy (an Actor factory has no stable_type_limit) |
Completion result. Registers synchronously with no return value. Adapter/factory mismatches
for a stable type intending to use relocation, and type duplicates, surface as a configuration
error in app.run(...)'s startup validation.
When to use. Register the corresponding role when this node actually hosts Actors/Spots
(server), or only references Actors/Spots another node hosts as a messaging target (client).
See the actor-relocation category for relocation-policy selection criteria.
RouteMesh Channel registration (configuration time)¶
Registers logical ChannelName membership within the same MeshNode.
play.channel("play.api").server()
.set_weight(100)
.add_handler_group("api");
play.channel("play.events").client();
Options. After channel(channel_name), call .client() or .server() exactly once.
.client() only creates the send path and has no modifiers. Commonly used modifiers of
.server() are as follows.
| Modifier | Default | Meaning |
|---|---|---|
.set_weight(int) |
100 (range 0..10000) |
The relative weight for this Server to be selected as a request/send target. 0 excludes it from selection |
.add_handler_group(group_name) |
None | Links a handler group registered with options.handlers().group(group_name) |
.add_send_handler<THandler, TMessage>(packet_name = {}) |
The packet name is determined from the message type | Registers a one-way handler directly on this channel |
.add_request_handler<THandler, TRequest, TReply>(packet_name = {}) |
The packet name is determined from the message type | Registers a request/reply handler directly on this channel |
Completion result. Registers synchronously with no return value. If a handler with the same
packet name is exposed on the same channel twice, app.run(...) fails at startup configuration
before it receives any message.
When to use. Use .server() when registering a handler that send_to_channel/
request_to_channel (messaging-execution category) will receive. If this MeshNode only calls
another node's Server and places no handler of its own, register only .client(). Use
add_client_server_channel instead if communication must cross different processes.
add_client_server_channel (configuration time)¶
Registers an independent ClientServer Channel unrelated to RouteMesh.
options.add_client_server_channel("payments.api").server()
.listen(6001)
.set_weight(100)
.add_handler_group("payments");
options.add_client_server_channel("payments.api").client()
.connect("payments-1:6001");
Options. Commonly used modifiers are as follows.
| Modifier | Default | Meaning |
|---|---|---|
.server().listen(port = 0) |
0 (automatic bind) |
This Server's receiving port |
.server().set_bind_host(string) / .set_advertise_host(string) |
Root configure_network() default |
The bind/advertise host that applies only to this Server |
.server().set_weight(int) / .add_send_handler/.add_request_handler |
Same as RouteMesh Channel Server | Weight and handler registration |
.client().connect(endpoint) |
manual | Connects to a specific Server manually. Omitting it finds the target via automatic discovery |
Completion result. Registers synchronously with no return value. A Client/Server using automatic discovery without a Location Store registration surfaces as a configuration error in startup validation.
When to use. Use this for request/reply or one-way messaging between independent services that are not RouteMesh members. Between nodes in the same RouteMesh, use RouteMesh Channel registration instead.
add_fanout_channel (configuration time)¶
Registers a channel dedicated to classic fanout. It is the target publisher_t::publish
(messaging-execution category) publishes to.
options.add_fanout_channel("lobby.events")
.enable_publisher(7001)
.add_handler_group("events");
// automatic subscriber — automatically discovers publishers of the same ChannelName from the
// location store.
options.add_fanout_channel("lobby.events")
.enable_subscriber();
// manual subscriber — uses only the specified endpoints. Combining it with enable_subscriber()
// fails startup.
options.add_fanout_channel("lobby.events")
.connect("lobby-1:7001");
Options. Commonly used modifiers are as follows.
| Modifier | Default | Meaning |
|---|---|---|
.enable_publisher(endpoint) / .enable_publisher(port = 0) |
None | Registers this channel's publisher role and receiving endpoint |
.set_bind_host(string) / .set_advertise_host(string) / .set_routing_id(routing_id) / .set_automatic_routing_id_prefix(prefix) |
Root default, or issued by the Framework | The bind/advertise host and RID that apply only to the publisher |
.enable_subscriber() |
— | automatic subscriber. Finds every valid publisher of the same ChannelName from the Location Store |
.connect(endpoint) |
— | manual subscriber. Uses only the specified endpoint |
.subscriber_connections() |
— | Returns a runtime handle (connect/disconnect/list_connections) over the set of manual subscriber endpoints |
.add_handler_group(group_name) |
None | Links a typed event handler group |
Completion result. Registers synchronously with no return value. Configuring both automatic subscriber and manual subscriber on the same fanout channel surfaces as a startup failure.
When to use. Use this when creating a new observation/notification channel where the publisher need not know its subscribers. If a reply is needed, use RouteMesh Channel or ClientServer Channel registration instead.
add_stream_node (configuration time)¶
Registers a listener that accepts external STREAM connections.
options.add_stream_node("public-gateway")
.bind(9001)
.enable_actor_dispatch()
.register_session<game_session_t>();
Options. Commonly used modifiers are as follows.
| Modifier | Default | Meaning |
|---|---|---|
.bind(endpoint) / .bind(port = 0) |
0 (automatic bind) |
This STREAM listener's receiving port |
.set_bind_host(string) / .set_advertise_host(string) |
Root configure_network() default |
The bind/advertise host that applies only to this listener |
.set_tls_server(cert_path, key_path, require_client_certificate = false) |
No TLS | TLS server certificate/key, and whether to require mutual authentication |
.enable_actor_dispatch() |
Disabled | Dispatches an incoming message to a bound Actor via global ActorId lookup. Calling it twice on the same builder fails startup |
.register_session<TSession>() |
None | Registers a Session type that inherits packet_stream_session_t. A stream node declares exactly one packet session |
.register_session(name) |
— | An overload dedicated to low-level configuration that must name the Session explicitly |
Completion result. Registers synchronously with no return value. A TLS configuration error or
a duplicate register_session call surfaces as a configuration error in startup validation.
When to use. Use this to open a gateway that external clients connect to directly over the STREAM protocol. See the stream-session category for the exact Session/Actor wiring rules.
Manual peer connections (configuration time and runtime)¶
Connects to a specific endpoint manually, without automatic discovery. Called via
mesh_node_builder_t::peer_connections().
play.peer_connections().connect("play-node-2:5501");
std::vector<zlink::framework::mesh_peer_connection_t> connections =
play.peer_connections().list_connections();
Options. This call carries the following modifiers.
| Modifier | Default | Meaning |
|---|---|---|
.connect(endpoint) |
No expected RID | The admission handshake determines the remote identity |
.connect(expected_routing_id, endpoint) |
— | Does not admit if the handshake identity differs |
.disconnect(endpoint) |
— | Releases a registered connection |
.list_connections() |
— | Queries the currently registered connection list |
Completion result. Registers/releases synchronously with no return value. If both MeshNodes
are Object Clients and neither has RouteMesh Channel Server membership, this connection intent
stays in the list but never becomes a ready peer. If either side has any Channel Server
membership, including one with weight 0, ordinary peer admission/liveness rules apply.
When to use. Use this to configure RouteMesh with a fixed peer list, without automatic discovery (a Location Store).
use_filter<TFilter> (configuration time)¶
Inserts common logic (authentication, logging, etc.) in front of every handler dispatch.
options.use_filter<authentication_filter_t>();
class authentication_filter_t {
public:
zlink::framework::task_t<void> invoke(
const zlink::framework::handler_filter_context_t &context,
zlink::framework::handler_next_t next) {
if (!is_authenticated(context)) {
co_return; // not calling next() ends the request as rejected
}
co_await next();
}
};
Options. This call carries the following modifiers.
| Modifier | Default | Meaning |
|---|---|---|
.use_filter<TFilter>() |
None (runs in registration order) | Adds a filter type to the dispatch chain |
Completion result. Registers synchronously with no return value. Calling next() runs the
remaining filters and the handler. Not calling next() in a request ends it as rejected, and
calling next() twice fails with invalid_operation. context.dispatch_kind distinguishes
node_direct_send/node_direct_request/channel_send/channel_request/classic_fanout —
channel_send/channel_request include both RouteMesh and ClientServer.
When to use. Use this when common preprocessing/validation must repeat across individual handlers. A filter does not construct the business reply itself — it only expresses rejection, and the handler does the rest. Does not apply to Spot/Actor handlers or STREAM sessions.
Other host-wide options (configuration time)¶
Configuration that ends with a single simple value, which zlink_framework_options_t provides.
options.services().add_singleton<order_repository_t>();
options.configure_network().set_bind_host("0.0.0.0");
auto &dispatch = options.configure_dispatch();
dispatch
.set_core_hwm_profile(zlink::framework::core_hwm_profile_t::low_latency)
.set_application_job_queue_profile(
zlink::framework::application_job_queue_profile_t::low_latency);
options.metadata()
.allow_session_to_actor("trace-id")
.allow_actor_to_session("server-region");
options.configure_stream_compression().use_lz4();
options.set_application_version(2);
options.handler_coroutine_workers(8);
Options. Commonly used entries are as follows.
| Modifier | Default | Meaning |
|---|---|---|
.services() |
— | service_collection_t. Registers the service lifetime (add_singleton/add_scoped/add_transient/add_factory) of handlers and hosted components |
.metadata().allow_session_to_actor(key) / .allow_actor_to_session(key) |
Keys not specified are not forwarded | Adds a metadata key to forward across the STREAM session↔Actor relay to a direction-specific allowlist |
.configure_network() |
bind_host() is 127.0.0.1 |
The default bind/advertise host used unless an individual listen call overrides it |
.worker() |
worker_options_t default |
The bounded worker pool's minimum/maximum thread count, idle timeout, and queue cap (the pool RunCpuWorker/RunIoWorker use) |
.configure_dispatch() |
Framework dispatch/diagnostics defaults; both profiles are balanced; manual values are unset |
Configures dispatch/diagnostics and the Core HWM memory/budget/profile inputs plus the host-wide Application Job Queue profile or exact manual permit limit on dispatch_options_t |
.configure_stream_compression() |
No compression | The STREAM default compression codec (use_default()/use_lz4()/use(codec)/disable()) |
.set_max_pending(count) |
Framework default | The host-wide pending-queue cap |
.set_application_version(version) / .set_maintenance_wave(wave) |
0 / none (no exclusion) |
The deployment version and maintenance wave every local MeshNode publishes |
.set_default_request_timeout(timeout) |
Framework default | The host-wide default request timeout |
.handler_coroutine_workers(count) |
Framework default | The number of workers that run handler coroutines |
.codecs() |
Only JSON registered | options.codecs().use(extension). See the Codec registration entry in messaging-execution category |
Completion result. Most execute synchronously with no return value; .services()/
.configure_network()/.worker()/.configure_dispatch() return
the corresponding builder or options object to continue further configuration on. Exceeding a
value's range surfaces as a configuration error in startup validation.
Core owns its byte-budget calculation. The Framework does not divide that budget by connection
count. The Application Job Queue is a separate job-count limit; its manual range is
1..2,147,483,647, with omission selecting the Auto profile. See the
Core/Framework API contract.
When to use. Use this to adjust host-wide settings that end with a single simple value and do not belong to a dedicated category above (host lifecycle, topology registration, diagnostics).
Runtime weight query/change¶
Changes placement weight or channel weight without redeploying.
zlink::framework::route_mesh_runtime_options_t &placement =
route_mesh_runtime_options; // instance injected from DI
placement.placement_weight(50); // lowers the share of new Actor/Spot placement routed to this node
placement.channel("play.api").weight(0); // excludes this Channel Server from selection
Options. This entry point has two independent properties.
| Property | Default | Meaning |
|---|---|---|
route_mesh_runtime_options_t::placement_weight()/(value) |
The value at registration time | The node-level Actor/Spot placement weight |
route_mesh_runtime_options_t::channel(name).weight()/(value) |
The value at registration time | The ChannelName-level Server selection weight |
Completion result. A synchronous get/set. It applies immediately with no separate completion signal. Querying an unregistered ChannelName is a configuration error.
When to use. Use this to adjust placement or traffic share while running. Transport options,
including max_message_size, cannot be changed through this path — configure them only before
startup.
Topology status query/observation¶
Checks the operational status of each of RouteMesh/ClientServer/Fanout. The three runtimes provide
the same shape (one snapshot query, streaming observation with observe).
zlink::framework::mesh_node_snapshot_t status = route_mesh_runtime.snapshot("play");
bool can_place_new_objects = status.is_ready && status.placement.is_available;
auto observation = route_mesh_runtime.observe(
"play",
/*capacity=*/64,
[](const auto &observed) {
// check observed.status.channels, observed.status.peers
});
Options. The correspondence among the three runtimes is as follows.
| Runtime | Target | Returned snapshot |
|---|---|---|
route_mesh_runtime_t |
MeshName | mesh_node_snapshot_t (includes channels, peers, placement) |
client_server_runtime_t |
ChannelName | client_server_channel_snapshot_t (includes servers) |
fanout_runtime_t |
ChannelName | fanout_channel_snapshot_t (includes publishers) |
Completion result. snapshot(...) is a synchronous call that returns a value immediately.
observe(...) delivers observed_status_t<TStatus> to the callback, and the loss field
(coalesced_count/discarded_terminal_count) tells you whether observations were lost. Ending
the observation calls close() on the returned observation handle (or destroys the
unique_ptr). Querying a fanout that only registered a manual ChannelName via
fanout_runtime_t is a configuration error.
When to use. Use this to judge a specific MeshName/ChannelName's availability, or to narrow
the scope of a failure. If host-wide status is needed, use is_ready (host-lifecycle category)
or status() (observability-diagnostics category).
See the Channel messaging exact interface and the Configuration and host exact interface (Korean-only) for the full rationale.