Repository navigation
RoutingGrain pool saturates fleet-wide under ThreadPool starvation; pre-subscribe wait dominates slot occupancy on starved silos #5291
Description
What is failing
RoutingGrain (MeshWeaver.Hosting.Orleans) saturated its 64-slot routing pool across all five portal pods for ~6 minutes: 68 threshold crossings between 12:53:55Z and 12:59:44Z. No delivery is actually stuck — channel queues are empty, deepest per-channel queue is 0, and the oldest in-flight leg is milliseconds old (one at 3.7 s) — so the condition is throughput exhaustion, not a head-of-line block or a leaked slot.
Probable cause — moderate confidence
The gauge pair is the discriminator the alert itself defines (core issue #5018): on pod memex-portal-…-sddb5 the crossings read waiting for a pool slot 57–61 with subscribing ≤ 1. That is the pre-subscribe wait — legs holding a pool slot while queued for a ThreadPool thread, i.e. ThreadPool starvation on that silo, exactly the "CPU-starved silo raises this with nothing stuck" case the message documents. On the 7cc85f47c-* pods the same threshold is crossed with all gauges near zero, which is plain load the silos absorb. So the probable cause is thread starvation on one silo driving fleet-wide saturation noise; I cannot fully separate a genuinely undersized routing pool (64) from a transient CPU/thread shortfall from this evidence alone. Not a slot leak: every oldest-leg age is young, so legs are terminating and slots return.
Impact
Short user-visible delays in stream dispatch during the window (one leg waited 3.7 s); the episodes drain on their own. 68 critical alerts in 6 minutes across the whole fleet also pollutes the incident stream — related to core issue #5118's stated worry, and to the fingerprint-splitting flood described in rbuergi/Feedback/route-backpressure-fingerprint-splits-20260921-2236 (this incident folded cleanly, variants=1, so the masking gap did not fire here).
Where to look
src/MeshWeaver.Hosting.Orleans/RoutingGrain.cs, methodReportSaturation— the log site and the pool/threshold configuration.- The slot-acquisition path: the wait for a ThreadPool thread between slot acquisition and the subscribe prologue (the unbounded pre-timeout wait named in the alert text).
- Whether the reporting threshold (64) should scale with pool size / silo CPU budget — core issue RoutingGrain back-pressure, thread-shortage shape (waiting-for-slot high, subscribing ≤1): compile-off-ThreadPool fix #5327/#5375 live on memex-cloud, pending on memex; post-fix residual under verification #5118 proposes exactly this.
- Background: Routing back-pressure alerts now name what they actually saw, ActionBlockWedgePrevention.
- Known related core issues: RoutingGrain dispatch slots saturate with legs head-of-line blocked behind one shared-cache destination while the second pod starves on load #5009 (head-of-line, fixed, awaiting verification), RoutingGrain dispatch pool saturates repeatedly on portal silos; slots held through unbounded ThreadPool wait and episode-stamp splitting floods incidents #5018 (pool gauges), RoutingGrain back-pressure, thread-shortage shape (waiting-for-slot high, subscribing ≤1): compile-off-ThreadPool fix #5327/#5375 live on memex-cloud, pending on memex; post-fix residual under verification #5118 (threshold/severity). Not a duplicate of any of those: those concern the alert semantics; this is a live saturation episode.
Suggested severity: M — primary-path degradation that self-drained within 6 minutes, with a clear capacity/config remedy (scale the routing pool or fix the thread shortage on the starved silo).
Evidence
| Fingerprint | 6c32ab10d8637061 |
| Category | MeshWeaver.Hosting.Orleans.RoutingGrain |
| Severity | Critical |
| Namespace | memex-cloud |
| Pods | memex-portal-deployment-5c89c7f5f9-sddb5, memex-portal-deployment-7cc85f47c-bhfjm, memex-portal-deployment-7cc85f47c-nm2qp, memex-portal-deployment-7cc85f47c-ck7f4, memex-portal-deployment-7cc85f47c-9j7hd |
| Occurrences | 68 |
| First seen | 2026-09-22 12:53:55Z |
| Last seen | 2026-09-22 12:59:44Z |
| 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 12:59:30Z memex-portal-deployment-5c89c7f5f9-sddb5 crit: MeshWeaver.Hosting.Orleans.RoutingGrain[0]
[ROUTE] Routing back-pressure [06cd449e#6 started 2026-09-22T12:59:30.4966939Z]: 64 route dispatches in flight (reporting threshold 64); ordered channels queued 4 over 1 stream destination(s), deepest per-channel queue 0, routing pool subscribing 1, waiting for a pool slot 57; oldest leg in flight 6 ms — dispatch → FractalStars/_Entitlements/iser.steinmetz (delivery 0R5HIeDgLUCG5Qqg1MtiIw). Latest dispatch target thomager12/_Install/HomeAssistant — 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 CROS…[truncated]
2026-09-22 12:59:36Z memex-portal-deployment-7cc85f47c-9j7hd crit: MeshWeaver.Hosting.Orleans.RoutingGrain[0]
[ROUTE] Routing back-pressure [90a873ea#5 started 2026-09-22T12:59:36.4206697Z]: 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 0; oldest leg in flight 18 ms — dispatch → DoublePendulum/Pendulum (delivery 6eJvN7Fpmk-9jijoEqHP1w). Latest dispatch target MyAi/Panel — 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 CROSSING, so like the in-flight count it is p…[truncated]
2026-09-22 12:59:37Z memex-portal-deployment-5c89c7f5f9-sddb5 crit: MeshWeaver.Hosting.Orleans.RoutingGrain[0]
[ROUTE] Routing back-pressure [430ce9b7#1 started 2026-09-22T12:59:37.5516220Z]: 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 61; oldest leg in flight 3671 ms — dispatch → nerijus.vasiliauskas/_Install/EntityViews (delivery HUvNXHSn_UmL7p7rtQdlKA). Latest dispatch target AppleMaps/_Access/roland.buergi_Access — 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 …[truncated]
2026-09-22 12:59:41Z memex-portal-deployment-7cc85f47c-9j7hd crit: MeshWeaver.Hosting.Orleans.RoutingGrain[0]
[ROUTE] Routing back-pressure [90a873ea#6 started 2026-09-22T12:59:41.4128363Z]: 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 1, waiting for a pool slot 0; oldest leg in flight 16 ms — dispatch → HomeAssistant/Home (delivery IdDpQb1wkk2Umy5MdQLrbw). Latest dispatch target ICloud/Calendar — 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 CROSSING, so like the in-flight count it is p…[truncated]
2026-09-22 12:59:44Z memex-portal-deployment-5c89c7f5f9-sddb5 crit: MeshWeaver.Hosting.Orleans.RoutingGrain[0]
[ROUTE] Routing back-pressure [14d3a580#1 started 2026-09-22T12:59:44.0877848Z]: 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 1, waiting for a pool slot 61; oldest leg in flight 7 ms — dispatch → Anthropic (delivery zyf7L0fIQkOIqCyQgkVv3A). Latest dispatch target FractalStars/_Access/rene.hug_Access — 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 CROSSING, so like the in-flight c…[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/6c32ab10d8637061. Recurrences are folded into this issue rather than opening new ones.
Activity
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.
rbuergi commentedon Sep 22, 2026
Why this is its own issue, and what stops the next one — posted on each RoutingGrain back-pressure incident issue filed 2026-09-22; not a disposition of this one.
This incident's fingerprint was split from its siblings by two values the incident normaliser did not mask — the delivery id and a single-segment destination in the line's
oldest leg in flight … — dispatch → <address> (delivery <id>)field — not by the episode stamp, which was already masked. Fixed in Systemorph/MeshWeaver.Plugins#2286 (merged 2026-09-22T21:46Z asb922f070fc): over the 146 back-pressure samples quoted in the open core issues (43 filed fingerprints), the identity now yields 3 (one per leg kind, plus the pre-#5151 wording). It reaches production with the portal roll; existing incidents re-address onto their successors throughLogIncidentCorpusMigration.What the crossing itself means is a separate question and is not answered by the fix above. A line reading high
waiting for a pool slotwithrouting pool subscribing 0is legs the ThreadPool had not yet run (the pool's cap is 256 and no permit is held — see #5261 (comment)); that starvation root is being investigated separately (family: #5018, #4886). A line with a non-zerodeepest per-channel queueon acache/…destination and an old oldest leg is #5009's falsifier and should be raised there.Disposition stays with triage: a symptom gauge can invert the causal arrow, so this issue's own sample may carry evidence its siblings do not.