Skip to content

Commit bebd337

Browse files
committed
Close pending statements on connection close
When connection is closed without closing its statements, the JDBC spec does NOT require to close such statements automatically. But pending statements are causing problems (see #101) and having them auto-closed seems to be a reasonable expectation from a client point of view. This change adds tracking and auto-closing of statements on connection close. It also adds/improves synchronizaton on closing connections/ statements/result sets. Straighforward sync on current object is used deliberately instead of other concurrency methods to keep the synchronization impl as simple as possible. Testing: new tests are added to check auto-closing of result sets and statements. Fixes: #101
1 parent fcd50cd commit bebd337

File tree

4 files changed

+144
-29
lines changed

4 files changed

+144
-29
lines changed

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

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
import java.sql.Savepoint;
2121
import java.sql.Statement;
2222
import java.sql.Struct;
23-
import java.util.Map;
24-
import java.util.Properties;
23+
import java.util.*;
2524
import java.util.concurrent.Executor;
2625
import org.duckdb.user.DuckDBMap;
2726
import org.duckdb.user.DuckDBUserArray;
@@ -37,6 +36,9 @@ public final class DuckDBConnection implements java.sql.Connection {
3736
boolean transactionRunning;
3837
final String url;
3938
private final boolean readOnly;
39+
// Tracking set for statements created from this connection,
40+
// must only be used from outside with track/untrack accessors.
41+
private final Set<DuckDBPreparedStatement> statements = new LinkedHashSet<>();
4042

4143
public static DuckDBConnection newConnection(String url, boolean readOnly, Properties properties)
4244
throws SQLException {
@@ -110,15 +112,32 @@ protected void finalize() throws Throwable {
110112
close();
111113
}
112114

113-
public synchronized void close() throws SQLException {
114-
if (conn_ref != null) {
115+
public void close() throws SQLException {
116+
if (conn_ref == null) {
117+
return;
118+
}
119+
synchronized (this) {
120+
if (conn_ref == null) {
121+
return;
122+
}
123+
// Closing remaining statements is not required by JDBC spec,
124+
// but it is a reasonable expectation from clients point of view.
125+
List<DuckDBPreparedStatement> stmtList = new ArrayList<>(statements);
126+
for (DuckDBPreparedStatement stmt : stmtList) {
127+
stmt.close();
128+
}
115129
DuckDBNative.duckdb_jdbc_disconnect(conn_ref);
116130
conn_ref = null;
117131
}
118132
}
119133

120134
public boolean isClosed() throws SQLException {
121-
return conn_ref == null;
135+
if (conn_ref == null) {
136+
return true;
137+
}
138+
synchronized (this) {
139+
return conn_ref == null;
140+
}
122141
}
123142

124143
public boolean isValid(int timeout) throws SQLException {
@@ -371,4 +390,19 @@ public void registerArrowStream(String name, Object arrow_array_stream) {
371390
long array_stream_address = getArrowStreamAddress(arrow_array_stream);
372391
DuckDBNative.duckdb_jdbc_arrow_register(conn_ref, array_stream_address, name.getBytes(StandardCharsets.UTF_8));
373392
}
393+
394+
void trackStatement(DuckDBPreparedStatement stmt) throws SQLException {
395+
synchronized (this) {
396+
if (isClosed()) {
397+
throw new SQLException("Connection was closed");
398+
}
399+
this.statements.add(stmt);
400+
}
401+
}
402+
403+
void untrackStatement(DuckDBPreparedStatement stmt) throws SQLException {
404+
synchronized (this) {
405+
this.statements.remove(stmt);
406+
}
407+
}
374408
}

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

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.duckdb;
22

3-
import java.io.ByteArrayOutputStream;
43
import java.io.IOException;
54
import java.io.InputStream;
65
import java.io.Reader;
@@ -31,7 +30,6 @@
3130
import java.time.LocalDateTime;
3231
import java.time.OffsetDateTime;
3332
import java.util.ArrayList;
34-
import java.util.Arrays;
3533
import java.util.Calendar;
3634
import java.util.List;
3735
import java.util.logging.Level;
@@ -61,16 +59,14 @@ public DuckDBPreparedStatement(DuckDBConnection conn) throws SQLException {
6159
throw new SQLException("connection parameter cannot be null");
6260
}
6361
this.conn = conn;
62+
this.conn.trackStatement(this);
6463
}
6564

6665
public DuckDBPreparedStatement(DuckDBConnection conn, String sql) throws SQLException {
67-
if (conn == null) {
68-
throw new SQLException("connection parameter cannot be null");
69-
}
66+
this(conn);
7067
if (sql == null) {
7168
throw new SQLException("sql query parameter cannot be null");
7269
}
73-
this.conn = conn;
7470
this.isPreparedStatement = true;
7571
prepare(sql);
7672
}
@@ -299,15 +295,24 @@ public void clearParameters() throws SQLException {
299295

300296
@Override
301297
public void close() throws SQLException {
302-
if (select_result != null) {
303-
select_result.close();
304-
select_result = null;
298+
if (conn == null) {
299+
return;
305300
}
306-
if (stmt_ref != null) {
307-
DuckDBNative.duckdb_jdbc_release(stmt_ref);
308-
stmt_ref = null;
301+
synchronized (this) {
302+
if (conn == null) {
303+
return;
304+
}
305+
if (select_result != null) {
306+
select_result.close();
307+
select_result = null;
308+
}
309+
if (stmt_ref != null) {
310+
DuckDBNative.duckdb_jdbc_release(stmt_ref);
311+
stmt_ref = null;
312+
}
313+
conn.untrackStatement(this);
314+
conn = null; // we use this as a check for closed-ness
309315
}
310-
conn = null; // we use this as a check for closed-ness
311316
}
312317

313318
protected void finalize() throws Throwable {
@@ -352,9 +357,14 @@ public void setQueryTimeout(int seconds) throws SQLException {
352357
* It is not safe to call this function when the connection is already closed.
353358
*/
354359
@Override
355-
public synchronized void cancel() throws SQLException {
356-
if (conn.conn_ref != null) {
357-
DuckDBNative.duckdb_jdbc_interrupt(conn.conn_ref);
360+
public void cancel() throws SQLException {
361+
synchronized (this) {
362+
if (isClosed()) {
363+
throw new SQLException("Statement was closed");
364+
}
365+
if (conn.conn_ref != null) {
366+
DuckDBNative.duckdb_jdbc_interrupt(conn.conn_ref);
367+
}
358368
}
359369
}
360370

@@ -559,7 +569,12 @@ public int getResultSetHoldability() throws SQLException {
559569

560570
@Override
561571
public boolean isClosed() throws SQLException {
562-
return conn == null;
572+
if (conn == null) {
573+
return true;
574+
}
575+
synchronized (this) {
576+
return conn == null;
577+
}
563578
}
564579

565580
@Override
@@ -576,7 +591,9 @@ public boolean isPoolable() throws SQLException {
576591
public void closeOnCompletion() throws SQLException {
577592
if (isClosed())
578593
throw new SQLException("Statement is closed");
579-
closeOnCompletion = true;
594+
synchronized (this) {
595+
closeOnCompletion = true;
596+
}
580597
}
581598

582599
@Override

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import java.time.LocalDateTime;
3232
import java.time.OffsetDateTime;
3333
import java.time.OffsetTime;
34-
import java.util.Arrays;
3534
import java.util.Calendar;
3635
import java.util.Map;
3736
import java.util.Objects;
@@ -92,12 +91,19 @@ public synchronized boolean next() throws SQLException {
9291
return true;
9392
}
9493

95-
public synchronized void close() throws SQLException {
96-
if (result_ref != null) {
94+
public void close() throws SQLException {
95+
if (result_ref == null) {
96+
return;
97+
}
98+
synchronized (this) {
99+
if (result_ref == null) {
100+
return;
101+
}
97102
DuckDBNative.duckdb_jdbc_free_result(result_ref);
98103
// Nullness is used to determine whether we're closed
99104
result_ref = null;
100-
105+
}
106+
synchronized (stmt) {
101107
// isCloseOnCompletion() throws if already closed, and we can't check for isClosed() because it could change
102108
// between when we check and call isCloseOnCompletion, so access the field directly.
103109
if (stmt.closeOnCompletion) {
@@ -110,8 +116,13 @@ protected void finalize() throws Throwable {
110116
close();
111117
}
112118

113-
public synchronized boolean isClosed() throws SQLException {
114-
return result_ref == null;
119+
public boolean isClosed() throws SQLException {
120+
if (result_ref == null) {
121+
return true;
122+
}
123+
synchronized (this) {
124+
return result_ref == null;
125+
}
115126
}
116127

117128
private void check(int columnIndex) throws SQLException {

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.duckdb.test.Assertions.fail;
2121
import static org.duckdb.test.Runner.runTests;
2222

23+
import java.io.File;
2324
import java.math.BigDecimal;
2425
import java.math.BigInteger;
2526
import java.nio.ByteBuffer;
@@ -4740,6 +4741,58 @@ public static void test_typed_connection_properties() throws Exception {
47404741
}
47414742
}
47424743

4744+
// https://github.com/duckdb/duckdb-java/issues/101
4745+
public static void test_unclosed_statement_does_not_hang() throws Exception {
4746+
String dbName = "test_issue_101.db";
4747+
String url = JDBC_URL + dbName;
4748+
Connection conn = DriverManager.getConnection(url);
4749+
Statement stmt = conn.createStatement();
4750+
stmt.execute("select 42");
4751+
// statement not closed explicitly
4752+
conn.close();
4753+
assertTrue(stmt.isClosed());
4754+
Connection connOther = DriverManager.getConnection(url);
4755+
connOther.close();
4756+
assertTrue(new File(dbName).delete());
4757+
}
4758+
4759+
public static void test_result_set_auto_closed() throws Exception {
4760+
try (Connection conn = DriverManager.getConnection(JDBC_URL)) {
4761+
Statement stmt = conn.createStatement();
4762+
ResultSet rs1 = stmt.executeQuery("select 42");
4763+
ResultSet rs2 = stmt.executeQuery("select 43");
4764+
assertTrue(rs1.isClosed());
4765+
stmt.close();
4766+
assertTrue(rs2.isClosed());
4767+
}
4768+
}
4769+
4770+
public static void test_statements_auto_closed_on_conn_close() throws Exception {
4771+
Connection conn = DriverManager.getConnection(JDBC_URL);
4772+
Statement stmt1 = conn.createStatement();
4773+
stmt1.execute("select 42");
4774+
PreparedStatement stmt2 = conn.prepareStatement("select 43");
4775+
stmt2.execute();
4776+
Statement stmt3 = conn.createStatement();
4777+
stmt3.execute("select 44");
4778+
stmt3.close();
4779+
conn.close();
4780+
assertTrue(stmt1.isClosed());
4781+
assertTrue(stmt2.isClosed());
4782+
}
4783+
4784+
public static void test_statement_auto_closed_on_completion() throws Exception {
4785+
try (Connection conn = DriverManager.getConnection(JDBC_URL)) {
4786+
Statement stmt = conn.createStatement();
4787+
stmt.closeOnCompletion();
4788+
assertTrue(stmt.isCloseOnCompletion());
4789+
try (ResultSet rs = stmt.executeQuery("select 42")) {
4790+
rs.next();
4791+
}
4792+
assertTrue(stmt.isClosed());
4793+
}
4794+
}
4795+
47434796
public static void main(String[] args) throws Exception {
47444797
System.exit(runTests(args, TestDuckDBJDBC.class, TestExtensionTypes.class));
47454798
}

0 commit comments

Comments
 (0)