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", db_strerror(ret
));
306 CtdlLogPrintf(CTDL_EMERG
, "exit code %d\n", ret
);
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", db_strerror(ret
));
326 dbenv
->close(dbenv
, 0);
327 CtdlLogPrintf(CTDL_EMERG
, "exit code %d\n", ret
);
331 if ((ret
= dbenv
->set_lk_detect(dbenv
, DB_LOCK_DEFAULT
))) {
332 CtdlLogPrintf(CTDL_EMERG
, "cdb_*: set_lk_detect: %s\n", db_strerror(ret
));
333 dbenv
->close(dbenv
, 0);
334 CtdlLogPrintf(CTDL_EMERG
, "exit code %d\n", ret
);
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_EMERG
, "dbenv->open: %s\n", db_strerror(ret
));
356 dbenv
->close(dbenv
, 0);
357 CtdlLogPrintf(CTDL_EMERG
, "exit code %d\n", ret
);
361 CtdlLogPrintf(CTDL_INFO
, "Starting up DB\n");
363 for (i
= 0; i
< MAXCDB
; ++i
) {
365 /* Create a database handle */
366 ret
= db_create(&dbp
[i
], dbenv
, 0);
368 CtdlLogPrintf(CTDL_EMERG
, "db_create: %s\n", db_strerror(ret
));
369 CtdlLogPrintf(CTDL_EMERG
, "exit code %d\n", ret
);
374 /* Arbitrary names for our tables -- we reference them by
375 * number, so we don't have string names for them.
377 snprintf(dbfilename
, sizeof dbfilename
, "cdb.%02x", i
);
379 ret
= dbp
[i
]->open(dbp
[i
],
384 DB_CREATE
| DB_AUTO_COMMIT
| DB_THREAD
,
387 CtdlLogPrintf(CTDL_EMERG
, "db_open[%02x]: %s\n", i
, db_strerror(ret
));
389 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");
391 CtdlLogPrintf(CTDL_EMERG
, "exit code %d\n", ret
);
399 /* Make sure we own all the files, because in a few milliseconds
400 * we're going to drop root privs.
402 void cdb_chmod_data(void) {
405 char filename
[PATH_MAX
];
407 dp
= opendir(ctdl_data_dir
);
409 while (d
= readdir(dp
), d
!= NULL
) {
410 if (d
->d_name
[0] != '.') {
411 snprintf(filename
, sizeof filename
,
412 "%s/%s", ctdl_data_dir
, d
->d_name
);
413 CtdlLogPrintf(9, "chmod(%s, 0600) returned %d\n",
414 filename
, chmod(filename
, 0600)
416 CtdlLogPrintf(9, "chown(%s, CTDLUID, -1) returned %d\n",
417 filename
, chown(filename
, CTDLUID
, (-1))
424 CtdlLogPrintf(CTDL_DEBUG
, "open_databases() finished\n");
426 CtdlRegisterProtoHook(cmd_cull
, "CULL", "Cull database logs");
431 * Close all of the db database files we've opened. This can be done
432 * in a loop, since it's just a bunch of closes.
434 void close_databases(void)
439 ctdl_thread_internal_free_tsd();
441 if ((ret
= dbenv
->txn_checkpoint(dbenv
, 0, 0, 0))) {
442 CtdlLogPrintf(CTDL_EMERG
,
443 "txn_checkpoint: %s\n", db_strerror(ret
));
446 /* print some statistics... */
448 dbenv
->lock_stat_print(dbenv
, DB_STAT_ALL
);
451 /* close the tables */
452 for (a
= 0; a
< MAXCDB
; ++a
) {
453 CtdlLogPrintf(CTDL_INFO
, "Closing database %02x\n", a
);
454 ret
= dbp
[a
]->close(dbp
[a
], 0);
456 CtdlLogPrintf(CTDL_EMERG
,
457 "db_close: %s\n", db_strerror(ret
));
462 /* Close the handle. */
463 ret
= dbenv
->close(dbenv
, 0);
465 CtdlLogPrintf(CTDL_EMERG
,
466 "DBENV->close: %s\n", db_strerror(ret
));
472 * Compression functions only used if we have zlib
474 void cdb_decompress_if_necessary(struct cdbdata
*cdb
)
476 static int magic
= COMPRESS_MAGIC
;
480 if (cdb
->ptr
== NULL
)
482 if (memcmp(cdb
->ptr
, &magic
, sizeof(magic
)))
486 /* At this point we know we're looking at a compressed item. */
488 struct CtdlCompressHeader zheader
;
489 char *uncompressed_data
;
490 char *compressed_data
;
491 uLongf destLen
, sourceLen
;
493 memcpy(&zheader
, cdb
->ptr
, sizeof(struct CtdlCompressHeader
));
495 compressed_data
= cdb
->ptr
;
496 compressed_data
+= sizeof(struct CtdlCompressHeader
);
498 sourceLen
= (uLongf
) zheader
.compressed_len
;
499 destLen
= (uLongf
) zheader
.uncompressed_len
;
500 uncompressed_data
= malloc(zheader
.uncompressed_len
);
502 if (uncompress((Bytef
*) uncompressed_data
,
503 (uLongf
*) & destLen
,
504 (const Bytef
*) compressed_data
,
505 (uLong
) sourceLen
) != Z_OK
) {
506 CtdlLogPrintf(CTDL_EMERG
, "uncompress() error\n");
511 cdb
->len
= (size_t) destLen
;
512 cdb
->ptr
= uncompressed_data
;
513 #else /* HAVE_ZLIB */
514 CtdlLogPrintf(CTDL_EMERG
, "Database contains compressed data, but this citserver was built without compression support.\n");
516 #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
;
744 cdb_decompress_if_necessary(tempcdb
);
750 * Free a cdbdata item.
752 * Note that we only free the 'ptr' portion if it is not NULL. This allows
753 * other code to assume ownership of that memory simply by storing the
754 * pointer elsewhere and then setting 'ptr' to NULL. cdb_free() will then
757 void cdb_free(struct cdbdata
*cdb
)
765 void cdb_close_cursor(int cdb
)
767 if (MYCURSORS
[cdb
] != NULL
)
768 cclose(MYCURSORS
[cdb
]);
770 MYCURSORS
[cdb
] = NULL
;
774 * Prepare for a sequential search of an entire database.
775 * (There is guaranteed to be no more than one traversal in
776 * progress per thread at any given time.)
778 void cdb_rewind(int cdb
)
782 if (MYCURSORS
[cdb
] != NULL
) {
783 CtdlLogPrintf(CTDL_EMERG
,
784 "cdb_rewind: must close cursor on database %d before reopening.\n",
787 /* cclose(MYCURSORS[cdb]); */
791 * Now initialize the cursor
793 ret
= dbp
[cdb
]->cursor(dbp
[cdb
], MYTID
, &MYCURSORS
[cdb
], 0);
795 CtdlLogPrintf(CTDL_EMERG
, "cdb_rewind: db_cursor: %s\n",
803 * Fetch the next item in a sequential search. Returns a pointer to a
804 * cdbdata structure, or NULL if we've hit the end.
806 struct cdbdata
*cdb_next_item(int cdb
)
809 struct cdbdata
*cdbret
;
812 /* Initialize the key/data pair so the flags aren't set. */
813 memset(&key
, 0, sizeof(key
));
814 memset(&data
, 0, sizeof(data
));
815 data
.flags
= DB_DBT_MALLOC
;
817 ret
= MYCURSORS
[cdb
]->c_get(MYCURSORS
[cdb
], &key
, &data
, DB_NEXT
);
820 if (ret
!= DB_NOTFOUND
) {
821 CtdlLogPrintf(CTDL_EMERG
, "cdb_next_item(%d): %s\n",
822 cdb
, db_strerror(ret
));
825 cclose(MYCURSORS
[cdb
]);
826 MYCURSORS
[cdb
] = NULL
;
827 return NULL
; /* presumably, end of file */
830 cdbret
= (struct cdbdata
*) malloc(sizeof(struct cdbdata
));
831 cdbret
->len
= data
.size
;
832 cdbret
->ptr
= data
.data
;
833 cdb_decompress_if_necessary(cdbret
);
841 * Transaction-based stuff. I'm writing this as I bake cookies...
844 void cdb_begin_transaction(void)
847 bailIfCursor(MYCURSORS
,
848 "can't begin transaction during r/o cursor");
851 CtdlLogPrintf(CTDL_EMERG
,
852 "cdb_begin_transaction: ERROR: nested transaction\n");
859 void cdb_end_transaction(void)
863 for (i
= 0; i
< MAXCDB
; i
++)
864 if (MYCURSORS
[i
] != NULL
) {
865 CtdlLogPrintf(CTDL_WARNING
,
866 "cdb_end_transaction: WARNING: cursor %d still open at transaction end\n",
868 cclose(MYCURSORS
[i
]);
873 CtdlLogPrintf(CTDL_EMERG
,
874 "cdb_end_transaction: ERROR: txcommit(NULL) !!\n");
883 * Truncate (delete every record)
885 void cdb_trunc(int cdb
)
892 CtdlLogPrintf(CTDL_EMERG
,
893 "cdb_trunc must not be called in a transaction.\n");
896 bailIfCursor(MYCURSORS
,
897 "attempt to write during r/o cursor");
902 if ((ret
= dbp
[cdb
]->truncate(dbp
[cdb
], /* db */
903 NULL
, /* transaction ID */
904 &count
, /* #rows deleted */
906 if (ret
== DB_LOCK_DEADLOCK
) {
910 CtdlLogPrintf(CTDL_EMERG
,
911 "cdb_truncate(%d): %s\n", cdb
,