Skip to content

Per-hub Autofac child scopes cost ~55 KB per hub: replace Autofac with the standard container plus a per-hub overlay #7145

Closed

Description

@rbuergi

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):

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's IMessageHub).
    • 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 an IRegistrationSource), ModuleOwnedRootSource, ModuleContexts / ModuleServiceForwarders (take an ILifetimeScope caller), ModuleServices (reads IComponentRegistry, 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 for ILifetimeScope / Autofac before any public surface goes.

Plan

  1. Build the hub overlay behind SetupModules, with the root still on Autofac.
  2. Port the module machinery off ILifetimeScope / IComponentRegistry onto the overlay's own hooks.
  3. 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 the Pairs-with: / Implementers: declarations the gates ask for.

Not established

Activity

  1. added
    sev:HBLOCKING: primary path broken, intermittent user-visible failure, or silently wrong results.
    on Oct 24, 2026
  2. added 2 commits that reference this issue on Oct 25, 2026
  3. rbuergi commented on Oct 26, 2026

    @rbuergi
    ContributorAuthor

    Verified-closing: #7145. #7147 (MeshServiceProvider replacing 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's main except comments and docs.

    Deployed (checked 2026-10-26 ~07:00Z)

    • /api/version on memex.systemorph.com, control.systemorph.com and memex.meshweaver.cloud all report commit a75a8ef4940d. git merge-base --is-ancestor 2cc600e3 a75a8ef4 holds, and a75a8ef4 is 103 commits after refactor(di): replace Autofac with MeshWeaver's own container, MeshServiceProvider (#7145) #7147.
    • The control instance's Ops/Actions show Roll memex → 3.1.11668, Roll control → 3.1.11668 and Roll memex-cloud → 3.1.11668. The memex roll is state: Done (callback success, finished 02:57Z). All three versions are past 3.1.11621.
    • All three portals serve. /health → module_forwarders: Healthy on each (178 / 132 / 182 root forwarders, every one resolving in its module's current generation). This is the ModuleOwnedRootSource path of the new container. The entries that are degraded are pending_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 in Doc/Architecture/PortalHeapIsHubs → Per-hub DI and type metadata:

    • DI-container rows dropped from 27.1 MB (44.5 KB/hub) to 1.6 MB (2.7 KB/hub).
    • Wrapper registrations (ExternalComponentRegistration) dropped from 18,146 to 0.
    • Live heap dropped from 150.2 MB to 119.0 MB.

    Doc/Architecture/HubServiceLayers is served by the control instance. No production heap dump has been taken since the roll: the newest AnalyzeDump on 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)

    • Core a75a8ef494: no package or code reference. Directory.Packages.props and every *.csproj are clean. What remains:
      • history and explanation in docs: Architecture.md, DebuggingNativeCrashes.md, DisposedScopeAndDyingHubs.md, PortalHeapIsHubs.md, HubServiceLayers.md, FaultClassification.md, LiveModuleUpdate.md and a few others;
      • .claude/skills/sigsegv/SKILL.md:122 ("since removed");
      • the inert scripts/type-forwards.allow refactor(di): replace Autofac with MeshWeaver's own container, MeshServiceProvider (#7145) #7147 entries;
      • one XML-doc comment in src/MeshWeaver.Messaging.Contract/FaultClassifier.cs:159.
    • MeshWeaver.Plugins 0a498fcd2: no package or code reference. What remains: a doc comment in src/Memex.Portal.Shared.Test/PlatformReferences.cs:20 and historical stack frames in AI/TeardownGatedCallbacks.md, marked "since removed".
    • Memex, Education, Reinsurance, SocialMedia, Manufacturing, Crm, Deploy: zero hits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    sev:HBLOCKING: primary path broken, intermittent user-visible failure, or silently wrong results.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions