Repository navigation
RoutingGrain routing pool exhausted by slow dispatch legs — 51 of 64 in-flight dispatches waiting on a pool slot, oldest leg 26s #5261
Description
What is failing. The RoutingGrain on the memex portal silo hit its routing back-pressure threshold: 64 route dispatches in flight, of which 51 were waiting for a routing pool slot while only ~13 slots existed, every ordered channel was at depth 0 (no head-of-line queueing), and the oldest leg had been in flight ~26 seconds. Message delivery on that pod was stalled behind dispatch legs that were dispatched but not terminating.
Probable cause — medium confidence. The gauges on the line point at slow leg termination, not queue build-up: channel queues are all empty and subscribing is 0, so the 51 waiters are blocked purely on routing-pool slots held by legs that have not come back. A 26-second-old leg is far above normal routing latency and consistent with downstream delivery (pod-hub I/O) that is slow or hung — the same family as core issues #5009 (head-of-line, fixed in PR 5151, awaiting production verification) and #5018 (load verdict). It is also consistent with the line's own documented failure mode of a CPU/thread-starved silo holding slots during the unbounded pre-timeout wait. I cannot distinguish slow-I/O load from a slot leak from this single line — the discriminator (a clear line or a higher episode stamp for activation 1f8bc242#3) was never logged. If neither ever follows, the verdict shifts to a leaked slot.
Impact. A single occurrence on a single pod (memex-portal-deployment-5c99d79997-xs87x) at 2026-09-22T17:45Z, so this is not an outage — but while the episode lasted, all stream routing on that pod was delayed behind the exhausted pool. This log site is a known ticket-flood family (feedback node rbuergi/Feedback/route-backpressure-fingerprint-splits-20260921-2236, core #5118): dozens of sibling Critical incidents exist in Admin/_LogIncident for the same condition, so before doing new work, consolidate this with the existing routing-saturation issues (#5009, #5018, #5118) — it may simply be a production recurrence of #5009 pending its verification.
Where to look. src/MeshWeaver.Hosting.Orleans/RoutingGrain.cs, method ReportSaturation (the log site) and the routing-pool slot acquire/release path around dispatch legs; check whether activation 1f8bc242 ever logged a higher episode or a drain after 17:45:19Z, and correlate pod-hub delivery latency on that pod at that time.
Evidence
| Fingerprint | 98112c599c16b3ae |
| Category | MeshWeaver.Hosting.Orleans.RoutingGrain |
| Severity | Critical |
| Namespace | memex |
| Pods | memex-portal-deployment-5c99d79997-xs87x |
| Occurrences | 1 |
| First seen | 2026-09-22 17:45:19Z |
| Last seen | 2026-09-22 17:45:19Z |
| Routing | not determined — no configured route matches the category MeshWeaver.Hosting.Orleans.RoutingGrain. This repository is the configured fallback, not a finding about who owns the fault; the category names the LOGGER, which may not be the subject. |
Recent log lines
2026-09-22 17:45:19Z memex-portal-deployment-5c99d79997-xs87x crit: MeshWeaver.Hosting.Orleans.RoutingGrain[0]
[ROUTE] Routing back-pressure [1f8bc242#3 started 2026-09-22T17:45:19.1437726Z]: 64 route dispatches in flight (reporting threshold 64); ordered channels queued 0 over 0 stream destination(s), deepest per-channel queue 0, routing pool subscribing 0, waiting for a pool slot 51; oldest leg in flight 26372 ms — dispatch → Ops/Logs/memex-cloud-1790087369057460175-4645f8-v7q45 (delivery nwzA1tz0skCOOmSyJTsSbQ). Latest dispatch target UWDeepfield/LossEvent — the address that happened to cross the threshold, NOT a diagnosis. A slot is held from dispatch until the leg terminates, INCLUDING the unbounded wait for a ThreadPool thread before the leg's own timeouts start, so a CPU-starved silo raises this with nothing stuck. A CHANNEL is (destination, stream), so a deepest queue of 1 or more means legs of the SAME stream are blocked behind one another (head-of-line within one channel); 0 means nothing is waiting on anything, so read it as load. Many channels over FEW destinations is a multiplexer hub draining in parallel, which is load too — before issue #5009 those legs shared one channel and read as head-of-line. 🚨 THE TWO POOL GAUGES ANSWER DIFFERENT QUESTIONS AND ONLY ONE SEES THE WAIT NAMED ABOVE — issue #5018. 'subscribing' counts legs inside their SUBSCRIBE PROLOGUE only, so a leg still waiting for a ThreadPool thread and a leg already past its subscribe BOTH read 0 there; 'waiting for a pool slot' is the gauge that sees the pre-subscribe wait. High while subscribing is below the pool's cap is a THREAD shortage; both near zero means the legs are past their subscribe and the wait is downstream I/O. 🚨 READ THE OLDEST LEG FIRST: it is the only load-vs-leak discriminator available from a SINGLE line — every leg young is load the silo is absorbing, one leg minutes old is a slot that never came back and the label names it — whereas the episode stamp below needs a SECOND line to say anything. 🚨 Deepest is sampled AT THE …[truncated]
Re-addressed by the current identity function: this incident inherited e4a97855ab595beb (Systemorph/MeshWeaver.Plugins#1796). Those nodes are superseded and will not fold, file or comment again.
Opened automatically from Admin/_LogIncident/98112c599c16b3ae. Recurrences are folded into this issue rather than opening new ones.
Activity
rbuergi commentedon Sep 22, 2026
#5261 is not a recurrence of #5009 — and it is #5009's first post-roll POSITIVE reading
#5261's body offers that it "may simply be a production recurrence of #5009". Its own log text refutes that, using the discriminator #5009 comment 3 fixed in advance: "a line still spelling
deepest per-destination queueis a PRE-FIX REPLICA, not a result" — #5151 renamed the field.ordered channels queued 0 over 0 stream destination(s), deepest per-channel queue 0. Incident 2026-09-22T17:45:19Z, podmemex-portal-deployment-5c99d79997-xs87x, namespacememex.per-destination. RoutingGrain dispatch slots saturate with legs head-of-line blocked behind one shared-cache destination while the second pod starves on load #5009 comment 22 records that an incident-store search forper-channel queue/ordered channels queuedfound no hit anywhere. RoutingGrain routing pool exhausted by slow dispatch legs — 51 of 64 in-flight dispatches waiting on a pool slot, oldest leg 26s #5261 is 1 of 1 carrying the post-fix spelling.So on that replica the head-of-line queueing is gone: 0 channels queued over 0 destinations, deepest 0. That is the positive signal #5151 has been missing, and it is the first one on file.
And in-flight still reached 64 anyway — for a different reason. #5261's own gauges:
waiting for a pool slot 51,subscribing 0, deepest 0, oldest leg 26,372 ms, that leg's targetOps/Logs/memex-cloud-…-v7q45. By the log line's own rubric ("High [waiting] while subscribing is below the pool's cap is a THREAD shortage") this reads as the unbounded pre-subscribe wait the slot is held across — not a too-coarse channel key.🚨 So deduping #5261 into #5009 would destroy the only post-roll positive sample on file. They are two defects that raise the same gauge, which is exactly the trap of reading a symptom gauge as an identity.
Not excluded, and worth the next session's first hour: a slot that is never decremented. Core #5281 (
f2f00c9db4, merged 18:35:15Z) fixed precisely the class "a pooled leaf holds a permit across work that cannot observe its token" — inOrleansRoutingService.cs, the same file family — so that shape was live today rather than speculative. It is not this fix: it touches the teardown unsubscribe leaf, not the routing dispatch leg or the pool's slot accounting, and it merged 50 minutes after the #5261 incident, so that pod cannot have carried it. The place to start is its new guard,test/MeshWeaver.Documentation.Test/PooledLeafObservesItsTokenGuard.cs— does its sweep cover the routing dispatch leg?What is NOT established here: none of this was read from code. The channel key's construction, the pool's cap, and whether any path fails to release a slot are all unverified — the above is the issue record plus #5261's own instrument reading, with no control run and no two-sided verification. #5037 and #2299 were not examined at all.
🤖 Generated with Claude Code
rbuergi commentedon Sep 22, 2026
Read against the code: the 51 waiters are ThreadPool work items that had not started, not legs waiting for a pool slot that was taken
Picking up the 19:06Z comment's open question ("the pool's cap, and whether any path fails to release a slot are all unverified"). Read from
mainatf2f00c9db4:1. The pool is not small. This body says "only ~13 slots existed". The Routing pool's cap is 256 (
IoPoolOptions.Routing,src/MeshWeaver.Mesh.Contract/Threading/IoPoolOptions.cs). Nothing in the line measures 13; that number is 64 − 51.2. What the two gauges count, in
IoPool.SubscribeThroughPoolCore:waiting for a pool slot=_waiting: incremented at SUBSCRIBE, decremented when the setup leaf gets past_gate.WaitAsync(or is cancelled/unsubscribed first — every exit path callsLeaveWaitOnce, exactly once, latch-guarded).routing pool subscribing=_inFlight: incremented only after the gate grants, decremented inReleaseGate()as soon assource.Subscribe(observer)returns.Observable.FromAsync(…).SubscribeOn(TaskPoolScheduler.Default)— it cannot reach the gate until the ThreadPool runs it.So
subscribing 0withwaiting 51and a 256-permit gate means no permit was held and 51 setup leaves had not yet been run by the ThreadPool. A leaked permit is excluded by the same reading: a leaked one would have to have been taken by a leaf that incremented_inFlight, andReleaseGateis in afinally. The routing-side slot (inFlightRoutes) is a counter only — nothing throttles, queues or refuses on it (ReportSaturation's own doc) — so it cannot be what the 51 are waiting for.Verdict: thread shortage on pod
xs87xat 17:45:19Z, exactly the rubric's "High while subscribing is below the pool's cap is a THREAD shortage". That root — what starves the ThreadPool — is being worked separately as the starvation investigation, not here.3. The 26 s oldest leg is a separate reading and is not a leak.
dispatch → Ops/Logs/memex-cloud-…-v7q45is a grain route (Dispatch, not the ordered stream branch) to aHosting/LogEntrynode — i.e. a node aLogsInstanceAction on the control instance had just landed. Such a leg is bounded byResolveTimeout(30 s) plus the grain call's own retries, so 26 s is inside its bounds; with the ThreadPool short it is also consistent with the leg itself waiting for a thread. Worth knowing when reading any future sample: aLogsaction writes a burst of fresh nodes whose first messages each activate a per-node hub, on the same silo the reading was taken from.4. The flood half. This incident's fingerprint split from its siblings not on the episode stamp but on the oldest-leg label's delivery id and single-segment destination, which the normaliser did not mask. Fixed in Systemorph/MeshWeaver.Plugins#2286 (measured over the 146 back-pressure samples in the open core issues: 43 filed fingerprints → 3).
Status: open, owner-side. Nothing in core's routing code is implicated by this sample. What would close it is the starvation root and a post-fix reading; the one read that attributes this particular episode is a
LogsInstanceAction (not mine to run):{ "requestedAction": "Logs", "deployment": "Deployments/memex", "query": "LocalSiloHealthMonitor", "sinceMinutes": 360 }— a thread-pool-delay / "has not been responsive" line on
memex-portal-deployment-5c99d79997-xs87xaround 17:44–17:46Z confirms the verdict above; its absence would leave the 51 unexplained and should be written back here.What I did NOT establish: I ran no
Logsread, have no dump of that pod, and did not find what blocked the threads. I did not check whether a second line (clear or higher episode) for activation1f8bc242followed.rbuergi commentedon Sep 22, 2026
Closing as a duplicate of #5118 — the fingerprint-split flood.
Why this is a duplicate. Every one of these comes from the same log site —
[ROUTE] Routing back-pressure, categoryMeshWeaver.Hosting.Orleans.RoutingGrain— split into one issue per grain activation. The 8-hex activation id survived normalization (a0e2984b→a0e2{n}b):HexBlobrequired 16+ chars, andNumber()then masked only the digit-run preceded by a digit, leaving a per-activation residue in the hash.Measured over 101 verbatim bursts drawn from 29 issues, covering 42 activation ids: the old parser yields 41 distinct fingerprints, the current parser yields 1.
The fix is already merged in MeshWeaver.Plugins — #2165 (
MixedHexId), #2167, #2168 (positional episode/target masking) and #2218 (the portal re-parses raw samples, instead of hashing text a stale watcher already masked).It is not live yet. The control instance is sealed on an older set because the Plugins Portal image rebuild workflow fails at its GitHub App token step —
actions: writeis not granted to the installation — so core's rebuild is never dispatched. Until that is granted, new split issues will keep arriving, and an issue whose activation is still alive may reopen here.Not closed by this. The underlying routing condition — cache-destination head-of-line blocking versus ThreadPool starvation — is a real defect and stays open in #5118. The normaliser itself is tracked in #5270 and #5271.