Ordered Route Channels — the FIFO Key Is (Destination, Stream)

The router has to preserve the order of some deliveries and is free to overlap the rest. Getting the boundary between those two sets wrong is not a performance opinion: too narrow and a mirror is corrupted, too wide and the busiest destination in the mesh gets one lane.

It was too wide, and that is the whole of this page.

What must be ordered, and why nothing re-sends it

Data sync is a delta protocol with a receive-side monotonicity guard. SynchronizationStream's update path discards any frame whose owner version is below the mirror's current version, and nothing ever re-sends it. Reorder two frames of one stream and the earlier one — possibly the one carrying a layout area's actual content — is dropped as stale forever: the subscriber keeps its "Building layout…" base frame, its wait dies on its own timeout, and nothing is logged above Debug on either side. Measured before the FIFO existed: one Orleans suite run produced 854 stream-routed deliveries, 46 destinations with out-of-order posts and 115 inverted pairs.

So the router claims the order on the routing grain's turn[StatelessWorker(1)], non-reentrant, one turn per silo, and therefore the last point at which arrival order is still authoritative — and drains it off the turn through OrderedRouteDispatcher, one in-flight leg per channel, in arrival order. See Controlled I/O Pooling for the pool the legs are subscribed through.

That guard operates per mirror, i.e. per synchronization stream. Two frames of two different streams have no ordering relationship at all.

The defect: the channel was the destination

The FIFO's key was the destination address. A stream-routed address is not one consumer, it is a multiplexer:

Address Fronts
cache/{meshId} one sync/{streamId} sub-hub per node the process has a mirror of — every node stream in the whole process
portal/{userId} one per layout area that viewer has open

DataExtensions.RouteStreamMessage receives every frame at the hub's own address and forwards it internally by StreamId, so the router never sees a per-stream address to key on. Keying on the address alone therefore put a whole process's data-sync traffic on one channel with one in-flight IPodHubGrain.Deliver grain call — a directed call carrying the full delivery, which Orleans' JsonCodec copies in both directions.

Where that destination is on another silo, the channel's service rate is one network round trip per frame. The arrival rate of a busy portal's change fan-out is not.

What production measured

memex-cloud, 2026-09-20, 19:07–20:34Z, both portal pods, 17 crossings of the 64-slot reporting threshold:

Pod In flight Destinations queued Deepest queue Target that crossed
…-s246c 64 1 62 cache/12xX8OXQIEmdwvf_ZZ8LjA
…-ndnxt 64 0 0 a different address every time

The same pair on 2026-09-19 (cache/YORFhwiyqEyTP9ESF-NUNg, then the same 12xX8… eleven minutes later) and again 2026-08-12. The episode stamps rose on both activations throughout, so every episode drained — no slot ever leaked, and no leg was ever stuck.

The asymmetry is the tell. cache/{meshId} is keyed by the mesh hub's id, which is per PROCESS, so each pod has its own cache address and its own PodHubGrain activation on its own silo. A pod's channel to its OWN cache hub is a local call and drains as fast as it fills — deepest 0. Its channel to the PEER pod's cache hub is a network round trip, and that is the one that stacked 62 legs. One pod reporting head-of-line blocking while the other reports load was never two faults; it was one fault seen from the two ends of one cross-silo channel.

Why the reading "a leg is not completing" was wrong

The log line's own rule — deepest ≥ 1 means legs are blocked behind a leg — was applied correctly and still produced the wrong conclusion, because it is silent about why the legs share a queue. They were 62 independent streams with no ordering relationship, waiting on each other because the channel key could not tell them apart. Nothing was slow; the lane was single.

That is also why the earlier work at this log site did not close it. The amplifiers were real and were fixed — a storage query per routed message, O(node-size) JSON patch construction on hub action blocks, a slot leak on a cancelled drain, and seven copies of every timed-out delivery (A Timed-Out Delivery Is Still Held by the Callee) — and every one of them lowered the arrival rate or the service time of a lane that was still one lane wide. A single-server queue whose arrival rate exceeds its service rate recurs at any offered load above that rate; reducing the load moves the threshold, it does not remove it.

The fix: the channel is (destination, payload identity)

The ordering domain is the stream, so the channel key is the stream. The router cannot read a stream id out of the payload — on the turn the payload is RawJson, and parsing it there is the one thing the turn must never do — but it does not have to: the identity is already on the envelope.

MessageDelivery.Package stamps IDiagnosticKeyed.DiagnosticKey onto the delivery's properties immediately before erasing the payload type, and for every StreamMessage that key is the stream id. Every mesh delivery reaches the router through Package, so RoutingGrain reads it with one dictionary lookup of a string — no cast, nothing parsed, nothing deserialized:

var orderingKey = DeliveryIdentity.Read(delivery);
orderedDispatcher.Enqueue(addressPath, orderingKey, leg, onLegCompleted);

The key is a (string, string?) pair, never the two concatenated: mesh addresses contain both / and ~, so any single delimiter makes ("cache/a", "b/c") and ("cache/a/b", "c") the same channel — which would silently re-serialise unrelated streams, or let two frames of one stream overtake each other.

