Skip to content
Open
Changes from all commits
Commits
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
54 changes: 54 additions & 0 deletions tests/test_suite.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,66 @@
#!/usr/bin/env python3

import os
import sqlite3
import sys
import threading
import time
import libsql
import pytest
import tempfile


def test_blocking_execute_releases_gil():
# https://github.com/tursodatabase/libsql-python/issues/113
#
# A blocked cursor.execute() must not hold the GIL, otherwise a
# Python-level timeout (e.g. concurrent.futures.Future.result(timeout=)
# from another thread) can never actually fire: the waiting thread times
# out at the OS level but then hangs trying to reacquire the GIL from the
# thread stuck inside the extension call.
#
# Here one connection holds a write lock and a second connection blocks
# on it (via sqlite's busy handler, entirely inside the Rust extension).
# A background thread ticks a counter while the main thread is blocked;
# if the GIL isn't released the ticker starves for the whole wait.
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "locked.db")
wait_seconds = 1.5

writer = libsql.connect(path, timeout=wait_seconds)
writer.execute("CREATE TABLE t (x INTEGER)")
writer.execute("BEGIN IMMEDIATE")
writer.execute("INSERT INTO t VALUES (1)")

blocked = libsql.connect(path, timeout=wait_seconds)

ticks = []
stop = threading.Event()

def ticker():
while not stop.is_set():
ticks.append(time.monotonic())
time.sleep(0.05)

ticker_thread = threading.Thread(target=ticker, daemon=True)
ticker_thread.start()
try:
with pytest.raises(Exception):
blocked.execute("INSERT INTO t VALUES (2)")
finally:
stop.set()
ticker_thread.join(timeout=5)
writer.rollback()

# With the GIL held throughout the blocking call, the ticker thread
# would be starved and record close to zero ticks during the wait.
assert len(ticks) >= 5, (
f"background thread only ticked {len(ticks)} times while "
f"cursor.execute() was blocked for ~{wait_seconds}s -- GIL was "
"likely held during the blocking call"
)


@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
def test_connection_timeout(provider):
conn = connect(provider, ":memory:", timeout=1.0)
Expand Down