cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
nuke_db.c
Go to the documentation of this file.
1#define _POSIX_C_SOURCE 200809L
2#ifndef _DEFAULT_SOURCE
3#define _DEFAULT_SOURCE
4#endif
5#if defined(__APPLE__)
6/* Keep kqueue/kevent visible: strict _POSIX_C_SOURCE hides them on macOS. */
7#define _DARWIN_C_SOURCE
8#endif
9#include <stdatomic.h>
11#include <cwist/core/macros.h>
13#include <cwist/sys/sys_info.h>
14#include <sqlite3.h>
15#include <stdio.h>
16#include <stdlib.h>
17#include <string.h>
18#include <unistd.h>
19#include <signal.h>
20#include <pthread.h>
21#include <sys/stat.h>
22#include <sys/mman.h>
23#include <sys/types.h>
24#include <sys/time.h>
25#include <errno.h>
26#include <fcntl.h>
27
28#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
29#include <sys/event.h>
30#endif
31
37#ifndef NSIG
38#if defined(_NSIG)
39#define NSIG _NSIG
40#elif defined(SIGRTMAX)
41#define NSIG (SIGRTMAX + 1)
42#else
43#define NSIG 32
44#endif
45#endif
46
47static cwist_nuke_db_t g_nuke = {0};
48static pthread_t g_sync_thread = 0;
49static _Atomic bool g_running = false; /* read by the sync thread, written by init/close */
50static pthread_mutex_t g_nuke_lock = PTHREAD_MUTEX_INITIALIZER;
51static sigset_t g_sigset;
52
53#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
54// @brief bsd compatible sigtimedwait
55static inline int bsd_sigtimedwait(const sigset_t *set, siginfo_t *info,
56 const struct timespec *timeout) {
57 int kq = kqueue();
58 if (kq < 0) {
59 return -1;
60 }
61
62 struct kevent changes[NSIG];
63 int nchanges = 0;
64
65 for (int sig = 1; sig < NSIG; sig++) {
66 if (sigismember(set, sig)) {
67 EV_SET(&changes[nchanges], sig, EVFILT_SIGNAL, EV_ADD | EV_ENABLE | EV_ONESHOT, 0, 0,
68 NULL);
69 nchanges++;
70 }
71 }
72
73 if (nchanges == 0) {
74 close(kq);
75 errno = EINVAL;
76 return -1;
77 }
78
79 struct kevent event;
80 int ret = kevent(kq, changes, nchanges, &event, 1, timeout);
81
82 int saved_errno = errno;
83 close(kq);
84
85 if (ret < 0) {
86 errno = saved_errno;
87 return -1;
88 }
89
90 if (ret == 0) {
91 errno = EAGAIN;
92 return -1;
93 }
94
95 int sig = (int)event.ident;
96
97 if (info != NULL) {
98 memset(info, 0, sizeof(siginfo_t));
99 info->si_signo = sig;
100 }
101
102 return sig;
103}
104#endif
105
111static uint64_t nuke_estimate_required_ram(const char *disk_path) {
112 struct stat st;
113 uint64_t db_size = 0;
114 if (disk_path && stat(disk_path, &st) == 0 && st.st_size > 0) {
115 db_size = (uint64_t)st.st_size;
116 }
117 // Need room for the DB plus SQLite cache/overhead. Double the DB and add base buffer.
118 uint64_t required = (db_size * 2) + CWIST_MIB(32);
119 if (required < CWIST_MIB(64)) {
120 required = CWIST_MIB(64);
121 }
122 return required;
123}
124
130static bool nuke_disk_has_tables(sqlite3 *db) {
131 if (!db) return true;
132 sqlite3_stmt *stmt = NULL;
133 bool has_tables = true;
134 if (sqlite3_prepare_v2(db, "SELECT count(*) FROM sqlite_master WHERE type='table';", -1, &stmt,
135 NULL) == SQLITE_OK) {
136 if (sqlite3_step(stmt) == SQLITE_ROW) {
137 has_tables = sqlite3_column_int(stmt, 0) > 0;
138 }
139 }
140 if (stmt) sqlite3_finalize(stmt);
141 return has_tables;
142}
143
149static bool nuke_integrity_ok(sqlite3 *db) {
150 if (!db) return false;
151 sqlite3_stmt *stmt = NULL;
152 bool healthy = false;
153 if (sqlite3_prepare_v2(db, "PRAGMA integrity_check;", -1, &stmt, NULL) == SQLITE_OK) {
154 if (sqlite3_step(stmt) == SQLITE_ROW) {
155 const unsigned char *state = sqlite3_column_text(stmt, 0);
156 healthy = (state && strcmp((const char *)state, "ok") == 0);
157 }
158 }
159 if (stmt) sqlite3_finalize(stmt);
160 return healthy;
161}
162
163// Helper: Backup source_db to dest_db
170static int nuke_backup(sqlite3 *dest, sqlite3 *source) {
171 if (!dest || !source) return -1;
172 sqlite3_backup *backup = sqlite3_backup_init(dest, "main", source, "main");
173 if (!backup) {
174 fprintf(stderr, "[NukeDB] sqlite3_backup_init failed: %s\n", sqlite3_errmsg(dest));
175 return -1;
176 }
177
178 int rc = sqlite3_backup_step(backup, -1); // Copy all pages
179 if (rc != SQLITE_DONE) {
180 fprintf(stderr, "[NukeDB] Backup step failed: %d (%s)\n", rc, sqlite3_errmsg(dest));
181 }
182
183 sqlite3_backup_finish(backup);
184 return (rc == SQLITE_DONE) ? 0 : -1;
185}
186
190static void nuke_switch_to_disk(void) {
191 if (g_nuke.is_disk_mode) return;
192
193 printf("[NukeDB] CRITICAL: Low RAM detected (%zu bytes). Switching to Disk DB.\n",
194 (size_t)cwist_get_available_ram());
195
196 pthread_mutex_lock(&g_nuke_lock);
197
198 // 1. Flush Memory -> Disk
199 nuke_backup(g_nuke.disk_db, g_nuke.mem_db);
200
201 // 2. Set mode flag
202 sqlite3_db_release_memory(g_nuke.mem_db);
203
204 g_nuke.is_disk_mode = true;
205
206 pthread_mutex_unlock(&g_nuke_lock);
207}
208
209// Internal commit hook to trigger immediate sync
215static int nuke_commit_hook(void *arg) {
216 CWIST_UNUSED(arg);
217 if (g_running && !g_nuke.is_disk_mode && g_sync_thread != 0) {
218 // Send signal to wake up sync thread
219 pthread_kill(g_sync_thread, SIGUSR2);
220 }
221 return 0;
222}
223
229 pthread_mutex_lock(&g_nuke_lock);
230
231 if (g_nuke.is_disk_mode) {
232 if (g_nuke.disk_db) {
233 sqlite3_wal_checkpoint_v2(g_nuke.disk_db, "main", SQLITE_CHECKPOINT_PASSIVE, NULL,
234 NULL);
235 pthread_mutex_unlock(&g_nuke_lock);
236 return 0;
237 }
238 pthread_mutex_unlock(&g_nuke_lock);
239 return -1;
240 }
241
242 // Safety: If initial load failed, don't overwrite disk with empty memory DB
243 if (!g_nuke.load_successful) {
244 pthread_mutex_unlock(&g_nuke_lock);
245 return -1;
246 }
247
248 if (!g_nuke.mem_db || !g_nuke.disk_db) {
249 pthread_mutex_unlock(&g_nuke_lock);
250 return -1;
251 }
252
253 int rc = nuke_backup(g_nuke.disk_db, g_nuke.mem_db);
254 pthread_mutex_unlock(&g_nuke_lock);
255 return rc;
256}
257
258// Internal idempotent cleanup
262static void nuke_cleanup_internal(void) {
263 pthread_mutex_lock(&g_nuke_lock);
264
265 // Close Memory DB
266 if (g_nuke.mem_db) {
267 sqlite3_close(g_nuke.mem_db);
268 g_nuke.mem_db = NULL;
269 }
270
271 // Close Disk DB
272 if (g_nuke.disk_db) {
273 sqlite3_close(g_nuke.disk_db);
274 g_nuke.disk_db = NULL;
275 }
276
277 if (g_nuke.disk_path) {
278 cwist_free(g_nuke.disk_path);
279 g_nuke.disk_path = NULL;
280 }
281
282 g_nuke.auto_sync = false;
283 g_nuke.sync_interval_ms = 0;
284 g_nuke.is_disk_mode = false;
285 g_nuke.load_successful = false;
286
287 pthread_mutex_unlock(&g_nuke_lock);
288}
289
294 if (!g_running) return;
295 g_running = false;
296
297 // Wake up the sync thread if it's sleeping/waiting
298 if (g_sync_thread != 0) {
299 pthread_kill(g_sync_thread, SIGUSR1);
300 pthread_join(g_sync_thread, NULL);
301 g_sync_thread = 0;
302 }
303
305 nuke_cleanup_internal();
306}
307
313static void *sync_thread_func(void *arg) {
314 CWIST_UNUSED(arg);
315 struct timespec timeout;
316 int signum;
317
318 while (g_running) {
319 timeout.tv_sec = g_nuke.sync_interval_ms / 1000;
320 timeout.tv_nsec = (g_nuke.sync_interval_ms % 1000) * 1000000;
321
322 // Wait for signals (INT, TERM, USR1, USR2) or Timeout
323#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
324#define sigtimedwait bsd_sigtimedwait
325#endif
326
327 signum = sigtimedwait(&g_sigset, NULL, &timeout);
328
329 if (signum > 0) {
330 if (signum == SIGUSR1) {
331 // Requested to stop via cwist_nuke_close
332 break;
333 }
334
335 if (signum == SIGUSR2) {
336 // Immediate sync requested via commit hook
338 continue;
339 }
340
341 // Handle termination signals (SIGINT, SIGTERM)
342 printf("\n[NukeDB] Intercepted Signal %d. Saving data...\n", signum);
343
345 nuke_cleanup_internal();
346 g_running = false;
347
348 // Unblock the signal first
349 sigset_t s;
350 sigemptyset(&s);
351 sigaddset(&s, signum);
352 pthread_sigmask(SIG_UNBLOCK, &s, NULL);
353
354 // Restore default handler and re-raise
355 signal(signum, SIG_DFL);
356 raise(signum);
357 return NULL;
358 } else { // Timeout (EAGAIN) or Interruption
359 if (errno == EAGAIN && g_running && g_nuke.auto_sync) {
361 nuke_switch_to_disk();
362 } else {
364 }
365 }
366 }
367 }
368 return NULL;
369}
370
377int cwist_nuke_init(const char *disk_path, int sync_interval_ms) {
378 if (g_running) return CWIST_NUKE_ERR_GENERIC; // Already running
379 if (!disk_path) return CWIST_NUKE_ERR_GENERIC;
380
381 uint64_t required_ram = nuke_estimate_required_ram(disk_path);
382 uint64_t available_ram = cwist_get_available_ram();
383 if (available_ram > 0 && available_ram < required_ram) {
384 fprintf(stderr,
385 "[NukeDB] Insufficient RAM for in-memory mode (have %zu bytes, need ~%zu bytes).\n",
386 (size_t)available_ram, (size_t)required_ram);
388 }
389
390 g_nuke.disk_path = cwist_strdup(disk_path);
391 if (!g_nuke.disk_path) {
393 }
394 g_nuke.sync_interval_ms = sync_interval_ms > 0 ? sync_interval_ms : 1000;
395 g_nuke.auto_sync = (sync_interval_ms > 0);
396 g_nuke.is_disk_mode = false;
397 g_nuke.load_successful = false;
398
399 // 1. Open Disk DB
400 int rc = sqlite3_open(disk_path, &g_nuke.disk_db);
401 if (rc != SQLITE_OK) {
402 fprintf(stderr, "[NukeDB] Failed to open disk DB: %s\n", sqlite3_errmsg(g_nuke.disk_db));
403 nuke_cleanup_internal();
405 }
406
407 if (!nuke_integrity_ok(g_nuke.disk_db)) {
408 fprintf(stderr, "[NukeDB] Integrity check failed for '%s'. Aborting in-memory mode.\n",
409 disk_path);
410 nuke_cleanup_internal();
412 }
413
414 sqlite3_exec(g_nuke.disk_db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
415 sqlite3_exec(g_nuke.disk_db, "PRAGMA synchronous=NORMAL;", NULL, NULL, NULL);
416
417 // 2. Open Memory DB
418 rc = sqlite3_open(":memory:", &g_nuke.mem_db);
419 if (rc != SQLITE_OK) {
420 fprintf(stderr, "[NukeDB] Failed to open memory DB: %s\n", sqlite3_errmsg(g_nuke.mem_db));
421 nuke_cleanup_internal();
423 }
424
425 // 3. Load Disk -> Memory
426 pthread_mutex_lock(&g_nuke_lock);
427
428 // Use sqlite3_backup as it is safer than raw deserialize for WAL/mode transitions
429 int backup_rc = nuke_backup(g_nuke.mem_db, g_nuke.disk_db);
430 if (backup_rc == 0) {
431 g_nuke.load_successful = true;
432 } else if (!nuke_disk_has_tables(g_nuke.disk_db)) {
433 g_nuke.load_successful = true;
434 } else {
435 fprintf(stderr,
436 "[NukeDB] Initial disk->memory load failed. Falling back to disk-only mode.\n");
437 g_nuke.is_disk_mode = true;
438 sqlite3_close(g_nuke.mem_db);
439 g_nuke.mem_db = NULL;
440 }
441 pthread_mutex_unlock(&g_nuke_lock);
442
443 // Register commit hook for immediate sync. Only needed while in-memory mode is active.
444 if (g_nuke.mem_db && !g_nuke.is_disk_mode) {
445 sqlite3_commit_hook(g_nuke.mem_db, nuke_commit_hook, NULL);
446 }
447
448 g_running = true;
449
450 // 4. Setup Signal Interception
451 sigemptyset(&g_sigset);
452 sigaddset(&g_sigset, SIGINT);
453 sigaddset(&g_sigset, SIGTERM);
454 sigaddset(&g_sigset, SIGUSR1);
455 sigaddset(&g_sigset, SIGUSR2);
456
457 pthread_sigmask(SIG_BLOCK, &g_sigset, NULL);
458
459 // 5. Start Sync/Signal Thread
460 if (pthread_create(&g_sync_thread, NULL, sync_thread_func, NULL) != 0) {
461 g_running = false;
462 pthread_sigmask(SIG_UNBLOCK, &g_sigset, NULL);
463 nuke_cleanup_internal();
465 }
466
467 // 6. Register atexit for ultimate safety
468 atexit(cwist_nuke_close);
469
470 return CWIST_NUKE_OK;
471}
472
478sqlite3 *cwist_nuke_get_db(void) {
479 if (g_nuke.is_disk_mode) return g_nuke.disk_db;
480 return g_nuke.mem_db;
481}
482
488 CWIST_UNUSED(signum);
489}
490
497unsigned char *cwist_nuke_serialize(sqlite3_int64 *out_size) {
498 if (!out_size) return NULL;
499 *out_size = 0;
500
501 pthread_mutex_lock(&g_nuke_lock);
502 sqlite3 *src = g_nuke.is_disk_mode ? g_nuke.disk_db : g_nuke.mem_db;
503 if (!src) {
504 pthread_mutex_unlock(&g_nuke_lock);
505 return NULL;
506 }
507
508 unsigned char *buf = sqlite3_serialize(src, "main", out_size, 0);
509 pthread_mutex_unlock(&g_nuke_lock);
510 return buf;
511}
512
520int cwist_nuke_deserialize(unsigned char *data, sqlite3_int64 data_len) {
521 if (!data || data_len <= 0) return -1;
522
523 pthread_mutex_lock(&g_nuke_lock);
524
525 if (g_nuke.is_disk_mode || !g_nuke.mem_db) {
526 pthread_mutex_unlock(&g_nuke_lock);
527 return -1;
528 }
529
530 /* SQLITE_DESERIALIZE_FREEONCLOSE: SQLite will free the buffer when the
531 * database connection is closed.
532 * SQLITE_DESERIALIZE_RESIZABLE: allows the in-memory DB to grow. */
533 int rc = sqlite3_deserialize(g_nuke.mem_db, "main", data, data_len, data_len,
534 SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE);
535 pthread_mutex_unlock(&g_nuke_lock);
536 return (rc == SQLITE_OK) ? 0 : -1;
537}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
Convenience size-conversion and utility macros shared across CWIST.
#define CWIST_UNUSED(x)
Mark an intentionally unused variable to silence compiler warnings.
Definition macros.h:23
#define CWIST_MIB(x)
Convert binary mebibytes to bytes.
Definition macros.h:16
int cwist_nuke_deserialize(unsigned char *data, sqlite3_int64 data_len)
Replace the in-memory NUKE database with serialized contents.
Definition nuke_db.c:520
sqlite3 * cwist_nuke_get_db(void)
Return the SQLite handle that callers should use for the NUKE database.
Definition nuke_db.c:478
void cwist_nuke_close(void)
Stop the sync thread, flush state, and tear down the global NUKE context.
Definition nuke_db.c:293
int cwist_nuke_sync(void)
Persist the current in-memory database back to disk or checkpoint WAL mode.
Definition nuke_db.c:228
int cwist_nuke_init(const char *disk_path, int sync_interval_ms)
Initialize the global NUKE database mirror around one disk database.
Definition nuke_db.c:377
void cwist_nuke_signal_handler(int signum)
Placeholder signal handler (no-op).
Definition nuke_db.c:487
#define NSIG
Definition nuke_db.c:43
unsigned char * cwist_nuke_serialize(sqlite3_int64 *out_size)
Serialize the current NUKE database contents into a buffer.
Definition nuke_db.c:497
High-Performance Read-Optimal Persistent Store using SQLite.
#define CWIST_NUKE_ERR_LOW_MEMORY
Definition nuke_db.h:23
#define CWIST_NUKE_ERR_GENERIC
Definition nuke_db.h:22
#define CWIST_NUKE_OK
Return codes for cwist_nuke_init.
Definition nuke_db.h:21
Nuke DB Context.
Definition nuke_db.h:36
sqlite3 * disk_db
The backup disk database handle.
Definition nuke_db.h:38
char * disk_path
Path to the disk database file.
Definition nuke_db.h:39
bool is_disk_mode
True if running in low-memory disk fallback mode.
Definition nuke_db.h:43
bool load_successful
True if initial load from disk was successful.
Definition nuke_db.h:44
bool auto_sync
Whether auto-sync is enabled.
Definition nuke_db.h:40
sqlite3 * mem_db
The active in-memory database handle.
Definition nuke_db.h:37
int sync_interval_ms
Sync interval in milliseconds.
Definition nuke_db.h:41
System Resource Monitoring Utilities.
bool cwist_is_ram_critical(uint64_t threshold_bytes)
Check if system RAM is critically low.
Definition sys_info.c:100
uint64_t cwist_get_available_ram(void)
Get the available (free + reclaimable) system RAM in bytes.
Definition sys_info.c:90