Skip to content

Commit 30c2932

Browse files
committed
libsql-sqlite3: Make libsql_stmt_interrupt() abort an in-flight step
libsql_stmt_interrupt() set a per-statement isInterrupted flag, but the VDBE execution loop only ever checked the connection-wide db->u1.isInterrupted. A statement already executing inside sqlite3_step() therefore ran to completion regardless of the request; the flag was only observed at the next step() entry. Check p->isInterrupted alongside db->u1.isInterrupted at both VDBE interrupt-check sites so an interrupt requested mid-execution aborts the running statement promptly with SQLITE_INTERRUPT, without touching the connection-wide interrupt state (other statements keep running). Set and clear the flag atomically, mirroring sqlite3_interrupt(), since the request may come from another thread. Tests: - test/interruptstmt.test: deterministic regression via a new sqlite_stmt_interrupt_count test hook, covering the in-loop check and the connection-flag-stays-clear property. - test/interrupttest.c: standalone multi-threaded test of the real cross-thread case (interrupt a step() in flight) and statement-level granularity. Build and run with `make interrupttest`.
1 parent 61d629a commit 30c2932

8 files changed

Lines changed: 337 additions & 7 deletions

File tree

libsql-sqlite3/Makefile.in

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,6 +1682,11 @@ threadtest: threadtest3$(TEXE)
16821682
threadtest5: sqlite3.c $(TOP)/test/threadtest5.c
16831683
$(LTLINK) $(TOP)/test/threadtest5.c sqlite3.c -o $@ $(TLIBS)
16841684

1685+
# Multi-threaded test of the libsql_stmt_interrupt() API. Builds a small
1686+
# standalone binary; run it directly to check the result.
1687+
interrupttest: sqlite3.c $(TOP)/test/interrupttest.c
1688+
$(LTLINK) $(TOP)/test/interrupttest.c sqlite3.c -o $@ $(TLIBS)
1689+
16851690
# Standard install and cleanup targets
16861691
#
16871692
liblibsql_wasm_install: liblibsql_wasm

libsql-sqlite3/main.mk

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,11 @@ threadtest3$(EXE): sqlite3.o $(THREADTEST3_SRC) $(TOP)/src/test_multiplex.c
10471047
threadtest: threadtest3$(EXE)
10481048
./threadtest3$(EXE)
10491049

1050+
# Multi-threaded test of the libsql_stmt_interrupt() API. Builds a small
1051+
# standalone binary; run it directly to check the result.
1052+
interrupttest$(EXE): sqlite3.o $(TOP)/test/interrupttest.c
1053+
$(TCCX) $(TOP)/test/interrupttest.c sqlite3.o -o $@ $(THREADLIB)
1054+
10501055
TEST_EXTENSION = $(SHPREFIX)testloadext.$(SO)
10511056
$(TEST_EXTENSION): $(TOP)/src/test_loadext.c
10521057
$(MKSHLIB) $(TOP)/src/test_loadext.c -o $(TEST_EXTENSION)

