-
Notifications
You must be signed in to change notification settings - Fork 757
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Memory Leak in Microsoft.Extensions.Caching.Memory when handling exceptions #3533
Comments
From the source code, Microsoft.Extensions.Caching.Memory 2.1.2 (the newest version supported for ASP.NET Core 2.1 on .NET Framework) appears to have the same issue. Is the fix going to be backported to 2.1.* as well? Apart from In contrast, |
Yes, it looks like @wtgodbe is porting it to 2.1 with #3536.
Good point. In general,
|
I cannot reproduce the leak with the following program using using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
namespace CachingAsyncLeak
{
class Program
{
static void Main()
{
MainAsync().Wait();
}
static async Task MainAsync()
{
Type cacheEntryHelperType = Type.GetType(
"Microsoft.Extensions.Caching.Memory.CacheEntryHelper, Microsoft.Extensions.Caching.Memory",
throwOnError: true);
PropertyInfo scopesProperty = cacheEntryHelperType.GetProperty(
"Scopes",
BindingFlags.Static | BindingFlags.NonPublic);
var getScopes = (Func<object>)scopesProperty.GetMethod.CreateDelegate(typeof(Func<object>));
using (var cache = new MemoryCache(new MemoryCacheOptions()))
{
while (true)
{
Debug.Assert(getScopes() == null);
try
{
await cache.GetOrCreateAsync("key", Factory); // Tried .ConfigureAwait(false), too.
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
async Task<int> Factory(ICacheEntry entry)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
Debug.Assert(getScopes() != null);
throw new ApplicationException("Ouch.");
}
}
catch (ApplicationException)
{
}
}
}
}
}
} <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.1.2" />
</ItemGroup>
</Project> |
Thanks for the catch here. I believe you are right, When we have 2 caches, an "outer" cache depending on an "inner" cache. The "inner" cache entries will maintain a reference back to the "outer" cache entries through its To fix that leak, I also |
I suspect there is a third risk of leaks, if the factory starts a long-lived chain of tasks or threads whose execution contexts inherit the I don't have time to test that scenario right now. Anyway, if it really is a problem, I think it can be handled as a separate issue (https://github.com/dotnet/extensions/issues/3547), perhaps only documented rather than fixed. A fix there might involve assigning |
Duplicating dotnet/runtime#42321 in dotnet/extensions in order to allow this issue to be backported to 3.1.
Description
We are leaking objects when calling
MemoryCache.GetOrCreate
and the factory method is throwing an exception.Repro
Run the following application, take a memory snapshot, run for a while and take another snapshot
Analysis
It appears the issue is that we aren't Disposing the CacheEntry instances when an exception is being thrown:
https://github.com/dotnet/runtime/blob/33dba9518b4eb7fbc487fadc9718c408f95a826c/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/MemoryCacheExtensions.cs#L98-L112
The reason we aren't calling Dispose is to fix an issue that we were caching
null
when an Exception was being thrown. See aspnet/Caching#216.Disposing the CacheEntry is important because every time you create a CacheEntry, it gets stuck in an AsyncLocal "stack":
https://github.com/dotnet/runtime/blob/33dba9518b4eb7fbc487fadc9718c408f95a826c/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntryHelper.cs#L28-L36
and disposing it is what "pops" it off the stack:
https://github.com/dotnet/runtime/blob/33dba9518b4eb7fbc487fadc9718c408f95a826c/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntryHelper.cs#L50-L63
We should always be
Disposing
the entry. That way theScopeLease
object is always disposed, and the stack is cleared.However, we still need to fix the original problem: Don't cache
null
when an Exception happens. The reason this happens is because Disposing the CacheEntry is what "commits the entry into the cache". This method gets called from CacheEntry.Dispose:https://github.com/dotnet/runtime/blob/33dba9518b4eb7fbc487fadc9718c408f95a826c/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs#L111-L234
To fix this, we should set a flag indicating whether the
CacheEntry.Value
was ever set. If it wasn't set, we shouldn't be committing the value into the cache.cc @Tratcher @Pilchie
The text was updated successfully, but these errors were encountered: