Router · Messaging Transport over Valkey
Status
- Implemented in Sprint 8100.0011.0003.
- Core components: Gateway DI wiring, GatewayHostedService integration, GatewayTransportClient dispatch.
- Last updated: 2026-07-18 (UTC).
Purpose
Enable Gateway ↔ microservice Router traffic over an offline-friendly, Redis-compatible transport (Valkey) by using the existing Messaging transport layer:
- Router transport:
StellaOps.Router.Transport.Messaging - Messaging backend:
StellaOps.Messaging.Transport.Valkey
This supports environments where direct TCP/TLS microservice connections are undesirable, and where an internal message bus is the preferred control plane.
Implementation Summary
Libraries
StellaOps.Router.Transport.Messaging— Router transport layer over messagingStellaOps.Messaging.Transport.Valkey— Valkey/Redis backend for messagingStellaOps.Messaging— Core messaging abstractions and DI
Gateway Integration
- Program.cs — Conditional registration of
ValkeyTransportPluginandAddMessagingTransportServer() - GatewayOptions.cs —
GatewayMessagingTransportOptionsfor Valkey connection and queue configuration - GatewayHostedService.cs — Start/stop
MessagingTransportServer, subscribe to events - GatewayTransportClient.cs — Dispatch to
TransportType.Messagingconnections
High-Level Flow
- Microservice connects via messaging transport:
- publishes a slim
HELLOmessage with instance identity to the gateway control queue
- publishes a slim
- Gateway processes HELLO:
- registers the connection identity and requests endpoint metadata replay when needed
- Microservice answers the replay request:
- publishes an
EndpointsUpdateframe with endpoints, schemas, and OpenAPI metadata
- publishes an
- Gateway applies the metadata replay:
- updates routing state, effective claims, and aggregated OpenAPI
- Gateway routes an HTTP request to a microservice:
- publishes a REQUEST message to the service request queue
- Microservice handles request:
- executes handler (or ASP.NET bridge) and publishes a RESPONSE message
- Gateway returns response to the client.
Cancellation uses a separate control path:
- A messaging client advertises
messaging.cancel-control.v1in HELLO and heartbeat frames only when a frame-envelope verifier is registered. Without a verifier it does not create a dedicated control consumer; the legacy request-queue path remains available. - When a frame-envelope signer is registered, the gateway creates and signs a target-specific payload for every live capable connection for that service; the signed payload binds the target service and connection ID. It also publishes one unchanged CANCEL to the legacy per-service request queue whenever the live fleet includes a legacy client or no authenticated control target is available.
- Each capable connection independently consumes
router:cancels:{service}:{connection}; a blocked request batch therefore cannot delay cancellation consumption. - The client verifies both the gateway frame envelope and its signed target binding before changing cancellation state. A bounded, expiring exact-correlation tombstone suppresses a request when its cancellation overtakes it, and makes legacy/control duplicate delivery idempotent.
Messaging-specific recovery behavior:
- Startup resync: the gateway sends
ResyncRequestimmediately after a slimHELLO. - Administrative resync:
POST /api/v1/gateway/administration/router/resyncrequiresrouter:routing:write. OmitconnectionIdfor an intentional whole-fleet replay or provide one non-blank exact connection ID for a targeted replay; blank and stale selectors fail without broadening the request. - Gateway-state miss: if a heartbeat arrives for an unknown messaging connection, the gateway seeds minimal state from the heartbeat identity and requests replay instead of waiting for a reconnect.
- Crash/reconnect replacement: a new HELLO for the same service/version/instance/region supersedes the old logical connection immediately, removes it from cancellation fan-out, and invalidates its gateway routing cache entry even when no GOODBYE was delivered. A delayed heartbeat from the superseded connection cannot recreate that state; only a new HELLO may replace the current logical connection.
Queue Topology (Conceptual)
The Messaging transport uses a small set of queues (names are configurable):
- Gateway control queue: receives service-to-gateway HELLO / HEARTBEAT / ENDPOINTS_UPDATE / RESPONSE frames
- Per-service incoming queues: gateway publishes REQUEST / RESYNC_REQUEST and, while a live fleet still includes a legacy client or has no authenticated control target, one compatibility copy of CANCEL targeted to a service
- Per-connection cancellation control queues: gateway fans CANCEL to each live service connection that advertises
messaging.cancel-control.v1; the connection consumes this queue independently of request batches - Dead letter queues (optional): for messages that exceed retries/leases
Per-connection naming is intentional. Reusing one per-service or per-instance stream during overlapping reconnects would allow a competing consumer to take and reject a cancellation meant for the new logical connection. Each control stream is approximately trimmed to CancelControlApproximateMaxLength entries. Valkey stream/group metadata can remain after a connection disappears because the generic queue abstraction does not currently expose safe queue deletion; operators should monitor router:cancels:* cardinality and only remove lanes proven to belong to disconnected registrations after the local retention window. This residual metadata cleanup is an operational follow-up, not a reason to share a cancellation stream between replicas.
Configuration
Gateway YAML Configuration
Gateway:
Transports:
Messaging:
Enabled: true
ConnectionString: "valkey:6379"
Database: 0
RequestQueueTemplate: "router:requests:{service}"
CancelControlQueueTemplate: "router:cancels:{service}:{connection}"
ResponseQueueName: "router:responses"
ConsumerGroup: "router-gateway"
RequestTimeout: "30s"
LeaseDuration: "5m"
BatchSize: 10
MaxConcurrentRequests: 10
CancelTombstoneTtl: "5m"
MaxPendingCancellations: 10000
CancelControlApproximateMaxLength: 1000
HeartbeatInterval: "10s"
Router:
Frame:
Envelope:
ActiveKeyId: "primary"
Keys:
primary: "<shared-secret-provisioned-outside-source-control>"
ReplayWindow: "00:05:00"
The Router:Frame:Envelope section must be provisioned with compatible keys on the gateway and messaging microservices. A missing section intentionally leaves general legacy frame handling in bootstrap mode, but disables advertisement and consumption of messaging.cancel-control.v1; a gateway without a signer emits only the legacy CANCEL copy. A present but invalid section fails registration at startup.
Gateway DI Registration
// In Program.cs (already implemented)
if (bootstrapOptions.Transports.Messaging.Enabled)
{
builder.Services.AddMessagingTransport<ValkeyTransportPlugin>(
builder.Configuration, "Gateway:Transports:Messaging");
builder.Services.AddMessagingTransportServer();
}
Microservice
- Register router transports via plugin loading (no hard transport references in
StellaOps.Router.AspNet). - Use
AddRouterMicroservice(...)fromStellaOps.Router.AspNet; it resolves configured gateway transport types throughRouterTransportPluginLoader. - For messaging mode, the
StellaOps.Router.Transport.Messagingplugin registers:- backend messaging plugin loading (
AddMessagingPlugins(...), env/config keytransport=valkey) - Router messaging transport client (
AddMessagingTransportClient)
- backend messaging plugin loading (
- Ensure the following plugin DLLs are available either as service dependencies or under configured plugin directories:
StellaOps.Router.Transport.Messaging.dllStellaOps.Messaging.Transport.Valkey.dll
Operational Semantics (Draft)
- At-least-once delivery: message queues and leases imply retries are possible; handlers should be idempotent where feasible.
- Lease timeouts: the per-message guard uses
RequestTimeout; it may stop awaiting a handler that ignores cancellation.MaxConcurrentRequestsremains held until the underlying handler really exits, so abandoned work cannot escape the configured concurrency bound. Long-running tasks should respond with 202 + job id rather than blocking. - Cancellation compatibility: protected gateways send separately signed, target-bound copies to capable per-connection lanes. A mixed or legacy-only fleet also receives one unchanged compatibility CANCEL on the legacy service queue; an all-healthy-capable fleet omits that redundant copy. Duplicate control/legacy delivery is exact-correlation idempotent.
- Cancellation ordering: independent controls may overtake requests.
CancelTombstoneTtlmust be at leastRequestTimeout;MaxPendingCancellationsbounds memory retained by one client. - Determinism: message ordering may vary; Router must not depend on arrival order for correctness (only for freshness/telemetry).
- Push-first with recovery fallback: Valkey Pub/Sub notifications wake consumers immediately when possible. If notifications silently stop, the queue layer still wakes via timeout fallback, connection-restored hooks, and randomized proactive re-subscription so requests and resync control frames do not wedge forever.
- Queue fallback cost: every wake can perform
XAUTOCLAIMplusXREADGROUPchecks before sleeping again. That traffic is expected resilience overhead, but it is materially smaller than replaying the full endpoint catalog on every heartbeat interval.
Security Notes
- Messaging transport is internal. External identity must still be enforced at the Gateway.
- REQUEST, CANCEL, and RESYNC_REQUEST frames are authenticated with the frame-envelope verifier before dispatch or state mutation. Dedicated CANCEL frames also require a signed service/connection target binding, preventing a valid sealed frame from being copied to another lane within the replay window. Rejected controls are dead-lettered with an
envelope-invalid:*,invalid-cancel-control-target, orinvalid-cancel-control-target-bindingreason. - The Gateway must not trust client-supplied identity headers; it must overwrite reserved headers before dispatch.
Implementation Status
Completed (Sprint 8100.0011.0003)
- ✅ Wire Messaging transport into Gateway:
- start/stop
MessagingTransportServerinGatewayHostedService - subscribe to
OnHelloReceived,OnHeartbeatReceived,OnEndpointsUpdated,OnResponseReceived,OnConnectionClosedevents - reuse routing state updates and claims store updates
- start/stop
- ✅ Extend Gateway transport client to support
TransportType.Messagingfor dispatch. - ✅ Add config options (
GatewayMessagingTransportOptions) and DI mappings. - ✅ Switch messaging registration from periodic full HELLO replay to explicit
ResyncRequest/EndpointsUpdatecontrol frames.
Completed (Sprint 20260718_007)
- Authenticated per-connection cancellation control consumer independent of request batches.
- Capability-gated replica fan-out with legacy per-service dual-write for rolling compatibility.
- Bounded cancel-before-request tombstones and full-handler-lifetime concurrency enforcement.
Remaining Work
- Add deployment examples (compose/helm) for Valkey transport.
- Add integration tests using ValkeyFixture:
- microservice HELLO registration via messaging
- request dispatch + response return
- Validate streaming support (or document as out-of-scope).
