Skip to content

Commit e860720

Browse files
committed
Add support for query timeouts (1.3)
This is a backport of the PR duckdb#247 to `v1.3-ossivalis` stable branch. This change implements `Statement#setQueryTimeout()` method. It is implemented by scheduling a background task and calling `Statement#cancel()` when timeout expires. Timeouted statement has the same behaviour as it would be if cancelled manually - `SQLException` is thrown and the statement is closed. Timeout is applied for all `execute*` calls. For `executeBatch()` it is applied separately for every single query in a batch. Testing: new test added. Fixes: duckdb#212
1 parent d592827 commit e860720

File tree

7 files changed

+82
-8
lines changed

7 files changed

+82
-8
lines changed

src/jni/duckdb_java.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,11 @@ jobject _duckdb_jdbc_execute(JNIEnv *env, jclass, jobject stmt_ref_buf, jobjectA
240240

241241
res_ref->res = stmt_ref->stmt->Execute(duckdb_params, stream_results);
242242
if (res_ref->res->HasError()) {
243-
string error_msg = string(res_ref->res->GetError());
243+
std::string error_msg = std::string(res_ref->res->GetError());
244+
duckdb::ExceptionType error_type = res_ref->res->GetErrorType();
244245
res_ref->res = nullptr;
245-
ThrowJNI(env, error_msg.c_str());
246+
jclass exc_type = duckdb::ExceptionType::INTERRUPT == error_type ? J_SQLTimeoutException : J_SQLException;
247+
env->ThrowNew(exc_type, error_msg.c_str());
246248
return nullptr;
247249
}
248250
return env->NewDirectByteBuffer(res_ref.release(), 0);

src/jni/refs.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jmethodID J_String_getBytes;
1818
jclass J_Throwable;
1919
jmethodID J_Throwable_getMessage;
2020
jclass J_SQLException;
21+
jclass J_SQLTimeoutException;
2122

2223
jclass J_Bool;
2324
jclass J_Byte;
@@ -178,6 +179,7 @@ void create_refs(JNIEnv *env) {
178179
J_Throwable = make_class_ref(env, "java/lang/Throwable");
179180
J_Throwable_getMessage = get_method_id(env, J_Throwable, "getMessage", "()Ljava/lang/String;");
180181
J_SQLException = make_class_ref(env, "java/sql/SQLException");
182+
J_SQLTimeoutException = make_class_ref(env, "java/sql/SQLTimeoutException");
181183

182184
J_Bool = make_class_ref(env, "java/lang/Boolean");
183185
J_Byte = make_class_ref(env, "java/lang/Byte");

src/jni/refs.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ extern jmethodID J_String_getBytes;
1515
extern jclass J_Throwable;
1616
extern jmethodID J_Throwable_getMessage;
1717
extern jclass J_SQLException;
18+
extern jclass J_SQLTimeoutException;
1819

1920
extern jclass J_Bool;
2021
extern jclass J_Byte;

src/main/java/org/duckdb/DuckDBDriver.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import java.sql.SQLException;
77
import java.sql.SQLFeatureNotSupportedException;
88
import java.util.Properties;
9+
import java.util.concurrent.ScheduledThreadPoolExecutor;
10+
import java.util.concurrent.ThreadFactory;
911
import java.util.logging.Logger;
1012

1113
public class DuckDBDriver implements java.sql.Driver {
@@ -14,11 +16,16 @@ public class DuckDBDriver implements java.sql.Driver {
1416
public static final String DUCKDB_USER_AGENT_PROPERTY = "custom_user_agent";
1517
public static final String JDBC_STREAM_RESULTS = "jdbc_stream_results";
1618

19+
static final ScheduledThreadPoolExecutor scheduler;
20+
1721
static {
1822
try {
1923
DriverManager.registerDriver(new DuckDBDriver());
24+
ThreadFactory tf = r -> new Thread(r, "duckdb-query-cancel-scheduler-thread");
25+
scheduler = new ScheduledThreadPoolExecutor(1, tf);
26+
scheduler.setRemoveOnCancelPolicy(true);
2027
} catch (SQLException e) {
21-
e.printStackTrace();
28+
throw new RuntimeException(e);
2229
}
2330
}
2431

src/main/java/org/duckdb/DuckDBPreparedStatement.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static java.nio.charset.StandardCharsets.US_ASCII;
44
import static java.nio.charset.StandardCharsets.UTF_8;
5+
import static java.util.concurrent.TimeUnit.SECONDS;
56
import static org.duckdb.StatementReturnType.*;
67
import static org.duckdb.io.IOUtils.*;
78

@@ -37,6 +38,7 @@
3738
import java.util.ArrayList;
3839
import java.util.Calendar;
3940
import java.util.List;
41+
import java.util.concurrent.ScheduledFuture;
4042
import java.util.concurrent.locks.Lock;
4143
import java.util.concurrent.locks.ReentrantLock;
4244

@@ -59,6 +61,8 @@ public class DuckDBPreparedStatement implements PreparedStatement {
5961
private final List<String> batchedStatements = new ArrayList<>();
6062
private Boolean isBatch = false;
6163
private Boolean isPreparedStatement = false;
64+
private int queryTimeoutSeconds = 0;
65+
private ScheduledFuture<?> cancelQueryFuture = null;
6266

6367
public DuckDBPreparedStatement(DuckDBConnection conn) throws SQLException {
6468
if (conn == null) {
@@ -180,7 +184,14 @@ private boolean execute(boolean startTransaction) throws SQLException {
180184
startTransaction();
181185
}
182186

187+
if (queryTimeoutSeconds > 0) {
188+
cleanupCancelQueryTask();
189+
cancelQueryFuture =
190+
DuckDBDriver.scheduler.schedule(new CancelQueryTask(), queryTimeoutSeconds, SECONDS);
191+
}
192+
183193
resultRef = DuckDBNative.duckdb_jdbc_execute(stmtRef, params);
194+
cleanupCancelQueryTask();
184195
DuckDBResultSetMetaData resultMeta = DuckDBNative.duckdb_jdbc_query_result_meta(resultRef);
185196
selectResult = new DuckDBResultSet(conn, this, resultMeta, resultRef);
186197
returnsResultSet = resultMeta.return_type.equals(QUERY_RESULT);
@@ -356,6 +367,7 @@ public void close() throws SQLException {
356367
if (isClosed()) {
357368
return;
358369
}
370+
cleanupCancelQueryTask();
359371
if (selectResult != null) {
360372
selectResult.close();
361373
selectResult = null;
@@ -436,12 +448,16 @@ public void setEscapeProcessing(boolean enable) throws SQLException {
436448
@Override
437449
public int getQueryTimeout() throws SQLException {
438450
checkOpen();
439-
return 0;
451+
return queryTimeoutSeconds;
440452
}
441453

442454
@Override
443455
public void setQueryTimeout(int seconds) throws SQLException {
444456
checkOpen();
457+
if (seconds < 0) {
458+
throw new SQLException("Invalid negative timeout value: " + seconds);
459+
}
460+
this.queryTimeoutSeconds = seconds;
445461
}
446462

447463
@Override
@@ -1244,4 +1260,25 @@ private Lock getConnRefLock() throws SQLException {
12441260
throw new SQLException(e);
12451261
}
12461262
}
1263+
1264+
private void cleanupCancelQueryTask() {
1265+
if (cancelQueryFuture != null) {
1266+
cancelQueryFuture.cancel(false);
1267+
cancelQueryFuture = null;
1268+
}
1269+
}
1270+
1271+
private class CancelQueryTask implements Runnable {
1272+
@Override
1273+
public void run() {
1274+
try {
1275+
if (DuckDBPreparedStatement.this.isClosed()) {
1276+
return;
1277+
}
1278+
DuckDBPreparedStatement.this.cancel();
1279+
} catch (SQLException e) {
1280+
// suppress
1281+
}
1282+
}
1283+
}
12471284
}

src/test/java/org/duckdb/TestClosure.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ public static void test_statement_auto_closed_on_completion() throws Exception {
7777
public static void test_long_query_conn_close() throws Exception {
7878
Connection conn = DriverManager.getConnection(JDBC_URL);
7979
Statement stmt = conn.createStatement();
80-
stmt.execute("DROP TABLE IF EXISTS test_fib1");
8180
stmt.execute("CREATE TABLE test_fib1(i bigint, p double, f double)");
8281
stmt.execute("INSERT INTO test_fib1 values(1, 0, 1)");
8382
long start = System.currentTimeMillis();
@@ -108,7 +107,6 @@ public static void test_long_query_conn_close() throws Exception {
108107
public static void test_long_query_stmt_close() throws Exception {
109108
try (Connection conn = DriverManager.getConnection(JDBC_URL)) {
110109
Statement stmt = conn.createStatement();
111-
stmt.execute("DROP TABLE IF EXISTS test_fib1");
112110
stmt.execute("CREATE TABLE test_fib1(i bigint, p double, f double)");
113111
stmt.execute("INSERT INTO test_fib1 values(1, 0, 1)");
114112
long start = System.currentTimeMillis();
@@ -272,7 +270,7 @@ public static void test_stmt_can_only_cancel_self() throws Exception {
272270
ResultSet rs = stmt2.executeQuery(
273271
"WITH RECURSIVE cte AS ("
274272
+
275-
"SELECT * from test_fib1 UNION ALL SELECT cte.i + 1, cte.f, cte.p + cte.f from cte WHERE cte.i < 40000) "
273+
"SELECT * from test_fib1 UNION ALL SELECT cte.i + 1, cte.f, cte.p + cte.f from cte WHERE cte.i < 50000) "
276274
+ "SELECT avg(f) FROM cte")) {
277275
rs.next();
278276
assertTrue(rs.getDouble(1) > 0);
@@ -285,4 +283,31 @@ public static void test_stmt_can_only_cancel_self() throws Exception {
285283
assertFalse(stmt2.isClosed());
286284
}
287285
}
286+
287+
public static void test_stmt_query_timeout() throws Exception {
288+
try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) {
289+
stmt.setQueryTimeout(1);
290+
stmt.execute("CREATE TABLE test_fib1(i bigint, p double, f double)");
291+
stmt.execute("INSERT INTO test_fib1 values(1, 0, 1)");
292+
long start = System.currentTimeMillis();
293+
assertThrows(
294+
()
295+
-> stmt.executeQuery(
296+
"WITH RECURSIVE cte AS ("
297+
+
298+
"SELECT * from test_fib1 UNION ALL SELECT cte.i + 1, cte.f, cte.p + cte.f from cte WHERE cte.i < 150000) "
299+
+ "SELECT avg(f) FROM cte"),
300+
SQLTimeoutException.class);
301+
long elapsed = System.currentTimeMillis() - start;
302+
assertTrue(elapsed < 1500);
303+
assertFalse(conn.isClosed());
304+
assertTrue(stmt.isClosed());
305+
assertEquals(DuckDBDriver.scheduler.getQueue().size(), 0);
306+
}
307+
try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) {
308+
stmt.setQueryTimeout(1);
309+
assertThrows(() -> { stmt.execute("FAIL"); }, SQLException.class);
310+
assertEquals(DuckDBDriver.scheduler.getQueue().size(), 0);
311+
}
312+
}
288313
}

src/test/java/org/duckdb/TestDuckDBJDBC.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3455,7 +3455,7 @@ public static void test_query_progress() throws Exception {
34553455
@Override
34563456
public QueryProgress call() throws Exception {
34573457
try {
3458-
Thread.sleep(1500);
3458+
Thread.sleep(2500);
34593459
QueryProgress qp = stmt.getQueryProgress();
34603460
stmt.cancel();
34613461
return qp;

0 commit comments

Comments
 (0)