4 * Sleepycat (Berkeley) DB driver for Citadel
8 /*****************************************************************************
9 Tunable configuration parameters for the Sleepycat DB back end
10 *****************************************************************************/
12 /* Citadel will checkpoint the db at the end of every session, but only if
13 * the specified number of kilobytes has been written, or if the specified
14 * number of minutes has passed, since the last checkpoint.
16 #define MAX_CHECKPOINT_KBYTES 256
17 #define MAX_CHECKPOINT_MINUTES 15
19 /*****************************************************************************/
28 #include <sys/types.h>
34 #elif defined(HAVE_DB4_DB_H)
37 #error Neither <db.h> nor <db4/db.h> was found by configure. Install db4-devel.
41 #if DB_VERSION_MAJOR < 4 || DB_VERSION_MINOR < 1
42 #error Citadel requires Berkeley DB v4.1 or newer. Please upgrade.
46 #include <libcitadel.h>
49 #include "citserver.h"
52 #include "sysdep_decls.h"
57 #include "ctdl_module.h"
60 static DB *dbp[MAXCDB]; /* One DB handle for each Citadel database */
61 static DB_ENV *dbenv; /* The DB environment (global) */
69 /* Verbose logging callback */
70 void cdb_verbose_log(const DB_ENV *dbenv, const char *msg)
72 CtdlLogPrintf(CTDL_DEBUG, "BDB: %s\n", msg);
76 /* Verbose logging callback */
77 void cdb_verbose_err(const DB_ENV *dbenv, const char *errpfx, const char *msg)
79 CtdlLogPrintf(CTDL_ALERT, "BDB: %s\n", msg);
83 /* just a little helper function */
84 static void txabort(DB_TXN * tid)
88 ret = tid->abort(tid);
91 CtdlLogPrintf(CTDL_EMERG, "cdb_*: txn_abort: %s\n",
97 /* this one is even more helpful than the last. */
98 static void txcommit(DB_TXN * tid)
102 ret = tid->commit(tid, 0);
105 CtdlLogPrintf(CTDL_EMERG, "cdb_*: txn_commit: %s\n",
111 /* are you sensing a pattern yet? */
112 static void txbegin(DB_TXN ** tid)
116 ret = dbenv->txn_begin(dbenv, NULL, tid, 0);
119 CtdlLogPrintf(CTDL_EMERG, "cdb_*: txn_begin: %s\n",
125 static void dbpanic(DB_ENV * env, int errval)
127 CtdlLogPrintf(CTDL_EMERG, "cdb_*: Berkeley DB panic: %d\n", errval);
130 static void cclose(DBC * cursor)
134 if ((ret = cursor->c_close(cursor))) {
135 CtdlLogPrintf(CTDL_EMERG, "cdb_*: c_close: %s\n",
141 static void bailIfCursor(DBC ** cursors, const char *msg)
145 for (i = 0; i < MAXCDB; i++)
146 if (cursors[i] != NULL) {
147 CtdlLogPrintf(CTDL_EMERG,
148 "cdb_*: cursor still in progress on cdb %d: %s\n",
154 void check_handles(void *arg)
157 ThreadTSD *tsd = (ThreadTSD *) arg;
159 bailIfCursor(tsd->cursors, "in check_handles");
161 if (tsd->tid != NULL) {
162 CtdlLogPrintf(CTDL_EMERG,
163 "cdb_*: transaction still in progress!");
169 void cdb_check_handles(void)
171 check_handles(pthread_getspecific(ThreadKey));
176 * Cull the database logs
178 static void cdb_cull_logs(void)
187 /* Get the list of names. */
188 if ((ret = dbenv->log_archive(dbenv, &list, flags)) != 0) {
189 CtdlLogPrintf(CTDL_ERR, "cdb_cull_logs: %s\n", db_strerror(ret));
193 /* Print the list of names. */
195 for (file = list; *file != NULL; ++file) {
196 CtdlLogPrintf(CTDL_DEBUG, "Deleting log: %s\n", *file);
199 snprintf(errmsg, sizeof(errmsg),
200 " ** ERROR **\n \n \n "
201 "Citadel was unable to delete the "
202 "database log file '%s' because of the "
203 "following error:\n \n %s\n \n"
204 " This log file is no longer in use "
205 "and may be safely deleted.\n",
206 *file, strerror(errno));
207 aide_message(errmsg, "Database Warning Message");
215 * Manually initiate log file cull.
217 void cmd_cull(char *argbuf) {
218 if (CtdlAccessCheck(ac_internal)) return;
220 cprintf("%d Database log file cull completed.\n", CIT_OK);
225 * Request a checkpoint of the database.
227 void cdb_checkpoint(void)
230 // static time_t last_run = 0L;
232 /* Only do a checkpoint once per minute. */
234 * Don't need this any more, since the thread that calls us sleeps for 60 seconds between calls
236 if ((time(NULL) - last_run) < 60L) {
239 last_run = time(NULL);
242 CtdlLogPrintf(CTDL_DEBUG, "-- db checkpoint --\n");
243 ret = dbenv->txn_checkpoint(dbenv,
244 MAX_CHECKPOINT_KBYTES,
245 MAX_CHECKPOINT_MINUTES, 0);
248 CtdlLogPrintf(CTDL_EMERG, "cdb_checkpoint: txn_checkpoint: %s\n",
253 /* After a successful checkpoint, we can cull the unused logs */
254 if (config.c_auto_cull) {
262 * Open the various databases we'll be using. Any database which
263 * does not exist should be created. Note that we don't need a
264 * critical section here, because there aren't any active threads
265 * manipulating the database yet.
267 void open_databases(void)
271 char dbfilename[SIZ];
273 int dbversion_major, dbversion_minor, dbversion_patch;
274 int current_dbversion = 0;
276 CtdlLogPrintf(CTDL_DEBUG, "cdb_*: open_databases() starting\n");
277 CtdlLogPrintf(CTDL_DEBUG, "Compiled db: %s\n", DB_VERSION_STRING);
278 CtdlLogPrintf(CTDL_INFO, " Linked db: %s\n",
279 db_version(&dbversion_major, &dbversion_minor, &dbversion_patch));
281 current_dbversion = (dbversion_major * 1000000) + (dbversion_minor * 1000) + dbversion_patch;
283 CtdlLogPrintf(CTDL_DEBUG, "Calculated dbversion: %d\n", current_dbversion);
284 CtdlLogPrintf(CTDL_DEBUG, " Previous dbversion: %d\n", CitControl.MMdbversion);
286 if (CitControl.MMdbversion > current_dbversion) {
287 CtdlLogPrintf(CTDL_EMERG, "You are attempting to run the Citadel server using a version\n"
288 "of Berkeley DB that is older than that which last created or\n"
289 "updated the database. Because this would probably cause data\n"
290 "corruption or loss, the server is aborting execution now.\n");
294 CitControl.MMdbversion = current_dbversion;
298 CtdlLogPrintf(CTDL_INFO, "Linked zlib: %s\n", zlibVersion());
302 * Silently try to create the database subdirectory. If it's
303 * already there, no problem.
305 mkdir(ctdl_data_dir, 0700);
306 chmod(ctdl_data_dir, 0700);
307 chown(ctdl_data_dir, CTDLUID, (-1));
309 CtdlLogPrintf(CTDL_DEBUG, "cdb_*: Setting up DB environment\n");
310 db_env_set_func_yield(sched_yield);
311 ret = db_env_create(&dbenv, 0);
313 CtdlLogPrintf(CTDL_EMERG, "cdb_*: db_env_create: %s\n",
317 dbenv->set_errpfx(dbenv, "citserver");
318 dbenv->set_paniccall(dbenv, dbpanic);
319 dbenv->set_errcall(dbenv, cdb_verbose_err);
320 dbenv->set_errpfx(dbenv, "ctdl");
321 #if (DB_VERSION_MAJOR == 4) && (DB_VERSION_MINOR >= 3)
322 dbenv->set_msgcall(dbenv, cdb_verbose_log);
324 dbenv->set_verbose(dbenv, DB_VERB_DEADLOCK, 1);
325 dbenv->set_verbose(dbenv, DB_VERB_RECOVERY, 1);
328 * We want to specify the shared memory buffer pool cachesize,
329 * but everything else is the default.
331 ret = dbenv->set_cachesize(dbenv, 0, 64 * 1024, 0);
333 CtdlLogPrintf(CTDL_EMERG, "cdb_*: set_cachesize: %s\n",
335 dbenv->close(dbenv, 0);
339 if ((ret = dbenv->set_lk_detect(dbenv, DB_LOCK_DEFAULT))) {
340 CtdlLogPrintf(CTDL_EMERG, "cdb_*: set_lk_detect: %s\n",
342 dbenv->close(dbenv, 0);
346 flags = DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE | DB_INIT_TXN | DB_INIT_LOCK | DB_THREAD | DB_RECOVER;
347 CtdlLogPrintf(CTDL_DEBUG, "dbenv->open(dbenv, %s, %d, 0)\n", ctdl_data_dir, flags);
348 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
349 if (ret == DB_RUNRECOVERY) {
350 CtdlLogPrintf(CTDL_ALERT, "dbenv->open: %s\n", db_strerror(ret));
351 CtdlLogPrintf(CTDL_ALERT, "Attempting recovery...\n");
353 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
355 if (ret == DB_RUNRECOVERY) {
356 CtdlLogPrintf(CTDL_ALERT, "dbenv->open: %s\n", db_strerror(ret));
357 CtdlLogPrintf(CTDL_ALERT, "Attempting catastrophic recovery...\n");
358 flags &= ~DB_RECOVER;
359 flags |= DB_RECOVER_FATAL;
360 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
363 CtdlLogPrintf(CTDL_DEBUG, "dbenv->open: %s\n", db_strerror(ret));
364 dbenv->close(dbenv, 0);
368 CtdlLogPrintf(CTDL_INFO, "Starting up DB\n");
370 for (i = 0; i < MAXCDB; ++i) {
372 /* Create a database handle */
373 ret = db_create(&dbp[i], dbenv, 0);
375 CtdlLogPrintf(CTDL_DEBUG, "db_create: %s\n",
381 /* Arbitrary names for our tables -- we reference them by
382 * number, so we don't have string names for them.
384 snprintf(dbfilename, sizeof dbfilename, "cdb.%02x", i);
386 ret = dbp[i]->open(dbp[i],
391 DB_CREATE | DB_AUTO_COMMIT | DB_THREAD,
394 CtdlLogPrintf(CTDL_EMERG, "db_open[%d]: %s\n", i,
403 /* Make sure we own all the files, because in a few milliseconds
404 * we're going to drop root privs.
406 void cdb_chmod_data(void) {
409 char filename[PATH_MAX];
411 dp = opendir(ctdl_data_dir);
413 while (d = readdir(dp), d != NULL) {
414 if (d->d_name[0] != '.') {
415 snprintf(filename, sizeof filename,
416 "%s/%s", ctdl_data_dir, d->d_name);
417 CtdlLogPrintf(9, "chmod(%s, 0600) returned %d\n",
418 filename, chmod(filename, 0600)
420 CtdlLogPrintf(9, "chown(%s, CTDLUID, -1) returned %d\n",
421 filename, chown(filename, CTDLUID, (-1))
428 CtdlLogPrintf(CTDL_DEBUG, "open_databases() finished\n");
430 CtdlRegisterProtoHook(cmd_cull, "CULL", "Cull database logs");
435 * Close all of the db database files we've opened. This can be done
436 * in a loop, since it's just a bunch of closes.
438 void close_databases(void)
443 ctdl_thread_internal_free_tsd();
445 if ((ret = dbenv->txn_checkpoint(dbenv, 0, 0, 0))) {
446 CtdlLogPrintf(CTDL_EMERG,
447 "txn_checkpoint: %s\n", db_strerror(ret));
450 /* print some statistics... */
452 dbenv->lock_stat_print(dbenv, DB_STAT_ALL);
455 /* close the tables */
456 for (a = 0; a < MAXCDB; ++a) {
457 CtdlLogPrintf(CTDL_INFO, "Closing database %d\n", a);
458 ret = dbp[a]->close(dbp[a], 0);
460 CtdlLogPrintf(CTDL_EMERG,
461 "db_close: %s\n", db_strerror(ret));
466 /* Close the handle. */
467 ret = dbenv->close(dbenv, 0);
469 CtdlLogPrintf(CTDL_EMERG,
470 "DBENV->close: %s\n", db_strerror(ret));
476 * Compression functions only used if we have zlib
480 void cdb_decompress_if_necessary(struct cdbdata *cdb)
482 static int magic = COMPRESS_MAGIC;
483 struct CtdlCompressHeader zheader;
484 char *uncompressed_data;
485 char *compressed_data;
486 uLongf destLen, sourceLen;
490 if (cdb->ptr == NULL)
492 if (memcmp(cdb->ptr, &magic, sizeof(magic)))
495 /* At this point we know we're looking at a compressed item. */
496 memcpy(&zheader, cdb->ptr, sizeof(struct CtdlCompressHeader));
498 compressed_data = cdb->ptr;
499 compressed_data += sizeof(struct CtdlCompressHeader);
501 sourceLen = (uLongf) zheader.compressed_len;
502 destLen = (uLongf) zheader.uncompressed_len;
503 uncompressed_data = malloc(zheader.uncompressed_len);
505 if (uncompress((Bytef *) uncompressed_data,
506 (uLongf *) & destLen,
507 (const Bytef *) compressed_data,
508 (uLong) sourceLen) != Z_OK) {
509 CtdlLogPrintf(CTDL_EMERG, "uncompress() error\n");
514 cdb->len = (size_t) destLen;
515 cdb->ptr = uncompressed_data;
518 #endif /* HAVE_ZLIB */
522 * Store a piece of data. Returns 0 if the operation was successful. If a
523 * key already exists it should be overwritten.
525 int cdb_store(int cdb, void *ckey, int ckeylen, void *cdata, int cdatalen)
533 struct CtdlCompressHeader zheader;
534 char *compressed_data = NULL;
536 size_t buffer_len = 0;
540 memset(&dkey, 0, sizeof(DBT));
541 memset(&ddata, 0, sizeof(DBT));
544 ddata.size = cdatalen;
548 /* Only compress Visit records. Everything else is uncompressed. */
549 if (cdb == CDB_VISIT) {
551 zheader.magic = COMPRESS_MAGIC;
552 zheader.uncompressed_len = cdatalen;
553 buffer_len = ((cdatalen * 101) / 100) + 100
554 + sizeof(struct CtdlCompressHeader);
555 destLen = (uLongf) buffer_len;
556 compressed_data = malloc(buffer_len);
557 if (compress2((Bytef *) (compressed_data +
559 CtdlCompressHeader)),
560 &destLen, (Bytef *) cdata, (uLongf) cdatalen,
562 CtdlLogPrintf(CTDL_EMERG, "compress2() error\n");
565 zheader.compressed_len = (size_t) destLen;
566 memcpy(compressed_data, &zheader,
567 sizeof(struct CtdlCompressHeader));
568 ddata.size = (size_t) (sizeof(struct CtdlCompressHeader) +
569 zheader.compressed_len);
570 ddata.data = compressed_data;
575 ret = dbp[cdb]->put(dbp[cdb], /* db */
576 MYTID, /* transaction ID */
581 CtdlLogPrintf(CTDL_EMERG, "cdb_store(%d): %s\n", cdb,
587 free(compressed_data);
592 bailIfCursor(MYCURSORS,
593 "attempt to write during r/o cursor");
598 if ((ret = dbp[cdb]->put(dbp[cdb], /* db */
599 tid, /* transaction ID */
603 if (ret == DB_LOCK_DEADLOCK) {
607 CtdlLogPrintf(CTDL_EMERG, "cdb_store(%d): %s\n",
608 cdb, db_strerror(ret));
615 free(compressed_data);
624 * Delete a piece of data. Returns 0 if the operation was successful.
626 int cdb_delete(int cdb, void *key, int keylen)
633 memset(&dkey, 0, sizeof dkey);
638 ret = dbp[cdb]->del(dbp[cdb], MYTID, &dkey, 0);
640 CtdlLogPrintf(CTDL_EMERG, "cdb_delete(%d): %s\n", cdb,
642 if (ret != DB_NOTFOUND)
646 bailIfCursor(MYCURSORS,
647 "attempt to delete during r/o cursor");
652 if ((ret = dbp[cdb]->del(dbp[cdb], tid, &dkey, 0))
653 && ret != DB_NOTFOUND) {
654 if (ret == DB_LOCK_DEADLOCK) {
658 CtdlLogPrintf(CTDL_EMERG, "cdb_delete(%d): %s\n",
659 cdb, db_strerror(ret));
669 static DBC *localcursor(int cdb)
674 if (MYCURSORS[cdb] == NULL)
675 ret = dbp[cdb]->cursor(dbp[cdb], MYTID, &curs, 0);
678 MYCURSORS[cdb]->c_dup(MYCURSORS[cdb], &curs,
682 CtdlLogPrintf(CTDL_EMERG, "localcursor: %s\n", db_strerror(ret));
691 * Fetch a piece of data. If not found, returns NULL. Otherwise, it returns
692 * a struct cdbdata which it is the caller's responsibility to free later on
693 * using the cdb_free() routine.
695 struct cdbdata *cdb_fetch(int cdb, void *key, int keylen)
698 struct cdbdata *tempcdb;
702 memset(&dkey, 0, sizeof(DBT));
707 memset(&dret, 0, sizeof(DBT));
708 dret.flags = DB_DBT_MALLOC;
709 ret = dbp[cdb]->get(dbp[cdb], MYTID, &dkey, &dret, 0);
714 memset(&dret, 0, sizeof(DBT));
715 dret.flags = DB_DBT_MALLOC;
717 curs = localcursor(cdb);
719 ret = curs->c_get(curs, &dkey, &dret, DB_SET);
722 while (ret == DB_LOCK_DEADLOCK);
726 if ((ret != 0) && (ret != DB_NOTFOUND)) {
727 CtdlLogPrintf(CTDL_EMERG, "cdb_fetch(%d): %s\n", cdb,
734 tempcdb = (struct cdbdata *) malloc(sizeof(struct cdbdata));
736 if (tempcdb == NULL) {
737 CtdlLogPrintf(CTDL_EMERG,
738 "cdb_fetch: Cannot allocate memory for tempcdb\n");
742 tempcdb->len = dret.size;
743 tempcdb->ptr = dret.data;
745 cdb_decompress_if_necessary(tempcdb);
752 * Free a cdbdata item.
754 * Note that we only free the 'ptr' portion if it is not NULL. This allows
755 * other code to assume ownership of that memory simply by storing the
756 * pointer elsewhere and then setting 'ptr' to NULL. cdb_free() will then
759 void cdb_free(struct cdbdata *cdb)
767 void cdb_close_cursor(int cdb)
769 if (MYCURSORS[cdb] != NULL)
770 cclose(MYCURSORS[cdb]);
772 MYCURSORS[cdb] = NULL;
776 * Prepare for a sequential search of an entire database.
777 * (There is guaranteed to be no more than one traversal in
778 * progress per thread at any given time.)
780 void cdb_rewind(int cdb)
784 if (MYCURSORS[cdb] != NULL) {
785 CtdlLogPrintf(CTDL_EMERG,
786 "cdb_rewind: must close cursor on database %d before reopening.\n",
789 /* cclose(MYCURSORS[cdb]); */
793 * Now initialize the cursor
795 ret = dbp[cdb]->cursor(dbp[cdb], MYTID, &MYCURSORS[cdb], 0);
797 CtdlLogPrintf(CTDL_EMERG, "cdb_rewind: db_cursor: %s\n",
805 * Fetch the next item in a sequential search. Returns a pointer to a
806 * cdbdata structure, or NULL if we've hit the end.
808 struct cdbdata *cdb_next_item(int cdb)
811 struct cdbdata *cdbret;
814 /* Initialize the key/data pair so the flags aren't set. */
815 memset(&key, 0, sizeof(key));
816 memset(&data, 0, sizeof(data));
817 data.flags = DB_DBT_MALLOC;
819 ret = MYCURSORS[cdb]->c_get(MYCURSORS[cdb], &key, &data, DB_NEXT);
822 if (ret != DB_NOTFOUND) {
823 CtdlLogPrintf(CTDL_EMERG, "cdb_next_item(%d): %s\n",
824 cdb, db_strerror(ret));
827 cclose(MYCURSORS[cdb]);
828 MYCURSORS[cdb] = NULL;
829 return NULL; /* presumably, end of file */
832 cdbret = (struct cdbdata *) malloc(sizeof(struct cdbdata));
833 cdbret->len = data.size;
834 cdbret->ptr = data.data;
836 cdb_decompress_if_necessary(cdbret);
845 * Transaction-based stuff. I'm writing this as I bake cookies...
848 void cdb_begin_transaction(void)
851 bailIfCursor(MYCURSORS,
852 "can't begin transaction during r/o cursor");
855 CtdlLogPrintf(CTDL_EMERG,
856 "cdb_begin_transaction: ERROR: nested transaction\n");
863 void cdb_end_transaction(void)
867 for (i = 0; i < MAXCDB; i++)
868 if (MYCURSORS[i] != NULL) {
869 CtdlLogPrintf(CTDL_WARNING,
870 "cdb_end_transaction: WARNING: cursor %d still open at transaction end\n",
872 cclose(MYCURSORS[i]);
877 CtdlLogPrintf(CTDL_EMERG,
878 "cdb_end_transaction: ERROR: txcommit(NULL) !!\n");
887 * Truncate (delete every record)
889 void cdb_trunc(int cdb)
896 CtdlLogPrintf(CTDL_EMERG,
897 "cdb_trunc must not be called in a transaction.\n");
900 bailIfCursor(MYCURSORS,
901 "attempt to write during r/o cursor");
906 if ((ret = dbp[cdb]->truncate(dbp[cdb], /* db */
907 NULL, /* transaction ID */
908 &count, /* #rows deleted */
910 if (ret == DB_LOCK_DEADLOCK) {
914 CtdlLogPrintf(CTDL_EMERG,
915 "cdb_truncate(%d): %s\n", cdb,