Skip to content

fix: share one ProxyGenerator in AsyncInterceptorExtensions.Wrap (memory leak) - #128

Merged
SebastianKuesters merged 1 commit into
mainfrom
fix/share-proxy-generator
Sep 17, 2026
Merged

SebastianKuesters merged 1 commit into
mainfrom
fix/share-proxy-generator

Conversation

@danielgtmn

Copy link
Copy Markdown
Member

Problem

AsyncInterceptorExtensions.Wrap created new ProxyGenerator() on every call. Each generator owns its own ModuleScope, so every call emitted a new dynamic assembly with a new proxy type. Dynamic types from a non-collectible AssemblyBuilder are never unloaded.

Wemogy.Infrastructure.Database.Core 5.0.0 (DatabaseRepositoryFactoryFactory) calls retryProxy.Wrap<TDatabaseRepository>(...) for every repository, and repositories are registered AddScoped — so a consuming API emits new types on every request.

Measured in the VAULO/snaatch API on dev (net8.0):

  • 600 × GET api/spaces → +145 MiB, 600 × GET api/files → +280 MiB, not released after idle
  • GC heap dump: 72,490 RuntimeTypeBuilder, 215,334 RuntimeMethodBuilder, 436,647 SignatureHelper
  • The API is OOM-killed at 2 GiB under E2E load

Fix

One private static readonly ProxyGenerator shared by both overloads. Castle caches the proxy type per interface/options in the generator's scope, and ProxyGenerator is thread-safe. Behaviour of the wrapped object is unchanged.

Tests

  • New AsyncInterceptorExtensionsTests.Wrap_ShouldReuseTheProxyTypeAcrossCalls — fails on main (Expected … Castle.Proxies.IFlakyServiceProxy, but found Castle.Proxies.IFlakyServiceProxy: same name, different type), passes with the fix
  • Wemogy.Core.Tests: 174/174 on net8.0 and net10.0

After merge

A release is needed; consumers (e.g. snaatch, which references Wemogy.Core directly) can bump to it without waiting for a new Wemogy.Infrastructure.Database release.

A new ProxyGenerator per call emits a new dynamic assembly and proxy type on
every Wrap. Those types cannot be unloaded, so wrapping per request leaks memory.
Wemogy.Infrastructure.Database.Core 5.0.0 wraps every scoped repository this way,
which OOM-kills the consuming API under load.
@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Wemogy.Core 60% 49% 783 ➖
Wemogy.Core 60% 49% 783 ➖
Summary 60% (1786 / 2960) 49% (542 / 1104) 1566 ➖

@danielgtmn
danielgtmn requested a balanced review from Copilot September 17, 2026 08:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused fix addresses the leak and includes appropriate regression coverage.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@SebastianKuesters
SebastianKuesters merged commit c8cd9e3 into main Sep 17, 2026
5 checks passed
@SebastianKuesters
SebastianKuesters deleted the fix/share-proxy-generator branch September 17, 2026 20:12
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.

3 participants