Skip to content

Commit 471dd86

Browse files
committed
Support pinning DB instances (1.3)
This is a backport of the PR #238 to `v1.3-ossivalis` stable branch. 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 6fe8ec4 commit 471dd86

12 files changed

Lines changed: 332 additions & 29 deletions

File tree

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;
@@ -251,13 +270,19 @@ jobject _duckdb_jdbc_execute(JNIEnv *env, jclass, jobject stmt_ref_buf, jobjectA
251270
}
252271

253272
void _duckdb_jdbc_release(JNIEnv *env, jclass, jobject stmt_ref_buf) {
273+
if (nullptr == stmt_ref_buf) {
274+
return;
275+
}
254276
auto stmt_ref = (StatementHolder *)env->GetDirectBufferAddress(stmt_ref_buf);
255277
if (stmt_ref) {
256278
delete stmt_ref;
257279
}
258280
}
259281

260282
void _duckdb_jdbc_free_result(JNIEnv *env, jclass, jobject res_ref_buf) {
283+
if (nullptr == res_ref_buf) {
284+
return;
285+
}
261286
auto res_ref = (ResultHolder *)env->GetDirectBufferAddress(res_ref_buf);
262287
if (res_ref) {
263288
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
@@ -1,9 +1,9 @@
11
package org.duckdb;
22

33
import static java.nio.charset.StandardCharsets.UTF_8;
4+
import static org.duckdb.JdbcUtils.*;
45

56
import java.lang.reflect.InvocationTargetException;
6-
import java.math.BigInteger;
77
import java.nio.ByteBuffer;
88
import java.sql.Array;
99
import java.sql.Blob;
@@ -24,7 +24,6 @@
2424
import java.sql.Struct;
2525
import java.util.*;
2626
import java.util.concurrent.Executor;
27-
import java.util.concurrent.locks.Lock;
2827
import java.util.concurrent.locks.ReentrantLock;
2928
import org.duckdb.user.DuckDBMap;
3029
import org.duckdb.user.DuckDBUserArray;
@@ -47,17 +46,11 @@ public final class DuckDBConnection implements java.sql.Connection {
4746

4847
public static DuckDBConnection newConnection(String url, boolean readOnly, Properties properties)
4948
throws SQLException {
50-
if (!url.startsWith("jdbc:duckdb:")) {
51-
throw new SQLException("DuckDB JDBC URL needs to start with 'jdbc:duckdb:'");
49+
if (null == properties) {
50+
properties = new Properties();
5251
}
53-
String db_dir = url.substring("jdbc:duckdb:".length()).trim();
54-
if (db_dir.length() == 0) {
55-
db_dir = ":memory:";
56-
}
57-
if (db_dir.startsWith("memory:")) {
58-
db_dir = ":" + db_dir;
59-
}
60-
ByteBuffer nativeReference = DuckDBNative.duckdb_jdbc_startup(db_dir.getBytes(UTF_8), readOnly, properties);
52+
String dbName = dbNameFromUrl(url);
53+
ByteBuffer nativeReference = DuckDBNative.duckdb_jdbc_startup(dbName.getBytes(UTF_8), readOnly, properties);
6154
return new DuckDBConnection(nativeReference, url, readOnly);
6255
}
6356

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

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
11
package org.duckdb;
22

3+
import java.nio.ByteBuffer;
34
import java.sql.Connection;
45
import java.sql.DriverManager;
56
import java.sql.DriverPropertyInfo;
67
import java.sql.SQLException;
78
import java.sql.SQLFeatureNotSupportedException;
8-
import java.util.LinkedHashMap;
9-
import java.util.Map;
10-
import java.util.Properties;
9+
import java.util.*;
1110
import java.util.concurrent.ScheduledThreadPoolExecutor;
1211
import java.util.concurrent.ThreadFactory;
12+
import static org.duckdb.JdbcUtils.*;
13+
14+
import java.util.concurrent.locks.ReentrantLock;
1315
import java.util.logging.Logger;
1416

1517
public class DuckDBDriver implements java.sql.Driver {
1618

1719
public static final String DUCKDB_READONLY_PROPERTY = "duckdb.read_only";
1820
public static final String DUCKDB_USER_AGENT_PROPERTY = "custom_user_agent";
1921
public static final String JDBC_STREAM_RESULTS = "jdbc_stream_results";
22+
public static final String JDBC_PIN_DB = "jdbc_pin_db";
23+
24+
static final String DUCKDB_URL_PREFIX = "jdbc:duckdb:";
25+
static final String MEMORY_DB = ":memory:";
2026

2127
static final ScheduledThreadPoolExecutor scheduler;
2228

29+
private static final LinkedHashMap<String, ByteBuffer> pinnedDbRefs = new LinkedHashMap<>();
30+
private static final ReentrantLock pinnedDbRefsLock = new ReentrantLock();
31+
private static boolean pinnedDbRefsShutdownHookRegistered = false;
32+
private static boolean pinnedDbRefsShutdownHookRun = false;
33+
2334
static {
2435
try {
2536
DriverManager.registerDriver(new DuckDBDriver());
@@ -61,7 +72,14 @@ public Connection connect(String url, Properties info) throws SQLException {
6172
// to be established.
6273
info.remove("path");
6374

64-
return DuckDBConnection.newConnection(url, read_only, info);
75+
String pinDbOptStr = removeOption(info, JDBC_PIN_DB);
76+
boolean pinDBOpt = isStringTruish(pinDbOptStr, false);
77+
78+
DuckDBConnection conn = DuckDBConnection.newConnection(url, read_only, info);
79+
80+
pinDB(pinDBOpt, url, conn);
81+
82+
return conn;
6583
}
6684

6785
public boolean acceptsURL(String url) throws SQLException {
@@ -112,6 +130,55 @@ private static ParsedProps parsePropsFromUrl(String url) throws SQLException {
112130
return new ParsedProps(shortUrl, props);
113131
}
114132

133+
private static void pinDB(boolean pinnedDbOpt, String url, DuckDBConnection conn) throws SQLException {
134+
if (!pinnedDbOpt) {
135+
return;
136+
}
137+
String dbName = dbNameFromUrl(url);
138+
if (":memory:".equals(dbName)) {
139+
return;
140+
}
141+
142+
pinnedDbRefsLock.lock();
143+
try {
144+
// Actual native DB cache uses absolute paths to file DBs,
145+
// but that should not make the difference unless CWD is changed,
146+
// that is not expected for a JVM process, see JDK-4045688.
147+
if (pinnedDbRefsShutdownHookRun || pinnedDbRefs.containsKey(dbName)) {
148+
return;
149+
}
150+
// No need to hold connRef lock here, this connection is not
151+
// yet available to client at this point, so it cannot be closed.
152+
ByteBuffer dbRef = DuckDBNative.duckdb_jdbc_create_db_ref(conn.connRef);
153+
pinnedDbRefs.put(dbName, dbRef);
154+
155+
if (!pinnedDbRefsShutdownHookRegistered) {
156+
Runtime.getRuntime().addShutdownHook(new Thread(new PinnedDbRefsShutdownHook()));
157+
pinnedDbRefsShutdownHookRegistered = true;
158+
}
159+
} finally {
160+
pinnedDbRefsLock.unlock();
161+
}
162+
}
163+
164+
public static boolean releaseDB(String url) throws SQLException {
165+
pinnedDbRefsLock.lock();
166+
try {
167+
if (pinnedDbRefsShutdownHookRun) {
168+
return false;
169+
}
170+
String dbName = dbNameFromUrl(url);
171+
ByteBuffer dbRef = pinnedDbRefs.remove(dbName);
172+
if (null == dbRef) {
173+
return false;
174+
}
175+
DuckDBNative.duckdb_jdbc_destroy_db_ref(dbRef);
176+
return true;
177+
} finally {
178+
pinnedDbRefsLock.unlock();
179+
}
180+
}
181+
115182
private static class ParsedProps {
116183
final String shortUrl;
117184
final LinkedHashMap<String, String> props;
@@ -125,4 +192,23 @@ private ParsedProps(String shortUrl, LinkedHashMap<String, String> props) {
125192
this.props = props;
126193
}
127194
}
195+
196+
private static class PinnedDbRefsShutdownHook implements Runnable {
197+
@Override
198+
public void run() {
199+
pinnedDbRefsLock.lock();
200+
try {
201+
List<ByteBuffer> dbRefsList = new ArrayList<>(pinnedDbRefs.values());
202+
Collections.reverse(dbRefsList);
203+
for (ByteBuffer dbRef : dbRefsList) {
204+
DuckDBNative.duckdb_jdbc_destroy_db_ref(dbRef);
205+
}
206+
pinnedDbRefsShutdownHookRun = true;
207+
} catch (SQLException e) {
208+
e.printStackTrace();
209+
} finally {
210+
pinnedDbRefsLock.unlock();
211+
}
212+
}
213+
}
128214
}

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)