Skip to content

Commit f5afbf6

Browse files
bluestreak01claude
andcommitted
feat(ilp): QWiP store-and-forward client buffer
Opt-in durable buffer for the QWP WebSocket ingest client. Outgoing batches are persisted to disk before they leave the wire; the server's cumulative ACK trims sealed segments; on restart or transient failure, the I/O thread silently reconnects and replays whatever is still on disk. On-disk format is the QWP wire frame captured verbatim, wrapped in an 8-byte SF envelope (CRC32C + length) so torn tails and silent bit-rot are caught on recovery. Filenames encode (baseSeq, lastSeq) so trim and recovery don't have to scan sealed segments. Auto-reconnect absorbs all transient connection failures with exponential backoff (capped at 30s); only fatal SF storage errors (corruption, frame larger than segment cap) propagate to the user. flush() under SF returns once data is on disk, not on server ACK — natural backpressure when SF total disk cap is reached makes flush() block until ACKs free space. All file I/O goes through a new native Files layer ported from the upstream QuestDB server repo: open/read/write/fsync/truncate/allocate/ length/lock/mkdir/exists/remove/rename/dir-iteration. Software CRC32C implementation (Castagnoli, polynomial 0x1EDC6F41) added alongside. Configured via connect string: ws::addr=... ;store_and_forward=on ;sf_dir=/var/lib/qdb/sf ;sf_max_bytes=67108864 (per-segment rotation; default 64 MiB) ;sf_max_total_bytes=4G (hard cap → backpressure; default unlimited) ;sf_fsync=on (fsync after every append; default off) 49 new tests across 6 files: native Files (9), CRC32C (7 incl. property- fuzz + bit-flip), SegmentLog (14), SegmentLog torture (7 incl. randomized op-sequence fuzzer + multi-crash), SF integration (10 incl. multi- reconnect + replay-during-replay + stress), connect-string from-config (11). 1956 tests pass total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 64b7ee6 commit f5afbf6

20 files changed

Lines changed: 5137 additions & 39 deletions

core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ set(
5353
src/main/c/share/cpprt_overrides.cpp
5454
src/main/c/share/byte_sink.cpp
5555
src/main/c/share/byte_sink.h
56+
src/main/c/share/crc32c.c
5657
)
5758

5859
# libzstd is included via a git submodule at src/main/c/share/zstd (pinned to

core/src/main/c/share/crc32c.c

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*+*****************************************************************************
2+
* ___ _ ____ ____
3+
* / _ \ _ _ ___ ___| |_| _ \| __ )
4+
* | | | | | | |/ _ \/ __| __| | | | _ \
5+
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
6+
* \__\_\\__,_|\___||___/\__|____/|____/
7+
*
8+
* Copyright (c) 2014-2019 Appsicle
9+
* Copyright (c) 2019-2026 QuestDB
10+
*
11+
* Licensed under the Apache License, Version 2.0 (the "License");
12+
* you may not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing, software
18+
* distributed under the License is distributed on an "AS IS" BASIS,
19+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20+
* See the License for the specific language governing permissions and
21+
* limitations under the License.
22+
*
23+
******************************************************************************/
24+
25+
#include <jni.h>
26+
#include <stdint.h>
27+
#include <stddef.h>
28+
29+
#define CRC32C_POLY_REVERSED 0x82F63B78u
30+
31+
static uint32_t crc32c_table[256];
32+
static volatile int crc32c_table_ready = 0;
33+
34+
static void crc32c_init(void) {
35+
for (int i = 0; i < 256; i++) {
36+
uint32_t c = (uint32_t) i;
37+
for (int j = 0; j < 8; j++) {
38+
c = (c & 1u) ? (c >> 1) ^ CRC32C_POLY_REVERSED : (c >> 1);
39+
}
40+
crc32c_table[i] = c;
41+
}
42+
crc32c_table_ready = 1;
43+
}
44+
45+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Crc32c_update
46+
(JNIEnv *e, jclass cl, jint seed, jlong addr, jlong len) {
47+
if (len <= 0) {
48+
return seed;
49+
}
50+
if (!crc32c_table_ready) {
51+
crc32c_init();
52+
}
53+
uint32_t crc = ~((uint32_t) seed);
54+
const uint8_t *buf = (const uint8_t *) (uintptr_t) addr;
55+
size_t n = (size_t) len;
56+
while (n--) {
57+
crc = (crc >> 8) ^ crc32c_table[(crc ^ *buf++) & 0xffu];
58+
}
59+
return (jint) ~crc;
60+
}

core/src/main/c/share/files.c

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,249 @@
2222
*
2323
******************************************************************************/
2424

25+
#define _GNU_SOURCE
26+
2527
#include <unistd.h>
28+
#include <sys/stat.h>
29+
#include <sys/file.h>
30+
#include <fcntl.h>
31+
#include <errno.h>
32+
#include <stdlib.h>
33+
#include <string.h>
34+
#include <dirent.h>
35+
#include <stdint.h>
36+
2637
#include "files.h"
2738

