Skip to content

Fix statement_timeout interrupting most queries - #737

Merged
flavorjones merged 4 commits into
sparklemotion:mainfrom
palkan:fix/statement-timeout
Oct 13, 2026
Merged

flavorjones merged 4 commits into
sparklemotion:mainfrom
palkan:fix/statement-timeout

Conversation

@palkan

@palkan palkan commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

When statement_timeout is set, the queries triggering the progress handler more than once are interrupted regardless of the timeout value: the current implementation just doesn't use the configured timeout and set the deadline to currentTime (see the diff).

Fast queries still complete (because they trigger the progress handler only once), so it may seem to work but in reality it doesn't. (I caught this bug while writing a test confirming the timeout works in my app; haven't seen in prod env though.)

(A similar bug/fix was reported/proposed earlier in #705).

@flavorjones

Copy link
Copy Markdown
Member

Thanks for the pull request. I'll review as soon as I get a chance.

Can you please consider whether we need a specific new test for this behavior? I haven't formed an opinion so your feedback would be welcome, and "no" might be a fine answer.

@palkan
palkan force-pushed the fix/statement-timeout branch from 39d5e52 to 5021744 Compare August 31, 2026 17:39
@palkan

palkan commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor Author

@flavorjones I think, we missed one check (added): setting timeout to 0 should allow the original interrupted query to complete. Added timings assertions, too (just in case our timeout calculation is not what it says).

Also, tweaked the numbers a bit to reduce flakiness (10ms timeout could be too small to trigger the second progress handler invocation and cause the interruption). (That's why a couple of jobs failed the last time.)

thibaudgg added a commit to csa-admin-org/csa-admin that referenced this pull request Sep 11, 2026
sqlite3-ruby 2.9 `statement_timeout=` installs a progress handler
every 1000 VM opcodes but never adds the millisecond budget to the
deadline. The second tick interrupts immediately, so cheap
`GROUP BY` / `WHERE state = 'inactive'` die in ~150ms with
`query_timeout`. Raising 5s to 10s changes nothing. Upstream:
sparklemotion/sqlite3-ruby#737.

`sqlite3_step` holds the GVL, so a sleeper plus `interrupt`, or
Ruby `Timeout.timeout`, cannot cancel either. There is no public
`progress_handler` API.

Until that gem ships a real budget, drop the setter. Support SELECT
is uncapped in-process; Cloudflare ~100s is the remaining limit.
@jasl

jasl commented Oct 13, 2026

Copy link
Copy Markdown

Friendly ping, this fix works

@flavorjones

Copy link
Copy Markdown
Member

@jasl I'm digging out from under a large open-source backlog, but I promise this is on my short list and I'll get to it soon!

@jasl

jasl commented Oct 13, 2026

Copy link
Copy Markdown

@jasl I'm digging out from under a large open-source backlog, but I promise this is on my short list and I'll get to it soon!

No rush! I've forked it as a temporary workaround.
Thank you, and @palkan, for your long-term contribution to the Ruby Community.

@flavorjones
flavorjones force-pushed the fix/statement-timeout branch from f96fd10 to 57f237e Compare October 13, 2026 21:13
@flavorjones

Copy link
Copy Markdown
Member

@palkan Thanks for the fix! I rebased onto main and pushed three commits to finish it up.

Your commit corrects the deadline arithmetic. The deadline was cleared only after a statement was prepared, so once an earlier execution's deadline had expired, each of these raised SQLite3::InterruptException almost immediately:

fde1678 splits the test into one test per behavior. d0c0df4 clears the deadline before a statement is prepared, when it starts executing, and before execute_batch2 runs its SQL, with a test for each case. 57f237e adds a CHANGELOG entry.

The deadline belongs to the connection, so a nested or interleaved execution on the same connection, such as a query inside a custom function, resets the outer execution's deadline. That predates this PR, so I left it alone. execute_batch2 gets one timeout for the whole call.

@flavorjones
flavorjones enabled auto-merge (squash) October 13, 2026 21:42
palkan and others added 4 commits October 13, 2026 18:03
The deadline was cleared only after a statement was prepared. Once an
earlier execution's deadline had expired, re-executing a prepared
statement, calling `Database#execute_batch2`, or preparing a statement
that reloads the schema could raise `SQLite3::InterruptException`
almost immediately. Rails caches prepared statements, so repeated
queries in Rails were affected.

Clear the deadline before a statement is prepared, when it starts
executing, and before `execute_batch2` runs its SQL.
@flavorjones
flavorjones force-pushed the fix/statement-timeout branch from 57f237e to 477ee55 Compare October 13, 2026 22:03
@flavorjones
flavorjones disabled auto-merge October 13, 2026 22:21
@flavorjones
flavorjones merged commit c35ae35 into sparklemotion:main Oct 13, 2026
@palkan

palkan commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

@flavorjones Awesome! Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants