Skip to content

Commit 1bee2b2

Browse files
committed
Fix concurrent SQLite connection reinitialization
Assisted-by: Codex:gpt-6
1 parent 36418c4 commit 1bee2b2

2 files changed

Lines changed: 90 additions & 15 deletions

File tree

‎crates/stdlib/src/_sqlite3.rs‎

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,21 +1105,45 @@ mod _sqlite3 {
11051105
..
11061106
} = args;
11071107

1108-
zelf.statement_cache.lock().capacity = cached_statements.max(0) as usize;
1109-
1110-
zelf.detect_types.store(detect_types, Ordering::Relaxed);
1111-
zelf.check_same_thread
1112-
.store(check_same_thread, Ordering::Relaxed);
1113-
*zelf.autocommit.lock() = autocommit;
1114-
*zelf.thread_ident.lock() = std::thread::current().id();
1115-
let _ = unsafe { zelf.isolation_level.swap(isolation_level.0) };
1116-
1117-
let previous = zelf.db.lock().replace(db);
1118-
Radium::store(&zelf.initialized, true, Ordering::Release);
1119-
// An old callback's finalizer can reenter __init__ during teardown.
1120-
// Release any database it installed without holding the Rust lock.
1121-
with_deferred_drops(|| drop(previous));
1122-
Ok(())
1108+
loop {
1109+
let mut current = zelf.db.lock();
1110+
if current.is_none() {
1111+
let previous = unsafe { zelf.isolation_level.swap(isolation_level.0.clone()) };
1112+
if previous.as_ref().is_some_and(|previous| {
1113+
!isolation_level
1114+
.0
1115+
.as_ref()
1116+
.is_some_and(|requested| previous.is(requested))
1117+
}) {
1118+
// A str subclass can finalize too. Keep the requested
1119+
// value alive, but retire other values before publication.
1120+
drop(current);
1121+
with_deferred_drops(|| drop(previous));
1122+
continue;
1123+
}
1124+
1125+
// No destructor-capable owner remains to release here.
1126+
// Publish the settings and database under the same lock.
1127+
zelf.statement_cache.lock().capacity = cached_statements.max(0) as usize;
1128+
zelf.detect_types.store(detect_types, Ordering::Relaxed);
1129+
zelf.check_same_thread
1130+
.store(check_same_thread, Ordering::Relaxed);
1131+
*zelf.autocommit.lock() = autocommit;
1132+
*zelf.thread_ident.lock() = std::thread::current().id();
1133+
*current = Some(db);
1134+
Radium::store(&zelf.initialized, true, Ordering::Release);
1135+
return Ok(());
1136+
}
1137+
drop(current);
1138+
1139+
// A concurrent initializer or finalizer installed another database.
1140+
// Finish its teardown outside the lock before trying to publish.
1141+
with_deferred_drops(|| {
1142+
Radium::store(&zelf.initialized, false, Ordering::Release);
1143+
zelf.reset_factories(vm);
1144+
zelf.drop_db(vm)
1145+
})?;
1146+
}
11231147
}
11241148
}
11251149

‎extra_tests/snippets/stdlib_sqlite.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,3 +964,54 @@ def close(resource, finished):
964964

965965

966966
blob_close_during_cursor_reset()
967+
968+
969+
def reinit_during_isolation_cleanup():
970+
import threading
971+
972+
failures = []
973+
armed = False
974+
975+
class Callback:
976+
def __call__(self, sql):
977+
pass
978+
979+
def __del__(self):
980+
try:
981+
connection.close()
982+
except Exception as exc:
983+
failures.append(exc)
984+
985+
def replace():
986+
try:
987+
connection.__init__(":memory:", check_same_thread=False, autocommit=True)
988+
connection.set_trace_callback(Callback())
989+
except Exception as exc:
990+
failures.append(exc)
991+
992+
class Level(str):
993+
def __del__(self):
994+
# CPython releases this subclass during argument conversion;
995+
# RustPython retains it until the isolation level is replaced.
996+
if armed:
997+
worker = threading.Thread(target=replace, daemon=True)
998+
worker.start()
999+
worker.join(5)
1000+
if worker.is_alive():
1001+
failures.append("reentrant initializer blocked")
1002+
1003+
connection = sqlite.connect(
1004+
":memory:", isolation_level=Level(""), check_same_thread=False
1005+
)
1006+
armed = True
1007+
try:
1008+
connection.__init__(":memory:", check_same_thread=False, autocommit=False)
1009+
assert not failures, failures
1010+
assert connection.execute("SELECT 1").fetchone() == (1,)
1011+
assert connection.autocommit is False
1012+
assert connection.in_transaction
1013+
finally:
1014+
connection.close()
1015+
1016+
1017+
reinit_during_isolation_cleanup()

0 commit comments

Comments
 (0)