39+
#define RESTARTABLE(_expr_, _rc_) \
40+
do { _rc_ = (_expr_); } while ((_rc_) == -1 && errno == EINTR)
41+
2842
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_close0
2943
(JNIEnv *e, jclass cl, jint fd) {
3044
return close((int) fd);
3145
}
46+
47+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_openRO0
48+
(JNIEnv *e, jclass cl, jlong lpszName) {
49+
int fd;
50+
RESTARTABLE(open((const char *) (uintptr_t) lpszName, O_RDONLY), fd);
51+
return (jint) fd;
52+
}
53+
54+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_openRW0
55+
(JNIEnv *e, jclass cl, jlong lpszName) {
56+
int fd;
57+
RESTARTABLE(open((const char *) (uintptr_t) lpszName, O_CREAT | O_RDWR, 0644), fd);
58+
return (jint) fd;
59+
}
60+
61+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_openAppend0
62+
(JNIEnv *e, jclass cl, jlong lpszName) {
63+
int fd;
64+
RESTARTABLE(open((const char *) (uintptr_t) lpszName, O_CREAT | O_WRONLY | O_APPEND, 0644), fd);
65+
return (jint) fd;
66+
}
67+
68+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_openCleanRW0
69+
(JNIEnv *e, jclass cl, jlong lpszName, jlong size) {
70+
int fd;
71+
RESTARTABLE(open((const char *) (uintptr_t) lpszName, O_CREAT | O_TRUNC | O_RDWR, 0644), fd);
72+
if (fd < 0) {
73+
return -1;
74+
}
75+
if (size > 0) {
76+
int rc;
77+
RESTARTABLE(ftruncate(fd, (off_t) size), rc);
78+
if (rc != 0) {
79+
int saved = errno;
80+
close(fd);
81+
errno = saved;
82+
return -1;
83+
}
84+
}
85+
return (jint) fd;
86+
}
87+
88+
JNIEXPORT jlong JNICALL Java_io_questdb_client_std_Files_read
89+
(JNIEnv *e, jclass cl, jint fd, jlong addr, jlong len, jlong offset) {
90+
ssize_t res;
91+
RESTARTABLE(pread((int) fd, (void *) (uintptr_t) addr, (size_t) len, (off_t) offset), res);
92+
return (jlong) res;
93+
}
94+
95+
JNIEXPORT jlong JNICALL Java_io_questdb_client_std_Files_write
96+
(JNIEnv *e, jclass cl, jint fd, jlong addr, jlong len, jlong offset) {
97+
ssize_t res;
98+
RESTARTABLE(pwrite((int) fd, (const void *) (uintptr_t) addr, (size_t) len, (off_t) offset), res);
99+
return (jlong) res;
100+
}
101+
102+
JNIEXPORT jlong JNICALL Java_io_questdb_client_std_Files_append
103+
(JNIEnv *e, jclass cl, jint fd, jlong addr, jlong len) {
104+
ssize_t res;
105+
RESTARTABLE(write((int) fd, (const void *) (uintptr_t) addr, (size_t) len), res);
106+
return (jlong) res;
107+
}
108+
109+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_fsync
110+
(JNIEnv *e, jclass cl, jint fd) {
111+
int res;
112+
RESTARTABLE(fsync((int) fd), res);
113+
return res;
114+
}
115+
116+
JNIEXPORT jboolean JNICALL Java_io_questdb_client_std_Files_truncate
117+
(JNIEnv *e, jclass cl, jint fd, jlong size) {
118+
int res;
119+
RESTARTABLE(ftruncate((int) fd, (off_t) size), res);
120+
return res == 0 ? JNI_TRUE : JNI_FALSE;
121+
}
122+
123+
JNIEXPORT jboolean JNICALL Java_io_questdb_client_std_Files_allocate
124+
(JNIEnv *e, jclass cl, jint fd, jlong size) {
125+
#if defined(__linux__)
126+
int res = posix_fallocate((int) fd, 0, (off_t) size);
127+
if (res == 0) {
128+
return JNI_TRUE;
129+
}
130+
if (res != EINVAL && res != EOPNOTSUPP) {
131+
errno = res;
132+
return JNI_FALSE;
133+
}
134+
/* fall through to ftruncate */
135+
#elif defined(__APPLE__)
136+
fstore_t fst;
137+
fst.fst_flags = F_ALLOCATECONTIG | F_ALLOCATEALL;
138+
fst.fst_posmode = F_PEOFPOSMODE;
139+
fst.fst_offset = 0;
140+
fst.fst_length = (off_t) size;
141+
fst.fst_bytesalloc = 0;
142+
if (fcntl((int) fd, F_PREALLOCATE, &fst) == -1) {
143+
fst.fst_flags = F_ALLOCATEALL;
144+
(void) fcntl((int) fd, F_PREALLOCATE, &fst);
145+
/* if F_PREALLOCATE fails we still try ftruncate to set logical size */
146+
}
147+
#endif
148+
int res2;
149+
RESTARTABLE(ftruncate((int) fd, (off_t) size), res2);
150+
return res2 == 0 ? JNI_TRUE : JNI_FALSE;
151+
}
152+
153+
JNIEXPORT jlong JNICALL Java_io_questdb_client_std_Files_length
154+
(JNIEnv *e, jclass cl, jint fd) {
155+
struct stat st;
156+
if (fstat((int) fd, &st) != 0) {
157+
return -1;
158+
}
159+
return (jlong) st.st_size;
160+
}
161+
162+
JNIEXPORT jlong JNICALL Java_io_questdb_client_std_Files_length0
163+
(JNIEnv *e, jclass cl, jlong lpszName) {
164+
struct stat st;
165+
if (stat((const char *) (uintptr_t) lpszName, &st) != 0) {
166+
return -1;
167+
}
168+
return (jlong) st.st_size;
169+
}
170+
171+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_lock
172+
(JNIEnv *e, jclass cl, jint fd) {
173+
return flock((int) fd, LOCK_EX | LOCK_NB);
174+
}
175+
176+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_mkdir0
177+
(JNIEnv *e, jclass cl, jlong lpszPath, jint mode) {
178+
return mkdir((const char *) (uintptr_t) lpszPath, (mode_t) mode);
179+
}
180+
181+
JNIEXPORT jboolean JNICALL Java_io_questdb_client_std_Files_exists0
182+
(JNIEnv *e, jclass cl, jlong lpszPath) {
183+
return access((const char *) (uintptr_t) lpszPath, F_OK) == 0 ? JNI_TRUE : JNI_FALSE;
184+
}
185+
186+
JNIEXPORT jboolean JNICALL Java_io_questdb_client_std_Files_remove0
187+
(JNIEnv *e, jclass cl, jlong lpszPath) {
188+
return remove((const char *) (uintptr_t) lpszPath) == 0 ? JNI_TRUE : JNI_FALSE;
189+
}
190+
191+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_rename0
192+
(JNIEnv *e, jclass cl, jlong lpszOld, jlong lpszNew) {
193+
return rename((const char *) (uintptr_t) lpszOld, (const char *) (uintptr_t) lpszNew);
194+
}
195+
196+
typedef struct {
197+
DIR *dir;
198+
struct dirent *entry;
199+
} qdb_find_t;
200+
201+
JNIEXPORT jlong JNICALL Java_io_questdb_client_std_Files_findFirst0
202+
(JNIEnv *e, jclass cl, jlong lpszName) {
203+
DIR *dir = opendir((const char *) (uintptr_t) lpszName);
204+
if (!dir) {
205+
return 0;
206+
}
207+
qdb_find_t *find = (qdb_find_t *) malloc(sizeof(qdb_find_t));
208+
if (!find) {
209+
closedir(dir);
210+
return 0;
211+
}
212+
find->dir = dir;
213+
errno = 0;
214+
find->entry = readdir(dir);
215+
if (!find->entry) {
216+
int saved = errno;
217+
closedir(dir);
218+
free(find);
219+
errno = saved;
220+
return 0;
221+
}
222+
return (jlong) (uintptr_t) find;
223+
}
224+
225+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_findNext
226+
(JNIEnv *e, jclass cl, jlong findPtr) {
227+
qdb_find_t *find = (qdb_find_t *) (uintptr_t) findPtr;
228+
if (!find) {
229+
return -1;
230+
}
231+
errno = 0;
232+
find->entry = readdir(find->dir);
233+
if (find->entry) {
234+
return 1;
235+
}
236+
return errno == 0 ? 0 : -1;
237+
}
238+
239+
JNIEXPORT jlong JNICALL Java_io_questdb_client_std_Files_findName
240+
(JNIEnv *e, jclass cl, jlong findPtr) {
241+
qdb_find_t *find = (qdb_find_t *) (uintptr_t) findPtr;
242+
if (!find || !find->entry) {
243+
return 0;
244+
}
245+
return (jlong) (uintptr_t) find->entry->d_name;
246+
}
247+
248+
JNIEXPORT jint JNICALL Java_io_questdb_client_std_Files_findType
249+
(JNIEnv *e, jclass cl, jlong findPtr) {
250+
qdb_find_t *find = (qdb_find_t *) (uintptr_t) findPtr;
251+
if (!find || !find->entry) {
252+
return 0;
253+
}
254+
return (jint) find->entry->d_type;
255+
}
256+
257+
JNIEXPORT void JNICALL Java_io_questdb_client_std_Files_findClose
258+
(JNIEnv *e, jclass cl, jlong findPtr) {
259+
qdb_find_t *find = (qdb_find_t *) (uintptr_t) findPtr;
260+
if (find) {
261+
closedir(find->dir);
262+
free(find);
263+
}
264+
}
265+
266+
JNIEXPORT jlong JNICALL Java_io_questdb_client_std_Files_getPageSize0
267+
(JNIEnv *e, jclass cl) {
268+
long sz = sysconf(_SC_PAGESIZE);
269+
return (jlong) (sz > 0 ? sz : 4096);
270+
}

0 commit comments

Comments
 (0)