Repository navigation
fix: share one ProxyGenerator in AsyncInterceptorExtensions.Wrap (memory leak) - #128
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
AsyncInterceptorExtensions.Wrapcreatednew ProxyGenerator()on every call. Each generator owns its ownModuleScope, so every call emitted a new dynamic assembly with a new proxy type. Dynamic types from a non-collectibleAssemblyBuilderare never unloaded.Wemogy.Infrastructure.Database.Core5.0.0 (DatabaseRepositoryFactoryFactory) callsretryProxy.Wrap<TDatabaseRepository>(...)for every repository, and repositories are registeredAddScoped— so a consuming API emits new types on every request.Measured in the VAULO/snaatch API on dev (net8.0):
GET api/spaces→ +145 MiB, 600 ×GET api/files→ +280 MiB, not released after idleRuntimeTypeBuilder, 215,334RuntimeMethodBuilder, 436,647SignatureHelperFix
One
private static readonly ProxyGeneratorshared by both overloads. Castle caches the proxy type per interface/options in the generator's scope, andProxyGeneratoris thread-safe. Behaviour of the wrapped object is unchanged.Tests
AsyncInterceptorExtensionsTests.Wrap_ShouldReuseTheProxyTypeAcrossCalls— fails on main (Expected … Castle.Proxies.IFlakyServiceProxy, but found Castle.Proxies.IFlakyServiceProxy: same name, different type), passes with the fixWemogy.Core.Tests: 174/174 on net8.0 and net10.0After merge
A release is needed; consumers (e.g. snaatch, which references
Wemogy.Coredirectly) can bump to it without waiting for a newWemogy.Infrastructure.Databaserelease.