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

NonRetryingExecutionStrategy does not set ExecutionStrategy.Current and is ignored. #34953

Open
crozone opened this issue Oct 22, 2024 · 0 comments

Comments

@crozone
Copy link

crozone commented Oct 22, 2024

Background

Connection Resiliency is implemented in EF Core by setting the execution strategy used by EF Core queries. Usually .EnableRetryOnFailure() is called at startup to enable a provider specialized retrying execution strategy, which is made available in the DBContext dependencies.

For example, on the Pomelo.EntityFrameworkCore.MySql provider this is the MySqlRetryingExecutionStrategy. On Npgsql this is the NpgsqlRetryingExecutionStrategy.

Note that both of these classes derive from the abstract ExecutionStrategy class, which in turn implements the IExecutionStrategy interface.

The issue

ExecutionStrategy.Current is a static property of the abstract type ExecutionStrategy backed by an AsyncLocal that is set to the current execution strategy when ExecutionStrategy.Execute() or ExecutionStrategy.ExecuteAsync() is called, such that code within the delegate passed to the execute methods can observe the ExecutionStrategy.Current value and use it appropriately.

Unfortunately, this property can only contain the abstract ExecutionStrategy class, not an instance of the IExecutionStrategy interface.

NonRetryingExecutionStrategy only implements IExecutionStrategy. It does not derive from ExecutionStrategy. Therefore it does not, and cannot, set ExecutionStrategy.Current to itself when NonRetryingExecutionStrategy.Execute() is called, so ExecutionStrategy.Current will be null when observed within the executed delegate:

var nonRetryingExecutionStrategy = new NonRetryingExecutionStrategy(dbContext);

await nonRetryingExecutionStrategy.ExecuteAsync(async () =>
{
    // ExecutionStrategy.Current is null here!
    // Any query you run here will use the default execution strategy
    Debug.Assert(ExecutionStrategy.Current != null); // Boom!
    Debug.Assert(ExecutionStrategy.Current == nonRetryingExecutionStrategy);
}

Several features rely on ExecutionStrategy.Current, notably QueryCompilationContext.IsBuffering, which is set based on ExecutionStrategy.Current.RetriesOnFailure. If ExecutionStrategy.Current is null, it will fall back to the value given by QueryCompilationContextDependencies, which usually contains the "globally" configured execution strategy set during application startup:

IsBuffering = ExecutionStrategy.Current?.RetriesOnFailure ?? dependencies.IsRetryingExecutionStrategy;

shouldBuffer: ExecutionStrategy.Current?.RetriesOnFailure ?? Dependencies.IsRetryingExecutionStrategy);

The issue conclusion

If the user has configured a retrying execution strategy using .EnableRetryOnFailure() (eg. MySqlRetryingExecutionStrategy), but wishes to disable this execution strategy for a single query to avoid query response buffering, they will likely attempt to do so by running the query within NonRetryingExecutionStrategy.Execute().

ExecutionStrategy.Current will be set to null, and the query will instead run as if it is executed within the default retrying query strategy, which causes the query response to buffer.

Workaround

It is currently possible to work around this issue either by creating a custom execution strategy that derives from ExecutionStrategy, or by abusing the existing execution strategies. For example, MySqlRetryingExecutionStrategy can be constructed with maxRetryCount = 0, which causes it to return ExecutionStrategy.RetriesOnFailure as false.

var mySqlNonRetryingExecutionStrategy = new MySqlRetryingExecutionStrategy(dbContext, 0);

await mySqlNonRetryingExecutionStrategy.ExecuteAsync(async () =>
{
    // ExecutionStrategy.Current is successfully set to our mySqlNonRetryingExecutionStrategy instance.
    // Any code that uses ExecutionStrategy.Current?.RetriesOnFailure will read it as false.
    Debug.Assert(ExecutionStrategy.Current != null); // OK!
    Debug.Assert(ExecutionStrategy.Current == mySqlNonRetryingExecutionStrategy); // OK!
}

Include provider and version information

EF Core version: release/8.0
Target framework: .NET 8.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants