Repository navigation
Per-hub Autofac child scopes cost ~55 KB per hub: replace Autofac with the standard container plus a per-hub overlay #7145
Description
Defect
Every hub owns an Autofac child lifetime scope with its own registrations (ServiceProviderExtensions.SetupModules → BeginLifetimeScope(c => Populate(...))). A scope with local registrations answers every service it resolves from its parents by wrapping each parent registration in an ExternalComponentRegistration, with its own resolve-pipeline builder and middleware declarations, kept for the life of the hub. That per-hub constant, multiplied by the hub population, is the largest block of the portal's out-of-memory heap (Refs #7082):
- memex portal OOM dump (2026-10-23, 14.6 GB core): 12,060,761
ExternalComponentRegistration, 50.8 MMiddlewareDeclaration, 5.78 GiB of Autofac rows in 12.3 GB of managed heap. - The dump's own counters size the population at ~100,000 hubs, so ~120 wrappers ≈ 58 KB of Autofac per hub.
- Measured in a test mesh after fix(hub): a hub scope no longer re-declares its ancestors' instances or the mesh's type definitions (#7082) #7131 (99 node hubs plus hosted hubs, 516 hubs): Autofac is still 55 KB per hub. fix(hub): a hub scope no longer re-declares its ancestors' instances or the mesh's type definitions (#7082) #7131 removed only the wasted part (the startable pass wrapping every ancestor instance registration) and the duplicated type definitions. What remains is the services a hub genuinely resolves. It cannot be removed while a hub is an Autofac child scope with registrations of its own, because the wrapper is how Autofac gives a parent-scoped service the hub's own dependencies.
Direction (maintainer)
One single DI container, the standard .NET one (Microsoft.Extensions.DependencyInjection), and Autofac removed entirely. The only reason for Autofac was to extend the container at each hub level. That extension becomes a thin, thread-safe layer of our own over the standard container:
- Root:
services.BuildServiceProvider()holds every root registration and owns every root singleton. - Per hub: an overlay provider holding the hub's own descriptors (frozen into an immutable lookup when the hub is built) plus a per-hub cache of the instances it actually resolved. Nothing is copied from the parent.
- Lookup order: the hub's own registrations, then each parent hub's in turn, then the root.
- Root singletons are served by the root container, shared by everyone.
- Root scoped/transient registrations resolved through a hub are activated by the overlay, with dependencies taken from the hub, so per-hub overrides keep winning (e.g. root
AddScoped<INodeValidator, RlsNodeValidator>()built with the hub'sIMessageHub). IEnumerable<T>is parent registrations first, then the hub's own.
- Thread safety: immutable registration lookups; at-most-once singleton construction per registration (synchronous, never awaited); recursion detected per resolve chain.
- Ownership: the layer disposes only what it built, in reverse order, when the hub ends. Externally owned forwarders (
NotOwnedServiceDescriptor) are never disposed. - Live module update: the last step of a lookup is a hook for services nobody registered, replacing
ModuleOwnedRootSource/ModuleFallbackSource(Autofac registration sources). Open design point: a root singleton built by the standard container cannot receive a module type by constructor injection; each case gets a forwarder registration or a lazy resolve.
Autofac surface to retire (measured in core src/)
src/MeshWeaver.ServiceProvider:ServiceProviderExtensions,ModuleServiceProvider,ModuleScopeOwnership(pipeline middleware),NotOwnedServiceDescriptor,HubScopeStartables,ReflectionCacheEviction.src/MeshWeaver.Mesh.Contract:MeshBuilder(registers anIRegistrationSource),ModuleOwnedRootSource,ModuleContexts/ModuleServiceForwarders(take anILifetimeScopecaller),ModuleServices(readsIComponentRegistry,IsRegisteredWithKey,TypedService/KeyedService).- 13 test files in core and 3 files in MeshWeaver.Plugins reference
ILifetimeScope/ Autofac. In-mesh NodeType sources compile only at runtime: sweep the live mesh forILifetimeScope/Autofacbefore any public surface goes.
Plan
- Build the hub overlay behind
SetupModules, with the root still on Autofac.- Gate: the full Messaging.Hub, Data, Graph, Hosting and Compiler.Pipeline suites, including
HubScopeDeclaresOnlyItsOwnServicesTest(override and startable negative controls),StreamReleasesItsHubTest(a disposed hub must be collectable) and the module-ownership diagnostics tests. - Yardstick: zero wrapper registrations per hub, and per-hub retained bytes measured against fix(hub): a hub scope no longer re-declares its ancestors' instances or the mesh's type definitions (#7082) #7131's 2.04 MB per node-hub activation.
- Gate: the full Messaging.Hub, Data, Graph, Hosting and Compiler.Pipeline suites, including
- Port the module machinery off
ILifetimeScope/IComponentRegistryonto the overlay's own hooks. - Switch the root to
BuildServiceProvider(), remove the Autofac packages, and update the Plugins and test call sites. This is a paired change: core lands first, with thePairs-with:/Implementers:declarations the gates ask for.
Not established
- How much of production's ~120 wrappers per hub was the
AutoActivatewaste that fix(hub): a hub scope no longer re-declares its ancestors' instances or the mesh's type definitions (#7082) #7131 removes. The next dump answers it: compare theExternalComponentRegistrationcount with the hub count. - Whether every module-owned type a root singleton depends on can move to a forwarder or a lazy resolve.
Activity
docs(heap): rank the next per-hub costs after the container replaceme…
Merge pull request #7147 from Systemorph/feat/7145-replace-autofac
rbuergi commentedon Oct 26, 2026
Verified-closing: #7145. #7147 (
MeshServiceProviderreplacing Autofac, merged 2026-10-25 07:48Z,2cc600e3) is running on the control instance, memex and the public instance, and Autofac is gone from every fleet repository'smainexcept comments and docs.Deployed (checked 2026-10-26 ~07:00Z)
/api/versionon memex.systemorph.com, control.systemorph.com and memex.meshweaver.cloud all report commita75a8ef4940d.git merge-base --is-ancestor 2cc600e3 a75a8ef4holds, anda75a8ef4is 103 commits after refactor(di): replace Autofac with MeshWeaver's own container, MeshServiceProvider (#7145) #7147.Ops/ActionsshowRoll memex → 3.1.11668,Roll control → 3.1.11668andRoll memex-cloud → 3.1.11668. The memex roll isstate: Done(callbacksuccess, finished 02:57Z). All three versions are past 3.1.11621./health→module_forwarders: Healthyon each (178 / 132 / 182 root forwarders, every one resolving in its module's current generation). This is theModuleOwnedRootSourcepath of the new container. The entries that are degraded arepending_module_activation/content-types, which are unrelated to DI.Per-hub cost. Measured with one harness on both sides (99 Markdown node hubs, 608
MessageHubs,dumpheap -stat -live), recorded inDoc/Architecture/PortalHeapIsHubs→ Per-hub DI and type metadata:ExternalComponentRegistration) dropped from 18,146 to 0.Doc/Architecture/HubServiceLayersis served by the control instance. No production heap dump has been taken since the roll: the newestAnalyzeDumpon control is the 2026-10-23 OOM dump (#7082), which predates the fix. So the per-hub number is the test-mesh measurement, not a production one. In production, the Autofac types that made up the 5.78 GiB block in that dump cannot exist, because the deployed build no longer references the package.Autofac across the fleet (
git grep -i autofac origin/main)a75a8ef494: no package or code reference.Directory.Packages.propsand every*.csprojare clean. What remains:Architecture.md,DebuggingNativeCrashes.md,DisposedScopeAndDyingHubs.md,PortalHeapIsHubs.md,HubServiceLayers.md,FaultClassification.md,LiveModuleUpdate.mdand a few others;.claude/skills/sigsegv/SKILL.md:122("since removed");scripts/type-forwards.allowrefactor(di): replace Autofac with MeshWeaver's own container, MeshServiceProvider (#7145) #7147 entries;src/MeshWeaver.Messaging.Contract/FaultClassifier.cs:159.0a498fcd2: no package or code reference. What remains: a doc comment insrc/Memex.Portal.Shared.Test/PlatformReferences.cs:20and historical stack frames inAI/TeardownGatedCallbacks.md, marked "since removed".