Skip to content

Commit 75fe8d6

Browse files
committed
column family commit hook implementation; bump version in pom file
1 parent 9d4fec3 commit 75fe8d6

6 files changed

Lines changed: 458 additions & 1 deletion

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.tidesdb</groupId>
88
<artifactId>tidesdb-java</artifactId>
9-
<version>0.6.3</version>
9+
<version>0.6.4</version>
1010
<packaging>jar</packaging>
1111

1212
<name>TidesDB Java</name>

src/main/c/com_tidesdb_TidesDB.c

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,158 @@ JNIEXPORT void JNICALL Java_com_tidesdb_TidesDBIterator_nativeFree(JNIEnv *env,
913913
}
914914
}
915915

916+
/**
917+
* Context stored as the commit hook ctx pointer.
918+
* Holds the JavaVM and a global reference to the Java CommitHook object.
919+
*/
920+
typedef struct
921+
{
922+
JavaVM *jvm;
923+
jobject hook_obj; /* global reference to CommitHook */
924+
} java_hook_ctx_t;
925+
926+
/**
927+
* C trampoline that bridges the tidesdb_commit_hook_fn callback to the Java CommitHook.onCommit
928+
* method. Fires synchronously on the committing thread (which is always a Java thread).
929+
*/
930+
static int java_commit_hook_trampoline(const tidesdb_commit_op_t *ops, int num_ops,
931+
uint64_t commit_seq, void *ctx)
932+
{
933+
java_hook_ctx_t *hctx = (java_hook_ctx_t *)ctx;
934+
JNIEnv *env = NULL;
935+
int need_detach = 0;
936+
937+
jint rc = (*hctx->jvm)->GetEnv(hctx->jvm, (void **)&env, JNI_VERSION_1_6);
938+
if (rc == JNI_EDETACHED)
939+
{
940+
if ((*hctx->jvm)->AttachCurrentThread(hctx->jvm, (void **)&env, NULL) != 0)
941+
return -1;
942+
need_detach = 1;
943+
}
944+
else if (rc != JNI_OK)
945+
{
946+
return -1;
947+
}
948+
949+
jint ret = -1;
950+
951+
/* Find CommitOp class and constructor: CommitOp(byte[], byte[], long, boolean) */
952+
jclass commitOpClass = (*env)->FindClass(env, "com/tidesdb/CommitOp");
953+
if (commitOpClass == NULL)
954+
goto cleanup;
955+
956+
jmethodID ctor = (*env)->GetMethodID(env, commitOpClass, "<init>", "([B[BJZ)V");
957+
if (ctor == NULL)
958+
goto cleanup;
959+
960+
/* Create CommitOp[] array */
961+
jobjectArray opsArray = (*env)->NewObjectArray(env, num_ops, commitOpClass, NULL);
962+
if (opsArray == NULL)
963+
goto cleanup;
964+
965+
for (int i = 0; i < num_ops; i++)
966+
{
967+
jbyteArray jkey = (*env)->NewByteArray(env, (jsize)ops[i].key_size);
968+
(*env)->SetByteArrayRegion(env, jkey, 0, (jsize)ops[i].key_size, (jbyte *)ops[i].key);
969+
970+
jbyteArray jvalue = NULL;
971+
if (ops[i].value != NULL && ops[i].value_size > 0)
972+
{
973+
jvalue = (*env)->NewByteArray(env, (jsize)ops[i].value_size);
974+
(*env)->SetByteArrayRegion(env, jvalue, 0, (jsize)ops[i].value_size,
975+
(jbyte *)ops[i].value);
976+
}
977+
978+
jobject opObj = (*env)->NewObject(env, commitOpClass, ctor, jkey, jvalue,
979+
(jlong)ops[i].ttl,
980+
ops[i].is_delete ? JNI_TRUE : JNI_FALSE);
981+
(*env)->SetObjectArrayElement(env, opsArray, i, opObj);
982+
983+
(*env)->DeleteLocalRef(env, opObj);
984+
(*env)->DeleteLocalRef(env, jkey);
985+
if (jvalue != NULL)
986+
(*env)->DeleteLocalRef(env, jvalue);
987+
}
988+
989+
/* Call CommitHook.onCommit(CommitOp[], long) */
990+
jclass hookClass = (*env)->GetObjectClass(env, hctx->hook_obj);
991+
jmethodID onCommit =
992+
(*env)->GetMethodID(env, hookClass, "onCommit", "([Lcom/tidesdb/CommitOp;J)I");
993+
994+
ret = (*env)->CallIntMethod(env, hctx->hook_obj, onCommit, opsArray, (jlong)commit_seq);
995+
996+
if ((*env)->ExceptionCheck(env))
997+
{
998+
(*env)->ExceptionClear(env);
999+
ret = -1;
1000+
}
1001+
1002+
(*env)->DeleteLocalRef(env, opsArray);
1003+
(*env)->DeleteLocalRef(env, commitOpClass);
1004+
(*env)->DeleteLocalRef(env, hookClass);
1005+
1006+
if (need_detach)
1007+
(*hctx->jvm)->DetachCurrentThread(hctx->jvm);
1008+
1009+
return (int)ret;
1010+
1011+
cleanup:
1012+
if ((*env)->ExceptionCheck(env))
1013+
(*env)->ExceptionClear(env);
1014+
if (need_detach)
1015+
(*hctx->jvm)->DetachCurrentThread(hctx->jvm);
1016+
return -1;
1017+
}
1018+
1019+
JNIEXPORT jlong JNICALL Java_com_tidesdb_ColumnFamily_nativeSetCommitHook(JNIEnv *env, jclass cls,
1020+
jlong cfHandle,
1021+
jobject hook,
1022+
jlong oldCtxHandle)
1023+
{
1024+
tidesdb_column_family_t *cf = (tidesdb_column_family_t *)(uintptr_t)cfHandle;
1025+
1026+
/* Free old context if present */
1027+
if (oldCtxHandle != 0)
1028+
{
1029+
java_hook_ctx_t *old_ctx = (java_hook_ctx_t *)(uintptr_t)oldCtxHandle;
1030+
(*env)->DeleteGlobalRef(env, old_ctx->hook_obj);
1031+
free(old_ctx);
1032+
}
1033+
1034+
/* If hook is NULL, clear the hook */
1035+
if (hook == NULL)
1036+
{
1037+
int result = tidesdb_cf_set_commit_hook(cf, NULL, NULL);
1038+
if (result != TDB_SUCCESS)
1039+
{
1040+
throwTidesDBException(env, result, getErrorMessage(result));
1041+
}
1042+
return 0;
1043+
}
1044+
1045+
/* Allocate new context */
1046+
java_hook_ctx_t *ctx = (java_hook_ctx_t *)malloc(sizeof(java_hook_ctx_t));
1047+
if (ctx == NULL)
1048+
{
1049+
throwTidesDBException(env, TDB_ERR_MEMORY, "Failed to allocate commit hook context");
1050+
return 0;
1051+
}
1052+
1053+
(*env)->GetJavaVM(env, &ctx->jvm);
1054+
ctx->hook_obj = (*env)->NewGlobalRef(env, hook);
1055+
1056+
int result = tidesdb_cf_set_commit_hook(cf, java_commit_hook_trampoline, ctx);
1057+
if (result != TDB_SUCCESS)
1058+
{
1059+
(*env)->DeleteGlobalRef(env, ctx->hook_obj);
1060+
free(ctx);
1061+
throwTidesDBException(env, result, getErrorMessage(result));
1062+
return 0;
1063+
}
1064+
1065+
return (jlong)(uintptr_t)ctx;
1066+
}
1067+
9161068
JNIEXPORT jdouble JNICALL Java_com_tidesdb_ColumnFamily_nativeRangeCost(JNIEnv *env, jclass cls,
9171069
jlong handle,
9181070
jbyteArray keyA,

src/main/java/com/tidesdb/ColumnFamily.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class ColumnFamily {
3030

3131
private final long nativeHandle;
3232
private final String name;
33+
private long commitHookCtxHandle = 0;
3334

3435
ColumnFamily(long nativeHandle, String name) {
3536
this.nativeHandle = nativeHandle;
@@ -146,6 +147,35 @@ public double rangeCost(byte[] keyA, byte[] keyB) throws TidesDBException {
146147
return nativeRangeCost(nativeHandle, keyA, keyB);
147148
}
148149

150+
/**
151+
* Sets a commit hook (Change Data Capture) for this column family.
152+
* The hook fires synchronously after every transaction commit, receiving the full
153+
* batch of committed operations atomically. Keep the callback fast to avoid
154+
* stalling writers.
155+
*
156+
* <p>Hooks are runtime-only and not persisted. After a database restart,
157+
* hooks must be re-registered by the application.</p>
158+
*
159+
* @param hook the commit hook callback
160+
* @throws TidesDBException if the hook cannot be set
161+
*/
162+
public void setCommitHook(CommitHook hook) throws TidesDBException {
163+
if (hook == null) {
164+
throw new IllegalArgumentException("Hook cannot be null, use clearCommitHook() instead");
165+
}
166+
commitHookCtxHandle = nativeSetCommitHook(nativeHandle, hook, commitHookCtxHandle);
167+
}
168+
169+
/**
170+
* Clears the commit hook for this column family.
171+
* After this call, no further commit callbacks will fire.
172+
*
173+
* @throws TidesDBException if the hook cannot be cleared
174+
*/
175+
public void clearCommitHook() throws TidesDBException {
176+
commitHookCtxHandle = nativeSetCommitHook(nativeHandle, null, commitHookCtxHandle);
177+
}
178+
149179
long getNativeHandle() {
150180
return nativeHandle;
151181
}
@@ -159,4 +189,5 @@ private static native void nativeUpdateRuntimeConfig(long handle, long writeBuff
159189
int skipListMaxLevel, float skipListProbability, double bloomFPR, int indexSampleRatio,
160190
int syncMode, long syncIntervalUs, boolean persistToDisk) throws TidesDBException;
161191
private static native double nativeRangeCost(long handle, byte[] keyA, byte[] keyB) throws TidesDBException;
192+
private static native long nativeSetCommitHook(long handle, CommitHook hook, long oldCtxHandle) throws TidesDBException;
162193
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
*
3+
* Copyright (C) TidesDB
4+
*
5+
* Original Author: Alex Gaetano Padula
6+
*
7+
* Licensed under the Mozilla Public License, v. 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* https://www.mozilla.org/en-US/MPL/2.0/
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
package com.tidesdb;
20+
21+
/**
22+
* Callback interface for commit hooks (Change Data Capture).
23+
* Invoked synchronously after a transaction commits to a column family.
24+
* The hook receives the full batch of committed operations atomically.
25+
*/
26+
@FunctionalInterface
27+
public interface CommitHook {
28+
29+
/**
30+
* Called after a transaction commits to a column family.
31+
*
32+
* @param ops array of committed operations
33+
* @param commitSeq monotonic commit sequence number
34+
* @return 0 on success, non-zero on failure (logged as warning, does not roll back)
35+
*/
36+
int onCommit(CommitOp[] ops, long commitSeq);
37+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
*
3+
* Copyright (C) TidesDB
4+
*
5+
* Original Author: Alex Gaetano Padula
6+
*
7+
* Licensed under the Mozilla Public License, v. 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* https://www.mozilla.org/en-US/MPL/2.0/
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
package com.tidesdb;
20+
21+
/**
22+
* Represents a single operation in a committed transaction batch.
23+
* Passed to the commit hook callback after a transaction commits.
24+
*/
25+
public class CommitOp {
26+
27+
private final byte[] key;
28+
private final byte[] value;
29+
private final long ttl;
30+
private final boolean delete;
31+
32+
/**
33+
* Creates a new CommitOp.
34+
*
35+
* @param key the key
36+
* @param value the value (null for deletes)
37+
* @param ttl time-to-live (-1 for no expiry)
38+
* @param delete true if this is a delete operation
39+
*/
40+
public CommitOp(byte[] key, byte[] value, long ttl, boolean delete) {
41+
this.key = key;
42+
this.value = value;
43+
this.ttl = ttl;
44+
this.delete = delete;
45+
}
46+
47+
/**
48+
* Gets the key for this operation.
49+
*
50+
* @return the key bytes
51+
*/
52+
public byte[] getKey() {
53+
return key;
54+
}
55+
56+
/**
57+
* Gets the value for this operation.
58+
*
59+
* @return the value bytes, or null for delete operations
60+
*/
61+
public byte[] getValue() {
62+
return value;
63+
}
64+
65+
/**
66+
* Gets the TTL for this operation.
67+
*
68+
* @return the TTL in seconds since epoch, or -1 for no expiry
69+
*/
70+
public long getTtl() {
71+
return ttl;
72+
}
73+
74+
/**
75+
* Returns whether this is a delete operation.
76+
*
77+
* @return true if delete, false if put
78+
*/
79+
public boolean isDelete() {
80+
return delete;
81+
}
82+
}

0 commit comments

Comments
 (0)