Skip to content

Commit 9785584

Browse files
authored
Replace per-connection execution lock with statement-level interrupts (#226)
Query timeouts previously relied on a per-connection execution lock that serialized every operation so the timer wheel's connection.interrupt() would unambiguously hit the timed-out operation. This serialized all concurrent work on a connection and added a mutex acquisition to every call. Use libsql_stmt_interrupt() instead: the timer wheel now interrupts the specific statement that timed out (via a new Interruptible trait, so the wheel can target either a statement or a connection), leaving other concurrent operations on the same connection untouched. This removes execution_lock entirely, along with acquire_execution_lock and the deadline/remaining plumbing it required. Depends on: tursodatabase/libsql#2248
2 parents d5691a8 + 368a962 commit 9785584

4 files changed

Lines changed: 179 additions & 154 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ crate-type = ["cdylib"]
1111

1212
[dependencies]
1313
glob-match = "0.2"
14-
libsql = { version = "0.10.0-pre.3", features = ["encryption"] }
14+
libsql = { version = "0.10.0-pre.4", features = ["encryption"] }
1515
napi = { version = "2", default-features = false, features = ["napi6", "tokio_rt", "async"] }
1616
napi-derive = "2"
1717
once_cell = "1.18.0"

integration-tests/tests/async.test.js

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -548,32 +548,35 @@ test.serial("Timeout on Statement.get() does not leak into later prepare()/EXPLA
548548
db.close();
549549
});
550550

551-
test.serial("Query timeout covers wait time on the execution lock", async (t) => {
552-
t.timeout(15_000);
553-
const [db, errorType] = await connect(":memory:", { defaultQueryTimeout: 200 });
554-
await db.exec("CREATE TABLE t(x INTEGER)");
555-
const insert = await db.prepare("INSERT INTO t VALUES (?)");
556-
for (let i = 0; i < 5000; i++) {
557-
await insert.run(i);
558-
}
559-
const stmt = await db.prepare("SELECT * FROM t ORDER BY x ASC");
551+
test.serial("Query timeout interrupts long-running queries under concurrency", async (t) => {
552+
t.timeout(30_000);
553+
const [db] = await connect(":memory:");
554+
555+
// Run many never-ending queries concurrently, each on its own statement so
556+
// they genuinely run in parallel. Every one must be interrupted by its own
557+
// query timeout, leaving no query running.
558+
const CONCURRENCY = 20;
559+
const sql =
560+
"WITH RECURSIVE inf(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM inf) SELECT * FROM inf";
560561

561562
let interrupts = 0;
562563
const promises = [];
563-
for (let i = 0; i < 50; i++) {
564+
for (let i = 0; i < CONCURRENCY; i++) {
564565
promises.push(
565-
stmt.all().catch((err) => {
566-
if (err.code === "SQLITE_INTERRUPT") {
567-
interrupts++;
568-
} else {
569-
throw err;
570-
}
571-
})
566+
db.prepare(sql).then((stmt) =>
567+
stmt.all(undefined, { queryTimeout: 100 }).catch((err) => {
568+
if (err.code === "SQLITE_INTERRUPT") {
569+
interrupts++;
570+
} else {
571+
throw err;
572+
}
573+
})
574+
)
572575
);
573576
}
574577
await Promise.all(promises);
575578

576-
t.true(interrupts > 0, `expected some queries to timeout, got ${interrupts}`);
579+
t.is(interrupts, CONCURRENCY, `expected every query to time out, got ${interrupts}`);
577580

578581
db.close();
579582
});

0 commit comments

Comments
 (0)