Skip to content
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

Merged
merged 4 commits into from
Nov 16, 2016

Conversation

anpete
Copy link
Contributor

@anpete anpete commented Nov 5, 2016

  • Initial impl. of context pooling.

@anpete
Copy link
Contributor Author

anpete commented Nov 5, 2016

@ajcvickers @AndriySvyryd

return context;
}

context = ActivatorUtilities.CreateInstance<TContext>(serviceProvider);
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@divega divega Nov 8, 2016

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();
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

@ajcvickers
Copy link
Member

Clean up transaction state? Reset command timeout?

@anpete
Copy link
Contributor Author

anpete commented Nov 7, 2016

@ajcvickers Thanks, was sure I had missed stuff. With transactions, is it just clearing IRelationalConnection.CurrentTransaction?

@ajcvickers
Copy link
Member

Not sure about the transactions. @AndriySvyryd would know.

@AndriySvyryd
Copy link
Member

Calling IRelationalConnection.Dispose should be enough to reset the transaction state

@AndriySvyryd
Copy link
Member

I think IDbContextPoolable should provide a way to subscribe to the resetting, so non-core services are able to participate

@anpete
Copy link
Contributor Author

anpete commented Nov 7, 2016

@AndriySvyryd It would be great to see an example of where this is needed to understand what the solution might look like.

@AndriySvyryd
Copy link
Member

@anpete The IRelationalConnection above for instance.

@anpete
Copy link
Contributor Author

anpete commented Nov 7, 2016

@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?

@AndriySvyryd
Copy link
Member

@anpete I believe it's because there was no real downside to this approach and it was less code

@anpete anpete force-pushed the fix-6923 branch 3 times, most recently from 6d1ac97 to 7e32823 Compare November 8, 2016 20:24
@anpete
Copy link
Contributor Author

anpete commented Nov 8, 2016

@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
Copy link
Member

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>
Copy link
Member

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;
Copy link
Member

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.

@AndriySvyryd
Copy link
Member

:shipit: contingent on @ajcvickers signoff

@ajcvickers
Copy link
Member

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.

@divega
Copy link
Contributor

divega commented Nov 8, 2016

I would prefer to sort out @ajcvickers' concerns in person. I would also like to see if we can use ActivatorUtilities.CreateFactory() to avoid some reflection/expression compilation overhead each time we actually do have to create a new context instance.

/// <returns>
/// The same service collection so that multiple calls can be chained.
/// </returns>
public static IServiceCollection AddDbContext<TContext>(
Copy link
Contributor

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.

@davidroth
Copy link
Contributor

davidroth commented Nov 10, 2016

@anpete Out of curiosity: Can you share some benchmarks about this improvement?

@smitpatel smitpatel changed the base branch from feature/1.2.0 to dev November 10, 2016 18:57
@anpete anpete force-pushed the fix-6923 branch 2 times, most recently from 11de381 to 0b1bfd1 Compare November 16, 2016 00:59
@anpete
Copy link
Contributor Author

anpete commented Nov 16, 2016

@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>)))
{
Copy link
Contributor Author

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anpete @divega Non-generic options is fine. Hopefully this makes it work with non-derived DbContext. Maybe add a test for that?

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest LGTM.

Copy link
Contributor Author

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.

Copy link
Member

@ajcvickers ajcvickers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

Copy link
Contributor

@divega divega left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@anpete anpete force-pushed the fix-6923 branch 2 times, most recently from 2f10fec to 40dd5b0 Compare November 16, 2016 19:24
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants