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 if (!IsEmptyStr(msg)) {
73 CtdlLogPrintf(CTDL_DEBUG, "DB: %s\n", msg);
78 /* Verbose logging callback */
79 void cdb_verbose_err(const DB_ENV *dbenv, const char *errpfx, const char *msg)
81 CtdlLogPrintf(CTDL_ALERT, "DB: %s\n", msg);
85 /* just a little helper function */
86 static void txabort(DB_TXN * tid)
90 ret = tid->abort(tid);
93 CtdlLogPrintf(CTDL_EMERG, "cdb_*: txn_abort: %s\n",
99 /* this one is even more helpful than the last. */
100 static void txcommit(DB_TXN * tid)
104 ret = tid->commit(tid, 0);
107 CtdlLogPrintf(CTDL_EMERG, "cdb_*: txn_commit: %s\n",
113 /* are you sensing a pattern yet? */
114 static void txbegin(DB_TXN ** tid)
118 ret = dbenv->txn_begin(dbenv, NULL, tid, 0);
121 CtdlLogPrintf(CTDL_EMERG, "cdb_*: txn_begin: %s\n",
127 static void dbpanic(DB_ENV * env, int errval)
129 CtdlLogPrintf(CTDL_EMERG, "cdb_*: Berkeley DB panic: %d\n", errval);
132 static void cclose(DBC * cursor)
136 if ((ret = cursor->c_close(cursor))) {
137 CtdlLogPrintf(CTDL_EMERG, "cdb_*: c_close: %s\n",
143 static void bailIfCursor(DBC ** cursors, const char *msg)
147 for (i = 0; i < MAXCDB; i++)
148 if (cursors[i] != NULL) {
149 CtdlLogPrintf(CTDL_EMERG,
150 "cdb_*: cursor still in progress on cdb %02x: %s\n",
156 void check_handles(void *arg)
159 ThreadTSD *tsd = (ThreadTSD *) arg;
161 bailIfCursor(tsd->cursors, "in check_handles");
163 if (tsd->tid != NULL) {
164 CtdlLogPrintf(CTDL_EMERG,
165 "cdb_*: transaction still in progress!");
171 void cdb_check_handles(void)
173 check_handles(pthread_getspecific(ThreadKey));
178 * Cull the database logs
180 static void cdb_cull_logs(void)
189 /* Get the list of names. */
190 if ((ret = dbenv->log_archive(dbenv, &list, flags)) != 0) {
191 CtdlLogPrintf(CTDL_ERR, "cdb_cull_logs: %s\n", db_strerror(ret));
195 /* Print the list of names. */
197 for (file = list; *file != NULL; ++file) {
198 CtdlLogPrintf(CTDL_DEBUG, "Deleting log: %s\n", *file);
201 snprintf(errmsg, sizeof(errmsg),
202 " ** ERROR **\n \n \n "
203 "Citadel was unable to delete the "
204 "database log file '%s' because of the "
205 "following error:\n \n %s\n \n"
206 " This log file is no longer in use "
207 "and may be safely deleted.\n",
208 *file, strerror(errno));
209 aide_message(errmsg, "Database Warning Message");
217 * Manually initiate log file cull.
219 void cmd_cull(char *argbuf) {
220 if (CtdlAccessCheck(ac_internal)) return;
222 cprintf("%d Database log file cull completed.\n", CIT_OK);
227 * Request a checkpoint of the database. Called once per minute by the thread manager.
229 void cdb_checkpoint(void)
233 CtdlLogPrintf(CTDL_DEBUG, "-- db checkpoint --\n");
234 ret = dbenv->txn_checkpoint(dbenv,
235 MAX_CHECKPOINT_KBYTES,
236 MAX_CHECKPOINT_MINUTES, 0);
239 CtdlLogPrintf(CTDL_EMERG, "cdb_checkpoint: txn_checkpoint: %s\n",
244 /* After a successful checkpoint, we can cull the unused logs */
245 if (config.c_auto_cull) {
253 * Open the various databases we'll be using. Any database which
254 * does not exist should be created. Note that we don't need a
255 * critical section here, because there aren't any active threads
256 * manipulating the database yet.
258 void open_databases(void)
264 int dbversion_major, dbversion_minor, dbversion_patch;
265 int current_dbversion = 0;
267 CtdlLogPrintf(CTDL_DEBUG, "cdb_*: open_databases() starting\n");
268 CtdlLogPrintf(CTDL_DEBUG, "Compiled db: %s\n", DB_VERSION_STRING);
269 CtdlLogPrintf(CTDL_INFO, " Linked db: %s\n",
270 db_version(&dbversion_major, &dbversion_minor, &dbversion_patch));
272 current_dbversion = (dbversion_major * 1000000) + (dbversion_minor * 1000) + dbversion_patch;
274 CtdlLogPrintf(CTDL_DEBUG, "Calculated dbversion: %d\n", current_dbversion);
275 CtdlLogPrintf(CTDL_DEBUG, " Previous dbversion: %d\n", CitControl.MMdbversion);
277 if ( (getenv("SUPPRESS_DBVERSION_CHECK") == NULL)
278 && (CitControl.MMdbversion > current_dbversion) ) {
279 CtdlLogPrintf(CTDL_EMERG, "You are attempting to run the Citadel server using a version\n"
280 "of Berkeley DB that is older than that which last created or\n"
281 "updated the database. Because this would probably cause data\n"
282 "corruption or loss, the server is aborting execution now.\n");
286 CitControl.MMdbversion = current_dbversion;
290 CtdlLogPrintf(CTDL_INFO, "Linked zlib: %s\n", zlibVersion());
294 * Silently try to create the database subdirectory. If it's
295 * already there, no problem.
297 mkdir(ctdl_data_dir, 0700);
298 chmod(ctdl_data_dir, 0700);
299 chown(ctdl_data_dir, CTDLUID, (-1));
301 CtdlLogPrintf(CTDL_DEBUG, "cdb_*: Setting up DB environment\n");
302 db_env_set_func_yield(sched_yield);
303 ret = db_env_create(&dbenv, 0);
305 CtdlLogPrintf(CTDL_EMERG, "cdb_*: db_env_create: %s\n",
309 dbenv->set_errpfx(dbenv, "citserver");
310 dbenv->set_paniccall(dbenv, dbpanic);
311 dbenv->set_errcall(dbenv, cdb_verbose_err);
312 dbenv->set_errpfx(dbenv, "ctdl");
313 #if (DB_VERSION_MAJOR == 4) && (DB_VERSION_MINOR >= 3)
314 dbenv->set_msgcall(dbenv, cdb_verbose_log);
316 dbenv->set_verbose(dbenv, DB_VERB_DEADLOCK, 1);
317 dbenv->set_verbose(dbenv, DB_VERB_RECOVERY, 1);
320 * We want to specify the shared memory buffer pool cachesize,
321 * but everything else is the default.
323 ret = dbenv->set_cachesize(dbenv, 0, 64 * 1024, 0);
325 CtdlLogPrintf(CTDL_EMERG, "cdb_*: set_cachesize: %s\n",
327 dbenv->close(dbenv, 0);
331 if ((ret = dbenv->set_lk_detect(dbenv, DB_LOCK_DEFAULT))) {
332 CtdlLogPrintf(CTDL_EMERG, "cdb_*: set_lk_detect: %s\n",
334 dbenv->close(dbenv, 0);
338 flags = DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE | DB_INIT_TXN | DB_INIT_LOCK | DB_THREAD | DB_RECOVER;
339 CtdlLogPrintf(CTDL_DEBUG, "dbenv->open(dbenv, %s, %d, 0)\n", ctdl_data_dir, flags);
340 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
341 if (ret == DB_RUNRECOVERY) {
342 CtdlLogPrintf(CTDL_ALERT, "dbenv->open: %s\n", db_strerror(ret));
343 CtdlLogPrintf(CTDL_ALERT, "Attempting recovery...\n");
345 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
347 if (ret == DB_RUNRECOVERY) {
348 CtdlLogPrintf(CTDL_ALERT, "dbenv->open: %s\n", db_strerror(ret));
349 CtdlLogPrintf(CTDL_ALERT, "Attempting catastrophic recovery...\n");
350 flags &= ~DB_RECOVER;
351 flags |= DB_RECOVER_FATAL;
352 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
355 CtdlLogPrintf(CTDL_DEBUG, "dbenv->open: %s\n", db_strerror(ret));
356 dbenv->close(dbenv, 0);
360 CtdlLogPrintf(CTDL_INFO, "Starting up DB\n");
362 for (i = 0; i < MAXCDB; ++i) {
364 /* Create a database handle */
365 ret = db_create(&dbp[i], dbenv, 0);
367 CtdlLogPrintf(CTDL_DEBUG, "db_create: %s\n",
373 /* Arbitrary names for our tables -- we reference them by
374 * number, so we don't have string names for them.
376 snprintf(dbfilename, sizeof dbfilename, "cdb.%02x", i);
378 ret = dbp[i]->open(dbp[i],
383 DB_CREATE | DB_AUTO_COMMIT | DB_THREAD,
386 CtdlLogPrintf(CTDL_EMERG, "db_open[%02x]: %s\n", i,
395 /* Make sure we own all the files, because in a few milliseconds
396 * we're going to drop root privs.
398 void cdb_chmod_data(void) {
401 char filename[PATH_MAX];
403 dp = opendir(ctdl_data_dir);
405 while (d = readdir(dp), d != NULL) {
406 if (d->d_name[0] != '.') {
407 snprintf(filename, sizeof filename,
408 "%s/%s", ctdl_data_dir, d->d_name);
409 CtdlLogPrintf(9, "chmod(%s, 0600) returned %d\n",
410 filename, chmod(filename, 0600)
412 CtdlLogPrintf(9, "chown(%s, CTDLUID, -1) returned %d\n",
413 filename, chown(filename, CTDLUID, (-1))
420 CtdlLogPrintf(CTDL_DEBUG, "open_databases() finished\n");
422 CtdlRegisterProtoHook(cmd_cull, "CULL", "Cull database logs");
427 * Close all of the db database files we've opened. This can be done
428 * in a loop, since it's just a bunch of closes.
430 void close_databases(void)
435 ctdl_thread_internal_free_tsd();
437 if ((ret = dbenv->txn_checkpoint(dbenv, 0, 0, 0))) {
438 CtdlLogPrintf(CTDL_EMERG,
439 "txn_checkpoint: %s\n", db_strerror(ret));
442 /* print some statistics... */
444 dbenv->lock_stat_print(dbenv, DB_STAT_ALL);
447 /* close the tables */
448 for (a = 0; a < MAXCDB; ++a) {
449 CtdlLogPrintf(CTDL_INFO, "Closing database %02x\n", a);
450 ret = dbp[a]->close(dbp[a], 0);
452 CtdlLogPrintf(CTDL_EMERG,
453 "db_close: %s\n", db_strerror(ret));
458 /* Close the handle. */
459 ret = dbenv->close(dbenv, 0);
461 CtdlLogPrintf(CTDL_EMERG,
462 "DBENV->close: %s\n", db_strerror(ret));
468 * Compression functions only used if we have zlib
472 void cdb_decompress_if_necessary(struct cdbdata *cdb)
474 static int magic = COMPRESS_MAGIC;
475 struct CtdlCompressHeader zheader;
476 char *uncompressed_data;
477 char *compressed_data;
478 uLongf destLen, sourceLen;
482 if (cdb->ptr == NULL)
484 if (memcmp(cdb->ptr, &magic, sizeof(magic)))
487 /* At this point we know we're looking at a compressed item. */
488 memcpy(&zheader, cdb->ptr, sizeof(struct CtdlCompressHeader));
490 compressed_data = cdb->ptr;
491 compressed_data += sizeof(struct CtdlCompressHeader);
493 sourceLen = (uLongf) zheader.compressed_len;
494 destLen = (uLongf) zheader.uncompressed_len;
495 uncompressed_data = malloc(zheader.uncompressed_len);
497 if (uncompress((Bytef *) uncompressed_data,
498 (uLongf *) & destLen,
499 (const Bytef *) compressed_data,
500 (uLong) sourceLen) != Z_OK) {
501 CtdlLogPrintf(CTDL_EMERG, "uncompress() error\n");
506 cdb->len = (size_t) destLen;
507 cdb->ptr = uncompressed_data;
510 #endif /* HAVE_ZLIB */
514 * Store a piece of data. Returns 0 if the operation was successful. If a
515 * key already exists it should be overwritten.
517 int cdb_store(int cdb, void *ckey, int ckeylen, void *cdata, int cdatalen)
525 struct CtdlCompressHeader zheader;
526 char *compressed_data = NULL;
528 size_t buffer_len = 0;
532 memset(&dkey, 0, sizeof(DBT));
533 memset(&ddata, 0, sizeof(DBT));
536 ddata.size = cdatalen;
540 /* Only compress Visit records. Everything else is uncompressed. */
541 if (cdb == CDB_VISIT) {
543 zheader.magic = COMPRESS_MAGIC;
544 zheader.uncompressed_len = cdatalen;
545 buffer_len = ((cdatalen * 101) / 100) + 100
546 + sizeof(struct CtdlCompressHeader);
547 destLen = (uLongf) buffer_len;
548 compressed_data = malloc(buffer_len);
549 if (compress2((Bytef *) (compressed_data +
551 CtdlCompressHeader)),
552 &destLen, (Bytef *) cdata, (uLongf) cdatalen,
554 CtdlLogPrintf(CTDL_EMERG, "compress2() error\n");
557 zheader.compressed_len = (size_t) destLen;
558 memcpy(compressed_data, &zheader,
559 sizeof(struct CtdlCompressHeader));
560 ddata.size = (size_t) (sizeof(struct CtdlCompressHeader) +
561 zheader.compressed_len);
562 ddata.data = compressed_data;
567 ret = dbp[cdb]->put(dbp[cdb], /* db */
568 MYTID, /* transaction ID */
573 CtdlLogPrintf(CTDL_EMERG, "cdb_store(%d): %s\n", cdb,
579 free(compressed_data);
584 bailIfCursor(MYCURSORS,
585 "attempt to write during r/o cursor");
590 if ((ret = dbp[cdb]->put(dbp[cdb], /* db */
591 tid, /* transaction ID */
595 if (ret == DB_LOCK_DEADLOCK) {
599 CtdlLogPrintf(CTDL_EMERG, "cdb_store(%d): %s\n",
600 cdb, db_strerror(ret));
607 free(compressed_data);
616 * Delete a piece of data. Returns 0 if the operation was successful.
618 int cdb_delete(int cdb, void *key, int keylen)
625 memset(&dkey, 0, sizeof dkey);
630 ret = dbp[cdb]->del(dbp[cdb], MYTID, &dkey, 0);
632 CtdlLogPrintf(CTDL_EMERG, "cdb_delete(%d): %s\n", cdb,
634 if (ret != DB_NOTFOUND)
638 bailIfCursor(MYCURSORS,
639 "attempt to delete during r/o cursor");
644 if ((ret = dbp[cdb]->del(dbp[cdb], tid, &dkey, 0))
645 && ret != DB_NOTFOUND) {
646 if (ret == DB_LOCK_DEADLOCK) {
650 CtdlLogPrintf(CTDL_EMERG, "cdb_delete(%d): %s\n",
651 cdb, db_strerror(ret));
661 static DBC *localcursor(int cdb)
666 if (MYCURSORS[cdb] == NULL)
667 ret = dbp[cdb]->cursor(dbp[cdb], MYTID, &curs, 0);
670 MYCURSORS[cdb]->c_dup(MYCURSORS[cdb], &curs,
674 CtdlLogPrintf(CTDL_EMERG, "localcursor: %s\n", db_strerror(ret));
683 * Fetch a piece of data. If not found, returns NULL. Otherwise, it returns
684 * a struct cdbdata which it is the caller's responsibility to free later on
685 * using the cdb_free() routine.
687 struct cdbdata *cdb_fetch(int cdb, void *key, int keylen)
690 struct cdbdata *tempcdb;
694 memset(&dkey, 0, sizeof(DBT));
699 memset(&dret, 0, sizeof(DBT));
700 dret.flags = DB_DBT_MALLOC;
701 ret = dbp[cdb]->get(dbp[cdb], MYTID, &dkey, &dret, 0);
706 memset(&dret, 0, sizeof(DBT));
707 dret.flags = DB_DBT_MALLOC;
709 curs = localcursor(cdb);
711 ret = curs->c_get(curs, &dkey, &dret, DB_SET);
714 while (ret == DB_LOCK_DEADLOCK);
718 if ((ret != 0) && (ret != DB_NOTFOUND)) {
719 CtdlLogPrintf(CTDL_EMERG, "cdb_fetch(%d): %s\n", cdb,
726 tempcdb = (struct cdbdata *) malloc(sizeof(struct cdbdata));
728 if (tempcdb == NULL) {
729 CtdlLogPrintf(CTDL_EMERG,
730 "cdb_fetch: Cannot allocate memory for tempcdb\n");
734 tempcdb->len = dret.size;
735 tempcdb->ptr = dret.data;
737 cdb_decompress_if_necessary(tempcdb);
744 * Free a cdbdata item.
746 * Note that we only free the 'ptr' portion if it is not NULL. This allows
747 * other code to assume ownership of that memory simply by storing the
748 * pointer elsewhere and then setting 'ptr' to NULL. cdb_free() will then
751 void cdb_free(struct cdbdata *cdb)
759 void cdb_close_cursor(int cdb)
761 if (MYCURSORS[cdb] != NULL)
762 cclose(MYCURSORS[cdb]);
764 MYCURSORS[cdb] = NULL;
768 * Prepare for a sequential search of an entire database.
769 * (There is guaranteed to be no more than one traversal in
770 * progress per thread at any given time.)
772 void cdb_rewind(int cdb)
776 if (MYCURSORS[cdb] != NULL) {
777 CtdlLogPrintf(CTDL_EMERG,
778 "cdb_rewind: must close cursor on database %d before reopening.\n",
781 /* cclose(MYCURSORS[cdb]); */
785 * Now initialize the cursor
787 ret = dbp[cdb]->cursor(dbp[cdb], MYTID, &MYCURSORS[cdb], 0);
789 CtdlLogPrintf(CTDL_EMERG, "cdb_rewind: db_cursor: %s\n",
797 * Fetch the next item in a sequential search. Returns a pointer to a
798 * cdbdata structure, or NULL if we've hit the end.
800 struct cdbdata *cdb_next_item(int cdb)
803 struct cdbdata *cdbret;
806 /* Initialize the key/data pair so the flags aren't set. */
807 memset(&key, 0, sizeof(key));
808 memset(&data, 0, sizeof(data));
809 data.flags = DB_DBT_MALLOC;
811 ret = MYCURSORS[cdb]->c_get(MYCURSORS[cdb], &key, &data, DB_NEXT);
814 if (ret != DB_NOTFOUND) {
815 CtdlLogPrintf(CTDL_EMERG, "cdb_next_item(%d): %s\n",
816 cdb, db_strerror(ret));
819 cclose(MYCURSORS[cdb]);
820 MYCURSORS[cdb] = NULL;
821 return NULL; /* presumably, end of file */
824 cdbret = (struct cdbdata *) malloc(sizeof(struct cdbdata));
825 cdbret->len = data.size;
826 cdbret->ptr = data.data;
828 cdb_decompress_if_necessary(cdbret);
837 * Transaction-based stuff. I'm writing this as I bake cookies...
840 void cdb_begin_transaction(void)
843 bailIfCursor(MYCURSORS,
844 "can't begin transaction during r/o cursor");
847 CtdlLogPrintf(CTDL_EMERG,
848 "cdb_begin_transaction: ERROR: nested transaction\n");
855 void cdb_end_transaction(void)
859 for (i = 0; i < MAXCDB; i++)
860 if (MYCURSORS[i] != NULL) {
861 CtdlLogPrintf(CTDL_WARNING,
862 "cdb_end_transaction: WARNING: cursor %d still open at transaction end\n",
864 cclose(MYCURSORS[i]);
869 CtdlLogPrintf(CTDL_EMERG,
870 "cdb_end_transaction: ERROR: txcommit(NULL) !!\n");
879 * Truncate (delete every record)
881 void cdb_trunc(int cdb)
888 CtdlLogPrintf(CTDL_EMERG,
889 "cdb_trunc must not be called in a transaction.\n");
892 bailIfCursor(MYCURSORS,
893 "attempt to write during r/o cursor");
898 if ((ret = dbp[cdb]->truncate(dbp[cdb], /* db */
899 NULL, /* transaction ID */
900 &count, /* #rows deleted */
902 if (ret == DB_LOCK_DEADLOCK) {
906 CtdlLogPrintf(CTDL_EMERG,
907 "cdb_truncate(%d): %s\n", cdb,