Skip to content

Commit 66bf0e7

Browse files
committed
Support pinning DB instances
Currently when the last connection to a DB instance is closed, this DB instance is destroyed. This is not a big problem with file DBs, where subsequent connection from the same process can just re-open the file (though some state, like attached DBs is lost), but this is a problem with tagged in-memory DBs (`jdbc:duckdb:memory:tag1` URLs), where all DB state is lost when the DB is closed. This does not apply to untagged `:memory:` DBs, which are private to a single connection. This change adds new connection property `jdbc_pin_db` (boolean, `false` by default), when it is enabled, then the DB is pinned and is kept alive in-memory even after the last connection to it is closed. `DuckDBDriver.releaseDB(url)` method is added to allow client code to release such DBs. DBs that are left pinned are released automatically on JVM shutdown. Testing: new test added.
1 parent d7c1d91 commit 66bf0e7

File tree

12 files changed

+269
-34
lines changed

12 files changed

+269
-34
lines changed

duckdb_java.def

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1arrow_1register
2424
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1arrow_1stream
2525
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1connect
2626
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1appender
27+
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1db_1ref
28+
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1destroy_1db_1ref
2729
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1extension_1type
2830
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1disconnect
2931
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute

duckdb_java.exp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1arrow_1register
2121
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1arrow_1stream
2222
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1connect
2323
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1appender
24+
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1db_1ref
25+
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1destroy_1db_1ref
2426
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1extension_1type
2527
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1disconnect
2628
_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute

