Skip to content

bpo-27334: roll back transaction if sqlite3 context manager fails to commit #26202

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

Merged
merged 26 commits into from
Aug 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1361e6
bpo-27334: sqlite3 context manager now rolls back if commit fails
May 15, 2021
7111bba
Add NEWS
May 17, 2021
0b63628
Add explanatory comments to test
May 17, 2021
fce98f1
PEP 8
May 17, 2021
3edde50
Prevent accidentally clearing the current exception
May 19, 2021
99c4157
... and clean up
May 19, 2021
581bc44
Use test.support.SHORT_TIMEOUT to wait for sub-process
May 19, 2021
31f43fa
Adjust comment
May 19, 2021
a66828b
Make rollback thread friendly, and use _pysqlite_seterror to check th…
May 20, 2021
cdb0d50
Remove unused variable
May 22, 2021
e5fdaf7
Dedent test script using if 1 trick
May 22, 2021
f67591c
Use test.support.TIMEOUT to adjust connection timeout
May 25, 2021
4e654d0
Address review: make news entry less low level-ish
Jun 2, 2021
98d3ada
Chain exceptions if rollback also fails
Jun 2, 2021
195edbe
Address review
Jun 2, 2021
da26486
Add managed connection helper
Jun 3, 2021
e5f9e16
Merge branch 'main' into sqlite-ctx-rollback
Jun 3, 2021
43ea594
Fix tests (cannot use managed_connect() bco. shared access
Jun 3, 2021
1d4bcf9
Revert "Fix tests (cannot use managed_connect() bco. shared access"
Jun 3, 2021
a4b388e
Fix tests (cannot use managed_connect() bco. shared access
Jun 3, 2021
b03ee62
Merge branch 'main' into sqlite-ctx-rollback
Jun 17, 2021
b4c8d39
Merge branch 'main' into sqlite-ctx-rollback
Jun 20, 2021
da65db8
Merge branch 'main' into sqlite-ctx-rollback
Jun 24, 2021
1c93e9f
Rewrite test
Jun 25, 2021
a32943c
Restore comments
Jun 27, 2021
175d4a0
Merge branch 'main' into sqlite-ctx-rollback
Aug 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 79 additions & 1 deletion Lib/sqlite3/test/dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,17 @@

import contextlib
import sqlite3 as sqlite
import subprocess
import sys
import threading
import unittest

from test.support import check_disallow_instantiation, threading_helper, bigmemtest
from test.support import (
bigmemtest,
check_disallow_instantiation,
threading_helper,
SHORT_TIMEOUT,
)
from test.support.os_helper import TESTFN, unlink


Expand Down Expand Up @@ -986,6 +992,77 @@ def test_on_conflict_replace(self):
self.assertEqual(self.cu.fetchall(), [('Very different data!', 'foo')])


class MultiprocessTests(unittest.TestCase):
CONNECTION_TIMEOUT = SHORT_TIMEOUT / 1000. # Defaults to 30 ms

def tearDown(self):
unlink(TESTFN)

def test_ctx_mgr_rollback_if_commit_failed(self):
# bpo-27334: ctx manager does not rollback if commit fails
SCRIPT = f"""if 1:
import sqlite3
def wait():
print("started")
assert "database is locked" in input()

cx = sqlite3.connect("{TESTFN}", timeout={self.CONNECTION_TIMEOUT})
cx.create_function("wait", 0, wait)
with cx:
cx.execute("create table t(t)")
try:
# execute two transactions; both will try to lock the db
cx.executescript('''
-- start a transaction and wait for parent
begin transaction;
select * from t;
select wait();
rollback;

-- start a new transaction; would fail if parent holds lock
begin transaction;
select * from t;
rollback;
''')
finally:
cx.close()
"""

# spawn child process
proc = subprocess.Popen(
[sys.executable, "-c", SCRIPT],
encoding="utf-8",
bufsize=0,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
self.addCleanup(proc.communicate)

# wait for child process to start
self.assertEqual("started", proc.stdout.readline().strip())

cx = sqlite.connect(TESTFN, timeout=self.CONNECTION_TIMEOUT)
try: # context manager should correctly release the db lock
with cx:
cx.execute("insert into t values('test')")
except sqlite.OperationalError as exc:
proc.stdin.write(str(exc))
else:
proc.stdin.write("no error")
finally:
cx.close()

# terminate child process
self.assertIsNone(proc.returncode)
try:
proc.communicate(input="end", timeout=SHORT_TIMEOUT)
except subprocess.TimeoutExpired:
proc.kill()
proc.communicate()
raise
self.assertEqual(proc.returncode, 0)


def suite():
tests = [
ClosedConTests,
Expand All @@ -995,6 +1072,7 @@ def suite():
CursorTests,
ExtensionTests,
ModuleTests,
MultiprocessTests,
OpenTests,
SqliteOnConflictTests,
ThreadTests,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The :mod:`sqlite3` context manager now performs a rollback (thus releasing the
database lock) if commit failed. Patch by Luca Citi and Erlend E. Aasland.
28 changes: 21 additions & 7 deletions Modules/_sqlite/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -1826,17 +1826,31 @@ pysqlite_connection_exit_impl(pysqlite_Connection *self, PyObject *exc_type,
PyObject *exc_value, PyObject *exc_tb)
/*[clinic end generated code: output=0705200e9321202a input=bd66f1532c9c54a7]*/
{
const char* method_name;
int commit = 0;
PyObject* result;

if (exc_type == Py_None && exc_value == Py_None && exc_tb == Py_None) {
method_name = "commit";
} else {
method_name = "rollback";
commit = 1;
result = pysqlite_connection_commit_impl(self);
}

result = PyObject_CallMethod((PyObject*)self, method_name, NULL);
if (!result) {
else {
result = pysqlite_connection_rollback_impl(self);
}

if (result == NULL) {
if (commit) {
/* Commit failed; try to rollback in order to unlock the database.
* If rollback also fails, chain the exceptions. */
PyObject *exc, *val, *tb;
PyErr_Fetch(&exc, &val, &tb);
result = pysqlite_connection_rollback_impl(self);
if (result == NULL) {
_PyErr_ChainExceptions(exc, val, tb);
}
else {
PyErr_Restore(exc, val, tb);
}
}
return NULL;
}
Py_DECREF(result);
Expand Down