Repository navigation
fix(modules): a withdrawn module is absent from the installed-module set, never a fault in the pre-warm sweep (#6557) - #6566
Conversation
…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>
There was a problem hiding this comment.
🟡 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
InstalledModulesand 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.
Test Results 20 files - 5 20 suites - 5 55m 31s ⏱️ - 2m 13s 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.♻️ This comment has been updated with latest results. |
…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>
…module-registered list Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…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>
…sets with DI lifetimes; plain-MS-DI publication residual documented Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
| yield return own.Registration.Metadata.TryGetValue(StandsInFor, out var stood) | ||
| && stood is IComponentRegistration inRoot | ||
| ? (inRoot, Side.Root) | ||
| : (own.Registration, Side.Module); |
There was a problem hiding this comment.
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.


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
InstalledModuleAssemblyregistration. 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 throwModule X is no longer held by this mesh's module registry(after). EveryGetServices<InstalledModuleAssembly>()— the pre-warm sweep's, the fingerprint's, the compile reference set's — faulted on it.Fix — one registry, container-independent
InstalledModules(MeshWeaver.Mesh.Contract), a mesh-scoped singleton owned byMeshBuilder: theInstalledModuleAssemblyinstances DECLARED in the service collection (image-bound modules, plus anything else that registers one), plus every held module by NAME, answered fromModuleContextsat 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, soGetServices<InstalledModuleAssembly>(), an injectedIEnumerable<>andInstalledModules.Current(sp)read the same set on both container kinds. No consumer changes, so callers in other repos keep working.IncompatibleModule(stderr,/health, the module-status surfaces), as before.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:GetServices,InstalledModules.Currentand the fingerprint;ProbeDynamicTypesandWarmDynamicTypesboth complete with noFaultedoutcome.new ServiceCollection()+BuildServiceProvider(), theIRootContainerBuilthook run as a host does): the module is listed before the withdrawal and absent after it; the enumeration does not throw.MeshBuilder.csreverted, 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).MeshWeaver.Compiler.Pipeline.Test: 1412/1412 passed.Memex.Portal.Shared.Testmodule 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 contractMirror-sync: none — no i18n catalog change
🤖 Generated with Claude Code