Repository navigation
Sqlite cursor garbage-collection issue with Python 3.11.0b3 #94028
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on Jun 20, 2022 Thanks, again, for the report and investigation, @coleifer. FTR, I can reproduce this on macOS 12.2.1.
AFAICS (I might have missed something), this is what I believe happens1 :
Preconditions
There exists a cursor cache. A new cursor is created and cached each time an SQL statement is executed. The cache key is the SQL string. If the same SQL query is executed, the new cursor will replace the existing cursor in the cache.
The problem
Cursor 1 executes the query
select count(*) from t. Cursor 2 executes the same query (so it is reused from the LRU cache). When fetching the result from the second cursor,Noneis returned instead of a valid row.Timeline (relevant bits)
Note: This all happens in the same connection.
- Create a new cursor used for executing
select count(*) from t - This (1.) implicitly creates a new
sqlite3_stmt*forselect count(*) from t; the new statement is now in the LRU cache. - The statement is stepped once in
execute()and completed infetchone(). - Cursor is cached using the SQL string as key
- Later, a new cursor is created for executing the same query (
select count(*) from t)
No newsqlite3_stmt*is created; the existing statement is fetched from the LRU cache and assigned to the cursor. - The statement is stepped once in
execute(); it is now ready to return a result; data count is != 0. - The new cursor is put in the cache, replacing the previous cached cursor, since they use the same cache key.
The old cursor has no references anymore, so it is deallocated; its "connected" statement is reset and cleared.
Remember that the statement object is shared between the two cursors. - The new cursor executes
fetchone(), but this fails, since the data count is now 0; the statement was recently reset.
The fix in #94042 works, because it clears the current statement from the cursor when the statement is stepped through via
iternext. This means that in 7., there is no "connected" statement to reset; the cursor is deallocated, and nothing happens to the statement object in the new cursor.Footnotes
-
sorry if it comes out a little bit confusing, I've only had time for one cup of coffee ☕ ↩
- Create a new cursor used for executing
Another solution could be to modify cursor dealloc to only reset statements that are completed, but IIRC, that would (or could) break other corner cases. The best thing to do, imo, is to have the cursor clean up as soon as possible, and that is what #94042 does.
FTR, the repro does not need the custom transaction handling. Leaving
isolation_levelat default and removing the explicit BEGIN is ok.- added a commit that references this issue
on Jun 21, 2022 - added a commit that references this issue
on Jun 21, 2022 - added a commit that references this issue
on Jun 26, 2022
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Bug report
This was reported to me on coleifer/peewee#2580 and, as I've managed to reproduce the issue, was asked by @erlend-aasland to submit a ticket here. Note that this issue does not manifest on other versions of Python (2.7, 3.6, 3.9 and 3.10) -- it appears to be a new issue on 3.11.0b3
The original reporter bisected and found the following commit introduced the regression: 3df0fc8
I believe this is a cursor garbage collection issue, though I may be mistaken. Keeping the cursors around in memory seems to cause the problem to manifest, but if you comment-out the line indicated below then the tests will pass. Note that Peewee doesn't do anything weird like keep the cursors in memory, this was just the first way I've been able to successfully reproduce the issue.
The test below does the following:
What is doubly-confusing about this error is that, when the failure occurs, the SQL being executed should definitely NOT be returning
Nonefrom the call tofetchone(). If the table did not exist, we would get a different error from Sqlite. If the table does exist and we just can't see any rows, then we should be getting 0 as the test asserts. Instead, thefetchone()is returningNone.Reproduce:
On 3.11.0b3 the above fails on the indicated line with the following exception:
Your environment