Skip to content

Commit 48df5f8

Browse files
committed
Fix deadlock when fetching next chunk (1.3)
This is a backport of the PR duckdb#242 to `v1.3-ossivalis` stable branch. When connection is being closed, it closes all statements and all result sets on these statements. All this is done while holding connection lock. When fetching next chunk, result set needs to take both result set and connection locks. Because of the wrong order of taking these locks, concurrent `close()` call on result set, initiated from `Connection#close()` was causing a deadlock. This change reorders locks in `ResultSet#fetchChunk()` to fix this. Testing: new test added that reproduces the deadlock reliably (may need to raise the number of iterations). Fixes: duckdb#241
1 parent b391841 commit 48df5f8

File tree

5 files changed

+44
-10
lines changed

5 files changed

+44
-10
lines changed

src/main/java/org/duckdb/DuckDBConnection.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public void close() throws SQLException {
129129
return;
130130
}
131131

132-
// Mark this instance as 'closing' to skip untrack call in
132+
// Mark this instance as 'closing' to skip untrack logic in
133133
// prepared statements, that requires connection lock and can
134134
// cause a deadlock when the statement closure is caused by the
135135
// connection interrupt called by us.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ public void close() throws SQLException {
361361

362362
// Untrack prepared statement from parent connection,
363363
// if 'closing' flag is set it means that the parent connection itself
364-
// is being closed and we don't need to call untrack from the statement.
364+
// is being closed and we don't need to untrack this instance from the statement.
365365
if (!conn.closing) {
366366
conn.connRefLock.lock();
367367
try {

src/main/java/org/duckdb/DuckDBResultSet.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,22 +1399,25 @@ private void checkOpen() throws SQLException {
13991399
}
14001400

14011401
private DuckDBVector[] fetchChunk() throws SQLException {
1402-
// Take both result set and connection locks for fetching
1403-
resultRefLock.lock();
1402+
// Take both result set and connection locks for fetching,
1403+
// connection lock must be taken first because concurrent
1404+
// rs#close() call can be initiated from conn#close()
1405+
// that holds connection lock.
1406+
conn.connRefLock.lock();
14041407
try {
1405-
checkOpen();
1406-
conn.connRefLock.lock();
1408+
conn.checkOpen();
1409+
resultRefLock.lock();
14071410
try {
1408-
conn.checkOpen();
1411+
checkOpen();
14091412
return DuckDBNative.duckdb_jdbc_fetch(resultRef, conn.connRef);
14101413
} finally {
1411-
conn.connRefLock.unlock();
1414+
resultRefLock.unlock();
14121415
}
14131416
} catch (SQLException e) {
14141417
close();
14151418
throw e;
14161419
} finally {
1417-
resultRefLock.unlock();
1420+
conn.connRefLock.unlock();
14181421
}
14191422
}
14201423
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import java.io.File;
77
import java.sql.*;
8+
import java.util.Properties;
89
import java.util.concurrent.ExecutorService;
910
import java.util.concurrent.Executors;
1011
import java.util.concurrent.Future;
@@ -220,4 +221,34 @@ public static void test_results_close_prepared_stmt_no_crash() throws Exception
220221
}
221222
}
222223
}
224+
225+
public static void test_results_fetch_no_hang() throws Exception {
226+
ExecutorService executor = Executors.newSingleThreadExecutor();
227+
Properties config = new Properties();
228+
config.put(DuckDBDriver.JDBC_STREAM_RESULTS, true);
229+
long rowsCount = 1 << 24;
230+
int iterations = 1;
231+
for (int i = 0; i < iterations; i++) {
232+
try (Connection conn = DriverManager.getConnection(JDBC_URL, config);
233+
Statement stmt = conn.createStatement();
234+
ResultSet rs = stmt.executeQuery("SELECT i, i::VARCHAR FROM range(0, " + rowsCount + ") AS t(i)")) {
235+
executor.submit(() -> {
236+
try {
237+
Thread.sleep(100);
238+
conn.close();
239+
} catch (Exception e) {
240+
e.printStackTrace();
241+
}
242+
});
243+
long[] resultsCount = new long[1];
244+
assertThrows(() -> {
245+
while (rs.next()) {
246+
resultsCount[0]++;
247+
}
248+
}, SQLException.class);
249+
assertTrue(resultsCount[0] > 0);
250+
assertTrue(resultsCount[0] < rowsCount);
251+
}
252+
}
253+
}
223254
}

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(1000);
3458+
Thread.sleep(1500);
34593459
QueryProgress qp = stmt.getQueryProgress();
34603460
stmt.cancel();
34613461
return qp;

0 commit comments

Comments
 (0)