Product / version: ABP Framework 10.7.0 (Volo.Abp.Core, Volo.Abp.AspNetCore). Also present on abpframework/abp dev at ec3203128f.
Severity: memory leak; blocks large integration-test suites (out-of-memory).
Summary
Volo.Abp.Internal.Telemetry.TelemetryService.AddActivityAsync subscribes two handlers on every call,
AppDomain.CurrentDomain.ProcessExit and Console.CancelKeyPress, and never removes them. Both
lambdas share one closure with the Task.Run lambda, which uses _serviceScopeFactory, so the closure
captures this. Each handler therefore roots:
static AppDomain.ProcessExit / Console.CancelKeyPress invocation list
-> TelemetryService+<>c__DisplayClass7_0
-> TelemetryService
-> AutofacServiceScopeFactory
-> root Autofac LifetimeScope (the whole container)
-> ComponentRegistry -> ... -> AbpApplicationWithExternalServiceProvider
AbpApplicationBase calls AddActivityAsync(ActivityNameConsts.ApplicationRun) once per application
at initialization. Shutdown() and Dispose() do not remove the handlers, so a disposed
application, its container and every singleton in it stay reachable until the process exits.
Why it triggers in non-web processes
Telemetry runs only when IAbpHostEnvironment.IsDevelopment(). In a process with no web host (for example an
AbpIntegratedTest<T> test host) whose module graph includes AbpAspNetCoreModule,
AbpAspNetCoreModule.PreConfigureServices sets the environment from
services.GetHostingEnvironment(). When no IWebHostEnvironment is registered, that returns an
EmptyHostingEnvironment with EnvironmentName = Development. So every integration test runs in
"Development", sends telemetry, and leaks its application.
Impact we measured
- Our host EF Core integration test assembly has about 470
AbpIntegratedTesttests, run serially in one process. - Each test's application stays on the heap, about 17–19 MB per test, growing in a straight line.
- At about 476 tests the process reaches .NET's default GC heap hard limit (75% of a 10 GiB container, 7.5 GiB). The last tests fail with
OutOfMemoryExceptionwhile the Autofac container builds. - A long-running web app pays less, but every
AddActivityAsync/AddErrorActivityAsynccall adds two more handlers that are never removed, so theProcessExitinvocation list grows for the life of the process.
Repro
- Create a module that depends on
AbpAspNetCoreModuleandAbpAutofacModule. Do not registerIWebHostEnvironment, and do not setAbp:Telemetry:IsEnabled. - In a loop:
AbpApplicationFactory.Create<TModule>(o => o.UseAutofac()),Initialize(),Shutdown(),Dispose(), then keep aWeakReferenceto the application. - Run
GC.Collect()andGC.WaitForPendingFinalizers(). EveryWeakReferenceis still alive. - A heap root analysis (ClrMD
GCRoot, ordotnet-gcdump) shows the path above. SettingAbp:Telemetry:IsEnabled=falsemakes the applications collectible.
Suggested fixes
- Do not subscribe per call. Track pending telemetry tasks in one place, for example a static or per-application collection, and wait for them once on
ProcessExit/CancelKeyPressor inOnApplicationShutdown. At the least, unsubscribe the handlers whentelemetryTaskcompletes. - Make sure the handlers capture only the task, not
this. Today they share a closure with theTask.Runlambda. - Unregister telemetry handlers in
AbpApplicationBase.Shutdown()/Dispose(). - Consider not treating the
EmptyHostingEnvironmentfallback as "Development" for telemetry, or not sending telemetry from test hosts (AbpIntegratedTest,AbpWebApplicationFactoryIntegratedTest).
Our workaround
Test configuration sets Abp:Telemetry:IsEnabled=false in our AbpIntegratedTest base class (our module test bases already did).