Two deltas it does carry

Named here rather than left to be discovered:

Control traffic is no longer ordered against data frames. A message with no identity — a DisposeRequest, a heartbeat, a correlated response — stays on the destination-wide channel, so it can now overtake or be overtaken by a data frame for a stream on the same destination. Nothing relies on that pairing. The entire data-sync protocol is keyed: DataChangedEvent, SubscribeRequest, UnsubscribeRequest, StreamErrorEvent, StreamEndedEvent and every IUserAction carry the stream id, so all of them keep their stream's lane. Responses correlate by request id, not by order. And a frame racing its destination's teardown was already a race the router never decided — the one case where a drop at teardown matters is a user action, and that is protected by its UserActionAccepted receipt and the sub-hub registration grace, never by router ordering (see Data Sync and CRDT).

The entry count is bounded by in-flight legs, not by in-flight destinations. An entry exists only while a channel holds a leg, and every held leg already holds a route slot — so the bound is the same quantity the saturation report prints, at tens of bytes per entry against a whole delivery each. The previous key made the entry count smaller but made the queue behind one entry the unbounded quantity, which is what 62 was.

This raises no bound

Nothing was widened. The legs a silo may have in flight are still capped by the routing IIoPool (256, unchanged); the receiving PodHubGrain is still non-reentrant and still serialises its own hand-offs; SaturationThreshold is still 64 and still gates a log line and nothing else. What changed is only that legs with no ordering relationship no longer wait on each other — the pipelining a correct channel key allows and an over-coarse one forbade. PodHubGrain.Deliver is a synchronous hand-off into the target hub's own queue, so what overlaps is the network, not any work.

One identity, two readers

DeliveryIdentity.Read is shared with MessageStormBreaker, which keys its per-second rate counters on (sender, target, type, identity). Both layers are asking the same question — which messages are about one thing? — and both fail the same way when they cannot answer it: the breaker folds a legitimate fan-out into one bucket and drops it, the router folds independent streams into one channel and queues them. Resolving it in one place is what keeps "one rate bucket" and "one ordered channel" from drifting apart.

What the log line now says

ordered channels queued 64 over 1 stream destination(s), deepest per-channel queue 0

A channel is (destination, stream), so a non-zero Deepest is a much stronger statement than it was: frames of the same stream are stacking up, which really is one destination not keeping up with one producer. The former reading — many unrelated streams sharing a lane — now reports as many channels over few destinations, which is load. Both counts are printed because their ratio is the remaining discriminator.

The rest of the line is unchanged, including the parts that matter most: a slot is held across the unbounded wait for a ThreadPool thread before the leg's own timeouts start, so a CPU-starved silo still raises this with nothing stuck; and the episode stamp, not the depth, is what says whether an episode drained.

🚨 The load shape at this site is a separate defect and is not closed by this. deepest 0 with many channels is dispatch volume or ThreadPool starvation on the silo — a slot charged for a wait that has no bound, where load and a leak are not distinguishable from the count. Nothing here narrows it.

Controls

Test Side it holds
OrderedRouteDispatcherTest.IndependentStreamsOnOneDestination_NeverWaitOnEachOther the blocking stops: two streams, one destination, first leg never terminates, both still subscribed. Times out on the destination-only key
RoutingBackpressureShapeTest.OneMultiplexerDestination_ManyStreams_IsBreadthNotHeadOfLine the production shape: 64 legs, 1 destination, 64 streams ⇒ 64 channels / 1 destination / deepest 0. Reports deepest 63 on the destination-only key
OrderedRouteDispatcherTest.FramesOfOneStream_OnAMultiplexer_StillSubscribeInOrderOneAtATime the invariant the narrowing could have broken: one stream still serialised, still in arrival order, on a destination now serving many channels
OrderedRouteDispatcherTest.ChannelKeyIsAPair_NotAConcatenation the key is a pair; no delimiter collision
DeliveryIdentitySurvivesPackagingTest the seam the whole fix rests on — the identity is stamped by Package, readable off the envelope after the type is gone and after a JSON hop, and null for a payload that exposes none
RoutingBackpressureShapeTest.SameInFlightCount_MeansBothBusyAndBlocked_… the in-flight count alone still cannot tell breadth from blocking
OrderedRouteDispatcherDrainRecursionTest a 32-deep backlog behind one head leg still drains without recursing its stack — now built on one shared stream id, because a backlog that deep only exists within a channel

Where it lives

The FIFO and its channel key src/MeshWeaver.Hosting.Orleans/OrderedRouteDispatcher.cs
The read on the turn, and the report src/MeshWeaver.Hosting.Orleans/RoutingGrain.cs
The shared identity reader src/MeshWeaver.Messaging.Contract/DeliveryIdentity.cs
Where the identity is stamped MessageDelivery.Package
The other reader src/MeshWeaver.Messaging.Hub/MessageStormBreaker.cs
Why the FIFO cannot simply be removed Pod-Hub Delivery — the Transport Swap and its Roll Plan
The guard the order protects Data Sync and CRDT
How to READ the report this renamed, and the other two verdicts at that log site Reading a Routing Saturation Report