Skip to content

Commit 2a80893

Browse files
author
Erlend Egeberg Aasland
authored
[3.10] bpo-27334: roll back transaction if sqlite3 context manager fails to commit (GH-26202) (GH-27943)
1 parent 81fa08c commit 2a80893

File tree

3 files changed

+98
-8
lines changed

3 files changed

+98
-8
lines changed

Lib/sqlite3/test/dbapi.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@
2020
# misrepresented as being the original software.
2121
# 3. This notice may not be removed or altered from any source distribution.
2222

23+
import subprocess
2324
import threading
2425
import unittest
2526
import sqlite3 as sqlite
2627
import sys
2728

28-
from test.support import check_disallow_instantiation
29+
from test.support import check_disallow_instantiation, SHORT_TIMEOUT
2930
from test.support.os_helper import TESTFN, unlink
3031

3132

@@ -958,6 +959,77 @@ def test_on_conflict_replace(self):
958959
self.assertEqual(self.cu.fetchall(), [('Very different data!', 'foo')])
959960

960961

962+
class MultiprocessTests(unittest.TestCase):
963+
CONNECTION_TIMEOUT = SHORT_TIMEOUT / 1000. # Defaults to 30 ms
964+
965+
def tearDown(self):
966+
unlink(TESTFN)
967+
968+
def test_ctx_mgr_rollback_if_commit_failed(self):
969+
# bpo-27334: ctx manager does not rollback if commit fails
970+
SCRIPT = f"""if 1:
971+
import sqlite3
972+
def wait():
973+
print("started")
974+
assert "database is locked" in input()
975+
976+
cx = sqlite3.connect("{TESTFN}", timeout={self.CONNECTION_TIMEOUT})
977+
cx.create_function("wait", 0, wait)
978+
with cx:
979+
cx.execute("create table t(t)")
980+
try:
981+
# execute two transactions; both will try to lock the db
982+
cx.executescript('''
983+
-- start a transaction and wait for parent
984+
begin transaction;
985+
select * from t;
986+
select wait();
987+
rollback;
988+
989+
-- start a new transaction; would fail if parent holds lock
990+
begin transaction;
991+
select * from t;
992+
rollback;
993+
''')
994+
finally:
995+
cx.close()
996+
"""
997+
998+
# spawn child process
999+
proc = subprocess.Popen(
1000+
[sys.executable, "-c", SCRIPT],
1001+
encoding="utf-8",
1002+
bufsize=0,
1003+
stdin=subprocess.PIPE,
1004+
stdout=subprocess.PIPE,
1005+
)
1006+
self.addCleanup(proc.communicate)
1007+
1008+
# wait for child process to start
1009+
self.assertEqual("started", proc.stdout.readline().strip())
1010+
1011+
cx = sqlite.connect(TESTFN, timeout=self.CONNECTION_TIMEOUT)
1012+
try: # context manager should correctly release the db lock
1013+
with cx:
1014+
cx.execute("insert into t values('test')")
1015+
except sqlite.OperationalError as exc:
1016+
proc.stdin.write(str(exc))
1017+
else:
1018+
proc.stdin.write("no error")
1019+
finally:
1020+
cx.close()
1021+
1022+
# terminate child process
1023+
self.assertIsNone(proc.returncode)
1024+
try:
1025+
proc.communicate(input="end", timeout=SHORT_TIMEOUT)
1026+
except subprocess.TimeoutExpired:
1027+
proc.kill()
1028+
proc.communicate()
1029+
raise
1030+
self.assertEqual(proc.returncode, 0)
1031+
1032+
9611033
def suite():
9621034
tests = [
9631035
ClosedConTests,
@@ -967,6 +1039,7 @@ def suite():
9671039
CursorTests,
9681040
ExtensionTests,
9691041
ModuleTests,
1042+
MultiprocessTests,
9701043
SqliteOnConflictTests,
9711044
ThreadTests,
9721045
UninitialisedConnectionTests,
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: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,17 +1871,32 @@ pysqlite_connection_exit_impl(pysqlite_Connection *self, PyObject *exc_type,
18711871
PyObject *exc_value, PyObject *exc_tb)
18721872
/*[clinic end generated code: output=0705200e9321202a input=bd66f1532c9c54a7]*/
18731873
{
1874-
const char* method_name;
1874+
int commit = 0;
18751875
PyObject* result;
18761876

18771877
if (exc_type == Py_None && exc_value == Py_None && exc_tb == Py_None) {
1878-
method_name = "commit";
1879-
} else {
1880-
method_name = "rollback";
1878+
commit = 1;
1879+
result = pysqlite_connection_commit_impl(self);
18811880
}
1882-
1883-
result = PyObject_CallMethod((PyObject*)self, method_name, NULL);
1884-
if (!result) {
1881+
else {
1882+
result = pysqlite_connection_rollback_impl(self);
1883+
}
1884+
1885+
if (result == NULL) {
1886+
if (commit) {
1887+
/* Commit failed; try to rollback in order to unlock the database.
1888+
* If rollback also fails, chain the exceptions. */
1889+
PyObject *exc, *val, *tb;
1890+
PyErr_Fetch(&exc, &val, &tb);
1891+
result = pysqlite_connection_rollback_impl(self);
1892+
if (result == NULL) {
1893+
_PyErr_ChainExceptions(exc, val, tb);
1894+
}
1895+
else {
1896+
Py_DECREF(result);
1897+
PyErr_Restore(exc, val, tb);
1898+
}
1899+
}
18851900
return NULL;
18861901
}
18871902
Py_DECREF(result);

0 commit comments

Comments
 (0)