libsql-sqlite3/src/test1.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8970,6 +8970,7 @@ int Sqlitetest1_Init(Tcl_Interp *interp){
89708970
extern int sqlite3_search_count;
89718971
extern int sqlite3_found_count;
89728972
extern int sqlite3_interrupt_count;
8973+
extern int sqlite3_stmt_interrupt_count;
89738974
extern int sqlite3_open_file_count;
89748975
extern int sqlite3_sort_count;
89758976
extern int sqlite3_current_time;
@@ -9306,8 +9307,10 @@ int Sqlitetest1_Init(Tcl_Interp *interp){
93069307
(char*)&sqlite3_max_blobsize, TCL_LINK_INT);
93079308
Tcl_LinkVar(interp, "sqlite_like_count",
93089309
(char*)&sqlite3_like_count, TCL_LINK_INT);
9309-
Tcl_LinkVar(interp, "sqlite_interrupt_count",
9310+
Tcl_LinkVar(interp, "sqlite_interrupt_count",
93109311
(char*)&sqlite3_interrupt_count, TCL_LINK_INT);
9312+
Tcl_LinkVar(interp, "sqlite_stmt_interrupt_count",
9313+
(char*)&sqlite3_stmt_interrupt_count, TCL_LINK_INT);
93119314
Tcl_LinkVar(interp, "sqlite_open_file_count",
93129315
(char*)&sqlite3_open_file_count, TCL_LINK_INT);
93139316
Tcl_LinkVar(interp, "sqlite_current_time",

libsql-sqlite3/src/vdbe.c

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ int sqlite3_search_count = 0;
7272
*/
7373
#ifdef SQLITE_TEST
7474
int sqlite3_interrupt_count = 0;
75+
/*
76+
** As above, but simulates a statement-level interrupt
77+
** (libsql_stmt_interrupt()) on the statement that is currently executing,
78+
** rather than a connection-wide sqlite3_interrupt().
79+
*/
80+
int sqlite3_stmt_interrupt_count = 0;
7581
#endif
7682

7783
/*
@@ -895,7 +901,9 @@ int sqlite3VdbeExec(
895901
p->iCurrentTime = 0;
896902
assert( p->explain==0 );
897903
db->busyHandler.nBusy = 0;
898-
if( AtomicLoad(&db->u1.isInterrupted) ) goto abort_due_to_interrupt;
904+
if( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ){
905+
goto abort_due_to_interrupt;
906+
}
899907
sqlite3VdbeIOTraceSql(p);
900908
#ifdef SQLITE_DEBUG
901909
sqlite3BeginBenignMalloc();
@@ -964,6 +972,12 @@ int sqlite3VdbeExec(
964972
sqlite3_interrupt(db);
965973
}
966974
}
975+
if( sqlite3_stmt_interrupt_count>0 ){
976+
sqlite3_stmt_interrupt_count--;
977+
if( sqlite3_stmt_interrupt_count==0 ){
978+
libsql_stmt_interrupt((sqlite3_stmt*)p);
979+
}
980+
}
967981
#endif
968982

969983
/* Sanity checking on other operands */
@@ -1085,7 +1099,9 @@ case OP_Goto: { /* jump */
10851099
** checks on every opcode. This helps sqlite3_step() to run about 1.5%
10861100
** faster according to "valgrind --tool=cachegrind" */
10871101
check_for_interrupt:
1088-
if( AtomicLoad(&db->u1.isInterrupted) ) goto abort_due_to_interrupt;
1102+
if( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) ){
1103+
goto abort_due_to_interrupt;
1104+
}
10891105
#ifndef SQLITE_OMIT_PROGRESS_CALLBACK
10901106
/* Call the progress callback if it is configured and the required number
10911107
** of VDBE ops have been executed (either since this invocation of
@@ -9430,7 +9446,7 @@ default: { /* This is really OP_Noop, OP_Explain */
94309446
** flag.
94319447
*/
94329448
abort_due_to_interrupt:
9433-
assert( AtomicLoad(&db->u1.isInterrupted) );
9449+
assert( AtomicLoad(&db->u1.isInterrupted) || AtomicLoad(&p->isInterrupted) );
94349450
rc = SQLITE_INTERRUPT;
94359451
goto abort_due_to_error;
94369452
}

libsql-sqlite3/src/vdbeapi.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,9 @@ void libsql_stmt_interrupt(sqlite3_stmt *pStmt){
897897
(void)SQLITE_MISUSE_BKPT;
898898
return;
899899
}
900-
v->isInterrupted = 1;
900+
/* Set atomically: this may be called from a different thread than the one
901+
** executing the statement, mirroring sqlite3_interrupt(). */
902+
AtomicStore(&v->isInterrupted, 1);
901903
}
902904

903905
/*
@@ -915,7 +917,7 @@ int sqlite3_step(sqlite3_stmt *pStmt){
915917
return SQLITE_MISUSE_BKPT;
916918
}
917919
db = v->db;
918-
if( v->isInterrupted ){
920+
if( AtomicLoad(&v->isInterrupted) ){
919921
rc = SQLITE_INTERRUPT;
920922
v->rc = rc;
921923
db->errCode = rc;

libsql-sqlite3/src/vdbeaux.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3632,7 +3632,7 @@ int sqlite3VdbeReset(Vdbe *p){
36323632
#ifdef SQLITE_DEBUG
36333633
p->nWrite = 0;
36343634
#endif
3635-
p->isInterrupted = 0;
3635+
AtomicStore(&p->isInterrupted, 0);
36363636

36373637
/* Save profiling information from this VDBE run.
36383638
*/
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# 2026 libsql
2+
#
3+
# The author disclaims copyright to this source code. In place of
4+
# a legal notice, here is a blessing:
5+
#
6+
# May you do good and not evil.
7+
# May you find forgiveness for yourself and forgive others.
8+
# May you share freely, never taking more than you give.
9+
#
10+
#***********************************************************************
11+
# This file implements regression tests for the libsql_stmt_interrupt()
12+
# API, which interrupts a single prepared statement rather than every
13+
# statement on the connection the way sqlite3_interrupt() does.
14+
#
15+
# The interesting property is that an interrupt requested while a statement
16+
# is *executing* (inside sqlite3_step()) is observed from within the VDBE
17+
# loop, not merely at the next entry to sqlite3_step(). Like interrupt.test,
18+
# this is exercised deterministically via the ::sqlite_stmt_interrupt_count
19+
# test variable: when positive it is decremented once per VDBE opcode and,
20+
# on reaching zero, fires libsql_stmt_interrupt() on the running statement.
21+
22+
set testdir [file dirname $argv0]
23+
source $testdir/tester.tcl
24+
set DB [sqlite3_connection_pointer db]
25+
26+
# A table large enough that a scan/join loops through the VDBE's
27+
# check_for_interrupt label far more than 100 times, so that the small
28+
# trigger values below all land mid-execution. (Note: the queries below scan
29+
# the rows -- e.g. sum(a) -- rather than bare count(*), which the planner
30+
# satisfies with a single OP_Count opcode and never loops.)
31+
do_test interruptstmt-1.0 {
32+
execsql {
33+
CREATE TABLE t1(a);
34+
WITH RECURSIVE c(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM c WHERE i<400)
35+
INSERT INTO t1 SELECT i FROM c;
36+
SELECT count(*) FROM t1;
37+
}
38+
} {400}
39+
40+
# Run $sql with the statement-interrupt trigger armed to fire after $n VDBE
41+
# opcodes. Returns a list of {errorcode catchsql-result}.
42+
proc run_with_trigger {sql n} {
43+
set ::sqlite_stmt_interrupt_count $n
44+
set r [catchsql $sql]
45+
set ::sqlite_stmt_interrupt_count 0
46+
list [db errorcode] $r
47+
}
48+
49+
# For a statement that runs many opcodes, arming the trigger at a low count
50+
# must abort it mid-execution with SQLITE_INTERRUPT -- and must never set the
51+
# connection-wide interrupt flag (that is the whole point of doing this at the
52+
# statement level rather than with sqlite3_interrupt()).
53+
foreach {tn sql} {
54+
1 {SELECT sum(a) FROM t1}
55+
2 {SELECT count(*) FROM t1 a, t1 b}
56+
} {
57+
foreach n {1 2 5 20 100} {
58+
do_test interruptstmt-2.$tn.$n.rc {
59+
run_with_trigger $sql $n
60+
} {9 {1 interrupted}}
61+
do_test interruptstmt-2.$tn.$n.flag {
62+
sqlite3_is_interrupted $DB
63+
} 0
64+
}
65+
}
66+
67+
# With the trigger set higher than the whole program, the statement runs to
68+
# completion and returns the correct result.
69+
do_test interruptstmt-3.1 {
70+
run_with_trigger {SELECT sum(a) FROM t1} 1000000
71+
} {0 {0 80200}}
72+
do_test interruptstmt-3.2 {
73+
run_with_trigger {SELECT count(*) FROM t1 a, t1 b} 100000000
74+
} {0 {0 160000}}
75+
76+
# The checks above confirm the statement ends in SQLITE_INTERRUPT, but a
77+
# trailing sqlite3_step() would report that at step entry even if the running
78+
# step had been allowed to finish first. To prove the interrupt is honored
79+
# *mid-execution* (the in-loop check, not just the step-entry check), count how
80+
# many rows the scan actually visited via a Tcl function and confirm it stopped
81+
# well short of the full table.
82+
set ::nrows 0
83+
proc rowtick {} { incr ::nrows; return 1 }
84+
db function rowtick -argcount 0 rowtick
85+
86+
do_test interruptstmt-5.1 {
87+
set ::nrows 0
88+
set r [run_with_trigger {SELECT count(*) FROM t1 WHERE rowtick()} 50]
89+
list $r [expr {$::nrows>0 && $::nrows<400}]
90+
} {{9 {1 interrupted}} 1}
91+
92+
# Sanity check the instrument: without an interrupt the same scan visits every
93+
# row.
94+
do_test interruptstmt-5.2 {
95+
set ::nrows 0
96+
set r [run_with_trigger {SELECT count(*) FROM t1 WHERE rowtick()} 100000000]
97+
list $r $::nrows
98+
} {{0 {0 400}} 400}
99+
100+
# After all of the above the connection must be left un-interrupted, and a
101+
# fresh statement runs normally.
102+
do_test interruptstmt-4.1 {
103+
sqlite3_is_interrupted $DB
104+
} 0
105+
do_test interruptstmt-4.2 {
106+
execsql {SELECT count(*) FROM t1}
107+
} {400}
108+
109+
finish_test

0 commit comments

Comments
 (0)