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 13, 2020
1 parent 63d84c9 commit 558499d
Show file tree
Hide file tree
Showing 8 changed files with 556 additions and 43 deletions.
15 changes: 13 additions & 2 deletions src/EFCore/DbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public class DbContext :

private IServiceScope _serviceScope;
private IDbContextPool _dbContextPool;
private Action<DbContext> _returnToPool;
private bool _initializing;
private bool _disposed;

Expand Down Expand Up @@ -623,8 +624,9 @@ public virtual async Task<int> SaveChangesAsync(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
void IDbContextPoolable.SetPool(IDbContextPool contextPool)
void IDbContextPoolable.SetPool(IDbContextPool contextPool, Action<DbContext> returnToPool)
{
_returnToPool = returnToPool;
_dbContextPool = contextPool;
_lease = 1;
}
Expand Down Expand Up @@ -652,8 +654,9 @@ DbContextPoolConfigurationSnapshot IDbContextPoolable.SnapshotConfiguration()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
void IDbContextPoolable.Resurrect(DbContextPoolConfigurationSnapshot configurationSnapshot)
void IDbContextPoolable.Resurrect(DbContextPoolConfigurationSnapshot configurationSnapshot, Action<DbContext> returnToPool)
{
_returnToPool = returnToPool;
_disposed = false;
++_lease;

Expand Down Expand Up @@ -757,6 +760,14 @@ public virtual void Dispose()

private bool DisposeSync()
{
if (_returnToPool != null)
{
_returnToPool(this);
_returnToPool = null;

return false;
}

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

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

serviceCollection.TryAddSingleton(
sp => new DbContextPool<TContextImplementation>(
sp.GetService<DbContextOptions<TContextImplementation>>(), standalone: false));

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 +363,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 +565,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 +619,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 +684,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 +746,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 +807,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 +822,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
41 changes: 33 additions & 8 deletions src/EFCore/Internal/DbContextPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace Microsoft.EntityFrameworkCore.Internal
public class DbContextPool<TContext> : IDbContextPool, IDisposable, IAsyncDisposable
where TContext : DbContext
{
private readonly bool _standalone;
private const int DefaultPoolSize = 32;

private readonly ConcurrentQueue<TContext> _pool = new ConcurrentQueue<TContext>();
Expand Down Expand Up @@ -72,7 +73,7 @@ void IDisposable.Dispose()
{
if (!_contextPool.Return(Context))
{
((IDbContextPoolable)Context).SetPool(null);
((IDbContextPoolable)Context).SetPool(null, null);
Context.Dispose();
}

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

Expand All @@ -103,8 +104,20 @@ 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)
: this(options, standalone: true)
{
}

/// <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>
public DbContextPool([NotNull] DbContextOptions<TContext> options, bool standalone)
{
_standalone = standalone;
_maxSize = options.FindExtension<CoreOptionsExtension>()?.MaxPoolSize ?? DefaultPoolSize;

options.Freeze();
Expand All @@ -118,7 +131,7 @@ public DbContextPool([NotNull] DbContextOptions options)
}
}

private static Func<TContext> CreateActivator(DbContextOptions options)
private static Func<TContext> CreateActivator(DbContextOptions<TContext> options)
{
var constructors
= typeof(TContext).GetTypeInfo().DeclaredConstructors
Expand Down Expand Up @@ -151,13 +164,24 @@ var constructors
/// </summary>
public virtual TContext Rent()
{
var returnToPool = _standalone
? c =>
{
if (!Return((TContext)c))
{
((IDbContextPoolable)c).SetPool(null, null);
c.Dispose();
}
}
: (Action<DbContext>)null;

if (_pool.TryDequeue(out var context))
{
Interlocked.Decrement(ref _count);

Check.DebugAssert(_count >= 0, $"_count is {_count}");

((IDbContextPoolable)context).Resurrect(_configurationSnapshot);
((IDbContextPoolable)context).Resurrect(_configurationSnapshot, returnToPool);

return context;
}
Expand All @@ -170,7 +194,8 @@ public virtual TContext Rent()
(IDbContextPoolable)context,
c => c.SnapshotConfiguration());

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

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

return context;
}
Expand Down Expand Up @@ -227,7 +252,7 @@ public virtual void Dispose()

while (_pool.TryDequeue(out var context))
{
((IDbContextPoolable)context).SetPool(null);
((IDbContextPoolable)context).SetPool(null, null);
context.Dispose();
}
}
Expand All @@ -244,7 +269,7 @@ public virtual async ValueTask DisposeAsync()

while (_pool.TryDequeue(out var context))
{
((IDbContextPoolable)context).SetPool(null);
((IDbContextPoolable)context).SetPool(null, null);
await context.DisposeAsync().ConfigureAwait(false);
}
}
Expand Down
Loading

0 comments on commit 558499d

Please sign in to comment.