diff --git a/ext/sqlite3/database.c b/ext/sqlite3/database.c index daadd74f..c096b4a7 100644 --- a/ext/sqlite3/database.c +++ b/ext/sqlite3/database.c @@ -417,8 +417,10 @@ rb_sqlite3_statement_timeout(void *context) clock_gettime(CLOCK_MONOTONIC, ¤tTime); if (!timespecisset(&ctx->stmt_deadline)) { - // Set stmt_deadline if not already set - ctx->stmt_deadline = currentTime; + struct timespec timeout; + timeout.tv_sec = ctx->stmt_timeout / 1000; + timeout.tv_nsec = (ctx->stmt_timeout % 1000) * 1000000L; + timespecadd(¤tTime, &timeout, &ctx->stmt_deadline); } else if (timespecafter(¤tTime, &ctx->stmt_deadline)) { return 1; } diff --git a/ext/sqlite3/timespec.h b/ext/sqlite3/timespec.h index 322fe758..66ee5c53 100644 --- a/ext/sqlite3/timespec.h +++ b/ext/sqlite3/timespec.h @@ -15,6 +15,15 @@ (vsp)->tv_nsec += 1000000000L; \ } \ } while (0) +#define timespecadd(tsp, usp, vsp) \ + do { \ + (vsp)->tv_sec = (tsp)->tv_sec + (usp)->tv_sec; \ + (vsp)->tv_nsec = (tsp)->tv_nsec + (usp)->tv_nsec; \ + if ((vsp)->tv_nsec >= 1000000000L) { \ + (vsp)->tv_sec++; \ + (vsp)->tv_nsec -= 1000000000L; \ + } \ + } while (0) #define timespecafter(tsp, usp) \ (((tsp)->tv_sec > (usp)->tv_sec) || \ ((tsp)->tv_sec == (usp)->tv_sec && (tsp)->tv_nsec > (usp)->tv_nsec)) diff --git a/test/test_integration_statement.rb b/test/test_integration_statement.rb index 2e180acd..934b5de3 100644 --- a/test/test_integration_statement.rb +++ b/test/test_integration_statement.rb @@ -193,18 +193,48 @@ def test_committing_tx_with_statement_active end def test_long_running_statements_get_interrupted_when_statement_timeout_set - @db.statement_timeout = 10 + @db.statement_timeout = 100 + started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) assert_raises(SQLite3::InterruptException) do @db.execute <<~SQL WITH RECURSIVE r(i) AS ( VALUES(0) UNION ALL SELECT i FROM r - LIMIT 100000 + LIMIT 10000000 ) SELECT i FROM r ORDER BY i LIMIT 1; SQL end + elapsed_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000 + assert_operator elapsed_ms, :>=, 100, "interrupted before the timeout elapsed" + assert_operator elapsed_ms, :<, 200, "interrupted too long after the timeout" + + # a fast query is not affected + assert_equal 3, @db.execute("select count(*) from foo").first.first + + # (regression) Ensure queries that finish within the timeout complete, + # even ones running many instructions (time is instructions-based, not wall call) + @db.statement_timeout = 1_000 + result = @db.execute <<~SQL + WITH RECURSIVE r(i) AS (VALUES(0) UNION ALL SELECT i+1 FROM r LIMIT 100000) + SELECT count(i) FROM r; + SQL + assert_equal 100_000, result.first.first + + # no timeout — the original interrupted query completes + @db.statement_timeout = 0 + result = @db.execute <<~SQL + WITH RECURSIVE r(i) AS ( + VALUES(0) + UNION ALL + SELECT i FROM r + LIMIT 10000000 + ) + SELECT i FROM r ORDER BY i LIMIT 1; + SQL + assert_equal [[0]], result + ensure @db.statement_timeout = 0 end end