-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
ExecuteAsync and RunInTransactionAsync are not safe together. #1250
Comments
I've done a bit more digging and discovered the following:
I think 4. is the problem. I've verified this by printing the connection and thread from inside WriteAsync and from in the RunInTransactionAsync callback. When you begin the transaction using RunInTransactionAsync then the ExecuteAsync call ends up using that transaction if it runs before the transaction ends (its the same connection). So calling sqlite3_changes gets the value from the update. The reason that always using a transaction works is TransactAsync gets a transactionLock as well as a connection lock, (the connection lock is skipped due to FullMutex but the transactionLock isn't) The result is that if you mix RunInTransactionAsync calls with ExecuteAsync calls then the ExecuteAsync calls can inadvertently be included in the transaction if they execute while the transaction is open. |
Could not reproduce, got it working flawlessly here: Things of note:
I think there might be an issue with the manual insert. Maybe especially when it's async. |
As a proof of concept and a further demonstration it does actually work reliably: The same code but this time doing them in bulk, batches of 1000000 records. Still not missing a single beat. |
Can you try again but exactly follow my example code and use Execute to add one at a time and not InsertAll. I actually don't care if InsertAll works, because that's not what I'm trying to do. In my use case I'm not actually doing bulk inserts, It's just that we have had this bug several times from the field and the code I posted above reliably reproduces it quickly. (in the actual app it doesn't happen very often (two reports in a year) but that's because we are not inserting/updating at such a high rate. |
InsertAll by default starts a new non async transaction so it's a completely different scenario and not relevant to this bug report. |
Read the first comment I made and see the Gist I posted there. It is not a bulk insert. The bulk insert was merely a proof of concept, as I wrote. To illustrate that even massive inserts will not cause the issue you're describing. The point was... the issue is with your code. Not with SQLite-NET nor with SQLite itself. There is no variation I tried where I could replicate the issue you're seeing. |
Your first Gist uses Insert and not Execute as per my bug report (not sure if this matter) I've created a reproduction github repo, so that it's super easy for you to use exactly the same code and setup as me: https://github.com/trampster/SqliteNetRowsChangedIssue Just clone it and do Here are my results:
That first failure took about 1 minute. The other failures where quicker, in none of the runs did it work. If you think it's a problem with how I'm using Sqlite-net then all the code is there please tell me what I'm doing wrong. |
This is your version. Albeit with random data instead of a boolean but, well, that's irrelevant. And... it's not failing. |
Please clone my repo and run it, so you are using exactly the same code and environment as me. Godot has a single threaded sycronization context which might change things, there could also be other differences Please I'm begging you to actually run the exact unmodified code from my linked reproduction repo. Just clone and dotnet run change nothing else. |
Godot would have no effect on this situation, since I am using .NET's Tasks, like you. Godot does not even have Tasks. There is no ambiguity. Anyhow, running your exact repo is indeed failing. I am analyzing it now. |
The difference?
The explanation? Honestly, I have no clue. Here's my altered version of your code: TL;DR -- For some reason, changing the BOOL is causing the failure. There is no problem with any sort of mutex lock on the database between the 2 different ASYNC calls. |
It still fails for me when setting the Name to DateTime.UtcNow as per your change. It does take a lot more iterations to fail. But this was on my work machine with a high end CPU and a fast SSD so it didn't take long. The reason it does more iterations to fail is because getting DateTime.UtcNow is a lot slower than a hardcoded bool in the sql string.
|
While actually changing the BOOL, not the String. The difference? Pulling the rowsChanged check out of the transaction. Where it never should've been in the first place, since the transaction isn't completed yet by that point. As you can see, I added a method of confirming that the number of records is exactly the same as the index (loop) counter. Data consistency would not only require that, you would also expect to see that. And, throughout this 3600 second (1 hour) run, 10 consecutive tests, it remained entirely consistent. Which means there never was any actual problem, the function always did just add 1 person. rowsChanged simply reported a different number because the transaction had not yet completed. So, I stand by my original conclusion -- There is no problem running these 2 different ASYNC methods side by side. The index and actual person counters confirm this. |
If I do an Insert (using an Execute) (which should always change 1 row) inside an async transaction and at the same time do an Update that changes all rows using ExecuteAsync. Then sometimes the Insert will return the rows changed value from the Update.
The following code reproduces it very quickly:
Interestingly if you put the Update inside a transaction it also doesn't have this problem. Which is what we have done for update as a work around.
It should be noted that the rows changed comes from a call to sqlite3_changes which happens after the query is completed.
Looking at the code ExectueAsync gets a lock on the connection. But RunInTransactionAsync gets a connection from a pool and locks on that (different connection maybe?). But I'm not sure why that would matter as the transaction should isolate it anyway.
So I'm not clear on why this is happening.
The text was updated successfully, but these errors were encountered: