Skip to content

Commit

Permalink
Allow DbContextFactory to use a pool
Browse files Browse the repository at this point in the history
Fixes #21247

Uses the existing pooling code, although some divergence here could result in better perf, which could be relevant for high-throughput cases like SingleQuery.

Has the same restrictions as regular pooling since the context instances are reused.
  • Loading branch information
ajcvickers committed Jun 14, 2020
1 parent 63d84c9 commit 4fa6721
Show file tree
Hide file tree
Showing 10 changed files with 570 additions and 71 deletions.
8 changes: 8 additions & 0 deletions src/EFCore/DbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,14 @@ public virtual void Dispose()

private bool DisposeSync()
{
if (_dbContextPool != null
&& _dbContextPool.IsStandalone)
{
_dbContextPool.Return(this);

return false;
}

if (_dbContextPool == null
&& !_disposed)
{
Expand Down
151 changes: 130 additions & 21 deletions src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -325,14 +325,30 @@ public static IServiceCollection AddDbContextPool<TContextService, TContextImple
Check.NotNull(serviceCollection, nameof(serviceCollection));
Check.NotNull(optionsAction, nameof(optionsAction));

AddPoolingOptions<TContextImplementation>(serviceCollection, optionsAction, poolSize);

serviceCollection.TryAddSingleton<DbContextPool<TContextImplementation>>();
serviceCollection.AddScoped<DbContextPool<TContextImplementation>.Lease>();

serviceCollection.AddScoped(
sp => (TContextService)sp.GetService<DbContextPool<TContextImplementation>.Lease>().Context);

return serviceCollection;
}

private static void AddPoolingOptions<TContext>(
IServiceCollection serviceCollection,
Action<IServiceProvider, DbContextOptionsBuilder> optionsAction, int poolSize)
where TContext : DbContext
{
if (poolSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(poolSize), CoreStrings.InvalidPoolSize);
}

CheckContextConstructors<TContextImplementation>();
CheckContextConstructors<TContext>();

AddCoreServices<TContextImplementation>(
AddCoreServices<TContext>(
serviceCollection,
(sp, ob) =>
{
Expand All @@ -344,17 +360,6 @@ public static IServiceCollection AddDbContextPool<TContextService, TContextImple
((IDbContextOptionsBuilderInfrastructure)ob).AddOrUpdateExtension(extension);
},
ServiceLifetime.Singleton);

serviceCollection.TryAddSingleton(
sp => new DbContextPool<TContextImplementation>(
sp.GetService<DbContextOptions<TContextImplementation>>()));

serviceCollection.AddScoped<DbContextPool<TContextImplementation>.Lease>();

serviceCollection.AddScoped(
sp => (TContextService)sp.GetService<DbContextPool<TContextImplementation>.Lease>().Context);

return serviceCollection;
}

/// <summary>
Expand Down Expand Up @@ -557,10 +562,10 @@ public static IServiceCollection AddDbContext<TContextService, TContextImplement
/// of given <see cref="DbContext"/> type.
/// </para>
/// <para>
/// Using this method to register a factory is recommended for Blazor applications.
/// Registering a factory instead of registering the context type directly allows for easy creation of new
/// <see cref="DbContext" /> instances.
/// This is most useful where the dependency injection scope is not aligned with the context lifetime, such as in Blazor.
/// Registering a factory is recommended for Blazor applications and other situations where the dependency
/// injection scope is not aligned with the context lifetime.
/// </para>
/// <para>
/// Use this method when using dependency injection in your application, such as with Blazor.
Expand Down Expand Up @@ -611,10 +616,10 @@ public static IServiceCollection AddDbContextFactory<TContext>(
/// of given <see cref="DbContext"/> type.
/// </para>
/// <para>
/// Using this method to register a factory is recommended for Blazor applications.
/// Registering a factory instead of registering the context type directly allows for easy creation of new
/// <see cref="DbContext" /> instances.
/// This is most useful where the dependency injection scope is not aligned with the context lifetime, such as in Blazor.
/// Registering a factory is recommended for Blazor applications and other situations where the dependency
/// injection scope is not aligned with the context lifetime.
/// </para>
/// <para>
/// Use this method when using dependency injection in your application, such as with Blazor.
Expand Down Expand Up @@ -676,10 +681,10 @@ public static IServiceCollection AddDbContextFactory<TContext, TFactory>(
/// of given <see cref="DbContext"/> type.
/// </para>
/// <para>
/// Using this method to register a factory is recommended for Blazor applications.
/// Registering a factory instead of registering the context type directly allows for easy creation of new
/// <see cref="DbContext" /> instances.
/// This is most useful where the dependency injection scope is not aligned with the context lifetime, such as in Blazor.
/// Registering a factory is recommended for Blazor applications and other situations where the dependency
/// injection scope is not aligned with the context lifetime.
/// </para>
/// <para>
/// Use this method when using dependency injection in your application, such as with Blazor.
Expand Down Expand Up @@ -738,10 +743,10 @@ public static IServiceCollection AddDbContextFactory<TContext>(
/// of given <see cref="DbContext"/> type.
/// </para>
/// <para>
/// Using this method to register a factory is recommended for Blazor applications.
/// Registering a factory instead of registering the context type directly allows for easy creation of new
/// <see cref="DbContext" /> instances.
/// This is most useful where the dependency injection scope is not aligned with the context lifetime, such as in Blazor.
/// Registering a factory is recommended for Blazor applications and other situations where the dependency
/// injection scope is not aligned with the context lifetime.
/// </para>
/// <para>
/// Use this method when using dependency injection in your application, such as with Blazor.
Expand Down Expand Up @@ -799,6 +804,8 @@ public static IServiceCollection AddDbContextFactory<TContext, TFactory>(
where TContext : DbContext
where TFactory : IDbContextFactory<TContext>
{
Check.NotNull(serviceCollection, nameof(serviceCollection));

AddCoreServices<TContext>(serviceCollection, optionsAction, lifetime);

serviceCollection.AddSingleton<IDbContextFactorySource<TContext>, DbContextFactorySource<TContext>>();
Expand All @@ -812,6 +819,108 @@ public static IServiceCollection AddDbContextFactory<TContext, TFactory>(
return serviceCollection;
}

/// <summary>
/// <para>
/// Registers an <see cref="IDbContextFactory{TContext}" /> in the <see cref="IServiceCollection" /> to create instances
/// of given <see cref="DbContext"/> type where instances are pooled for reuse.
/// </para>
/// <para>
/// Registering a factory instead of registering the context type directly allows for easy creation of new
/// <see cref="DbContext" /> instances.
/// Registering a factory is recommended for Blazor applications and other situations where the dependency
/// injection scope is not aligned with the context lifetime.
/// </para>
/// <para>
/// Use this method when using dependency injection in your application, such as with Blazor.
/// For applications that don't use dependency injection, consider creating <see cref="DbContext" />
/// instances directly with its constructor. The <see cref="DbContext.OnConfiguring" /> method can then be
/// overridden to configure a connection string and other options.
/// </para>
/// <para>
/// For more information on how to use this method, see the Entity Framework Core documentation at https://aka.ms/efdocs.
/// For more information on using dependency injection, see https://go.microsoft.com/fwlink/?LinkId=526890.
/// </para>
/// </summary>
/// <typeparam name="TContext"> The type of <see cref="DbContext" /> to be created by the factory. </typeparam>
/// <param name="serviceCollection"> The <see cref="IServiceCollection" /> to add services to. </param>
/// <param name="optionsAction">
/// <para>
/// A required action to configure the <see cref="DbContextOptions" /> for the context. When using
/// context pooling, options configuration must be performed externally; <see cref="DbContext.OnConfiguring" />
/// will not be called.
/// </para>
/// </param>
/// <param name="poolSize">
/// Sets the maximum number of instances retained by the pool.
/// </param>
/// <returns>
/// The same service collection so that multiple calls can be chained.
/// </returns>
public static IServiceCollection AddPooledDbContextFactory<TContext>(
[NotNull] this IServiceCollection serviceCollection,
[NotNull] Action<DbContextOptionsBuilder> optionsAction,
int poolSize = 128)
where TContext : DbContext
{
Check.NotNull(optionsAction, nameof(optionsAction));

return AddPooledDbContextFactory<TContext>(serviceCollection, (_, ob) => optionsAction(ob));
}

/// <summary>
/// <para>
/// Registers an <see cref="IDbContextFactory{TContext}" /> in the <see cref="IServiceCollection" /> to create instances
/// of given <see cref="DbContext"/> type where instances are pooled for reuse.
/// </para>
/// <para>
/// Registering a factory instead of registering the context type directly allows for easy creation of new
/// <see cref="DbContext" /> instances.
/// Registering a factory is recommended for Blazor applications and other situations where the dependency
/// injection scope is not aligned with the context lifetime.
/// </para>
/// <para>
/// Use this method when using dependency injection in your application, such as with Blazor.
/// For applications that don't use dependency injection, consider creating <see cref="DbContext" />
/// instances directly with its constructor. The <see cref="DbContext.OnConfiguring" /> method can then be
/// overridden to configure a connection string and other options.
/// </para>
/// <para>
/// For more information on how to use this method, see the Entity Framework Core documentation at https://aka.ms/efdocs.
/// For more information on using dependency injection, see https://go.microsoft.com/fwlink/?LinkId=526890.
/// </para>
/// </summary>
/// <typeparam name="TContext"> The type of <see cref="DbContext" /> to be created by the factory. </typeparam>
/// <param name="serviceCollection"> The <see cref="IServiceCollection" /> to add services to. </param>
/// <param name="optionsAction">
/// <para>
/// A required action to configure the <see cref="DbContextOptions" /> for the context. When using
/// context pooling, options configuration must be performed externally; <see cref="DbContext.OnConfiguring" />
/// will not be called.
/// </para>
/// </param>
/// <param name="poolSize">
/// Sets the maximum number of instances retained by the pool.
/// </param>
/// <returns>
/// The same service collection so that multiple calls can be chained.
/// </returns>
public static IServiceCollection AddPooledDbContextFactory<TContext>(
[NotNull] this IServiceCollection serviceCollection,
[NotNull] Action<IServiceProvider, DbContextOptionsBuilder> optionsAction,
int poolSize = 128)
where TContext : DbContext
{
Check.NotNull(serviceCollection, nameof(serviceCollection));
Check.NotNull(optionsAction, nameof(optionsAction));

AddPoolingOptions<TContext>(serviceCollection, optionsAction, poolSize);

serviceCollection.TryAddSingleton<DbContextPool<TContext>>();
serviceCollection.TryAddSingleton<IDbContextFactory<TContext>, PooledDbContextFactory<TContext>>();

return serviceCollection;
}

private static void AddCoreServices<TContextImplementation>(
IServiceCollection serviceCollection,
Action<IServiceProvider, DbContextOptionsBuilder> optionsAction,
Expand Down
72 changes: 43 additions & 29 deletions src/EFCore/Internal/DbContextPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ public class DbContextPool<TContext> : IDbContextPool, IDisposable, IAsyncDispos
{
private const int DefaultPoolSize = 32;

private readonly ConcurrentQueue<TContext> _pool = new ConcurrentQueue<TContext>();
private readonly ConcurrentQueue<DbContext> _pool = new ConcurrentQueue<DbContext>();
private bool? _standalone;

private readonly Func<TContext> _activator;
private readonly Func<DbContext> _activator;

private int _maxSize;
private int _count;
Expand All @@ -55,7 +56,7 @@ public Lease([NotNull] DbContextPool<TContext> contextPool)
{
_contextPool = contextPool;

Context = _contextPool.Rent();
Context = (TContext)_contextPool.Rent(standalone: false);
}

/// <summary>
Expand All @@ -70,12 +71,7 @@ void IDisposable.Dispose()
{
if (_contextPool != null)
{
if (!_contextPool.Return(Context))
{
((IDbContextPoolable)Context).SetPool(null);
Context.Dispose();
}

_contextPool.Return(Context);
_contextPool = null;
Context = null;
}
Expand All @@ -85,12 +81,7 @@ async ValueTask IAsyncDisposable.DisposeAsync()
{
if (_contextPool != null)
{
if (!_contextPool.Return(Context))
{
((IDbContextPoolable)Context).SetPool(null);
await Context.DisposeAsync().ConfigureAwait(false);
}

await _contextPool.ReturnAsync(Context).ConfigureAwait(false);
_contextPool = null;
Context = null;
}
Expand All @@ -103,7 +94,7 @@ async ValueTask IAsyncDisposable.DisposeAsync()
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public DbContextPool([NotNull] DbContextOptions options)
public DbContextPool([NotNull] DbContextOptions<TContext> options)
{
_maxSize = options.FindExtension<CoreOptionsExtension>()?.MaxPoolSize ?? DefaultPoolSize;

Expand All @@ -118,7 +109,7 @@ public DbContextPool([NotNull] DbContextOptions options)
}
}

private static Func<TContext> CreateActivator(DbContextOptions options)
private static Func<DbContext> CreateActivator(DbContextOptions<TContext> options)
{
var constructors
= typeof(TContext).GetTypeInfo().DeclaredConstructors
Expand Down Expand Up @@ -149,8 +140,12 @@ var constructors
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual TContext Rent()
public virtual DbContext Rent(bool standalone)
{
Check.DebugAssert(!_standalone.HasValue || _standalone.Value == standalone, "Pool should not switch standalone mode.");

_standalone = standalone;

if (_pool.TryDequeue(out var context))
{
Interlocked.Decrement(ref _count);
Expand All @@ -170,6 +165,7 @@ public virtual TContext Rent()
(IDbContextPoolable)context,
c => c.SnapshotConfiguration());


((IDbContextPoolable)context).SetPool(this);

return context;
Expand All @@ -181,22 +177,18 @@ public virtual TContext Rent()
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual bool Return([NotNull] TContext context)
public virtual void Return(DbContext context)
{
if (Interlocked.Increment(ref _count) <= _maxSize)
{
((IDbContextPoolable)context).ResetState();

_pool.Enqueue(context);

return true;
}

Interlocked.Decrement(ref _count);

Check.DebugAssert(_maxSize == 0 || _pool.Count <= _maxSize, $"_maxSize is {_maxSize}");

return false;
else
{
PooledReturn(context);
}
}

/// <summary>
Expand All @@ -205,15 +197,37 @@ public virtual bool Return([NotNull] TContext context)
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
DbContext IDbContextPool.Rent() => Rent();
public virtual async ValueTask ReturnAsync([NotNull] DbContext context)
{
if (Interlocked.Increment(ref _count) <= _maxSize)
{
await ((IDbContextPoolable)context).ResetStateAsync().ConfigureAwait(false);

_pool.Enqueue(context);
}
else
{
PooledReturn(context);
}
}

private void PooledReturn(DbContext context)
{
Interlocked.Decrement(ref _count);

Check.DebugAssert(_maxSize == 0 || _pool.Count <= _maxSize, $"_maxSize is {_maxSize}");

((IDbContextPoolable)context).SetPool(null);
context.Dispose();
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
bool IDbContextPool.Return(DbContext context) => Return((TContext)context);
public virtual bool IsStandalone => _standalone ?? false;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Loading

0 comments on commit 4fa6721

Please sign in to comment.