Memory/EF issues with the way queries are created
See original GitHub issueWe’ve been getting an issue where EF Core logs the following: More than twenty 'IServiceProvider' instances have been created for internal use by Entity Framework. This is commonly caused by injection of a new singleton service instance into every DbContext instance.
. We also noticed that after running for a short period of time the process was taking 6+Gb of RAM on our server.
We think we tracked it down to the fix for issue #9, i.e. this commit: https://github.com/BrighterCommand/Darker/commit/2a07b6831e96497da955cebe0cd0b05c804c40e4
Basically, it seems that creating all those service providers is causing EF Core issues.
Our fix works for us, although it may not work in every scenario.
Initially, what we did was in the ServiceCollectionExtensions.AddDarker
method we changed the services.AddSingleton<IQueryProcessor>
line to:
services.AddSingleton<IQueryProcessor>(sp =>
{
factory.SetProvider(sp);
var queryProcessor = builder.Build();
return queryProcessor;
});
And added a SetProvider()
method to the AspNetHandlerFactory
class, which we had to extract and rename because it was internal
.
public sealed class AcHandlerFactory : IQueryHandlerFactory, IQueryHandlerDecoratorFactory
{
private IServiceProvider _services;
public void SetProvider(IServiceProvider serviceProvider)
{
if (_services != null)
throw new InvalidOperationException($"A service provider is already assigned to the {nameof(AcHandlerFactory)}.");
_services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
IQueryHandler IQueryHandlerFactory.Create(Type handlerType)
{
if (_services == null)
throw new InvalidOperationException($"A service provider must be assigned to the {nameof(AcHandlerFactory)} in advance of its use.");
var result = (IQueryHandler)_services.GetService(handlerType);
return result;
}
// ... other things in the class
}
That fixed the initial issue… But since the service provider passed to the Singleton’s new factory function isn’t scoped, anything created that is scoped, say, something working inside a web request, causes the DI framework to break when calling GetService()
on the IServiceProvider
.
So… The next step was to ensure those didn’t break either. So, I now create a new scope for every query (we’ve done this kind of thing in Brighter before, and works well in back end applications such as windows services where there is no natural scope for a single piece of work)
That changes the handler factory to this:
public sealed class AcHandlerFactory : IQueryHandlerFactory, IQueryHandlerDecoratorFactory
{
private IServiceProvider _services;
private readonly Dictionary<IQueryHandler, IServiceScope> _scopes = new Dictionary<IQueryHandler, IServiceScope>();
public AcHandlerFactory()
{
}
public void SetProvider(IServiceProvider serviceProvider)
{
if (_services != null)
throw new InvalidOperationException($"A service provider is already assigned to the {nameof(AcHandlerFactory)}.");
_services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
IQueryHandler IQueryHandlerFactory.Create(Type handlerType)
{
if (_services == null)
throw new InvalidOperationException($"A service provider must be assigned to the {nameof(AcHandlerFactory)} in advance of its use.");
var scope = _services.CreateScope();
var result = (IQueryHandler)scope.ServiceProvider.GetService(handlerType);
_scopes.Add(result, scope);
return result;
}
void IQueryHandlerFactory.Release(IQueryHandler handler)
{
if (_scopes.TryGetValue(handler, out var scope))
{
scope.Dispose();
_scopes.Remove(handler);
}
}
T IQueryHandlerDecoratorFactory.Create<T>(Type decoratorType)
{
return (T)_services.GetService(decoratorType);
}
void IQueryHandlerDecoratorFactory.Release<T>(T handler)
{
// no op
}
}
The decorator part is not scoped here, but it may be useful to do that too.
I think the handler factory here needs to be extended to provide for a wider array of use cases (or have a way to pick a factory based on a specific use case). Instantiating a new service provider on each query appears to be hard on memory, at least when coupled with EF Core.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:8 (5 by maintainers)
Top GitHub Comments
@colinangusmackay I missed registering the default decorator for the fallback policy. I’ve fixed it now with release 2.0.78. I also tweaked the registration code and have cleaned it up a lot. Always easier to look back at code a short period after writing it.
As promised, an update
Checked and confirmed two out of three applications are working well with this fix. The last application is more complex and taking more time… and that’s the one that caused us most problems with Darker previously, so I’m treading more carefully with it.