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, "bdb(): txn_abort: %s\n", db_strerror(ret));
98 /* this one is even more helpful than the last. */
99 static void txcommit(DB_TXN * tid)
103 ret = tid->commit(tid, 0);
106 CtdlLogPrintf(CTDL_EMERG, "bdb(): txn_commit: %s\n", db_strerror(ret));
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, "bdb(): txn_begin: %s\n", db_strerror(ret));
124 static void dbpanic(DB_ENV * env, int errval)
126 CtdlLogPrintf(CTDL_EMERG, "bdb(): PANIC: %s\n", db_strerror(errval));
129 static void cclose(DBC * cursor)
133 if ((ret = cursor->c_close(cursor))) {
134 CtdlLogPrintf(CTDL_EMERG, "bdb(): c_close: %s\n", db_strerror(ret));
139 static void bailIfCursor(DBC ** cursors, const char *msg)
143 for (i = 0; i < MAXCDB; i++)
144 if (cursors[i] != NULL) {
145 CtdlLogPrintf(CTDL_EMERG,
146 "bdb(): cursor still in progress on cdb %02x: %s\n", i, msg);
151 void check_handles(void *arg)
154 ThreadTSD *tsd = (ThreadTSD *) arg;
156 bailIfCursor(tsd->cursors, "in check_handles");
158 if (tsd->tid != NULL) {
159 CtdlLogPrintf(CTDL_EMERG, "bdb(): transaction still in progress!");
165 void cdb_check_handles(void)
167 check_handles(pthread_getspecific(ThreadKey));
172 * Cull the database logs
174 static void cdb_cull_logs(void)
183 /* Get the list of names. */
184 if ((ret = dbenv->log_archive(dbenv, &list, flags)) != 0) {
185 CtdlLogPrintf(CTDL_ERR, "cdb_cull_logs: %s\n", db_strerror(ret));
189 /* Print the list of names. */
191 for (file = list; *file != NULL; ++file) {
192 CtdlLogPrintf(CTDL_DEBUG, "Deleting log: %s\n", *file);
195 snprintf(errmsg, sizeof(errmsg),
196 " ** ERROR **\n \n \n "
197 "Citadel was unable to delete the "
198 "database log file '%s' because of the "
199 "following error:\n \n %s\n \n"
200 " This log file is no longer in use "
201 "and may be safely deleted.\n",
202 *file, strerror(errno));
203 aide_message(errmsg, "Database Warning Message");
211 * Manually initiate log file cull.
213 void cmd_cull(char *argbuf) {
214 if (CtdlAccessCheck(ac_internal)) return;
216 cprintf("%d Database log file cull completed.\n", CIT_OK);
221 * Request a checkpoint of the database. Called once per minute by the thread manager.
223 void cdb_checkpoint(void)
227 CtdlLogPrintf(CTDL_DEBUG, "-- db checkpoint --\n");
228 ret = dbenv->txn_checkpoint(dbenv, MAX_CHECKPOINT_KBYTES, MAX_CHECKPOINT_MINUTES, 0);
231 CtdlLogPrintf(CTDL_EMERG, "cdb_checkpoint: txn_checkpoint: %s\n", db_strerror(ret));
235 /* After a successful checkpoint, we can cull the unused logs */
236 if (config.c_auto_cull) {
244 * Open the various databases we'll be using. Any database which
245 * does not exist should be created. Note that we don't need a
246 * critical section here, because there aren't any active threads
247 * manipulating the database yet.
249 void open_databases(void)
255 int dbversion_major, dbversion_minor, dbversion_patch;
256 int current_dbversion = 0;
258 CtdlLogPrintf(CTDL_DEBUG, "bdb(): open_databases() starting\n");
259 CtdlLogPrintf(CTDL_DEBUG, "Compiled db: %s\n", DB_VERSION_STRING);
260 CtdlLogPrintf(CTDL_INFO, " Linked db: %s\n",
261 db_version(&dbversion_major, &dbversion_minor, &dbversion_patch));
263 current_dbversion = (dbversion_major * 1000000) + (dbversion_minor * 1000) + dbversion_patch;
265 CtdlLogPrintf(CTDL_DEBUG, "Calculated dbversion: %d\n", current_dbversion);
266 CtdlLogPrintf(CTDL_DEBUG, " Previous dbversion: %d\n", CitControl.MMdbversion);
268 if ( (getenv("SUPPRESS_DBVERSION_CHECK") == NULL)
269 && (CitControl.MMdbversion > current_dbversion) ) {
270 CtdlLogPrintf(CTDL_EMERG, "You are attempting to run the Citadel server using a version\n"
271 "of Berkeley DB that is older than that which last created or\n"
272 "updated the database. Because this would probably cause data\n"
273 "corruption or loss, the server is aborting execution now.\n");
277 CitControl.MMdbversion = current_dbversion;
281 CtdlLogPrintf(CTDL_INFO, "Linked zlib: %s\n", zlibVersion());
285 * Silently try to create the database subdirectory. If it's
286 * already there, no problem.
288 mkdir(ctdl_data_dir, 0700);
289 chmod(ctdl_data_dir, 0700);
290 chown(ctdl_data_dir, CTDLUID, (-1));
292 CtdlLogPrintf(CTDL_DEBUG, "bdb(): Setting up DB environment\n");
293 db_env_set_func_yield(sched_yield);
294 ret = db_env_create(&dbenv, 0);
296 CtdlLogPrintf(CTDL_EMERG, "bdb(): db_env_create: %s\n", db_strerror(ret));
297 CtdlLogPrintf(CTDL_EMERG, "exit code %d\n", ret);
300 dbenv->set_errpfx(dbenv, "citserver");
301 dbenv->set_paniccall(dbenv, dbpanic);
302 dbenv->set_errcall(dbenv, cdb_verbose_err);
303 dbenv->set_errpfx(dbenv, "ctdl");
304 #if (DB_VERSION_MAJOR == 4) && (DB_VERSION_MINOR >= 3)
305 dbenv->set_msgcall(dbenv, cdb_verbose_log);
307 dbenv->set_verbose(dbenv, DB_VERB_DEADLOCK, 1);
308 dbenv->set_verbose(dbenv, DB_VERB_RECOVERY, 1);
311 * We want to specify the shared memory buffer pool cachesize,
312 * but everything else is the default.
314 ret = dbenv->set_cachesize(dbenv, 0, 64 * 1024, 0);
316 CtdlLogPrintf(CTDL_EMERG, "bdb(): set_cachesize: %s\n", db_strerror(ret));
317 dbenv->close(dbenv, 0);
318 CtdlLogPrintf(CTDL_EMERG, "exit code %d\n", ret);
322 if ((ret = dbenv->set_lk_detect(dbenv, DB_LOCK_DEFAULT))) {
323 CtdlLogPrintf(CTDL_EMERG, "bdb(): set_lk_detect: %s\n", db_strerror(ret));
324 dbenv->close(dbenv, 0);
325 CtdlLogPrintf(CTDL_EMERG, "exit code %d\n", ret);
329 flags = DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE | DB_INIT_TXN | DB_INIT_LOCK | DB_THREAD | DB_RECOVER;
330 CtdlLogPrintf(CTDL_DEBUG, "dbenv->open(dbenv, %s, %d, 0)\n", ctdl_data_dir, flags);
331 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
332 if (ret == DB_RUNRECOVERY) {
333 CtdlLogPrintf(CTDL_ALERT, "dbenv->open: %s\n", db_strerror(ret));
334 CtdlLogPrintf(CTDL_ALERT, "Attempting recovery...\n");
336 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
338 if (ret == DB_RUNRECOVERY) {
339 CtdlLogPrintf(CTDL_ALERT, "dbenv->open: %s\n", db_strerror(ret));
340 CtdlLogPrintf(CTDL_ALERT, "Attempting catastrophic recovery...\n");
341 flags &= ~DB_RECOVER;
342 flags |= DB_RECOVER_FATAL;
343 ret = dbenv->open(dbenv, ctdl_data_dir, flags, 0);
346 CtdlLogPrintf(CTDL_EMERG, "dbenv->open: %s\n", db_strerror(ret));
347 dbenv->close(dbenv, 0);
348 CtdlLogPrintf(CTDL_EMERG, "exit code %d\n", ret);
352 CtdlLogPrintf(CTDL_INFO, "Starting up DB\n");
354 for (i = 0; i < MAXCDB; ++i) {
356 /* Create a database handle */
357 ret = db_create(&dbp[i], dbenv, 0);
359 CtdlLogPrintf(CTDL_EMERG, "db_create: %s\n", db_strerror(ret));
360 CtdlLogPrintf(CTDL_EMERG, "exit code %d\n", ret);
365 /* Arbitrary names for our tables -- we reference them by
366 * number, so we don't have string names for them.
368 snprintf(dbfilename, sizeof dbfilename, "cdb.%02x", i);
370 ret = dbp[i]->open(dbp[i],
375 DB_CREATE | DB_AUTO_COMMIT | DB_THREAD,
379 CtdlLogPrintf(CTDL_EMERG, "db_open[%02x]: %s\n", i, db_strerror(ret));
381 CtdlLogPrintf(CTDL_EMERG, "You may need to tune your database; please read http://www.citadel.org/doku.php/faq:troubleshooting:out_of_lock_entries for more information.\n");
383 CtdlLogPrintf(CTDL_EMERG, "exit code %d\n", ret);
391 /* Make sure we own all the files, because in a few milliseconds
392 * we're going to drop root privs.
394 void cdb_chmod_data(void) {
397 char filename[PATH_MAX];
399 dp = opendir(ctdl_data_dir);
401 while (d = readdir(dp), d != NULL) {
402 if (d->d_name[0] != '.') {
403 snprintf(filename, sizeof filename,
404 "%s/%s", ctdl_data_dir, d->d_name);
405 CtdlLogPrintf(9, "chmod(%s, 0600) returned %d\n",
406 filename, chmod(filename, 0600)
408 CtdlLogPrintf(9, "chown(%s, CTDLUID, -1) returned %d\n",
409 filename, chown(filename, CTDLUID, (-1))
416 CtdlLogPrintf(CTDL_DEBUG, "open_databases() finished\n");
417 CtdlRegisterProtoHook(cmd_cull, "CULL", "Cull database logs");
422 * Close all of the db database files we've opened. This can be done
423 * in a loop, since it's just a bunch of closes.
425 void close_databases(void)
430 ctdl_thread_internal_free_tsd();
432 if ((ret = dbenv->txn_checkpoint(dbenv, 0, 0, 0))) {
433 CtdlLogPrintf(CTDL_EMERG,
434 "txn_checkpoint: %s\n", db_strerror(ret));
437 /* print some statistics... */
439 dbenv->lock_stat_print(dbenv, DB_STAT_ALL);
442 /* close the tables */
443 for (a = 0; a < MAXCDB; ++a) {
444 CtdlLogPrintf(CTDL_INFO, "Closing database %02x\n", a);
445 ret = dbp[a]->close(dbp[a], 0);
447 CtdlLogPrintf(CTDL_EMERG, "db_close: %s\n", db_strerror(ret));
452 /* Close the handle. */
453 ret = dbenv->close(dbenv, 0);
455 CtdlLogPrintf(CTDL_EMERG, "DBENV->close: %s\n", db_strerror(ret));
461 * Compression functions only used if we have zlib
463 void cdb_decompress_if_necessary(struct cdbdata *cdb)
465 static int magic = COMPRESS_MAGIC;
469 if (cdb->ptr == NULL)
471 if (memcmp(cdb->ptr, &magic, sizeof(magic)))
475 /* At this point we know we're looking at a compressed item. */
477 struct CtdlCompressHeader zheader;
478 char *uncompressed_data;
479 char *compressed_data;
480 uLongf destLen, sourceLen;
482 memcpy(&zheader, cdb->ptr, sizeof(struct CtdlCompressHeader));
484 compressed_data = cdb->ptr;
485 compressed_data += sizeof(struct CtdlCompressHeader);
487 sourceLen = (uLongf) zheader.compressed_len;
488 destLen = (uLongf) zheader.uncompressed_len;
489 uncompressed_data = malloc(zheader.uncompressed_len);
491 if (uncompress((Bytef *) uncompressed_data,
492 (uLongf *) & destLen,
493 (const Bytef *) compressed_data,
494 (uLong) sourceLen) != Z_OK) {
495 CtdlLogPrintf(CTDL_EMERG, "uncompress() error\n");
500 cdb->len = (size_t) destLen;
501 cdb->ptr = uncompressed_data;
502 #else /* HAVE_ZLIB */
503 CtdlLogPrintf(CTDL_EMERG, "Database contains compressed data, but this citserver was built without compression support.\n");
505 #endif /* HAVE_ZLIB */
511 * Store a piece of data. Returns 0 if the operation was successful. If a
512 * key already exists it should be overwritten.
514 int cdb_store(int cdb, void *ckey, int ckeylen, void *cdata, int cdatalen)
522 struct CtdlCompressHeader zheader;
523 char *compressed_data = NULL;
525 size_t buffer_len = 0;
529 memset(&dkey, 0, sizeof(DBT));
530 memset(&ddata, 0, sizeof(DBT));
533 ddata.size = cdatalen;
537 /* Only compress Visit records. Everything else is uncompressed. */
538 if (cdb == CDB_VISIT) {
540 zheader.magic = COMPRESS_MAGIC;
541 zheader.uncompressed_len = cdatalen;
542 buffer_len = ((cdatalen * 101) / 100) + 100
543 + sizeof(struct CtdlCompressHeader);
544 destLen = (uLongf) buffer_len;
545 compressed_data = malloc(buffer_len);
546 if (compress2((Bytef *) (compressed_data + sizeof(struct CtdlCompressHeader)),
547 &destLen, (Bytef *) cdata, (uLongf) cdatalen, 1) != Z_OK)
549 CtdlLogPrintf(CTDL_EMERG, "compress2() error\n");
552 zheader.compressed_len = (size_t) destLen;
553 memcpy(compressed_data, &zheader, sizeof(struct CtdlCompressHeader));
554 ddata.size = (size_t) (sizeof(struct CtdlCompressHeader) + zheader.compressed_len);
555 ddata.data = compressed_data;
560 ret = dbp[cdb]->put(dbp[cdb], /* db */
561 MYTID, /* transaction ID */
566 CtdlLogPrintf(CTDL_EMERG, "cdb_store(%d): %s\n", cdb, db_strerror(ret));
571 free(compressed_data);
576 bailIfCursor(MYCURSORS, "attempt to write during r/o cursor");
581 if ((ret = dbp[cdb]->put(dbp[cdb], /* db */
582 tid, /* transaction ID */
586 if (ret == DB_LOCK_DEADLOCK) {
590 CtdlLogPrintf(CTDL_EMERG, "cdb_store(%d): %s\n",
591 cdb, db_strerror(ret));
598 free(compressed_data);
608 * Delete a piece of data. Returns 0 if the operation was successful.
610 int cdb_delete(int cdb, void *key, int keylen)
617 memset(&dkey, 0, sizeof dkey);
622 ret = dbp[cdb]->del(dbp[cdb], MYTID, &dkey, 0);
624 CtdlLogPrintf(CTDL_EMERG, "cdb_delete(%d): %s\n", cdb, db_strerror(ret));
625 if (ret != DB_NOTFOUND) {
630 bailIfCursor(MYCURSORS, "attempt to delete during r/o cursor");
635 if ((ret = dbp[cdb]->del(dbp[cdb], tid, &dkey, 0))
636 && ret != DB_NOTFOUND) {
637 if (ret == DB_LOCK_DEADLOCK) {
641 CtdlLogPrintf(CTDL_EMERG, "cdb_delete(%d): %s\n",
642 cdb, db_strerror(ret));
652 static DBC *localcursor(int cdb)
657 if (MYCURSORS[cdb] == NULL)
658 ret = dbp[cdb]->cursor(dbp[cdb], MYTID, &curs, 0);
661 MYCURSORS[cdb]->c_dup(MYCURSORS[cdb], &curs,
665 CtdlLogPrintf(CTDL_EMERG, "localcursor: %s\n", db_strerror(ret));
674 * Fetch a piece of data. If not found, returns NULL. Otherwise, it returns
675 * a struct cdbdata which it is the caller's responsibility to free later on
676 * using the cdb_free() routine.
678 struct cdbdata *cdb_fetch(int cdb, void *key, int keylen)
681 struct cdbdata *tempcdb;
685 memset(&dkey, 0, sizeof(DBT));
690 memset(&dret, 0, sizeof(DBT));
691 dret.flags = DB_DBT_MALLOC;
692 ret = dbp[cdb]->get(dbp[cdb], MYTID, &dkey, &dret, 0);
697 memset(&dret, 0, sizeof(DBT));
698 dret.flags = DB_DBT_MALLOC;
700 curs = localcursor(cdb);
702 ret = curs->c_get(curs, &dkey, &dret, DB_SET);
705 while (ret == DB_LOCK_DEADLOCK);
709 if ((ret != 0) && (ret != DB_NOTFOUND)) {
710 CtdlLogPrintf(CTDL_EMERG, "cdb_fetch(%d): %s\n", cdb, db_strerror(ret));
716 tempcdb = (struct cdbdata *) malloc(sizeof(struct cdbdata));
718 if (tempcdb == NULL) {
719 CtdlLogPrintf(CTDL_EMERG, "cdb_fetch: Cannot allocate memory for tempcdb\n");
723 tempcdb->len = dret.size;
724 tempcdb->ptr = dret.data;
725 cdb_decompress_if_necessary(tempcdb);
731 * Free a cdbdata item.
733 * Note that we only free the 'ptr' portion if it is not NULL. This allows
734 * other code to assume ownership of that memory simply by storing the
735 * pointer elsewhere and then setting 'ptr' to NULL. cdb_free() will then
738 void cdb_free(struct cdbdata *cdb)
746 void cdb_close_cursor(int cdb)
748 if (MYCURSORS[cdb] != NULL) {
749 cclose(MYCURSORS[cdb]);
752 MYCURSORS[cdb] = NULL;
756 * Prepare for a sequential search of an entire database.
757 * (There is guaranteed to be no more than one traversal in
758 * progress per thread at any given time.)
760 void cdb_rewind(int cdb)
764 if (MYCURSORS[cdb] != NULL) {
765 CtdlLogPrintf(CTDL_EMERG,
766 "cdb_rewind: must close cursor on database %d before reopening.\n", cdb);
768 /* cclose(MYCURSORS[cdb]); */
772 * Now initialize the cursor
774 ret = dbp[cdb]->cursor(dbp[cdb], MYTID, &MYCURSORS[cdb], 0);
776 CtdlLogPrintf(CTDL_EMERG, "cdb_rewind: db_cursor: %s\n", db_strerror(ret));
783 * Fetch the next item in a sequential search. Returns a pointer to a
784 * cdbdata structure, or NULL if we've hit the end.
786 struct cdbdata *cdb_next_item(int cdb)
789 struct cdbdata *cdbret;
792 /* Initialize the key/data pair so the flags aren't set. */
793 memset(&key, 0, sizeof(key));
794 memset(&data, 0, sizeof(data));
795 data.flags = DB_DBT_MALLOC;
797 ret = MYCURSORS[cdb]->c_get(MYCURSORS[cdb], &key, &data, DB_NEXT);
800 if (ret != DB_NOTFOUND) {
801 CtdlLogPrintf(CTDL_EMERG, "cdb_next_item(%d): %s\n", cdb, db_strerror(ret));
804 cclose(MYCURSORS[cdb]);
805 MYCURSORS[cdb] = NULL;
806 return NULL; /* presumably, end of file */
809 cdbret = (struct cdbdata *) malloc(sizeof(struct cdbdata));
810 cdbret->len = data.size;
811 cdbret->ptr = data.data;
812 cdb_decompress_if_necessary(cdbret);
820 * Transaction-based stuff. I'm writing this as I bake cookies...
823 void cdb_begin_transaction(void)
826 bailIfCursor(MYCURSORS, "can't begin transaction during r/o cursor");
829 CtdlLogPrintf(CTDL_EMERG, "cdb_begin_transaction: ERROR: nested transaction\n");
836 void cdb_end_transaction(void)
840 for (i = 0; i < MAXCDB; i++)
841 if (MYCURSORS[i] != NULL) {
842 CtdlLogPrintf(CTDL_WARNING,
843 "cdb_end_transaction: WARNING: cursor %d still open at transaction end\n",
845 cclose(MYCURSORS[i]);
850 CtdlLogPrintf(CTDL_EMERG,
851 "cdb_end_transaction: ERROR: txcommit(NULL) !!\n");
861 * Truncate (delete every record)
863 void cdb_trunc(int cdb)
870 CtdlLogPrintf(CTDL_EMERG,
871 "cdb_trunc must not be called in a transaction.\n");
874 bailIfCursor(MYCURSORS, "attempt to write during r/o cursor");
879 if ((ret = dbp[cdb]->truncate(dbp[cdb], /* db */
880 NULL, /* transaction ID */
881 &count, /* #rows deleted */
883 if (ret == DB_LOCK_DEADLOCK) {
887 CtdlLogPrintf(CTDL_EMERG, "cdb_truncate(%d): %s\n", cdb, db_strerror(ret));
889 CtdlLogPrintf(CTDL_EMERG, "You may need to tune your database; please read http://www.citadel.org/doku.php/faq:troubleshooting:out_of_lock_entries for more information.\n");