Skip to content

fix(modules): a withdrawn module is absent from the installed-module set, never a fault in the pre-warm sweep (#6557) - #6566

Merged
8 commits merged into
mainfrom
fix/6557-withdrawn-module-absent-from-sweep
Oct 13, 2026
Merged

8 commits merged into
mainfrom
fix/6557-withdrawn-module-absent-from-sweep

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Refs #6557

Based on #6558 (fix/6556-prebuild-only-root-reachable, still open). #6558 removes the production trigger (Radzen wrongly withdrawn); this PR removes the latent half: ANY module withdrawn at boot for a genuine reason faulted the pre-warm sweep and refused the pod's readiness. Merge #6558 first; this diff then reduces to its own commit.

Root cause

Each module held in its own collectible context got its own transient InstalledModuleAssembly registration. A container's registrations are fixed once it is built, and the module set is not: built-before-published (#6542) withdraws a module AFTER the root container exists. The withdrawn module's registration stayed behind and could only answer the withdrawn boot generation (while its weak reference lived) or throw Module X is no longer held by this mesh's module registry (after). Every GetServices<InstalledModuleAssembly>() — the pre-warm sweep's, the fingerprint's, the compile reference set's — faulted on it.

Fix — one registry, container-independent

  • New InstalledModules (MeshWeaver.Mesh.Contract), a mesh-scoped singleton owned by MeshBuilder: the InstalledModuleAssembly instances DECLARED in the service collection (image-bound modules, plus anything else that registers one), plus every held module by NAME, answered from ModuleContexts at each read and simply absent once withdrawn or retired.
  • IEnumerable<InstalledModuleAssembly> is an explicit factory registration over that registry. MS DI (exact match before the enumerable call site) and Autofac (explicit registration before the implicit collection) both answer it ahead of the per-descriptor collection, so GetServices<InstalledModuleAssembly>(), an injected IEnumerable<> and InstalledModules.Current(sp) read the same set on both container kinds. No consumer changes, so callers in other repos keep working.
  • The per-module transient is gone. The withdrawal is still reported as an IncompatibleModule (stderr, /health, the module-status surfaces), as before.
  • An Autofac registration source was rejected for the same reason fix(modules): pre-build only root-reachable singletons — never circuit-scoped services (#6556) #6558 gives: plain BuildServiceProvider() hosts ignore sources.

Tests (MeshWeaver.Compiler.Pipeline.Test, Release)

New AWithdrawnModuleIsAbsentFromTheSweepTest. The module's root-reachable singleton throws, so it is withdrawn at boot. The test then forces a GC, as production collected the boot generation:

  • Autofac host (Monolith mesh): the enumeration does not throw; the module is absent from GetServices, InstalledModules.Current and the fingerprint; ProbeDynamicTypes and WarmDynamicTypes both complete with no Faulted outcome.
  • Plain MS DI host (new ServiceCollection() + BuildServiceProvider(), the IRootContainerBuilt hook run as a host does): the module is listed before the withdrawal and absent after it; the enumeration does not throw.
  • Negative control: with only MeshBuilder.cs reverted, both tests fail. The Autofac one fails with the production sentence: An exception was thrown while activating MeshWeaver.Mesh.InstalledModuleAssembly[] -> … Module MeshWeaver.Test.BootBuilt is no longer held by this mesh's module registry. The MS DI one fails because the withdrawn module is still listed.
  • ConfiguredModuleActivationTest, ModulesRunInTheirOwnContextTest, the boot-built tests and the live-swap tests are green (73/73 in the targeted run).
  • Full MeshWeaver.Compiler.Pipeline.Test: 1412/1412 passed. Memex.Portal.Shared.Test module and sealed-set tests: 455/455 passed. Both ran in Release, locally.

Touched projects build with -c Release -warnaserror: 0 warnings, 0 errors.

Doc

Doc/Architecture/LiveModuleUpdate → "Built before published": the "Open" paragraph now says how the defect was fixed. The re-appliable-seams row no longer says the registration is transient.

Deploy

This is boot-time module activation, so a new pod is a fresh process and nothing needs recycling.

Pairs-with: none — adds a public type (InstalledModules) and removes no public surface; GetServices<InstalledModuleAssembly>() keeps its contract
Mirror-sync: none — no i18n catalog change

🤖 Generated with Claude Code

…set, never a fault in it (#6557)

The installed-module set is one registry the mesh owns (InstalledModules):
the declared InstalledModuleAssembly instances plus every held module by
name, answered from ModuleContexts at each read and absent once withdrawn
or retired. IEnumerable<InstalledModuleAssembly> is an explicit factory
registration over it, answered ahead of the per-descriptor collection by
MS DI and Autofac alike, so a plain BuildServiceProvider() host reads the
same set as the Autofac host. Replaces the per-module transient that could
only throw once its module was withdrawn, faulting the pre-warm sweep and
refusing the pod's readiness.

Refs #6557

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 11:51
@rbuergi
rbuergi enabled auto-merge October 12, 2026 11:51

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.

🟡 Changes recommended

Module-contributed entries can still fault after withdrawal, module ordering changes, and circuit scopes are not preserved.

3 open findings
What changed in this PR

Introduces a live installed-module registry so withdrawn modules disappear from enumeration, while incorporating #6558’s scoped-service pre-warm changes.

Changes:

  • Adds InstalledModules and routes module enumeration through it.
  • Restricts pre-warming to root-reachable singleton graphs without scoped dependencies.
  • Adds withdrawal, Autofac/MS DI, boot, and live-swap coverage.
File Description
test/​MeshWeaver.Compiler.Pipeline.Test/​RootAndModuleSingletonsBuildInOneLockOrderTest.cs Updates singleton pre-build test usage.
test/​MeshWeaver.Compiler.Pipeline.Test/​ModuleRootServicesSwapTest.cs Adds circuit-bound live-swap coverage.
test/​MeshWeaver.Compiler.Pipeline.Test/​AWithdrawnModuleIsAbsentFromTheSweepTest.cs Tests withdrawn-module enumeration and pre-warming.
test/​MeshWeaver.Compiler.Pipeline.Test/​AModuleWhoseSingletonCannotBeBuiltAtBootTest.cs Expands boot activation scenarios.
src/​MeshWeaver.ServiceProvider/​ModuleServiceProvider.cs Adds root-reachable singleton dependency analysis.
src/​MeshWeaver.Mesh.Contract/​ModuleServices.cs Selects module registrations for pre-building.
src/​MeshWeaver.Mesh.Contract/​MeshBuilder.cs Registers and populates the installed-module registry.
src/​MeshWeaver.Mesh.Contract/​InstalledModules.cs Implements dynamic installed-module enumeration.
src/​MeshWeaver.Documentation/​Data/​Architecture/​LiveModuleUpdate.md Documents withdrawal and pre-build behavior.

🧠 Review effort: Balanced


💡 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 Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    20 files   -   5      20 suites   - 5   55m 31s ⏱️ - 2m 13s
11 992 tests  - 307  11 992 ✅  - 114  0 💤  - 193  0 ❌ ±0 
12 006 runs   - 307  12 006 ✅  - 114  0 💤  - 193  0 ❌ ±0 

Results for commit 613392c. ± Comparison against base commit f632b88.

This pull request removes 352 and adds 21 tests. Note that renamed tests count towards both.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
   --- End of inner exception stack trace ---, isDenial: True)
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
 ---> (Inner Exception #1) System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known<---
…
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "95e3cc56-d7a0-425b-a09c-3cb0947f052b")
Memex.Portal.Shared.Test.SessionDenialIsAnAnswerTest ‑ OnlyAVerdictReadsAsADenial(shape: "the same verdict nested, as a late denial dispatch"···, failure: System.InvalidOperationException: write failed
 ---> System.UnauthorizedAccessException: Access denied
   --- End of inner exception stack trace ---, isDenial: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's FIRST member", fault: System.AggregateException: One or more errors occurred. (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's SECOND member", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall nested in an aggregate inside a wrapper", fault: System.InvalidOperationException: outer
 ---> System.AggregateException: One or more errors occurred. (Value does not fall within the expected range.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.ArgumentException: Value does not fall within the expected range.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---

   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall wrapped as InnerException", fault: System.InvalidOperationException: outer
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "an aggregate of unrelated faults", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Value does not fall within the expected range.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
, expected: False)
MeshWeaver.Graph.Test.AModuleOverrideHidesTheRootRegistrationTest ‑ AModuleSingletonOverAModuleOwnedOverride_OfARootScopedService_IsBuiltBeforePublish
MeshWeaver.Graph.Test.AModuleOverrideHidesTheRootRegistrationTest ‑ WithoutTheOverride_TheRootScopedServiceIsReached_AndTheSingletonIsNotBuiltAtPublish
MeshWeaver.Graph.Test.AWithdrawnModuleIsAbsentFromTheSweepTest ‑ OnAPlainMsDiHost_AHeldModuleKeepsItsInstallPosition
…

♻️ This comment has been updated with latest results.

rbuergi and others added 3 commits October 12, 2026 16:21
…hdrawn module's contributed member is absent (#6557)

Review on #6566: a held module's slot was appended after every declared member,
reordering a configured [held A, image-bound B] to [B, A]; and an
InstalledModuleAssembly a held module registers through its own services was
forwarded as a Current transient that throws once the module is withdrawn.
Both are now InstalledModuleSlot declarations in the one ordered sequence,
absent once their module is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 14:25
@meshweaver-cloud
meshweaver-cloud Bot disabled auto-merge October 12, 2026 14:26
rbuergi added a commit that referenced this pull request Oct 12, 2026
…module-registered list

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi enabled auto-merge October 12, 2026 14:30

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.

🟡 Changes recommended

Dependency resolution mismatches can retain the lock inversion, while module enumeration bypasses DI lifetimes and has a retirement race.

3 open findings
3 resolved since last review

🧠 Review effort: Balanced

…etimes; the walk sees only what the module reaches (#6557)

- ModuleContexts.TryResolveCurrentModuleService reads the generation ONCE and answers null when that
  generation is disposed before or during the resolve; the contributed InstalledModuleAssembly slot
  uses it instead of Current() then ResolveModuleService().
- InstalledModules.Current composes an immutable result and activates a fixed declaration with the
  lifetime it declared (singleton once per mesh against the root, scoped once per scope, transient per read).
- ScopedDependencyWalk mirrors ModuleFallbackSource: a module that registers its own T never reaches the
  root's T, so a root-scoped T it overrides no longer keeps a root-reachable singleton from being built
  before publish. Pinned by AModuleOverrideHidesTheRootRegistrationTest (+ negative control).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 15:30
@meshweaver-cloud
meshweaver-cloud Bot disabled auto-merge October 12, 2026 15:32
rbuergi added a commit that referenced this pull request Oct 12, 2026
…sets with DI lifetimes; plain-MS-DI publication residual documented

Co-Authored-By: Claude Opus 5.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.

🟡 Changes recommended

The custom lifetime cache permanently retains activation exceptions, allowing one transient failure to poison future readiness sweeps.

1 open finding
3 resolved since last review

🧠 Review effort: Balanced

Conflict in LiveModuleUpdate: main's #6584/#6588 sections kept, its 'Open: a withdrawn module'
paragraph replaced by this branch's account of the fix. Two crefs made ambiguous by main's new
caller-carrying overloads (CS0419 under -warnaserror) now name the two-argument overloads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 13, 2026 08:45
@rbuergi
rbuergi enabled auto-merge October 13, 2026 08:45
@meshweaver-cloud
meshweaver-cloud Bot disabled auto-merge October 13, 2026 08:45
rbuergi added a commit that referenced this pull request Oct 13, 2026
ModuleOwnedRootSource: this branch's removal of the IncompatibleModule source branch kept, with
main's caller-carrying resolvers (#6584). ModuleServiceForwarders: both doc paragraphs kept.

Co-Authored-By: Claude Opus 5.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.

🟡 Changes recommended

The scoped-dependency walk still misses scoped services supplied through another module’s stand-ins.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Comment on lines +741 to +744
yield return own.Registration.Metadata.TryGetValue(StandsInFor, out var stood)
&& stood is IComponentRegistration inRoot
? (inRoot, Side.Root)
: (own.Registration, Side.Module);

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.

The gap is real, but this pull request does not open it, so it is not fixed here (converge rule: push only for a wrong result this diff causes).

ModuleFallbackSource produces those registrationsElsewhere stand-ins only for a type another module DECLARES. ModuleServices.ElsewhereSeenFrom selects the owner with IsOwned(type, g.Context), and contributors only for that owner's types. Every such type names a collectible assembly. ScopedDependencyWalk.RegistrationsOf never asks the root about a type that names a collectible assembly, on main and here alike. So for these stand-ins the walk saw a transient delegate with no dependencies before this change too, and the new yield break changes nothing for them. It only stops the walk from reaching a root registration that the module's own registration hides, which is the case AModuleOverrideHidesTheRootRegistrationTest pins.

Closing the gap means carrying the underlying registration's lifetime onto each elsewhere stand-in. Today registrationsElsewhere hands over bare resolvers, so that is a signature change across ModuleServiceProvider.CreateForCallers, ModuleServices and the swap stage, plus a two-module boot test. It is out of this fix's scope and is reported as a follow-up.

@rbuergi rbuergi closed this pull request by merging all changes into main in 2c0247f Oct 13, 2026
@rbuergi
rbuergi deleted the fix/6557-withdrawn-module-absent-from-sweep branch October 24, 2026 14:13
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