duckdb_java.map

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ DUCKDB_JAVA {
2323
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1arrow_1stream;
2424
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1connect;
2525
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1appender;
26+
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1db_1ref;
27+
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1destroy_1db_1ref;
2628
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1extension_1type;
2729
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1disconnect;
2830
Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute;

src/jni/duckdb_java.cpp

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,35 @@ jobject _duckdb_jdbc_startup(JNIEnv *env, jclass, jbyteArray database_j, jboolea
7171
std::unique_ptr<DBConfig> config = create_db_config(env, read_only, props);
7272
bool cache_instance = database != ":memory:" && !database.empty();
7373
auto shared_db = instance_cache.GetOrCreateInstance(database, *config, cache_instance);
74-
auto conn_holder = new ConnectionHolder(shared_db);
74+
auto conn_ref = new ConnectionHolder(shared_db);
7575

76-
return env->NewDirectByteBuffer(conn_holder, 0);
76+
return env->NewDirectByteBuffer(conn_ref, 0);
7777
}
7878

7979
jobject _duckdb_jdbc_connect(JNIEnv *env, jclass, jobject conn_ref_buf) {
80-
auto conn_ref = (ConnectionHolder *)env->GetDirectBufferAddress(conn_ref_buf);
80+
auto conn_ref = get_connection_ref(env, conn_ref_buf);
8181
auto config = ClientConfig::GetConfig(*conn_ref->connection->context);
8282
auto conn = new ConnectionHolder(conn_ref->db);
8383
conn->connection->context->config = config;
8484
return env->NewDirectByteBuffer(conn, 0);
8585
}
8686

87+
jobject _duckdb_jdbc_create_db_ref(JNIEnv *env, jclass, jobject conn_ref_buf) {
88+
auto conn_ref = get_connection_ref(env, conn_ref_buf);
89+
auto db_ref = conn_ref->create_db_ref();
90+
return env->NewDirectByteBuffer(db_ref, 0);
91+
}
92+
93+
void _duckdb_jdbc_destroy_db_ref(JNIEnv *env, jclass, jobject db_ref_buf) {
94+
if (nullptr == db_ref_buf) {
95+
return;
96+
}
97+
auto db_ref = (DBHolder *)env->GetDirectBufferAddress(db_ref_buf);
98+
if (db_ref) {
99+
delete db_ref;
100+
}
101+
}
102+
87103
jstring _duckdb_jdbc_get_schema(JNIEnv *env, jclass, jobject conn_ref_buf) {
88104
auto conn_ref = get_connection(env, conn_ref_buf);
89105
if (!conn_ref) {
@@ -163,6 +179,9 @@ jobject _duckdb_jdbc_query_progress(JNIEnv *env, jclass, jobject conn_ref_buf) {
163179
}
164180

165181
void _duckdb_jdbc_disconnect(JNIEnv *env, jclass, jobject conn_ref_buf) {
182+
if (nullptr == conn_ref_buf) {
183+
return;
184+
}
166185
auto conn_ref = (ConnectionHolder *)env->GetDirectBufferAddress(conn_ref_buf);
167186
if (conn_ref) {
168187
delete conn_ref;
@@ -249,13 +268,19 @@ jobject _duckdb_jdbc_execute(JNIEnv *env, jclass, jobject stmt_ref_buf, jobjectA
249268
}
250269

251270
void _duckdb_jdbc_release(JNIEnv *env, jclass, jobject stmt_ref_buf) {
271+
if (nullptr == stmt_ref_buf) {
272+
return;
273+
}
252274
auto stmt_ref = (StatementHolder *)env->GetDirectBufferAddress(stmt_ref_buf);
253275
if (stmt_ref) {
254276
delete stmt_ref;
255277
}
256278
}
257279

258280
void _duckdb_jdbc_free_result(JNIEnv *env, jclass, jobject res_ref_buf) {
281+
if (nullptr == res_ref_buf) {
282+
return;
283+
}
259284
auto res_ref = (ResultHolder *)env->GetDirectBufferAddress(res_ref_buf);
260285
if (res_ref) {
261286
delete res_ref;

src/jni/functions.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,27 @@ JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1connect(JNI
2626
}
2727
}
2828

29+
JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1db_1ref(JNIEnv * env, jclass param0, jobject param1) {
30+
try {
31+
return _duckdb_jdbc_create_db_ref(env, param0, param1);
32+
} catch (const std::exception &e) {
33+
duckdb::ErrorData error(e);
34+
ThrowJNI(env, error.Message().c_str());
35+
36+
return nullptr;
37+
}
38+
}
39+
40+
JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1destroy_1db_1ref(JNIEnv * env, jclass param0, jobject param1) {
41+
try {
42+
return _duckdb_jdbc_destroy_db_ref(env, param0, param1);
43+
} catch (const std::exception &e) {
44+
duckdb::ErrorData error(e);
45+
ThrowJNI(env, error.Message().c_str());
46+
47+
}
48+
}
49+
2950
JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1auto_1commit(JNIEnv * env, jclass param0, jobject param1, jboolean param2) {
3051
try {
3152
return _duckdb_jdbc_set_auto_commit(env, param0, param1, param2);

src/jni/functions.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ jobject _duckdb_jdbc_connect(JNIEnv * env, jclass param0, jobject param1);
1717

1818
JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1connect(JNIEnv * env, jclass param0, jobject param1);
1919

20+
jobject _duckdb_jdbc_create_db_ref(JNIEnv * env, jclass param0, jobject param1);
21+
22+
JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1db_1ref(JNIEnv * env, jclass param0, jobject param1);
23+
24+
void _duckdb_jdbc_destroy_db_ref(JNIEnv * env, jclass param0, jobject param1);
25+
26+
JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1destroy_1db_1ref(JNIEnv * env, jclass param0, jobject param1);
27+
2028
void _duckdb_jdbc_set_auto_commit(JNIEnv * env, jclass param0, jobject param1, jboolean param2);
2129

2230
JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1auto_1commit(JNIEnv * env, jclass param0, jobject param1, jboolean param2);

src/jni/holders.hpp

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44

55
#include <jni.h>
66

7+
/**
8+
* Holds a copy of a shared_ptr to an existing DB instance.
9+
* Is used to keep this DB alive (and accessible from DB cache)
10+
* even after the last connection to this DB is closed.
11+
*/
12+
struct DBHolder {
13+
duckdb::shared_ptr<duckdb::DuckDB> db;
14+
15+
DBHolder(duckdb::shared_ptr<duckdb::DuckDB> _db) : db(std::move(_db)) {};
16+
17+
DBHolder(const DBHolder &) = delete;
18+
19+
DBHolder &operator=(const DBHolder &) = delete;
20+
};
21+
722
/**
823
* Associates a duckdb::Connection with a duckdb::DuckDB. The DB may be shared amongst many ConnectionHolders, but the
924
* Connection is unique to this holder. Every Java DuckDBConnection has exactly 1 of these holders, and they are never
@@ -17,6 +32,10 @@ struct ConnectionHolder {
1732
ConnectionHolder(duckdb::shared_ptr<duckdb::DuckDB> _db)
1833
: db(_db), connection(duckdb::make_uniq<duckdb::Connection>(*_db)) {
1934
}
35+
36+
DBHolder *create_db_ref() {
37+
return new DBHolder(db);
38+
}
2039
};
2140

2241
struct StatementHolder {
@@ -28,17 +47,22 @@ struct ResultHolder {
2847
duckdb::unique_ptr<duckdb::DataChunk> chunk;
2948
};
3049

31-
/**
32-
* Throws a SQLException and returns nullptr if a valid Connection can't be retrieved from the buffer.
33-
*/
34-
inline duckdb::Connection *get_connection(JNIEnv *env, jobject conn_ref_buf) {
50+
inline ConnectionHolder *get_connection_ref(JNIEnv *env, jobject conn_ref_buf) {
3551
if (!conn_ref_buf) {
36-
throw duckdb::ConnectionException("Invalid connection");
52+
throw duckdb::ConnectionException("Invalid connection buffer ref");
3753
}
38-
auto conn_holder = (ConnectionHolder *)env->GetDirectBufferAddress(conn_ref_buf);
54+
auto conn_holder = reinterpret_cast<ConnectionHolder *>(env->GetDirectBufferAddress(conn_ref_buf));
3955
if (!conn_holder) {
40-
throw duckdb::ConnectionException("Invalid connection");
56+
throw duckdb::ConnectionException("Invalid connection buffer");
4157
}
58+
return conn_holder;
59+
}
60+
61+
/**
62+
* Throws a SQLException and returns nullptr if a valid Connection can't be retrieved from the buffer.
63+
*/
64+
inline duckdb::Connection *get_connection(JNIEnv *env, jobject conn_ref_buf) {
65+
auto conn_holder = get_connection_ref(env, conn_ref_buf);
4266
auto conn_ref = conn_holder->connection.get();
4367
if (!conn_ref || !conn_ref->context) {
4468
throw duckdb::ConnectionException("Invalid connection");

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

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
import static java.nio.charset.StandardCharsets.UTF_8;
44
import static org.duckdb.DuckDBDriver.JDBC_AUTO_COMMIT;
5-
import static org.duckdb.JdbcUtils.isStringTruish;
6-
import static org.duckdb.JdbcUtils.removeOption;
5+
import static org.duckdb.JdbcUtils.*;
76

87
import java.lang.reflect.InvocationTargetException;
98
import java.nio.ByteBuffer;
@@ -49,19 +48,13 @@ public final class DuckDBConnection implements java.sql.Connection {
4948

5049
public static DuckDBConnection newConnection(String url, boolean readOnly, Properties properties)
5150
throws SQLException {
52-
if (!url.startsWith("jdbc:duckdb:")) {
53-
throw new SQLException("DuckDB JDBC URL needs to start with 'jdbc:duckdb:'");
54-
}
55-
String db_dir = url.substring("jdbc:duckdb:".length()).trim();
56-
if (db_dir.length() == 0) {
57-
db_dir = ":memory:";
58-
}
59-
if (db_dir.startsWith("memory:")) {
60-
db_dir = ":" + db_dir;
51+
if (null == properties) {
52+
properties = new Properties();
6153
}
54+
String dbName = dbNameFromUrl(url);
6255
String autoCommitStr = removeOption(properties, JDBC_AUTO_COMMIT);
6356
boolean autoCommit = isStringTruish(autoCommitStr, true);
64-
ByteBuffer nativeReference = DuckDBNative.duckdb_jdbc_startup(db_dir.getBytes(UTF_8), readOnly, properties);
57+
ByteBuffer nativeReference = DuckDBNative.duckdb_jdbc_startup(dbName.getBytes(UTF_8), readOnly, properties);
6558
return new DuckDBConnection(nativeReference, url, readOnly, autoCommit);
6659
}
6760

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

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package org.duckdb;
22

3-
import static org.duckdb.JdbcUtils.isStringTruish;
4-
import static org.duckdb.JdbcUtils.removeOption;
3+
import static org.duckdb.JdbcUtils.*;
54

5+
import java.nio.ByteBuffer;
66
import java.sql.*;
7-
import java.util.LinkedHashMap;
8-
import java.util.Map;
9-
import java.util.Properties;
7+
import java.util.*;
108
import java.util.concurrent.locks.ReentrantLock;
119
import java.util.logging.Logger;
1210
import java.util.regex.Pattern;
@@ -17,15 +15,21 @@ public class DuckDBDriver implements java.sql.Driver {
1715
public static final String DUCKDB_USER_AGENT_PROPERTY = "custom_user_agent";
1816
public static final String JDBC_STREAM_RESULTS = "jdbc_stream_results";
1917
public static final String JDBC_AUTO_COMMIT = "jdbc_auto_commit";
18+
public static final String JDBC_PIN_DB = "jdbc_pin_db";
2019

21-
private static final String DUCKDB_URL_PREFIX = "jdbc:duckdb:";
20+
static final String DUCKDB_URL_PREFIX = "jdbc:duckdb:";
2221

2322
private static final String DUCKLAKE_OPTION = "ducklake";
2423
private static final String DUCKLAKE_ALIAS_OPTION = "ducklake_alias";
2524
private static final Pattern DUCKLAKE_ALIAS_OPTION_PATTERN = Pattern.compile("[a-zA-Z0-9_]+");
2625
private static final String DUCKLAKE_URL_PREFIX = "ducklake:";
2726
private static final ReentrantLock DUCKLAKE_INIT_LOCK = new ReentrantLock();
2827

28+
private static final LinkedHashMap<String, ByteBuffer> pinnedDbRefs = new LinkedHashMap<>();
29+
private static final ReentrantLock pinnedDbRefsLock = new ReentrantLock();
30+
private static boolean pinnedDbRefsShutdownHookRegistered = false;
31+
private static boolean pinnedDbRefsShutdownHookRun = false;
32+
2933
static {
3034
try {
3135
DriverManager.registerDriver(new DuckDBDriver());
@@ -60,12 +64,17 @@ public Connection connect(String url, Properties info) throws SQLException {
6064
// to be established.
6165
info.remove("path");
6266

67+
String pinDbOptStr = removeOption(info, JDBC_PIN_DB);
68+
boolean pinDBOpt = isStringTruish(pinDbOptStr, false);
69+
6370
String ducklake = removeOption(info, DUCKLAKE_OPTION);
6471
String ducklakeAlias = removeOption(info, DUCKLAKE_ALIAS_OPTION);
6572

66-
Connection conn = DuckDBConnection.newConnection(url, readOnly, info);
73+
DuckDBConnection conn = DuckDBConnection.newConnection(url, readOnly, info);
6774

68-
initDucklake(conn, url, ducklake, ducklakeAlias);
75+
pinDB(pinDBOpt, url, conn);
76+
77+
initDucklake(conn, ducklake, ducklakeAlias);
6978

7079
return conn;
7180
}
@@ -95,8 +104,7 @@ public Logger getParentLogger() throws SQLFeatureNotSupportedException {
95104
throw new SQLFeatureNotSupportedException("no logger");
96105
}
97106

98-
private static void initDucklake(Connection conn, String url, String ducklake, String ducklakeAlias)
99-
throws SQLException {
107+
private static void initDucklake(Connection conn, String ducklake, String ducklakeAlias) throws SQLException {
100108
if (null == ducklake) {
101109
return;
102110
}
@@ -154,6 +162,55 @@ private static ParsedProps parsePropsFromUrl(String url) throws SQLException {
154162
return new ParsedProps(shortUrl, props);
155163
}
156164

165+
private static void pinDB(boolean pinnedDbOpt, String url, DuckDBConnection conn) throws SQLException {
166+
if (!pinnedDbOpt) {
167+
return;
168+
}
169+
String dbName = dbNameFromUrl(url);
170+
if (":memory:".equals(dbName)) {
171+
return;
172+
}
173+
174+
pinnedDbRefsLock.lock();
175+
try {
176+
// Actual native DB cache uses absolute paths to file DBs,
177+
// but that should not make the difference unless CWD is changed,
178+
// that is not expected for a JVM process, see JDK-4045688.
179+
if (pinnedDbRefsShutdownHookRun || pinnedDbRefs.containsKey(dbName)) {
180+
return;
181+
}
182+
// No need to hold connRef lock here, this connection is not
183+
// yet available to client at this point, so it cannot be closed.
184+
ByteBuffer dbRef = DuckDBNative.duckdb_jdbc_create_db_ref(conn.connRef);
185+
pinnedDbRefs.put(dbName, dbRef);
186+
187+
if (!pinnedDbRefsShutdownHookRegistered) {
188+
Runtime.getRuntime().addShutdownHook(new Thread(new PinnedDbRefsShutdownHook()));
189+
pinnedDbRefsShutdownHookRegistered = true;
190+
}
191+
} finally {
192+
pinnedDbRefsLock.unlock();
193+
}
194+
}
195+
196+
public static boolean releaseDB(String url) throws SQLException {
197+
pinnedDbRefsLock.lock();
198+
try {
199+
if (pinnedDbRefsShutdownHookRun) {
200+
return false;
201+
}
202+
String dbName = dbNameFromUrl(url);
203+
ByteBuffer dbRef = pinnedDbRefs.remove(dbName);
204+
if (null == dbRef) {
205+
return false;
206+
}
207+
DuckDBNative.duckdb_jdbc_destroy_db_ref(dbRef);
208+
return true;
209+
} finally {
210+
pinnedDbRefsLock.unlock();
211+
}
212+
}
213+
157214
private static class ParsedProps {
158215
final String shortUrl;
159216
final LinkedHashMap<String, String> props;
@@ -167,4 +224,23 @@ private ParsedProps(String shortUrl, LinkedHashMap<String, String> props) {
167224
this.props = props;
168225
}
169226
}
227+
228+
private static class PinnedDbRefsShutdownHook implements Runnable {
229+
@Override
230+
public void run() {
231+
pinnedDbRefsLock.lock();
232+
try {
233+
List<ByteBuffer> dbRefsList = new ArrayList<>(pinnedDbRefs.values());
234+
Collections.reverse(dbRefsList);
235+
for (ByteBuffer dbRef : dbRefsList) {
236+
DuckDBNative.duckdb_jdbc_destroy_db_ref(dbRef);
237+
}
238+
pinnedDbRefsShutdownHookRun = true;
239+
} catch (SQLException e) {
240+
e.printStackTrace();
241+
} finally {
242+
pinnedDbRefsLock.unlock();
243+
}
244+
}
245+
}
170246
}

src/main/java/org/duckdb/DuckDBNative.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ final class DuckDBNative {
7373
static native ByteBuffer duckdb_jdbc_startup(byte[] path, boolean read_only, Properties props) throws SQLException;
7474

7575
// returns conn_ref connection reference object
76-
static native ByteBuffer duckdb_jdbc_connect(ByteBuffer db_ref) throws SQLException;
76+
static native ByteBuffer duckdb_jdbc_connect(ByteBuffer conn_ref) throws SQLException;
77+
78+
static native ByteBuffer duckdb_jdbc_create_db_ref(ByteBuffer conn_ref) throws SQLException;
79+
80+
static native void duckdb_jdbc_destroy_db_ref(ByteBuffer db_ref) throws SQLException;
7781

7882
static native void duckdb_jdbc_set_auto_commit(ByteBuffer conn_ref, boolean auto_commit) throws SQLException;
7983

0 commit comments

Comments
 (0)