Skip to content

chore: the per-compile _Activity hub retention is BOUNDED at 15 min, measured (#1324) - #1461

Merged
rbuergi merged 3 commits into
mainfrom
chore/1324-measure-bounded-activity-retention
Aug 14, 2026
Merged

rbuergi merged 3 commits into
mainfrom
chore/1324-measure-bounded-activity-retention

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Closes #1324 — on evidence, not with a fix. The issue's own question was "is the per-compile
_Activity hub retention BOUNDED?"
The answer is yes, at 15 minutes, and this PR is the
measurement that establishes it plus the guard that keeps it true.

The measurement

Two runs, deliberately kept distinct.

1. Unscaled, at production timer values — 15 min DisconnectTimeout, real 45 s heartbeat.
Three compiles of a config => config NodeType, then wait and count:

t per-compile activity hubs mesh total hubs
0 – 885 s 15 51 → 47
900 s 5 37
915 s 0 32

13 hubs reclaimed in 915 s = 15.25 min — one idle window plus the 15 s poll granularity. The
mesh's total hub count went 49 → 32. (The earlier 51 → 47 step near t+660s is the cache's own
10-minute idle sweep — a different reaper doing a different job.) Local one-off, 15m19s; far too
slow for CI, which is what run 2 is for.

2. A 1:20 scale model, committed as the CI guard — 20 s window, 1 s heartbeat.
13 hubs across 3 compile activities, all reclaimed at 20.3 s against the 20 s window. Exactly
one idle window, nothing re-arming the timer.

So the residual NodeTypeRecompileAlcLeakTest counts — ~6 hubs / 3–5 MB per recompile, essentially
all of it the _Activity/compile-<ts> node hub and its sync/ sub-hubs — is a transient with a
15-minute ceiling, not a leak.
That test simply runs for seconds and cannot watch it expire.

Why it is bounded — the chain the issue's earlier framing missed

An Activity node hub is one of the few hub kinds that carries an idle reaper of its own, in the
monolith as well as on Orleans:

ActivityNodeType.CreateMeshNode        HubConfiguration = … .AddKernelSubHubHandlers()
  → KernelHubConfigurationExtensions   ParentHub.GetService<IKernelHubConfigurator>()
  → KernelContainer.ConfigureSubHub    .WithInitialization(hub => DisposeOnTimeout(hub))
  → KernelContainer.DisposeOnTimeout   one-shot Timer, KernelHubOptions.IdleDisconnectTimeout

registered by KernelNodeType.AddKernel(), which AddGraph() includes. The timer is one-shot and
re-armed by every inbound message
— which is exactly why the 45 s heartbeat kept it at bay
forever, and why #1435's terminal-write mirror release is what let the clock finally run.

🚨 The part worth reading: I rejected the cheap version of this test

The obvious test is "shorten the window, assert the hub goes away". That test would have gone
green while production reclaimed nothing.
Shorten only the window and the 45 s heartbeat falls
outside it: the timer fires between two heartbeats, the test passes, and production — where 45 s
sits far inside 15 min — still never reclaims. So the window and the heartbeat are compressed
as a pair, at production's 1:20 ratio, and a regression of #1435 (a finished activity whose
mirror is left warm) keeps the hub alive here exactly as it does in production.

That is worth stating plainly because it is the second "cannot fail" guard on this one issue.
The first was NodeTypeAssemblyLeakTest, deleted earlier in #1324: it asserted a NodeType's load
context is collected after mesh disposal — something a portal never performs — so for the bug's
entire life it read as evidence that recompiles were clean while production grew 130–340 MB/min.
Two of them on one issue is a pattern, not a coincidence: an assertion whose precondition the
system never reaches is indistinguishable from a passing one.

The prohibition, made structural

#1324 forbids "fixing" this by shortening a timer, and #1435's whole lesson is that the clocks were
correct and something was resetting them. KernelHubOptions.IdleDisconnectTimeout therefore
defaults to the existing 15 minutes and changes no production behaviour — it exists so the
property is assertable inside a test budget.

An option added so a property is assertable is one refactor away from becoming an option someone
quietly dials down, so that is asserted too:

[Fact]
public void ProductionIdleDisconnectTimeout_IsTheDocumentedCeiling() =>
    new KernelHubOptions().IdleDisconnectTimeout.Should().Be(TimeSpan.FromMinutes(15), …);

What is in the diff

  • KernelHubOptions (new, MeshWeaver.Kernel) — IdleDisconnectTimeout, default 15 min.
    KernelContainer's static readonly DisconnectTimeout becomes an instance field read from it,
    falling back to the default when unregistered. No behaviour change.
  • CompileActivityHubRetentionTest (new) — the scaled guard plus the production-default
    assertion above.
  • NodeTypeRecompileAlcLeakTest — corrects the paragraph claiming "the monolith has no idle
    collection for node hubs at all … a host-lifecycle question"
    . The general statement is true; it
    is wrong for an Activity hub, which is the one this issue is about. Now carries the measured
    numbers and points at the new test.
  • MeshNodeStreamCache.md — a section documenting the ceiling, the chain, the measurement
    table, and why the pairing in the scaled test is load-bearing.

Verification

  • dotnet build -c Release -warnaserror: MeshWeaver.Kernel, MeshWeaver.Kernel.Hub,
    MeshWeaver.Hosting.Monolith.Test — all 0 Warning(s), 0 Error(s).
  • CompileActivityHubRetentionTest 2/2 · NodeTypeRecompileAlcLeakTest 2/2 · Release.
  • DocumentationLinkIntegrityTest 1/1.

No What's New entry: no user-visible behaviour change — a test, a doc, and an option whose
default is the value it replaced.

🤖 Generated with Claude Code

…measured (#1324)

#1324 asks whether the residual is a leak or a bounded retention. It is bounded, and the
ceiling is KernelContainer's 15-minute idle disconnect.

Measured unscaled at production timer values (15 min window, real 45 s heartbeat): the
per-compile activity hubs sit flat at 15 through t+885s, drop to 5 at t+900s and reach 0 at
t+915s — 13 hubs reclaimed in 15.25 min, mesh total 49 -> 32. So the ~6 hubs / 3-5 MB per
recompile that NodeTypeRecompileAlcLeakTest counts is a TRANSIENT it is too short-lived to
watch expire, not a leak.

Why it is bounded: an Activity node hub carries an idle reaper of its own, monolith
included — ActivityNodeType -> AddKernelSubHubHandlers() -> KernelContainer.DisposeOnTimeout,
registered by KernelNodeType.AddKernel() which AddGraph() includes. The timer is one-shot and
re-armed by every inbound message, which is exactly why the 45 s mirror heartbeat kept it at
bay until #1435 released the mirror on the terminal write.

The CI guard scales the window AND the heartbeat together, at production's 1:20 ratio. That
pairing is load-bearing: shortening only the window would put the 45 s heartbeat outside it,
so the timer would fire between two heartbeats and the test would pass while production
reclaimed nothing. That is the same shape as NodeTypeAssemblyLeakTest — deleted earlier in
this issue for asserting collection after a mesh disposal a portal never performs — and two
"cannot fail" guards on one issue is a pattern worth naming.

KernelHubOptions.IdleDisconnectTimeout defaults to the existing 15 minutes and changes no
production behaviour; it exists so the property is assertable in a test budget. A test asserts
that default stays 15 minutes, because #1324 prohibits shortening it and an option added for
assertability is one refactor away from being dialled down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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.

Pull request overview

This PR establishes (and guards in CI) that per-compile _Activity/compile-<ts> hubs are retained transiently but reclaimed within a bounded idle window (15 minutes in production), by adding a scale-model retention test and making the kernel hub idle timeout explicitly configurable (default unchanged).

Changes:

  • Introduces KernelHubOptions.IdleDisconnectTimeout (default 15 minutes) so the retention property can be asserted within a test budget without altering production behavior.
  • Adds CompileActivityHubRetentionTest to validate that per-compile activity hubs are reclaimed after one idle window (with heartbeat/window scaled together).
  • Updates the NodeTypeRecompileAlcLeakTest explanation and documents the measured ceiling in MeshNodeStreamCache.md.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/MeshWeaver.Hosting.Monolith.Test/NodeTypeRecompileAlcLeakTest.cs Updates commentary to reflect measured bounded retention for Activity hubs.
test/MeshWeaver.Hosting.Monolith.Test/CompileActivityHubRetentionTest.cs Adds a scaled CI guard that measures reclamation of per-compile activity hubs after the idle window.
src/MeshWeaver.Kernel/KernelHubOptions.cs Adds a DI-overridable (default 15 min) idle disconnect timeout for kernel-hosting hubs.
src/MeshWeaver.Kernel.Hub/KernelContainer.cs Uses KernelHubOptions (if registered) instead of a hard-coded 15-minute timeout.
src/MeshWeaver.Documentation/Data/Architecture/MeshNodeStreamCache.md Documents the bounded retention mechanism, ceiling, and measurement results.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

723 tests  ±0   723 ✅ ±0   10m 21s ⏱️ -22s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit 69e658e. ± Comparison against base commit 9fece59.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 920 tests  ±0   1 728 ✅ ±0   6m 1s ⏱️ +12s
   11 suites ±0     192 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 69e658e. ± Comparison against base commit 9fece59.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ±0     11 suites  ±0   5m 11s ⏱️ +13s
1 834 tests ±0  1 731 ✅ ±0  103 💤 ±0  0 ❌ ±0 
2 164 runs  ±0  2 061 ✅ ±0  103 💤 ±0  0 ❌ ±0 

Results for commit 69e658e. ± Comparison against base commit 9fece59.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 930 tests  ±0   1 920 ✅ ±0   8m 4s ⏱️ + 1m 25s
   11 suites ±0      10 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 69e658e. ± Comparison against base commit 9fece59.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

2 601 tests  +126   2 600 ✅ +126   8m 12s ⏱️ + 1m 14s
   12 suites ±  0       1 💤 ±  0 
   12 files   ±  0       0 ❌ ±  0 

Results for commit 69e658e. ± Comparison against base commit 9fece59.

This pull request removes 260 and adds 386 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ FailedCompile_FaultsActivityAndSurfacesFullDiagnostics
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ SuccessfulCompile_PopulatesCompiledSourcesSnapshot
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ SuccessfulCompile_ReportsActivityLogWithSourceQueriesAndMatchedPaths
MeshWeaver.Hosting.Monolith.Test.CompileFinishAndDisposeTest ‑ FailedCompile_StillReachesTerminalDisposedState_NotStuck
MeshWeaver.Hosting.Monolith.Test.CompileFinishAndDisposeTest ‑ TriggeredCompile_FinishesGeneration_AndActivityReachesTerminalDisposedState
MeshWeaver.Hosting.Monolith.Test.CompileLeafStabilityTest ‑ CompileAndGetConfigurations_FreshNodeTypes_AllEmitWithinBound
MeshWeaver.Hosting.Monolith.Test.CompileLeafStabilityTest ‑ Kickoff_FreshNodeTypes_AllSettle_NoStall
MeshWeaver.Hosting.Monolith.Test.CompileProgressOverlayAreaTest ‑ AreaOfAnInstanceMidCompile_RendersProgress_NotAreaNotFound
MeshWeaver.Hosting.Monolith.Test.CompileProgressOverlayAreaTest ‑ AreaThatGenuinelyHasNoRenderer_StillReportsAreaNotFound
MeshWeaver.Hosting.Monolith.Test.CompileProgressOverlayAreaTest ‑ CompilingAndMissing_AreMechanicallyDistinguishable
…
MeshWeaver.Hosting.Monolith.Test.CompileActivityHubRetentionTest ‑ PerCompileActivityHubs_AreReclaimedOnceTheirIdleWindowElapses
MeshWeaver.Hosting.Monolith.Test.CompileActivityHubRetentionTest ‑ ProductionIdleDisconnectTimeout_IsTheDocumentedCeiling
MeshWeaver.Hosting.Monolith.Test.CompileActivityNoPhantomPathTest ‑ CompiledNodeType_LastCompilationActivityPath_PointsAtAnExistingActivityNode
MeshWeaver.Hosting.Monolith.Test.CompileActivityNoPhantomPathTest ‑ SubscribingAbsentCompileActivity_InTightLoop_StaysResponsive_NoStorm
MeshWeaver.Hosting.Monolith.Test.CompileIncludeSystemScopeTest ‑ CodeInclude_ResolvesOnCompilePath_WhenNoAmbientIdentity
MeshWeaver.Hosting.Monolith.Test.CompileLegBoundWedgeTest ‑ HungAssemblyStoreUpload_StillSettlesOk_AndNamesTheLegOnTheActivityLog
MeshWeaver.Hosting.Monolith.Test.CompileLegBoundWedgeTest ‑ HungRoslynLeg_SettlesTerminalError_NamingTheLeg_ThenRecovers
MeshWeaver.Hosting.Monolith.Test.CompileSingleDriverConsistencyTest ‑ ConcurrentRequests_OnFreshNodeType_BothSucceedConsistently
MeshWeaver.Hosting.Monolith.Test.CompileSingleDriverConsistencyTest ‑ FreshCompile_TerminalStateIsComplete
MeshWeaver.Hosting.Monolith.Test.CompileSourceSnapshotWedgeTest ‑ HungSourceSnapshot_SettlesTerminalError_ThenFreshTriggerRecovers
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 402 tests   - 124   1 402 ✅  - 124   6m 59s ⏱️ -28s
   11 suites ±  0       0 💤 ±  0 
   11 files   ±  0       0 ❌ ±  0 

Results for commit 69e658e. ± Comparison against base commit 9fece59.

This pull request removes 384 and adds 260 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.CompileActivityNoPhantomPathTest ‑ CompiledNodeType_LastCompilationActivityPath_PointsAtAnExistingActivityNode
MeshWeaver.Hosting.Monolith.Test.CompileActivityNoPhantomPathTest ‑ SubscribingAbsentCompileActivity_InTightLoop_StaysResponsive_NoStorm
MeshWeaver.Hosting.Monolith.Test.CompileIncludeSystemScopeTest ‑ CodeInclude_ResolvesOnCompilePath_WhenNoAmbientIdentity
MeshWeaver.Hosting.Monolith.Test.CompileLegBoundWedgeTest ‑ HungAssemblyStoreUpload_StillSettlesOk_AndNamesTheLegOnTheActivityLog
MeshWeaver.Hosting.Monolith.Test.CompileLegBoundWedgeTest ‑ HungRoslynLeg_SettlesTerminalError_NamingTheLeg_ThenRecovers
MeshWeaver.Hosting.Monolith.Test.CompileSingleDriverConsistencyTest ‑ ConcurrentRequests_OnFreshNodeType_BothSucceedConsistently
MeshWeaver.Hosting.Monolith.Test.CompileSingleDriverConsistencyTest ‑ FreshCompile_TerminalStateIsComplete
MeshWeaver.Hosting.Monolith.Test.CompileSourceSnapshotWedgeTest ‑ HungSourceSnapshot_SettlesTerminalError_ThenFreshTriggerRecovers
MeshWeaver.Hosting.Monolith.Test.ContentNarrowingPersistenceTest ‑ ActivationEcho_MustNotDropUnknownContentMembers
MeshWeaver.Hosting.Monolith.Test.ContentNarrowingPersistenceTest ‑ CreateThenUpdate_PersistsWithoutRelyingOnActivationEcho
…
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ FailedCompile_FaultsActivityAndSurfacesFullDiagnostics
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ SuccessfulCompile_PopulatesCompiledSourcesSnapshot
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ SuccessfulCompile_ReportsActivityLogWithSourceQueriesAndMatchedPaths
MeshWeaver.Hosting.Monolith.Test.CompileFinishAndDisposeTest ‑ FailedCompile_StillReachesTerminalDisposedState_NotStuck
MeshWeaver.Hosting.Monolith.Test.CompileFinishAndDisposeTest ‑ TriggeredCompile_FinishesGeneration_AndActivityReachesTerminalDisposedState
MeshWeaver.Hosting.Monolith.Test.CompileLeafStabilityTest ‑ CompileAndGetConfigurations_FreshNodeTypes_AllEmitWithinBound
MeshWeaver.Hosting.Monolith.Test.CompileLeafStabilityTest ‑ Kickoff_FreshNodeTypes_AllSettle_NoStall
MeshWeaver.Hosting.Monolith.Test.CompileProgressOverlayAreaTest ‑ AreaOfAnInstanceMidCompile_RendersProgress_NotAreaNotFound
MeshWeaver.Hosting.Monolith.Test.CompileProgressOverlayAreaTest ‑ AreaThatGenuinelyHasNoRenderer_StillReportsAreaNotFound
MeshWeaver.Hosting.Monolith.Test.CompileProgressOverlayAreaTest ‑ CompilingAndMissing_AreMechanicallyDistinguishable
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    66 files  ±0      66 suites  ±0   44m 50s ⏱️ + 2m 14s
10 410 tests +2  10 104 ✅ +2  306 💤 ±0  0 ❌ ±0 
10 740 runs  +2  10 434 ✅ +2  306 💤 ±0  0 ❌ ±0 

Results for commit 69e658e. ± Comparison against base commit 9fece59.

♻️ This comment has been updated with latest results.

rbuergi and others added 2 commits August 14, 2026 08:35
… a build-time snapshot

The review read this as `.Where` filtering the timeout fallback into an empty
sequence and an opaque `InvalidOperationException`. That is not what happens —
`.Where` is UPSTREAM of `.FirstAsync()`, and `.Timeout(due, other)` wraps the
`FirstAsync()` observable, so `other` bypasses the filter entirely: the fallback
emits, the await completes, and the intended `remaining.Should().BeEmpty(...)`
diagnostic is what reports.

There was a real defect underneath it, though, and it is why the line reads that
way: `Observable.Return(remaining)` is evaluated EAGERLY when the chain is built,
so the fallback carried the pre-poll population rather than the latest one. The
emitted value is discarded — only the `remaining` variable, reassigned by the
`Select` side effect, feeds the assertion — so behaviour was correct, but a
fallback that silently disagrees with the variable it names is a trap for the
next reader. Deferred, with the operator ordering recorded on the line.

Verified: CompileActivityHubRetentionTest 2/2 Release, reclamation observed at
24 s against the 20 s scaled window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Is the per-compile _Activity hub retention BOUNDED? DisposeOnTimeout(15 min) should now reach it — measure past the horizon

2 participants