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

SaveChangesAsync doesn't preserve the sync context across retries #26763

Closed
roji opened this issue Nov 19, 2021 · 15 comments · Fixed by #26953
Closed

SaveChangesAsync doesn't preserve the sync context across retries #26763

roji opened this issue Nov 19, 2021 · 15 comments · Fixed by #26953
Assignees
Labels
area-save-changes closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Milestone

Comments

@roji
Copy link
Member

roji commented Nov 19, 2021

In this Helix build we have the following test failure, seems to be flaky:

    Microsoft.EntityFrameworkCore.Query.QueryBugsTest.SaveChangesAsync_accepts_changes_with_ConfigureAwait_true_22841 [FAIL]
      Assert.True() Failure
      Expected: True
      Actual:   False
      Stack Trace:
        D:\a\_work\1\s\test\EFCore.SqlServer.FunctionalTests\Query\QueryBugsTest.cs(9132,0): at Microsoft.EntityFrameworkCore.Query.QueryBugsTest.SaveChangesAsync_accepts_changes_with_ConfigureAwait_true_22841()
        --- End of stack trace from previous location ---
@ajcvickers
Copy link
Member

@roji Disable the test for now?

@roji
Copy link
Member Author

roji commented Nov 19, 2021

I think we've only seen it fail like once (AFAIK), will try to take a look at it in the next few days, if it fails again we can disable for sure...

@ajcvickers
Copy link
Member

@roji I am probably missing something, but it looks to me like SaveChangeAsync always uses ConfigureAwait(false).

var entitiesSaved = interceptionResult.HasResult
    ? interceptionResult.Result
    : await DbContextDependencies.StateManager
        .SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken)
        .ConfigureAwait(false);

var entitiesSaved = interceptionResult.HasResult

@roji
Copy link
Member Author

roji commented Nov 23, 2021

@ajcvickers as discussed offline, ConfigureAwait controls where the continuation runs, i.e. the code after SaveChangesAsync - and we don't really care about that.

@vonzshik
Copy link
Contributor

vonzshik commented Dec 8, 2021

@roji from the looks of it, SingleThreadSynchronizationContext22841 has the same issue SingleThreadSynchronizationContext had in npgsql (npgsql/npgsql/issues/3856).

@roji
Copy link
Member Author

roji commented Dec 8, 2021

@vonzshik you're right - I just discovered that on my own; though that's just a test issue (note also that the test asserts on the current thread, rather than on the current sync context).

Here's what I think is going on... Under normal circumstances, everything does work just fine; before AcceptAllChanges is called (the method that jumps into user code), all code is either synchronous, or asynchronous with ConfigureAwait(true).

However, if we retry because of an actual error, then the 2nd attempt by the execution strategy will have lost the synchronization context, since the 1st attempt will have been called with ConfigureAwait(false). This would also explain the flakiness of the test (as opposed to it always passing/failing) - we indeed have occasional errors when running our tests in the CI environment, so I'm thinking that in those cases the test failed.

@roji roji changed the title Test SaveChangesAsync_accepts_changes_with_ConfigureAwait_true_22841 seems flaky SaveChangesAsync doesn't preserve the sync context across retries Dec 8, 2021
roji added a commit to roji/efcore that referenced this issue Dec 8, 2021
roji added a commit to roji/efcore that referenced this issue Dec 9, 2021
roji added a commit to roji/efcore that referenced this issue Dec 9, 2021
@roji roji added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Dec 9, 2021
@ajcvickers
Copy link
Member

ajcvickers commented Dec 14, 2021

@roji SaveChangesAsync_accepts_changes_with_ConfigureAwait_true_22841 just failed on my C.I. run.

#26996

@ajcvickers ajcvickers reopened this Dec 14, 2021
@smitpatel
Copy link
Member

Also failed in #27011

@vonzshik
Copy link
Contributor

vonzshik commented Dec 15, 2021

@roji what if the problem here is in Thread.CurrentThread == trackingSynchronizationContext.Thread check? That is, while an async version of SaveChangesAsync is called, there is no guarantee that at any point it's actually going to yield, so it's not going to execute a continuation on that thread?

If so, calling await Task.Yield() just after the context is set should help.

@roji
Copy link
Member Author

roji commented Dec 16, 2021

@vonzshik that's exactly what #27019 is supposed to address (I did it just 4 hours before your comment 😉).

However, I can't see how SaveChangesAsync wouldn't yield... it's guaranteed to insert a row into SQL Server, which is supposed to always do networking and yield... If there's some situation in which SqlClient does sync I/O instead of async, then that would explain it; I'm not aware of such a thing (and it shouldn't happen), but it's possible.

@ajcvickers
Copy link
Member

@roji It's not doing "networking" when using LocalDb. It's doing named-pipes.

@roji
Copy link
Member Author

roji commented Dec 16, 2021

AFAIK that's still I/O that's supposed to be async... Put another way, if LocalDB communication were always sync, this test would have failed consistently, rather than being flaky. But maybe under some conditions SqlClient decides to do sync I/O...

@vonzshik
Copy link
Contributor

vonzshik commented Dec 16, 2021

AFAIK that's still I/O that's supposed to be async... Put another way, if LocalDB communication were always sync, this test would have failed consistently, rather than being flaky. But maybe under some conditions SqlClient decides to do sync I/O...

Well, for example, SqlClient's login handshake is sync only (because it's called from a constructor) :trollface:

https://github.com/dotnet/SqlClient/blob/main/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs#L513

@roji
Copy link
Member Author

roji commented Dec 16, 2021

😕

I'd still hope that ExecuteReaderAsync (as opposed to logging in) would actually be async - but you're right that we can't be sure...

@roji
Copy link
Member Author

roji commented Dec 17, 2021

Keeping open for a few days to make sure we re-discuss in triage and see if the test failed after #27019.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-save-changes closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants