4 * This is a data store backend for the Citadel server which uses Berkeley DB.
8 /*****************************************************************************
9 Tunable configuration parameters for the Berkeley 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 %02x: %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 ( (getenv("SUPPRESS_DBVERSION_CHECK") == NULL)
287 && (CitControl.MMdbversion > current_dbversion) ) {
288 CtdlLogPrintf(CTDL_EMERG, "You are attempting to run the Citadel server using a version\n"
289 "of Berkeley DB that is older than that which last created or\n"
290 "updated the database. Because this would probably cause data\n"
291 "corruption or loss, the server is aborting execution now.\n");
295 CitControl.MMdbversion = current_dbversion;
299 CtdlLogPrintf(CTDL_INFO, "Linked zlib: %s\n", zlibVersion());
303 * Silently try to create the database subdirectory. If it's
304 * already there, no problem.
306 mkdir(ctdl_data_dir, 0700);
307 chmod(ctdl_data_dir, 0700);
308 chown(ctdl_data_dir, CTDLUID, (-1));
310 CtdlLogPrintf(CTDL_DEBUG, "cdb_*: Setting up DB environment\n");
311 db_env_set_func_yield(sched_yield);
312 ret = db_env_create(&dbenv, 0);
314 CtdlLogPrintf(CTDL_EMERG, "cdb_*: db_env_create: %s\n",
318 dbenv->set_errpfx(dbenv, "citserver");
319 dbenv->set_paniccall(dbenv, dbpanic);
320 dbenv->set_errcall(dbenv, cdb_verbose_err);
321 dbenv->set_errpfx(dbenv, "ctdl");
322 #if (DB_VERSION_MAJOR == 4) && (DB_VERSION_MINOR >= 3)
323 dbenv->set_msgcall(dbenv, cdb_verbose_log);
325 dbenv->set_verbose(dbenv, DB_VERB_DEADLOCK, 1);
326 dbenv->set_verbose(dbenv, DB_VERB_RECOVERY, 1);
329 * We want to specify the shared memory buffer pool cachesize,
330 * but everything else is the default.
332 ret = dbenv->set_cachesize(dbenv, 0, 64 * 1024, 0);
334 CtdlLogPrintf(CTDL_EMERG, "cdb_*: set_cachesize: %s\n",
336 dbenv->close(dbenv, 0);
340 if ((ret = dbenv->set_lk_detect(dbenv, DB_LOCK_DEFAULT))) {
341 CtdlLogPrintf(CTDL_EMERG, "cdb_*: set_lk_detect: %s\n",
343 dbenv->close(dbenv, 0);
347 flags = DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE | DB_INIT_TXN | DB_INIT_LOCK | DB_THREAD | DB_RECOVER;
348 CtdlLogPrintf(CTDL_DEBUG, "dbenv->open(dbenv, %s, %d, 0)\n", ctdl_data_dir, flags);
349 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
350 if (ret == DB_RUNRECOVERY) {
351 CtdlLogPrintf(CTDL_ALERT, "dbenv->open: %s\n", db_strerror(ret));
352 CtdlLogPrintf(CTDL_ALERT, "Attempting recovery...\n");
354 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
356 if (ret == DB_RUNRECOVERY) {
357 CtdlLogPrintf(CTDL_ALERT, "dbenv->open: %s\n", db_strerror(ret));
358 CtdlLogPrintf(CTDL_ALERT, "Attempting catastrophic recovery...\n");
359 flags &= ~DB_RECOVER;
360 flags |= DB_RECOVER_FATAL;
361 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
364 CtdlLogPrintf(CTDL_DEBUG, "dbenv->open: %s\n", db_strerror(ret));
365 dbenv->close(dbenv, 0);
369 CtdlLogPrintf(CTDL_INFO, "Starting up DB\n");
371 for (i = 0; i < MAXCDB; ++i) {
373 /* Create a database handle */
374 ret = db_create(&dbp[i], dbenv, 0);
376 CtdlLogPrintf(CTDL_DEBUG, "db_create: %s\n",
382 /* Arbitrary names for our tables -- we reference them by
383 * number, so we don't have string names for them.
385 snprintf(dbfilename, sizeof dbfilename, "cdb.%02x", i);
387 ret = dbp[i]->open(dbp[i],
392 DB_CREATE | DB_AUTO_COMMIT | DB_THREAD,
395 CtdlLogPrintf(CTDL_EMERG, "db_open[%02x]: %s\n", i,
404 /* Make sure we own all the files, because in a few milliseconds
405 * we're going to drop root privs.
407 void cdb_chmod_data(void) {
410 char filename[PATH_MAX];
412 dp = opendir(ctdl_data_dir);
414 while (d = readdir(dp), d != NULL) {
415 if (d->d_name[0] != '.') {
416 snprintf(filename, sizeof filename,
417 "%s/%s", ctdl_data_dir, d->d_name);
418 CtdlLogPrintf(9, "chmod(%s, 0600) returned %d\n",
419 filename, chmod(filename, 0600)
421 CtdlLogPrintf(9, "chown(%s, CTDLUID, -1) returned %d\n",
422 filename, chown(filename, CTDLUID, (-1))
429 CtdlLogPrintf(CTDL_DEBUG, "open_databases() finished\n");
431 CtdlRegisterProtoHook(cmd_cull, "CULL", "Cull database logs");
436 * Close all of the db database files we've opened. This can be done
437 * in a loop, since it's just a bunch of closes.
439 void close_databases(void)
444 ctdl_thread_internal_free_tsd();
446 if ((ret = dbenv->txn_checkpoint(dbenv, 0, 0, 0))) {
447 CtdlLogPrintf(CTDL_EMERG,
448 "txn_checkpoint: %s\n", db_strerror(ret));
451 /* print some statistics... */
453 dbenv->lock_stat_print(dbenv, DB_STAT_ALL);
456 /* close the tables */
457 for (a = 0; a < MAXCDB; ++a) {
458 CtdlLogPrintf(CTDL_INFO, "Closing database %02x\n", a);
459 ret = dbp[a]->close(dbp[a], 0);
461 CtdlLogPrintf(CTDL_EMERG,
462 "db_close: %s\n", db_strerror(ret));
467 /* Close the handle. */
468 ret = dbenv->close(dbenv, 0);
470 CtdlLogPrintf(CTDL_EMERG,
471 "DBENV->close: %s\n", db_strerror(ret));
477 * Compression functions only used if we have zlib
481 void cdb_decompress_if_necessary(struct cdbdata *cdb)
483 static int magic = COMPRESS_MAGIC;
484 struct CtdlCompressHeader zheader;
485 char *uncompressed_data;
486 char *compressed_data;
487 uLongf destLen, sourceLen;
491 if (cdb->ptr == NULL)
493 if (memcmp(cdb->ptr, &magic, sizeof(magic)))
496 /* At this point we know we're looking at a compressed item. */
497 memcpy(&zheader, cdb->ptr, sizeof(struct CtdlCompressHeader));
499 compressed_data = cdb->ptr;
500 compressed_data += sizeof(struct CtdlCompressHeader);
502 sourceLen = (uLongf) zheader.compressed_len;
503 destLen = (uLongf) zheader.uncompressed_len;
504 uncompressed_data = malloc(zheader.uncompressed_len);
506 if (uncompress((Bytef *) uncompressed_data,
507 (uLongf *) & destLen,
508 (const Bytef *) compressed_data,
509 (uLong) sourceLen) != Z_OK) {
510 CtdlLogPrintf(CTDL_EMERG, "uncompress() error\n");
515 cdb->len = (size_t) destLen;
516 cdb->ptr = uncompressed_data;
519 #endif /* HAVE_ZLIB */
523 * Store a piece of data. Returns 0 if the operation was successful. If a
524 * key already exists it should be overwritten.
526 int cdb_store(int cdb, void *ckey, int ckeylen, void *cdata, int cdatalen)
534 struct CtdlCompressHeader zheader;
535 char *compressed_data = NULL;
537 size_t buffer_len = 0;
541 memset(&dkey, 0, sizeof(DBT));
542 memset(&ddata, 0, sizeof(DBT));
545 ddata.size = cdatalen;
549 /* Only compress Visit records. Everything else is uncompressed. */
550 if (cdb == CDB_VISIT) {
552 zheader.magic = COMPRESS_MAGIC;
553 zheader.uncompressed_len = cdatalen;
554 buffer_len = ((cdatalen * 101) / 100) + 100
555 + sizeof(struct CtdlCompressHeader);
556 destLen = (uLongf) buffer_len;
557 compressed_data = malloc(buffer_len);
558 if (compress2((Bytef *) (compressed_data +
560 CtdlCompressHeader)),
561 &destLen, (Bytef *) cdata, (uLongf) cdatalen,
563 CtdlLogPrintf(CTDL_EMERG, "compress2() error\n");
566 zheader.compressed_len = (size_t) destLen;
567 memcpy(compressed_data, &zheader,
568 sizeof(struct CtdlCompressHeader));
569 ddata.size = (size_t) (sizeof(struct CtdlCompressHeader) +
570 zheader.compressed_len);
571 ddata.data = compressed_data;
576 ret = dbp[cdb]->put(dbp[cdb], /* db */
577 MYTID, /* transaction ID */
582 CtdlLogPrintf(CTDL_EMERG, "cdb_store(%d): %s\n", cdb,
588 free(compressed_data);
593 bailIfCursor(MYCURSORS,
594 "attempt to write during r/o cursor");
599 if ((ret = dbp[cdb]->put(dbp[cdb], /* db */
600 tid, /* transaction ID */
604 if (ret == DB_LOCK_DEADLOCK) {
608 CtdlLogPrintf(CTDL_EMERG, "cdb_store(%d): %s\n",
609 cdb, db_strerror(ret));
616 free(compressed_data);
625 * Delete a piece of data. Returns 0 if the operation was successful.
627 int cdb_delete(int cdb, void *key, int keylen)
634 memset(&dkey, 0, sizeof dkey);
639 ret = dbp[cdb]->del(dbp[cdb], MYTID, &dkey, 0);
641 CtdlLogPrintf(CTDL_EMERG, "cdb_delete(%d): %s\n", cdb,
643 if (ret != DB_NOTFOUND)
647 bailIfCursor(MYCURSORS,
648 "attempt to delete during r/o cursor");
653 if ((ret = dbp[cdb]->del(dbp[cdb], tid, &dkey, 0))
654 && ret != DB_NOTFOUND) {
655 if (ret == DB_LOCK_DEADLOCK) {
659 CtdlLogPrintf(CTDL_EMERG, "cdb_delete(%d): %s\n",
660 cdb, db_strerror(ret));
670 static DBC *localcursor(int cdb)
675 if (MYCURSORS[cdb] == NULL)
676 ret = dbp[cdb]->cursor(dbp[cdb], MYTID, &curs, 0);
679 MYCURSORS[cdb]->c_dup(MYCURSORS[cdb], &curs,
683 CtdlLogPrintf(CTDL_EMERG, "localcursor: %s\n", db_strerror(ret));
692 * Fetch a piece of data. If not found, returns NULL. Otherwise, it returns
693 * a struct cdbdata which it is the caller's responsibility to free later on
694 * using the cdb_free() routine.
696 struct cdbdata *cdb_fetch(int cdb, void *key, int keylen)
699 struct cdbdata *tempcdb;
703 memset(&dkey, 0, sizeof(DBT));
708 memset(&dret, 0, sizeof(DBT));
709 dret.flags = DB_DBT_MALLOC;
710 ret = dbp[cdb]->get(dbp[cdb], MYTID, &dkey, &dret, 0);
715 memset(&dret, 0, sizeof(DBT));
716 dret.flags = DB_DBT_MALLOC;
718 curs = localcursor(cdb);
720 ret = curs->c_get(curs, &dkey, &dret, DB_SET);
723 while (ret == DB_LOCK_DEADLOCK);
727 if ((ret != 0) && (ret != DB_NOTFOUND)) {
728 CtdlLogPrintf(CTDL_EMERG, "cdb_fetch(%d): %s\n", cdb,
735 tempcdb = (struct cdbdata *) malloc(sizeof(struct cdbdata));
737 if (tempcdb == NULL) {
738 CtdlLogPrintf(CTDL_EMERG,
739 "cdb_fetch: Cannot allocate memory for tempcdb\n");
743 tempcdb->len = dret.size;
744 tempcdb->ptr = dret.data;
746 cdb_decompress_if_necessary(tempcdb);
753 * Free a cdbdata item.
755 * Note that we only free the 'ptr' portion if it is not NULL. This allows
756 * other code to assume ownership of that memory simply by storing the
757 * pointer elsewhere and then setting 'ptr' to NULL. cdb_free() will then
760 void cdb_free(struct cdbdata *cdb)
768 void cdb_close_cursor(int cdb)
770 if (MYCURSORS[cdb] != NULL)
771 cclose(MYCURSORS[cdb]);
773 MYCURSORS[cdb] = NULL;
777 * Prepare for a sequential search of an entire database.
778 * (There is guaranteed to be no more than one traversal in
779 * progress per thread at any given time.)
781 void cdb_rewind(int cdb)
785 if (MYCURSORS[cdb] != NULL) {
786 CtdlLogPrintf(CTDL_EMERG,
787 "cdb_rewind: must close cursor on database %d before reopening.\n",
790 /* cclose(MYCURSORS[cdb]); */
794 * Now initialize the cursor
796 ret = dbp[cdb]->cursor(dbp[cdb], MYTID, &MYCURSORS[cdb], 0);
798 CtdlLogPrintf(CTDL_EMERG, "cdb_rewind: db_cursor: %s\n",
806 * Fetch the next item in a sequential search. Returns a pointer to a
807 * cdbdata structure, or NULL if we've hit the end.
809 struct cdbdata *cdb_next_item(int cdb)
812 struct cdbdata *cdbret;
815 /* Initialize the key/data pair so the flags aren't set. */
816 memset(&key, 0, sizeof(key));
817 memset(&data, 0, sizeof(data));
818 data.flags = DB_DBT_MALLOC;
820 ret = MYCURSORS[cdb]->c_get(MYCURSORS[cdb], &key, &data, DB_NEXT);
823 if (ret != DB_NOTFOUND) {
824 CtdlLogPrintf(CTDL_EMERG, "cdb_next_item(%d): %s\n",
825 cdb, db_strerror(ret));
828 cclose(MYCURSORS[cdb]);
829 MYCURSORS[cdb] = NULL;
830 return NULL; /* presumably, end of file */
833 cdbret = (struct cdbdata *) malloc(sizeof(struct cdbdata));
834 cdbret->len = data.size;
835 cdbret->ptr = data.data;
837 cdb_decompress_if_necessary(cdbret);
846 * Transaction-based stuff. I'm writing this as I bake cookies...
849 void cdb_begin_transaction(void)
852 bailIfCursor(MYCURSORS,
853 "can't begin transaction during r/o cursor");
856 CtdlLogPrintf(CTDL_EMERG,
857 "cdb_begin_transaction: ERROR: nested transaction\n");
864 void cdb_end_transaction(void)
868 for (i = 0; i < MAXCDB; i++)
869 if (MYCURSORS[i] != NULL) {
870 CtdlLogPrintf(CTDL_WARNING,
871 "cdb_end_transaction: WARNING: cursor %d still open at transaction end\n",
873 cclose(MYCURSORS[i]);
878 CtdlLogPrintf(CTDL_EMERG,
879 "cdb_end_transaction: ERROR: txcommit(NULL) !!\n");
888 * Truncate (delete every record)
890 void cdb_trunc(int cdb)
897 CtdlLogPrintf(CTDL_EMERG,
898 "cdb_trunc must not be called in a transaction.\n");
901 bailIfCursor(MYCURSORS,
902 "attempt to write during r/o cursor");
907 if ((ret = dbp[cdb]->truncate(dbp[cdb], /* db */
908 NULL, /* transaction ID */
909 &count, /* #rows deleted */
911 if (ret == DB_LOCK_DEADLOCK) {
915 CtdlLogPrintf(CTDL_EMERG,
916 "cdb_truncate(%d): %s\n", cdb,