Skip to content

Commit 52702e8

Browse files
author
Erlend Egeberg Aasland
authored
[3.9] bpo-27334: roll back transaction if sqlite3 context manager fails to commit (GH-26202) (GH-27944)
1 parent 9d3b6b2 commit 52702e8

File tree

3 files changed

+102
-12
lines changed

3 files changed

+102
-12
lines changed

Lib/sqlite3/test/dbapi.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
# misrepresented as being the original software.
2222
# 3. This notice may not be removed or altered from any source distribution.
2323

24+
import subprocess
2425
import threading
2526
import unittest
2627
import sqlite3 as sqlite
28+
import sys
2729

28-
from test.support import TESTFN, unlink
30+
from test.support import SHORT_TIMEOUT, TESTFN, unlink
2931

3032

3133
class ModuleTests(unittest.TestCase):
@@ -954,6 +956,77 @@ def CheckOnConflictReplace(self):
954956
self.assertEqual(self.cu.fetchall(), [('Very different data!', 'foo')])
955957

956958

959+
class MultiprocessTests(unittest.TestCase):
960+
CONNECTION_TIMEOUT = SHORT_TIMEOUT / 1000. # Defaults to 30 ms
961+
962+
def tearDown(self):
963+
unlink(TESTFN)
964+
965+
def test_ctx_mgr_rollback_if_commit_failed(self):
966+
# bpo-27334: ctx manager does not rollback if commit fails
967+
SCRIPT = f"""if 1:
968+
import sqlite3
969+
def wait():
970+
print("started")
971+
assert "database is locked" in input()
972+
973+
cx = sqlite3.connect("{TESTFN}", timeout={self.CONNECTION_TIMEOUT})
974+
cx.create_function("wait", 0, wait)
975+
with cx:
976+
cx.execute("create table t(t)")
977+
try:
978+
# execute two transactions; both will try to lock the db
979+
cx.executescript('''
980+
-- start a transaction and wait for parent
981+
begin transaction;
982+
select * from t;
983+
select wait();
984+
rollback;
985+
986+
-- start a new transaction; would fail if parent holds lock
987+
begin transaction;
988+
select * from t;
989+
rollback;
990+
''')
991+
finally:
992+
cx.close()
993+
"""
994+
995+
# spawn child process
996+
proc = subprocess.Popen(
997+
[sys.executable, "-c", SCRIPT],
998+
encoding="utf-8",
999+
bufsize=0,
1000+
stdin=subprocess.PIPE,
1001+
stdout=subprocess.PIPE,
1002+
)
1003+
self.addCleanup(proc.communicate)
1004+
1005+
# wait for child process to start
1006+
self.assertEqual("started", proc.stdout.readline().strip())
1007+
1008+
cx = sqlite.connect(TESTFN, timeout=self.CONNECTION_TIMEOUT)
1009+
try: # context manager should correctly release the db lock
1010+
with cx:
1011+
cx.execute("insert into t values('test')")
1012+
except sqlite.OperationalError as exc:
1013+
proc.stdin.write(str(exc))
1014+
else:
1015+
proc.stdin.write("no error")
1016+
finally:
1017+
cx.close()
1018+
1019+
# terminate child process
1020+
self.assertIsNone(proc.returncode)
1021+
try:
1022+
proc.communicate(input="end", timeout=SHORT_TIMEOUT)
1023+
except subprocess.TimeoutExpired:
1024+
proc.kill()
1025+
proc.communicate()
1026+
raise
1027+
self.assertEqual(proc.returncode, 0)
1028+
1029+
9571030
def suite():
9581031
module_suite = unittest.makeSuite(ModuleTests, "Check")
9591032
connection_suite = unittest.makeSuite(ConnectionTests, "Check")
@@ -965,10 +1038,11 @@ def suite():
9651038
closed_cur_suite = unittest.makeSuite(ClosedCurTests, "Check")
9661039
on_conflict_suite = unittest.makeSuite(SqliteOnConflictTests, "Check")
9671040
uninit_con_suite = unittest.makeSuite(UninitialisedConnectionTests)
1041+
multiproc_con_suite = unittest.makeSuite(MultiprocessTests)
9681042
return unittest.TestSuite((
9691043
module_suite, connection_suite, cursor_suite, thread_suite,
9701044
constructor_suite, ext_suite, closed_con_suite, closed_cur_suite,
971-
on_conflict_suite, uninit_con_suite,
1045+
on_conflict_suite, uninit_con_suite, multiproc_con_suite,
9721046
))
9731047

9741048
def test():
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The :mod:`sqlite3` context manager now performs a rollback (thus releasing the
2+
database lock) if commit failed. Patch by Luca Citi and Erlend E. Aasland.

Modules/_sqlite/connection.c

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,22 +1771,36 @@ pysqlite_connection_enter(pysqlite_Connection* self, PyObject* args)
17711771
static PyObject *
17721772
pysqlite_connection_exit(pysqlite_Connection* self, PyObject* args)
17731773
{
1774-
PyObject* exc_type, *exc_value, *exc_tb;
1775-
const char* method_name;
1776-
PyObject* result;
1777-
1774+
PyObject *exc_type, *exc_value, *exc_tb;
17781775
if (!PyArg_ParseTuple(args, "OOO", &exc_type, &exc_value, &exc_tb)) {
17791776
return NULL;
17801777
}
17811778

1779+
int commit = 0;
1780+
PyObject *result;
17821781
if (exc_type == Py_None && exc_value == Py_None && exc_tb == Py_None) {
1783-
method_name = "commit";
1784-
} else {
1785-
method_name = "rollback";
1782+
commit = 1;
1783+
result = pysqlite_connection_commit(self, NULL);
17861784
}
1787-
1788-
result = PyObject_CallMethod((PyObject*)self, method_name, NULL);
1789-
if (!result) {
1785+
else {
1786+
result = pysqlite_connection_rollback(self, NULL);
1787+
}
1788+
1789+
if (result == NULL) {
1790+
if (commit) {
1791+
/* Commit failed; try to rollback in order to unlock the database.
1792+
* If rollback also fails, chain the exceptions. */
1793+
PyObject *exc, *val, *tb;
1794+
PyErr_Fetch(&exc, &val, &tb);
1795+
result = pysqlite_connection_rollback(self, NULL);
1796+
if (result == NULL) {
1797+
_PyErr_ChainExceptions(exc, val, tb);
1798+
}
1799+
else {
1800+
Py_DECREF(result);
1801+
PyErr_Restore(exc, val, tb);
1802+
}
1803+
}
17901804
return NULL;
17911805
}
17921806
Py_DECREF(result);

0 commit comments

Comments
 (0)