-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
PERF: Fix #6923 - Implement DbContext pooling for high scale scenarios. #6950
Conversation
anpete
commented
Nov 5, 2016
- Initial impl. of context pooling.
return context; | ||
} | ||
|
||
context = ActivatorUtilities.CreateInstance<TContext>(serviceProvider); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably has the same issues as #6826
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a DI bug? I can't think of any other way to do this - We can't call GetService here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is a D.I. bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, it looks like we should validate that the context type does not have a default ctor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this is a symptom of a wider issue. The DbContext class is being registered in D.I. as a scoped service, but it isn't really used that way because of the pool. So what happens if it depends on other scoped services? How does it get the correct instance of those services?
Two other architectures to consider:
- Make the pool explicit such that other services (e.g. controllers) depend on the pool and grab a context from the pool when needed. The DbContext would never be registered in D.I. at all.
- Be like DbConnection where the instance of DbContext itself is created by D,I, in the normal way, so it gets correctly injected other services, but the guts of DbContext is obtained from a pool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still want scoped here because we want still want context-per-request semantics. Its just that with pooling, a request defines the scope of exclusive ownership of a pooled instance, not the physical lifetime of the instance. Regarding "correct" instances, this is just the reset problem. Either, the scoped service is safe to re-use (because it is stateless and the scoped-ness is an artifact of our current architecture); or, we need a safe way of resetting the existing instance - like we do with the StateManager for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we leverage ActivactorUtilities.CreateFactory()
here?
{ | ||
// Race to set this | ||
_takenSnapshot = true; | ||
_configurationSnapshot = ((IDbContextPoolable)context).SnapshotConfiguration(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this need memory barrier protection?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate? The intent of this is we might end up calling Snapshot multiple times, but we don't care which one we end up with - they should all be equivalent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we guarantee that the object has been fully constructed when viewed from another thread? Pretty sure this needs some form of memory barrier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Would changing the snapshot to be a class and removing the flag be a better option?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see anyway to do it without a memory barrier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, so another thread could see _configurationSnapshot != null even though _configurationSnapshot was not fully constructed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes
Clean up transaction state? Reset command timeout? |
@ajcvickers Thanks, was sure I had missed stuff. With transactions, is it just clearing IRelationalConnection.CurrentTransaction? |
Not sure about the transactions. @AndriySvyryd would know. |
Calling |
I think |
@AndriySvyryd It would be great to see an example of where this is needed to understand what the solution might look like. |
@anpete The |
@AndriySvyryd Great! Will take a look. BTW, it looks like it is safe to call RelationalConnection.Dispose but then keep using it?! Is this by design? |
@anpete I believe it's because there was no real downside to this approach and it was less code |
6d1ac97
to
7e32823
Compare
@AndriySvyryd Added provider services reset in latest commit. |
/// <param name="serviceCollection"> The <see cref="IServiceCollection" /> to add services to. </param> | ||
/// <param name="optionsAction"> | ||
/// <para> | ||
/// An required action to configure the <see cref="DbContextOptions" /> for the context. When using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in "An"
/// <see cref="IServiceProvider" /> | ||
/// for internal Entity Framework services. | ||
/// </para> | ||
/// </summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should mention in the summary for both overloads that polling would be enabled so no state should be maintained on the context and OnConfiguring
will not be called
public class DbContextPool<TContext> : IDbContextPool, IDisposable | ||
where TContext : DbContext | ||
{ | ||
private const int DefaultPoolSize = 32; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Different hardware configurations need different values, so it would be more useful if this was based on ThreadPool.GetMinThreads
, though with current API it would be rarely used.
contingent on @ajcvickers signoff |
I'm still not happy with the use of scoped services like this. Feels like pit of failure to me since you have to understand the scoped-per-request-but-not-really semantics. If @divega is fine with it, then he's the boss. To me it feels like there are better architectural options. |
I would prefer to sort out @ajcvickers' concerns in person. I would also like to see if we can use |
/// <returns> | ||
/// The same service collection so that multiple calls can be chained. | ||
/// </returns> | ||
public static IServiceCollection AddDbContext<TContext>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing to chat about: Is it better to add these new overloads of AddDbContext<TContext>()
that take poolSize
or instead have a new method, e.g. AddDbContextPool<TContext>()
that makes it a bit clearer that you are forking from the regular behavior of AddDbContext
, for which poolSize
is optional (or not specified in all overloads) so that we can pick a good default for you, and for which we can also have very specific and separate documentation/xml comments.
@anpete Out of curiosity: Can you share some benchmarks about this improvement? |
11de381
to
0b1bfd1
Compare
@ajcvickers @AndriySvyryd @divega Added a commit which I think addresses the decisions from today. Let me know if I missed something. |
if (parameters.Length == 1 | ||
&& (parameters[0].ParameterType == typeof(DbContextOptions) | ||
|| parameters[0].ParameterType == typeof(DbContextOptions<TContext>))) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ajcvickers Spoke to @divega about allowing non-generic options here and he felt it was safe. Any concerns?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ajcvickers Added the test. Had to loosen the ctor validation logic slightly to ignore NonPublic ctors.
[NotNull] this IServiceCollection serviceCollection, | ||
[NotNull] Action<IServiceProvider, DbContextOptionsBuilder> optionsAction, | ||
int poolSize = 128) | ||
where TContext : DbContext |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming we can and it makes sense to use the API that @AndriySvyryd had suggested for an optimal pool size, I think we should consider making it of type int? or using overloads instead of an optional parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rest LGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@divega ThreadPool.GetMinThreads is not what we want here, at least not for web apps. For a web app, what we really want is something close to the average number of concurrent requests (FYI IIS defaults to 5000 for maxConcurrentRequests for example). So I think we may just need to pick a number that is somewhat sensible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
8fc217b
to
8696cb8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2f10fec
to
40dd5b0
Compare
- Initial impl. of context pooling.
1) Creates separate registration methods for pooling: AddDbContextPool 2) Runs OnConfiguring, throws if the options were modified. 3) Adds context ctor validation. 4) No longer uses DI for context instantiation.