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, "BDB: %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.
229 void cdb_checkpoint(void)
232 // static time_t last_run = 0L;
234 /* Only do a checkpoint once per minute. */
236 * Don't need this any more, since the thread that calls us sleeps for 60 seconds between calls
238 if ((time(NULL) - last_run) < 60L) {
241 last_run = time(NULL);
244 CtdlLogPrintf(CTDL_DEBUG, "-- db checkpoint --\n");
245 ret = dbenv->txn_checkpoint(dbenv,
246 MAX_CHECKPOINT_KBYTES,
247 MAX_CHECKPOINT_MINUTES, 0);
250 CtdlLogPrintf(CTDL_EMERG, "cdb_checkpoint: txn_checkpoint: %s\n",
255 /* After a successful checkpoint, we can cull the unused logs */
256 if (config.c_auto_cull) {
264 * Open the various databases we'll be using. Any database which
265 * does not exist should be created. Note that we don't need a
266 * critical section here, because there aren't any active threads
267 * manipulating the database yet.
269 void open_databases(void)
273 char dbfilename[SIZ];
275 int dbversion_major, dbversion_minor, dbversion_patch;
276 int current_dbversion = 0;
278 CtdlLogPrintf(CTDL_DEBUG, "cdb_*: open_databases() starting\n");
279 CtdlLogPrintf(CTDL_DEBUG, "Compiled db: %s\n", DB_VERSION_STRING);
280 CtdlLogPrintf(CTDL_INFO, " Linked db: %s\n",
281 db_version(&dbversion_major, &dbversion_minor, &dbversion_patch));
283 current_dbversion = (dbversion_major * 1000000) + (dbversion_minor * 1000) + dbversion_patch;
285 CtdlLogPrintf(CTDL_DEBUG, "Calculated dbversion: %d\n", current_dbversion);
286 CtdlLogPrintf(CTDL_DEBUG, " Previous dbversion: %d\n", CitControl.MMdbversion);
288 if ( (getenv("SUPPRESS_DBVERSION_CHECK") == NULL)
289 && (CitControl.MMdbversion > current_dbversion) ) {
290 CtdlLogPrintf(CTDL_EMERG, "You are attempting to run the Citadel server using a version\n"
291 "of Berkeley DB that is older than that which last created or\n"
292 "updated the database. Because this would probably cause data\n"
293 "corruption or loss, the server is aborting execution now.\n");
297 CitControl.MMdbversion = current_dbversion;
301 CtdlLogPrintf(CTDL_INFO, "Linked zlib: %s\n", zlibVersion());
305 * Silently try to create the database subdirectory. If it's
306 * already there, no problem.
308 mkdir(ctdl_data_dir, 0700);
309 chmod(ctdl_data_dir, 0700);
310 chown(ctdl_data_dir, CTDLUID, (-1));
312 CtdlLogPrintf(CTDL_DEBUG, "cdb_*: Setting up DB environment\n");
313 db_env_set_func_yield(sched_yield);
314 ret = db_env_create(&dbenv, 0);
316 CtdlLogPrintf(CTDL_EMERG, "cdb_*: db_env_create: %s\n",
320 dbenv->set_errpfx(dbenv, "citserver");
321 dbenv->set_paniccall(dbenv, dbpanic);
322 dbenv->set_errcall(dbenv, cdb_verbose_err);
323 dbenv->set_errpfx(dbenv, "ctdl");
324 #if (DB_VERSION_MAJOR == 4) && (DB_VERSION_MINOR >= 3)
325 dbenv->set_msgcall(dbenv, cdb_verbose_log);
327 dbenv->set_verbose(dbenv, DB_VERB_DEADLOCK, 1);
328 dbenv->set_verbose(dbenv, DB_VERB_RECOVERY, 1);
331 * We want to specify the shared memory buffer pool cachesize,
332 * but everything else is the default.
334 ret = dbenv->set_cachesize(dbenv, 0, 64 * 1024, 0);
336 CtdlLogPrintf(CTDL_EMERG, "cdb_*: set_cachesize: %s\n",
338 dbenv->close(dbenv, 0);
342 if ((ret = dbenv->set_lk_detect(dbenv, DB_LOCK_DEFAULT))) {
343 CtdlLogPrintf(CTDL_EMERG, "cdb_*: set_lk_detect: %s\n",
345 dbenv->close(dbenv, 0);
349 flags = DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE | DB_INIT_TXN | DB_INIT_LOCK | DB_THREAD | DB_RECOVER;
350 CtdlLogPrintf(CTDL_DEBUG, "dbenv->open(dbenv, %s, %d, 0)\n", ctdl_data_dir, flags);
351 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
352 if (ret == DB_RUNRECOVERY) {
353 CtdlLogPrintf(CTDL_ALERT, "dbenv->open: %s\n", db_strerror(ret));
354 CtdlLogPrintf(CTDL_ALERT, "Attempting recovery...\n");
356 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
358 if (ret == DB_RUNRECOVERY) {
359 CtdlLogPrintf(CTDL_ALERT, "dbenv->open: %s\n", db_strerror(ret));
360 CtdlLogPrintf(CTDL_ALERT, "Attempting catastrophic recovery...\n");
361 flags &= ~DB_RECOVER;
362 flags |= DB_RECOVER_FATAL;
363 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
366 CtdlLogPrintf(CTDL_DEBUG, "dbenv->open: %s\n", db_strerror(ret));
367 dbenv->close(dbenv, 0);
371 CtdlLogPrintf(CTDL_INFO, "Starting up DB\n");
373 for (i = 0; i < MAXCDB; ++i) {
375 /* Create a database handle */
376 ret = db_create(&dbp[i], dbenv, 0);
378 CtdlLogPrintf(CTDL_DEBUG, "db_create: %s\n",
384 /* Arbitrary names for our tables -- we reference them by
385 * number, so we don't have string names for them.
387 snprintf(dbfilename, sizeof dbfilename, "cdb.%02x", i);
389 ret = dbp[i]->open(dbp[i],
394 DB_CREATE | DB_AUTO_COMMIT | DB_THREAD,
397 CtdlLogPrintf(CTDL_EMERG, "db_open[%02x]: %s\n", i,
406 /* Make sure we own all the files, because in a few milliseconds
407 * we're going to drop root privs.
409 void cdb_chmod_data(void) {
412 char filename[PATH_MAX];
414 dp = opendir(ctdl_data_dir);
416 while (d = readdir(dp), d != NULL) {
417 if (d->d_name[0] != '.') {
418 snprintf(filename, sizeof filename,
419 "%s/%s", ctdl_data_dir, d->d_name);
420 CtdlLogPrintf(9, "chmod(%s, 0600) returned %d\n",
421 filename, chmod(filename, 0600)
423 CtdlLogPrintf(9, "chown(%s, CTDLUID, -1) returned %d\n",
424 filename, chown(filename, CTDLUID, (-1))
431 CtdlLogPrintf(CTDL_DEBUG, "open_databases() finished\n");
433 CtdlRegisterProtoHook(cmd_cull, "CULL", "Cull database logs");
438 * Close all of the db database files we've opened. This can be done
439 * in a loop, since it's just a bunch of closes.
441 void close_databases(void)
446 ctdl_thread_internal_free_tsd();
448 if ((ret = dbenv->txn_checkpoint(dbenv, 0, 0, 0))) {
449 CtdlLogPrintf(CTDL_EMERG,
450 "txn_checkpoint: %s\n", db_strerror(ret));
453 /* print some statistics... */
455 dbenv->lock_stat_print(dbenv, DB_STAT_ALL);
458 /* close the tables */
459 for (a = 0; a < MAXCDB; ++a) {
460 CtdlLogPrintf(CTDL_INFO, "Closing database %02x\n", a);
461 ret = dbp[a]->close(dbp[a], 0);
463 CtdlLogPrintf(CTDL_EMERG,
464 "db_close: %s\n", db_strerror(ret));
469 /* Close the handle. */
470 ret = dbenv->close(dbenv, 0);
472 CtdlLogPrintf(CTDL_EMERG,
473 "DBENV->close: %s\n", db_strerror(ret));
479 * Compression functions only used if we have zlib
483 void cdb_decompress_if_necessary(struct cdbdata *cdb)
485 static int magic = COMPRESS_MAGIC;
486 struct CtdlCompressHeader zheader;
487 char *uncompressed_data;
488 char *compressed_data;
489 uLongf destLen, sourceLen;
493 if (cdb->ptr == NULL)
495 if (memcmp(cdb->ptr, &magic, sizeof(magic)))
498 /* At this point we know we're looking at a compressed item. */
499 memcpy(&zheader, cdb->ptr, sizeof(struct CtdlCompressHeader));
501 compressed_data = cdb->ptr;
502 compressed_data += sizeof(struct CtdlCompressHeader);
504 sourceLen = (uLongf) zheader.compressed_len;
505 destLen = (uLongf) zheader.uncompressed_len;
506 uncompressed_data = malloc(zheader.uncompressed_len);
508 if (uncompress((Bytef *) uncompressed_data,
509 (uLongf *) & destLen,
510 (const Bytef *) compressed_data,
511 (uLong) sourceLen) != Z_OK) {
512 CtdlLogPrintf(CTDL_EMERG, "uncompress() error\n");
517 cdb->len = (size_t) destLen;
518 cdb->ptr = uncompressed_data;
521 #endif /* HAVE_ZLIB */
525 * Store a piece of data. Returns 0 if the operation was successful. If a
526 * key already exists it should be overwritten.
528 int cdb_store(int cdb, void *ckey, int ckeylen, void *cdata, int cdatalen)
536 struct CtdlCompressHeader zheader;
537 char *compressed_data = NULL;
539 size_t buffer_len = 0;
543 memset(&dkey, 0, sizeof(DBT));
544 memset(&ddata, 0, sizeof(DBT));
547 ddata.size = cdatalen;
551 /* Only compress Visit records. Everything else is uncompressed. */
552 if (cdb == CDB_VISIT) {
554 zheader.magic = COMPRESS_MAGIC;
555 zheader.uncompressed_len = cdatalen;
556 buffer_len = ((cdatalen * 101) / 100) + 100
557 + sizeof(struct CtdlCompressHeader);
558 destLen = (uLongf) buffer_len;
559 compressed_data = malloc(buffer_len);
560 if (compress2((Bytef *) (compressed_data +
562 CtdlCompressHeader)),
563 &destLen, (Bytef *) cdata, (uLongf) cdatalen,
565 CtdlLogPrintf(CTDL_EMERG, "compress2() error\n");
568 zheader.compressed_len = (size_t) destLen;
569 memcpy(compressed_data, &zheader,
570 sizeof(struct CtdlCompressHeader));
571 ddata.size = (size_t) (sizeof(struct CtdlCompressHeader) +
572 zheader.compressed_len);
573 ddata.data = compressed_data;
578 ret = dbp[cdb]->put(dbp[cdb], /* db */
579 MYTID, /* transaction ID */
584 CtdlLogPrintf(CTDL_EMERG, "cdb_store(%d): %s\n", cdb,
590 free(compressed_data);
595 bailIfCursor(MYCURSORS,
596 "attempt to write during r/o cursor");
601 if ((ret = dbp[cdb]->put(dbp[cdb], /* db */
602 tid, /* transaction ID */
606 if (ret == DB_LOCK_DEADLOCK) {
610 CtdlLogPrintf(CTDL_EMERG, "cdb_store(%d): %s\n",
611 cdb, db_strerror(ret));
618 free(compressed_data);
627 * Delete a piece of data. Returns 0 if the operation was successful.
629 int cdb_delete(int cdb, void *key, int keylen)
636 memset(&dkey, 0, sizeof dkey);
641 ret = dbp[cdb]->del(dbp[cdb], MYTID, &dkey, 0);
643 CtdlLogPrintf(CTDL_EMERG, "cdb_delete(%d): %s\n", cdb,
645 if (ret != DB_NOTFOUND)
649 bailIfCursor(MYCURSORS,
650 "attempt to delete during r/o cursor");
655 if ((ret = dbp[cdb]->del(dbp[cdb], tid, &dkey, 0))
656 && ret != DB_NOTFOUND) {
657 if (ret == DB_LOCK_DEADLOCK) {
661 CtdlLogPrintf(CTDL_EMERG, "cdb_delete(%d): %s\n",
662 cdb, db_strerror(ret));
672 static DBC *localcursor(int cdb)
677 if (MYCURSORS[cdb] == NULL)
678 ret = dbp[cdb]->cursor(dbp[cdb], MYTID, &curs, 0);
681 MYCURSORS[cdb]->c_dup(MYCURSORS[cdb], &curs,
685 CtdlLogPrintf(CTDL_EMERG, "localcursor: %s\n", db_strerror(ret));
694 * Fetch a piece of data. If not found, returns NULL. Otherwise, it returns
695 * a struct cdbdata which it is the caller's responsibility to free later on
696 * using the cdb_free() routine.
698 struct cdbdata *cdb_fetch(int cdb, void *key, int keylen)
701 struct cdbdata *tempcdb;
705 memset(&dkey, 0, sizeof(DBT));
710 memset(&dret, 0, sizeof(DBT));
711 dret.flags = DB_DBT_MALLOC;
712 ret = dbp[cdb]->get(dbp[cdb], MYTID, &dkey, &dret, 0);
717 memset(&dret, 0, sizeof(DBT));
718 dret.flags = DB_DBT_MALLOC;
720 curs = localcursor(cdb);
722 ret = curs->c_get(curs, &dkey, &dret, DB_SET);
725 while (ret == DB_LOCK_DEADLOCK);
729 if ((ret != 0) && (ret != DB_NOTFOUND)) {
730 CtdlLogPrintf(CTDL_EMERG, "cdb_fetch(%d): %s\n", cdb,
737 tempcdb = (struct cdbdata *) malloc(sizeof(struct cdbdata));
739 if (tempcdb == NULL) {
740 CtdlLogPrintf(CTDL_EMERG,
741 "cdb_fetch: Cannot allocate memory for tempcdb\n");
745 tempcdb->len = dret.size;
746 tempcdb->ptr = dret.data;
748 cdb_decompress_if_necessary(tempcdb);
755 * Free a cdbdata item.
757 * Note that we only free the 'ptr' portion if it is not NULL. This allows
758 * other code to assume ownership of that memory simply by storing the
759 * pointer elsewhere and then setting 'ptr' to NULL. cdb_free() will then
762 void cdb_free(struct cdbdata *cdb)
770 void cdb_close_cursor(int cdb)
772 if (MYCURSORS[cdb] != NULL)
773 cclose(MYCURSORS[cdb]);
775 MYCURSORS[cdb] = NULL;
779 * Prepare for a sequential search of an entire database.
780 * (There is guaranteed to be no more than one traversal in
781 * progress per thread at any given time.)
783 void cdb_rewind(int cdb)
787 if (MYCURSORS[cdb] != NULL) {
788 CtdlLogPrintf(CTDL_EMERG,
789 "cdb_rewind: must close cursor on database %d before reopening.\n",
792 /* cclose(MYCURSORS[cdb]); */
796 * Now initialize the cursor
798 ret = dbp[cdb]->cursor(dbp[cdb], MYTID, &MYCURSORS[cdb], 0);
800 CtdlLogPrintf(CTDL_EMERG, "cdb_rewind: db_cursor: %s\n",
808 * Fetch the next item in a sequential search. Returns a pointer to a
809 * cdbdata structure, or NULL if we've hit the end.
811 struct cdbdata *cdb_next_item(int cdb)
814 struct cdbdata *cdbret;
817 /* Initialize the key/data pair so the flags aren't set. */
818 memset(&key, 0, sizeof(key));
819 memset(&data, 0, sizeof(data));
820 data.flags = DB_DBT_MALLOC;
822 ret = MYCURSORS[cdb]->c_get(MYCURSORS[cdb], &key, &data, DB_NEXT);
825 if (ret != DB_NOTFOUND) {
826 CtdlLogPrintf(CTDL_EMERG, "cdb_next_item(%d): %s\n",
827 cdb, db_strerror(ret));
830 cclose(MYCURSORS[cdb]);
831 MYCURSORS[cdb] = NULL;
832 return NULL; /* presumably, end of file */
835 cdbret = (struct cdbdata *) malloc(sizeof(struct cdbdata));
836 cdbret->len = data.size;
837 cdbret->ptr = data.data;
839 cdb_decompress_if_necessary(cdbret);
848 * Transaction-based stuff. I'm writing this as I bake cookies...
851 void cdb_begin_transaction(void)
854 bailIfCursor(MYCURSORS,
855 "can't begin transaction during r/o cursor");
858 CtdlLogPrintf(CTDL_EMERG,
859 "cdb_begin_transaction: ERROR: nested transaction\n");
866 void cdb_end_transaction(void)
870 for (i = 0; i < MAXCDB; i++)
871 if (MYCURSORS[i] != NULL) {
872 CtdlLogPrintf(CTDL_WARNING,
873 "cdb_end_transaction: WARNING: cursor %d still open at transaction end\n",
875 cclose(MYCURSORS[i]);
880 CtdlLogPrintf(CTDL_EMERG,
881 "cdb_end_transaction: ERROR: txcommit(NULL) !!\n");
890 * Truncate (delete every record)
892 void cdb_trunc(int cdb)
899 CtdlLogPrintf(CTDL_EMERG,
900 "cdb_trunc must not be called in a transaction.\n");
903 bailIfCursor(MYCURSORS,
904 "attempt to write during r/o cursor");
909 if ((ret = dbp[cdb]->truncate(dbp[cdb], /* db */
910 NULL, /* transaction ID */
911 &count, /* #rows deleted */
913 if (ret == DB_LOCK_DEADLOCK) {
917 CtdlLogPrintf(CTDL_EMERG,
918 "cdb_truncate(%d): %s\n", cdb,