-
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
BeginTransaction with ReadUncommitted can cause dirty reads for out-of-transaction queries #11833
Comments
What version of Sql Server are you running? Prior to Sql 2014 transaction isolation levels can leak across pooled connections. |
SQL Server 14.0.1000.169 |
Would it be helpful if I reproduce this on Azure SQL? |
/cc @divega |
Closing old issue as this is no longer something we intend to implement. |
@ajcvickers so this is not a bug but working as intended? |
@dotnet/efcore Can someone provide more details on the reason for this being closed as won't-fix--I don't remember enough of the discussion in triage. |
Just for reference, we were a little disappointed to hear that this wasn't considered something worth resolving. We've since implemented a messy hack to help achieve what we want in our app (see remarks in the original post), but from our perspective it'd be much cleaner if ReadUncommitted were supported. Regardless of the above, the scope "leakage" behavior seems to be cause for some concern...it has the potential to cause significant side-effects on completely unrelated requests. From this perspective at least I feel that this needs some attention (even if it's dropping support for ReadUncommitted usage completely to protect against the leakage problem). |
Isn't this another occurrence of isolation level leak across pooled connections, i.e. dotnet/SqlClient#96 (which seems to still be an issue, regardless of SQL Server version)? |
@roji To take another look. |
I took another look at the scenario above, and it does seem like the root cause is dotnet/SqlClient#96, i.e. SqlClient leaking isolation levels across pooled connections.
So this doesn't seem to be related to EF Core. Until dotnet/SqlClient#96 is resolved, any application which executes a transaction in the non-default isolation level should probably reset the level back (e.g. by executing a second dummy transaction). |
@samcic you can test the above by running your same flow with pooling off - if I'm right, then it should work as expected. It would be good to get a confirmation for that. |
@roji Thanks for looking into this. I can confirm that putting |
@samcic thanks for confirming. A possibly lighter workaround would be to manually reset your isolation level back to ReadCommitted after switching to ReadUncommitted (or any other isolation level), by doing a no-op transaction. This would add a roundtrip, but fragmenting your connection pool via a different application name may be even more problematic. |
@roji Thanks very much, but I'm a little confused about how that would look exactly. Might you be kind enough to share the corresponding change to my original code sample above so I can see what you mean? I'm namely trying to understand why there wouldn't be race conditions with other requests grabbing the I'm also trying to understand what would happen if there were to be a transient issue (e.g. database server temporarily unavailable) which would mean the no-op couldn't be executed and the connection perhaps still left in |
@samcic you're right that it's a bit tricky, since EF Core automatically opens and closes connections between operations. However, you can explicitly open the connection yourself, and perform all the work plus the no-op transaction before closing it. The underlying DbConnection should never get returned to the pool until you close it, allowing you to to reset its isolation level. |
Consider the following HomeController, modified from a fresh Asp.Net Core 2.0 MVC template project in Visual Studio 2017 (15.6.6):
Steps to Reproduce the problem
Run the solution and navigate to /home/createuser1 in a browser (this will spin because of the delay waiting to commit)
Run all of the following steps while the above operation is still waiting to complete ("spinning")
Open another tab in the browser and navigate to /home/getuser1. This will return the expected empty result (because user1 hasn't yet been committed from /home/createuser1). Note also that navigating to /home/AssertReadCommitted will return "ok" (i.e. read-committed isolation for normal queries).
Open another tab in the browser and navigate to /home/createuser2. This will create another user immediately in a read-uncommitted transaction
Open another tab in the browser and navigate to /home/getuser1. This now returns the user1 record, which is unexpected because it still hasn't been committed. This implies that there has been a dirty read (I'm proposing that this is an issue). Note that navigating to /home/AssertReadCommitted will now also return an exception string showing that we have read-uncommitted isolation for normal queries.
Open another tab in the browser and navigate to /home/createuser3. This will create another user immediately in a read-committed transaction
Open another tab in the browser and navigate to /home/getuser1. Now the user1 record is not returned any more, because we're back to read-committed behavior after the creation of user3. Note also that navigating to /home/AssertReadCommitted will return "ok" (i.e. read-committed isolation for normal queries).
Remarks
I might be missing something, but it seems to me that creating a transaction with
BeginTransactionAsync
andReadUncommitted
should mean that only the queries within that transaction are affected by the isolation level. Each separate web request gets a different ApplicationDbContext instance if I'm not mistaken, but presumably they all use the same underlying "connection"/"session" to the database, hence the propagation of the transaction isolation level change to other web requests.My particular use case: I'm writing a booking application that involves payment. When someone creates a booking, I need to make sure that 1) there are no conflicts in time with other bookings, including other new bookings for which the payment might currently be being processed, and 2) the payment goes through successfully. My approach was going to be something along the lines of:
Given the above issue, I couldn't use this approach because it would mean that other normal GET/read queries might seeing uncommitted booking data (that may e.g. still fail payment), which of course I want to avoid.
I'd be grateful for any comments on whether or not this is indeed a bug, and the recommended approach for solving such scenarios.
Further technical details
EF Core version: Microsoft.AspNetCore.All 2.0.7, Microsoft.EntityFrameworkCore.Tools 2.0.2
Database Provider: Microsoft.EntityFrameworkCore.SqlServer
Operating system: Windows 10 Pro 10.0.16299
IDE: Visual Studio 2017 15.6.6
Corresponding VS Solution ZIP: TransactionsDemo.zip
The text was updated successfully, but these errors were encountered: