Skip to content

fix(routing): back-pressure is Critical only for a leg past its own bounds — the oldest leg decides the level - #5657

Merged
meshweaver-cloud[bot] merged 2 commits into
mainfrom
fix/log-leaks-routing-level
Sep 24, 2026
Merged

meshweaver-cloud[bot] merged 2 commits into
mainfrom
fix/log-leaks-routing-level

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Why tickets kept filing after #5322

#5322 levelled the [ROUTE] Routing back-pressure crossing by queue depth: deepest >= 1 stayed Critical. Every Critical quoted in the tickets filed after it shipped had a deep queue and a young oldest leg:

deepest oldest leg in flight
63 11 ms
62 343 ms
57 9 ms
54 81 ms
43 14 785 ms
42 1 715 ms
1 7 ms · 3 068 ms

(extracted verbatim from #5595, #5616, #5617, #5618, #5622, #5631, #5632, #5638 bodies and recurrence comments). Depth is sampled at the crossing, where it is largely an artefact of the threshold: 63 frames of one stream enqueued within 11 ms is a producer burst, not a stuck stream. Every one of those legs was inside its own bounds.

The rule now (permanent level decision, AGENTS.md → log levels)

  • Critical iff the oldest in-flight leg is older than LegSelfBound = ResolveTimeout + SubscriberProbeTimeout + StreamPostTimeout (100 s, the longest leg composition). A leg past its own timeouts cannot be a slow destination — it is a leaked slot (the label names it) or a starved silo that never started its timeouts (Portal routing legs starved of ThreadPool threads during install burst (routing saturation gauge, not a routing defect) #5389). Both real defects, both still file.
  • Everything younger is Warning — still logged with every discriminator, still paired with the cleared after … ms line; it no longer files a ticket for a burst the silo absorbed.
  • A latched episode escalates once. While an episode is held, the report re-reads its oldest leg on dispatches the grain makes anyway (rate-limited to 10 s, no timer/watchdog) and emits ONE Critical … has not drained after N ms: oldest leg … when a leg outlives its bounds under it. Before this, an episode that crossed on young legs and then leaked produced no second line at all.
  • The deepest channel is named (deepest per-channel queue 63 on cache/x [sync/…]), so a recurring deep queue points at the producer.
  • The bound is derived from the leg's constants, not chosen — raising it would change what a leg is allowed to do, so it cannot be used as a quietening knob.

Report logic moved from RoutingGrain into RoutingSaturationReport (clock-injectable) so the latch/escalation is testable without a cluster.

Verification

  • MeshWeaver.Hosting.Orleans.Test Release -warnaserror: 0 Warning(s), 0 Error(s). MeshWeaver.Documentation same.
  • BackPressureLevelByShapeTest 16/16; with the saturation/dispatcher suites 28/28.
  • Negative control: disabling the latched review makes ALatchedEpisodeWhoseLegOutlivesItsBounds_EscalatesExactlyOnce fail (15/16) — the escalation is what the test measures.

Doc: Doc/Architecture/ReadingARoutingSaturationReport — alerting-policy row and "The level rule, and why depth could not carry it".

Pairs-with: none — only internal members changed (RoutingGrain.SaturationLevel, RoutingGrain.SaturationThreshold moved to RoutingSaturationReport).

Refs #5118 #5595 #5486 #5638 #5622 #5616 #5617 #5618 #5631 #5632

Production effect only after the portals roll onto an image carrying this commit; no recycle needed beyond the roll (RoutingGrain activations are per-silo and end with the pod).

🤖 Generated with Claude Code

…ounds — the oldest leg's age decides the level

The routing back-pressure crossing was levelled by queue depth (#5322): deepest >= 1 stayed
Critical. Every Critical quoted in the tickets filed after that shipped (#5595, #5616-#5618,
#5622, #5631, #5632, #5638) had a deep queue AND a young oldest leg (63 deep at 11 ms, 54 at
81 ms, 43 at 14.8 s) — a producer burst on one stream, sampled at the crossing, inside the leg's
own bounds. Depth cannot tell that from a stuck stream; an age past the leg's own bounds can.

Permanent level decision (AGENTS.md, log levels): Critical is what the ticketing path files, so
it must mean act now.
- Level = Critical iff the oldest in-flight leg is older than LegSelfBound, the sum of the leg's
  own timeouts (resolve + subscriber probe + post = 100 s): its timeouts cannot explain it, so it
  is a leaked slot or a starved silo. Everything younger is Warning, still logged in full.
- A latched episode re-reads its oldest leg on dispatches it makes anyway (rate-limited to 10 s,
  no timer) and escalates ONCE with "has not drained" when a leg outlives its bounds under it —
  the one absence an event-driven report can state.
- The crossing names the deepest channel (destination [stream]) so a recurring deep queue points
  at its producer.
- Report logic moved out of RoutingGrain into RoutingSaturationReport (testable with a clock).

Refs #5118 #5595 #5486 #5638 #5622 #5616 #5617 #5618 #5631 #5632

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 24, 2026 11:07
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

0 files   -     3  0 suites   - 3   0s ⏱️ - 5m 47s
0 tests  - 2 010  0 ✅  - 2 010  0 💤 ±0  0 ❌ ±0 
0 runs   - 2 011  0 ✅  - 2 011  0 💤 ±0  0 ❌ ±0 

Results for commit 5421fc7. ± Comparison against base commit 777e848.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical build-documentation and saturation-tracking issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity · 2 Medium severity

Open (4)
What changed in this PR

Updates routing back-pressure severity to use oldest-leg age, adds latched escalation, names the deepest channel, and documents the policy.

Changes:

  • Moves reporting into RoutingSaturationReport.
  • Updates routing integration and channel diagnostics.
  • Adds tests and documentation for severity and escalation behavior.
File Summary
test/​MeshWeaver.Hosting.Orleans.Test/​RoutingBackpressureShapeTest.cs Updates routing saturation snapshot tests.
test/​MeshWeaver.Hosting.Orleans.Test/​BackPressureLevelByShapeTest.cs Tests severity and escalation behavior.
src/​MeshWeaver.Hosting.Orleans/​RoutingSaturationReport.cs Implements saturation reporting and escalation logic.
src/​MeshWeaver.Hosting.Orleans/​RoutingGrain.cs Integrates the saturation reporter.
src/​MeshWeaver.Hosting.Orleans/​OrderedRouteDispatcher.cs Reports deepest channel identity.
src/​MeshWeaver.Documentation/​Data/​Architecture/​ReadingARoutingSaturationReport.md Documents the revised alerting policy.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

/// routes terminate in milliseconds — and well below the 541-deep queue prod reached.
/// </summary>
internal const int SaturationThreshold = 64;
private RoutingSaturationReport SaturationReport

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5421fc7: every remaining reference now names RoutingSaturationReport.SaturationThreshold (the three test mentions and the doc). They were <c> text, not <see cref>, so the Release -warnaserror build was already clean (0 Warning(s), 0 Error(s), and CI's Consolidate test results passed) — but the names were stale, so they are updated.

Comment on lines +187 to +188
if (inFlight > SaturationThreshold / 2) return;
if (Interlocked.Exchange(ref saturationReported, 0) == 0) return;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considered and kept as designed. A single leaked leg after the episode clears is no longer back-pressure (the count is below half the threshold), so this report is not the instrument that should page for it — making it one would turn a saturation report into a general leak detector running outside any episode. It is still named, by two instruments: the NEXT crossing's oldest leg in flight reads that leg first (it is the oldest by construction) and, being past LegSelfBound, makes that crossing Critical; and the silo-stop residual (RoutingQuiescence, #2638/#2833) names any leg that never lands. What is lost is only the case of a leaked leg on a silo that never crosses again, which is by definition not under back-pressure.

Comment on lines +164 to +166
orderedDispatcher.QueueSnapshot,
quiescence is null ? null : quiescence.OldestInFlight,
() => (routingPool.CurrentInFlight, routingPool.CurrentlyWaiting)), null);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considered and kept as designed. The population is deliberately RoutingQuiescence's: it is the routing work this SILO holds (forward legs and the NACKs they carry — both dispatched by RoutingGrain, both bounded by the same timeouts), and RoutingGrain is [StatelessWorker(1)], so "another routing activation" is a previous activation of the same per-silo grain whose legs are still this silo's routing work. A NACK or predecessor leg held past LegSelfBound is exactly the leaked-slot / starved-silo condition the Critical exists for, and it is not mislabelled: the printed label is the tracker's own (nack → …, dispatch → …, stream-routed → …), so the line says which kind of work is old.

Comment on lines +214 to +226
private void ReviewLatchedEpisode()
{
if (oldestLeg is null) return;
var now = time.GetUtcNow().UtcDateTime;
var due = Volatile.Read(ref nextReviewTicks);
if (now.Ticks < due) return;
if (Interlocked.CompareExchange(ref nextReviewTicks, (now + LatchedReviewInterval).Ticks, due) != due) return;

var episode = Volatile.Read(ref saturationEpisode);
if (Volatile.Read(ref escalatedEpisode) == episode) return;
var oldest = oldestLeg();
if (SaturationLevel(oldest?.Age) != LogLevel.Critical) return;
if (Interlocked.Exchange(ref escalatedEpisode, episode) == episode) return;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5421fc7: after the scan and before escalating, the review revalidates that the latch is still held and the episode number is unchanged; a drained or superseded episode logs nothing. A residual window between that check and the log call remains (the two are not one atomic step, and serializing them would put a lock on the dispatch path); what can slip through it is a Critical about a leg that WAS past its own bounds when read — a true statement about a real leak, attributed to the episode it was read in.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

0 files   -     5  0 suites   - 5   0s ⏱️ - 15m 23s
0 tests  - 4 069  0 ✅  - 4 067  0 💤  - 2  0 ❌ ±0 
0 runs   - 4 073  0 ✅  - 4 071  0 💤  - 2  0 ❌ ±0 

Results for commit 5421fc7. ± Comparison against base commit 777e848.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

0 tests   - 443   0 ✅  - 443   0s ⏱️ - 1m 2s
0 suites  -   3   0 💤 ±  0 
0 files    -   3   0 ❌ ±  0 

Results for commit 5421fc7. ± Comparison against base commit 777e848.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

0 tests   - 1 648   0 ✅  - 1 648   0s ⏱️ - 3m 25s
0 suites  -     2   0 💤 ±    0 
0 files    -     2   0 ❌ ±    0 

Results for commit 5421fc7. ± Comparison against base commit 777e848.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

628 tests   - 114   628 ✅ + 78   35s ⏱️ - 6m 31s
  1 suites  -   2     0 💤  - 192 
  1 files    -   2     0 ❌ ±  0 

Results for commit 5421fc7. ± Comparison against base commit 777e848.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files  ± 0    1 suites  ±0   2m 39s ⏱️ -21s
342 tests +10  342 ✅ +10  0 💤 ±0  0 ❌ ±0 
346 runs  +10  346 ✅ +10  0 💤 ±0  0 ❌ ±0 

Results for commit 5421fc7. ± Comparison against base commit 777e848.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

  2 files   -    15    2 suites   - 15   3m 14s ⏱️ - 32m 31s
970 tests  - 8 274  970 ✅  - 8 080  0 💤  - 194  0 ❌ ±0 
974 runs   - 8 279  974 ✅  - 8 085  0 💤  - 194  0 ❌ ±0 

Results for commit 5421fc7. ± Comparison against base commit 777e848.

♻️ This comment has been updated with latest results.

…ating; references follow SaturationThreshold to RoutingSaturationReport

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit 3d1dc77 Sep 24, 2026
46 of 47 checks passed
@rbuergi
rbuergi deleted the fix/log-leaks-routing-level branch October 10, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants