diff options
Diffstat (limited to 'src/util/dict_lmdb.c')
-rw-r--r-- | src/util/dict_lmdb.c | 702 |
1 files changed, 702 insertions, 0 deletions
diff --git a/src/util/dict_lmdb.c b/src/util/dict_lmdb.c new file mode 100644 index 0000000..f508e70 --- /dev/null +++ b/src/util/dict_lmdb.c @@ -0,0 +1,702 @@ +/*++ +/* NAME +/* dict_lmdb 3 +/* SUMMARY +/* dictionary manager interface to OpenLDAP LMDB files +/* SYNOPSIS +/* #include <dict_lmdb.h> +/* +/* extern size_t dict_lmdb_map_size; +/* +/* DEFINE_DICT_LMDB_MAP_SIZE; +/* +/* DICT *dict_lmdb_open(path, open_flags, dict_flags) +/* const char *name; +/* const char *path; +/* int open_flags; +/* int dict_flags; +/* DESCRIPTION +/* dict_lmdb_open() opens the named LMDB database and makes +/* it available via the generic interface described in +/* dict_open(3). +/* +/* The dict_lmdb_map_size variable specifies the initial +/* database memory map size. When a map becomes full its size +/* is doubled, and other programs pick up the size change. +/* +/* This variable cannot be exported via the dict(3) API and +/* must therefore be defined in the calling program by invoking +/* the DEFINE_DICT_LMDB_MAP_SIZE macro at the global level. +/* DIAGNOSTICS +/* Fatal errors: cannot open file, file write error, out of +/* memory. +/* BUGS +/* The on-the-fly map resize operations require no concurrent +/* activity in the same database by other threads in the same +/* memory address space. +/* SEE ALSO +/* dict(3) generic dictionary manager +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Howard Chu +/* Symas Corporation +/* +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +#include <sys_defs.h> + +#ifdef HAS_LMDB + +/* System library. */ + +#include <sys/stat.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <htable.h> +#include <iostuff.h> +#include <vstring.h> +#include <myflock.h> +#include <stringops.h> +#include <slmdb.h> +#include <dict.h> +#include <dict_lmdb.h> +#include <warn_stat.h> + +/* Application-specific. */ + +typedef struct { + DICT dict; /* generic members */ + SLMDB slmdb; /* sane LMDB API */ + VSTRING *key_buf; /* key buffer */ + VSTRING *val_buf; /* value buffer */ +} DICT_LMDB; + + /* + * The LMDB database filename suffix happens to equal our DICT_TYPE_LMDB + * prefix, but that doesn't mean it is kosher to use DICT_TYPE_LMDB where a + * suffix is needed, so we define an explicit suffix here. + */ +#define DICT_LMDB_SUFFIX "lmdb" + + /* + * Make a safe string copy that is guaranteed to be null-terminated. + */ +#define SCOPY(buf, data, size) \ + vstring_str(vstring_strncpy(buf ? buf : (buf = vstring_alloc(10)), data, size)) + + /* + * Postfix writers recover from a "map full" error by increasing the memory + * map size with a factor DICT_LMDB_SIZE_INCR (up to some limit) and + * retrying the transaction. + * + * Each dict(3) API call is retried no more than a few times. For bulk-mode + * transactions the number of retries is proportional to the size of the + * address space. + * + * We do not expose these details to the Postfix user interface. The purpose of + * Postfix is to solve problems, not punt them to the user. + */ +#define DICT_LMDB_SIZE_INCR 2 /* Increase size by 1 bit on retry */ +#define DICT_LMDB_SIZE_MAX SSIZE_T_MAX + +#define DICT_LMDB_API_RETRY_LIMIT 2 /* Retries per dict(3) API call */ +#define DICT_LMDB_BULK_RETRY_LIMIT \ + ((int) (2 * sizeof(size_t) * CHAR_BIT)) /* Retries per bulk-mode + * transaction */ + +/* #define msg_verbose 1 */ + +/* dict_lmdb_lookup - find database entry */ + +static const char *dict_lmdb_lookup(DICT *dict, const char *name) +{ + DICT_LMDB *dict_lmdb = (DICT_LMDB *) dict; + MDB_val mdb_key; + MDB_val mdb_value; + const char *result = 0; + int status; + ssize_t klen; + + dict->error = 0; + klen = strlen(name); + + /* + * Sanity check. + */ + if ((dict->flags & (DICT_FLAG_TRY1NULL | DICT_FLAG_TRY0NULL)) == 0) + msg_panic("dict_lmdb_lookup: no DICT_FLAG_TRY1NULL | DICT_FLAG_TRY0NULL flag"); + + /* + * Optionally fold the key. + */ + if (dict->flags & DICT_FLAG_FOLD_FIX) { + if (dict->fold_buf == 0) + dict->fold_buf = vstring_alloc(10); + vstring_strcpy(dict->fold_buf, name); + name = lowercase(vstring_str(dict->fold_buf)); + } + + /* + * Acquire a shared lock. + */ + if ((dict->flags & DICT_FLAG_LOCK) + && myflock(dict->lock_fd, MYFLOCK_STYLE_FCNTL, MYFLOCK_OP_SHARED) < 0) + msg_fatal("%s: lock dictionary: %m", dict->name); + + /* + * See if this LMDB file was written with one null byte appended to key + * and value. + */ + if (dict->flags & DICT_FLAG_TRY1NULL) { + mdb_key.mv_data = (void *) name; + mdb_key.mv_size = klen + 1; + status = slmdb_get(&dict_lmdb->slmdb, &mdb_key, &mdb_value); + if (status == 0) { + dict->flags &= ~DICT_FLAG_TRY0NULL; + result = SCOPY(dict_lmdb->val_buf, mdb_value.mv_data, + mdb_value.mv_size); + } else if (status != MDB_NOTFOUND) { + msg_fatal("error reading %s:%s: %s", + dict_lmdb->dict.type, dict_lmdb->dict.name, + mdb_strerror(status)); + } + } + + /* + * See if this LMDB file was written with no null byte appended to key + * and value. + */ + if (result == 0 && (dict->flags & DICT_FLAG_TRY0NULL)) { + mdb_key.mv_data = (void *) name; + mdb_key.mv_size = klen; + status = slmdb_get(&dict_lmdb->slmdb, &mdb_key, &mdb_value); + if (status == 0) { + dict->flags &= ~DICT_FLAG_TRY1NULL; + result = SCOPY(dict_lmdb->val_buf, mdb_value.mv_data, + mdb_value.mv_size); + } else if (status != MDB_NOTFOUND) { + msg_fatal("error reading %s:%s: %s", + dict_lmdb->dict.type, dict_lmdb->dict.name, + mdb_strerror(status)); + } + } + + /* + * Release the shared lock. + */ + if ((dict->flags & DICT_FLAG_LOCK) + && myflock(dict->lock_fd, MYFLOCK_STYLE_FCNTL, MYFLOCK_OP_NONE) < 0) + msg_fatal("%s: unlock dictionary: %m", dict->name); + + return (result); +} + +/* dict_lmdb_update - add or update database entry */ + +static int dict_lmdb_update(DICT *dict, const char *name, const char *value) +{ + DICT_LMDB *dict_lmdb = (DICT_LMDB *) dict; + MDB_val mdb_key; + MDB_val mdb_value; + int status; + + dict->error = 0; + + /* + * Sanity check. + */ + if ((dict->flags & (DICT_FLAG_TRY1NULL | DICT_FLAG_TRY0NULL)) == 0) + msg_panic("dict_lmdb_update: no DICT_FLAG_TRY1NULL | DICT_FLAG_TRY0NULL flag"); + + /* + * Optionally fold the key. + */ + if (dict->flags & DICT_FLAG_FOLD_FIX) { + if (dict->fold_buf == 0) + dict->fold_buf = vstring_alloc(10); + vstring_strcpy(dict->fold_buf, name); + name = lowercase(vstring_str(dict->fold_buf)); + } + mdb_key.mv_data = (void *) name; + mdb_value.mv_data = (void *) value; + mdb_key.mv_size = strlen(name); + mdb_value.mv_size = strlen(value); + + /* + * If undecided about appending a null byte to key and value, choose a + * default depending on the platform. + */ + if ((dict->flags & DICT_FLAG_TRY1NULL) + && (dict->flags & DICT_FLAG_TRY0NULL)) { +#ifdef LMDB_NO_TRAILING_NULL + dict->flags &= ~DICT_FLAG_TRY1NULL; +#else + dict->flags &= ~DICT_FLAG_TRY0NULL; +#endif + } + + /* + * Optionally append a null byte to key and value. + */ + if (dict->flags & DICT_FLAG_TRY1NULL) { + mdb_key.mv_size++; + mdb_value.mv_size++; + } + + /* + * Acquire an exclusive lock. + */ + if ((dict->flags & DICT_FLAG_LOCK) + && myflock(dict->lock_fd, MYFLOCK_STYLE_FCNTL, MYFLOCK_OP_EXCLUSIVE) < 0) + msg_fatal("%s: lock dictionary: %m", dict->name); + + /* + * Do the update. + */ + status = slmdb_put(&dict_lmdb->slmdb, &mdb_key, &mdb_value, + (dict->flags & DICT_FLAG_DUP_REPLACE) ? 0 : MDB_NOOVERWRITE); + if (status != 0) { + if (status == MDB_KEYEXIST) { + if (dict->flags & DICT_FLAG_DUP_IGNORE) + /* void */ ; + else if (dict->flags & DICT_FLAG_DUP_WARN) + msg_warn("%s:%s: duplicate entry: \"%s\"", + dict_lmdb->dict.type, dict_lmdb->dict.name, name); + else + msg_fatal("%s:%s: duplicate entry: \"%s\"", + dict_lmdb->dict.type, dict_lmdb->dict.name, name); + } else { + msg_fatal("error updating %s:%s: %s", + dict_lmdb->dict.type, dict_lmdb->dict.name, + mdb_strerror(status)); + } + } + + /* + * Release the exclusive lock. + */ + if ((dict->flags & DICT_FLAG_LOCK) + && myflock(dict->lock_fd, MYFLOCK_STYLE_FCNTL, MYFLOCK_OP_NONE) < 0) + msg_fatal("%s: unlock dictionary: %m", dict->name); + + return (status); +} + +/* dict_lmdb_delete - delete one entry from the dictionary */ + +static int dict_lmdb_delete(DICT *dict, const char *name) +{ + DICT_LMDB *dict_lmdb = (DICT_LMDB *) dict; + MDB_val mdb_key; + int status = 1; + ssize_t klen; + + dict->error = 0; + klen = strlen(name); + + /* + * Sanity check. + */ + if ((dict->flags & (DICT_FLAG_TRY1NULL | DICT_FLAG_TRY0NULL)) == 0) + msg_panic("dict_lmdb_delete: no DICT_FLAG_TRY1NULL | DICT_FLAG_TRY0NULL flag"); + + /* + * Optionally fold the key. + */ + if (dict->flags & DICT_FLAG_FOLD_FIX) { + if (dict->fold_buf == 0) + dict->fold_buf = vstring_alloc(10); + vstring_strcpy(dict->fold_buf, name); + name = lowercase(vstring_str(dict->fold_buf)); + } + + /* + * Acquire an exclusive lock. + */ + if ((dict->flags & DICT_FLAG_LOCK) + && myflock(dict->lock_fd, MYFLOCK_STYLE_FCNTL, MYFLOCK_OP_EXCLUSIVE) < 0) + msg_fatal("%s: lock dictionary: %m", dict->name); + + /* + * See if this LMDB file was written with one null byte appended to key + * and value. + */ + if (dict->flags & DICT_FLAG_TRY1NULL) { + mdb_key.mv_data = (void *) name; + mdb_key.mv_size = klen + 1; + status = slmdb_del(&dict_lmdb->slmdb, &mdb_key); + if (status != 0) { + if (status == MDB_NOTFOUND) + status = 1; + else + msg_fatal("error deleting from %s:%s: %s", + dict_lmdb->dict.type, dict_lmdb->dict.name, + mdb_strerror(status)); + } else { + dict->flags &= ~DICT_FLAG_TRY0NULL; /* found */ + } + } + + /* + * See if this LMDB file was written with no null byte appended to key + * and value. + */ + if (status > 0 && (dict->flags & DICT_FLAG_TRY0NULL)) { + mdb_key.mv_data = (void *) name; + mdb_key.mv_size = klen; + status = slmdb_del(&dict_lmdb->slmdb, &mdb_key); + if (status != 0) { + if (status == MDB_NOTFOUND) + status = 1; + else + msg_fatal("error deleting from %s:%s: %s", + dict_lmdb->dict.type, dict_lmdb->dict.name, + mdb_strerror(status)); + } else { + dict->flags &= ~DICT_FLAG_TRY1NULL; /* found */ + } + } + + /* + * Release the exclusive lock. + */ + if ((dict->flags & DICT_FLAG_LOCK) + && myflock(dict->lock_fd, MYFLOCK_STYLE_FCNTL, MYFLOCK_OP_NONE) < 0) + msg_fatal("%s: unlock dictionary: %m", dict->name); + + return (status); +} + +/* dict_lmdb_sequence - traverse the dictionary */ + +static int dict_lmdb_sequence(DICT *dict, int function, + const char **key, const char **value) +{ + const char *myname = "dict_lmdb_sequence"; + DICT_LMDB *dict_lmdb = (DICT_LMDB *) dict; + MDB_val mdb_key; + MDB_val mdb_value; + MDB_cursor_op op; + int status; + + dict->error = 0; + + /* + * Determine the seek function. + */ + switch (function) { + case DICT_SEQ_FUN_FIRST: + op = MDB_FIRST; + break; + case DICT_SEQ_FUN_NEXT: + op = MDB_NEXT; + break; + default: + msg_panic("%s: invalid function: %d", myname, function); + } + + /* + * Acquire a shared lock. + */ + if ((dict->flags & DICT_FLAG_LOCK) + && myflock(dict->lock_fd, MYFLOCK_STYLE_FCNTL, MYFLOCK_OP_SHARED) < 0) + msg_fatal("%s: lock dictionary: %m", dict->name); + + /* + * Database lookup. + */ + status = slmdb_cursor_get(&dict_lmdb->slmdb, &mdb_key, &mdb_value, op); + + switch (status) { + + /* + * Copy the key and value so they are guaranteed null terminated. + */ + case 0: + *key = SCOPY(dict_lmdb->key_buf, mdb_key.mv_data, mdb_key.mv_size); + if (mdb_value.mv_data != 0 && mdb_value.mv_size > 0) + *value = SCOPY(dict_lmdb->val_buf, mdb_value.mv_data, + mdb_value.mv_size); + else + *value = ""; /* XXX */ + break; + + /* + * End-of-database. + */ + case MDB_NOTFOUND: + status = 1; + /* Not: mdb_cursor_close(). Wrong abstraction level. */ + break; + + /* + * Bust. + */ + default: + msg_fatal("error seeking %s:%s: %s", + dict_lmdb->dict.type, dict_lmdb->dict.name, + mdb_strerror(status)); + } + + /* + * Release the shared lock. + */ + if ((dict->flags & DICT_FLAG_LOCK) + && myflock(dict->lock_fd, MYFLOCK_STYLE_FCNTL, MYFLOCK_OP_NONE) < 0) + msg_fatal("%s: unlock dictionary: %m", dict->name); + + return (status); +} + +/* dict_lmdb_close - disassociate from data base */ + +static void dict_lmdb_close(DICT *dict) +{ + DICT_LMDB *dict_lmdb = (DICT_LMDB *) dict; + + slmdb_close(&dict_lmdb->slmdb); + if (dict_lmdb->key_buf) + vstring_free(dict_lmdb->key_buf); + if (dict_lmdb->val_buf) + vstring_free(dict_lmdb->val_buf); + if (dict->fold_buf) + vstring_free(dict->fold_buf); + dict_free(dict); +} + +/* dict_lmdb_longjmp - repeat bulk transaction */ + +static void dict_lmdb_longjmp(void *context, int val) +{ + DICT_LMDB *dict_lmdb = (DICT_LMDB *) context; + + dict_longjmp(&dict_lmdb->dict, val); +} + +/* dict_lmdb_notify - debug logging */ + +static void dict_lmdb_notify(void *context, int error_code,...) +{ + DICT_LMDB *dict_lmdb = (DICT_LMDB *) context; + va_list ap; + + va_start(ap, error_code); + switch (error_code) { + case MDB_SUCCESS: + msg_info("database %s:%s: using size limit %lu during open", + dict_lmdb->dict.type, dict_lmdb->dict.name, + (unsigned long) va_arg(ap, size_t)); + break; + case MDB_MAP_FULL: + msg_info("database %s:%s: using size limit %lu after MDB_MAP_FULL", + dict_lmdb->dict.type, dict_lmdb->dict.name, + (unsigned long) va_arg(ap, size_t)); + break; + case MDB_MAP_RESIZED: + msg_info("database %s:%s: using size limit %lu after MDB_MAP_RESIZED", + dict_lmdb->dict.type, dict_lmdb->dict.name, + (unsigned long) va_arg(ap, size_t)); + break; + case MDB_READERS_FULL: + msg_info("database %s:%s: pausing after MDB_READERS_FULL", + dict_lmdb->dict.type, dict_lmdb->dict.name); + break; + default: + msg_warn("unknown MDB error code: %d", error_code); + break; + } + va_end(ap); +} + +/* dict_lmdb_assert - report LMDB internal assertion failure */ + +static void dict_lmdb_assert(void *context, const char *text) +{ + DICT_LMDB *dict_lmdb = (DICT_LMDB *) context; + + msg_fatal("%s:%s: internal error: %s", + dict_lmdb->dict.type, dict_lmdb->dict.name, text); +} + +/* dict_lmdb_open - open LMDB data base */ + +DICT *dict_lmdb_open(const char *path, int open_flags, int dict_flags) +{ + DICT_LMDB *dict_lmdb; + DICT *dict; + struct stat st; + SLMDB slmdb; + char *mdb_path; + int mdb_flags, slmdb_flags, status; + int db_fd; + + /* + * Let the optimizer worry about eliminating redundant code. + */ +#define DICT_LMDB_OPEN_RETURN(d) do { \ + DICT *__d = (d); \ + myfree(mdb_path); \ + return (__d); \ + } while (0) + + mdb_path = concatenate(path, "." DICT_TYPE_LMDB, (char *) 0); + + /* + * Impedance adapters. + */ + mdb_flags = MDB_NOSUBDIR | MDB_NOLOCK; + if (open_flags == O_RDONLY) + mdb_flags |= MDB_RDONLY; + + slmdb_flags = 0; + if (dict_flags & DICT_FLAG_BULK_UPDATE) + slmdb_flags |= SLMDB_FLAG_BULK; + + /* + * Security violation. + * + * By default, LMDB 0.9.9 writes uninitialized heap memory to a + * world-readable database file, as chunks of up to 4096 bytes. This is a + * huge memory disclosure vulnerability: memory content that a program + * does not intend to share ends up in a world-readable file. The content + * of uninitialized heap memory depends on program execution history. + * That history includes code execution in other libraries that are + * linked into the program. + * + * This is a problem whenever the user who writes the database file differs + * from the user who reads the database file. For example, a privileged + * writer and an unprivileged reader. In the case of Postfix, the + * postmap(1) and postalias(1) commands would leak uninitialized heap + * memory, as chunks of up to 4096 bytes, from a root-privileged process + * that writes to a database file, to unprivileged processes that read + * from that database file. + * + * As a workaround the postmap(1) and postalias(1) commands turn on + * MDB_WRITEMAP which disables the use of malloc() in LMDB. However, that + * does not address several disclosures of stack memory. We don't enable + * this workaround for Postfix databases are maintained by Postfix daemon + * processes, because those are accessible only by the postfix user. + * + * LMDB 0.9.10 by default does not write uninitialized heap memory to file + * (specify MDB_NOMEMINIT to revert that change). We use the MDB_WRITEMAP + * workaround for older LMDB versions. + */ +#ifndef MDB_NOMEMINIT + if (dict_flags & DICT_FLAG_BULK_UPDATE) /* XXX Good enough */ + mdb_flags |= MDB_WRITEMAP; +#endif + + /* + * Gracefully handle most database open errors. + */ + if ((status = slmdb_init(&slmdb, dict_lmdb_map_size, DICT_LMDB_SIZE_INCR, + DICT_LMDB_SIZE_MAX)) != 0 + || (status = slmdb_open(&slmdb, mdb_path, open_flags, mdb_flags, + slmdb_flags)) != 0) { + /* This leaks a little memory that would have been used otherwise. */ + dict = dict_surrogate(DICT_TYPE_LMDB, path, open_flags, dict_flags, + "open database %s: %s", mdb_path, mdb_strerror(status)); + DICT_LMDB_OPEN_RETURN(dict); + } + + /* + * XXX Persistent locking belongs in mkmap_lmdb. + * + * We just need to acquire exclusive access momentarily. This establishes + * that no readers are accessing old (obsoleted by copy-on-write) txn + * snapshots, so we are free to reuse all eligible old pages. Downgrade + * the lock right after acquiring it. This is sufficient to keep out + * other writers until we are done. + */ + db_fd = slmdb_fd(&slmdb); + if (dict_flags & DICT_FLAG_BULK_UPDATE) { + if (myflock(db_fd, MYFLOCK_STYLE_FCNTL, MYFLOCK_OP_EXCLUSIVE) < 0) + msg_fatal("%s: lock dictionary: %m", mdb_path); + if (myflock(db_fd, MYFLOCK_STYLE_FCNTL, MYFLOCK_OP_SHARED) < 0) + msg_fatal("%s: unlock dictionary: %m", mdb_path); + } + + /* + * Bundle up. From here on no more assignments to slmdb. + */ + dict_lmdb = (DICT_LMDB *) dict_alloc(DICT_TYPE_LMDB, path, sizeof(*dict_lmdb)); + dict_lmdb->slmdb = slmdb; + dict_lmdb->dict.lookup = dict_lmdb_lookup; + dict_lmdb->dict.update = dict_lmdb_update; + dict_lmdb->dict.delete = dict_lmdb_delete; + dict_lmdb->dict.sequence = dict_lmdb_sequence; + dict_lmdb->dict.close = dict_lmdb_close; + + if (fstat(db_fd, &st) < 0) + msg_fatal("dict_lmdb_open: fstat: %m"); + dict_lmdb->dict.lock_fd = dict_lmdb->dict.stat_fd = db_fd; + dict_lmdb->dict.lock_type = MYFLOCK_STYLE_FCNTL; + dict_lmdb->dict.mtime = st.st_mtime; + dict_lmdb->dict.owner.uid = st.st_uid; + dict_lmdb->dict.owner.status = (st.st_uid != 0); + + dict_lmdb->key_buf = 0; + dict_lmdb->val_buf = 0; + + /* + * Warn if the source file is newer than the indexed file, except when + * the source file changed only seconds ago. + */ + if ((dict_flags & DICT_FLAG_LOCK) != 0 + && stat(path, &st) == 0 + && st.st_mtime > dict_lmdb->dict.mtime + && st.st_mtime < time((time_t *) 0) - 100) + msg_warn("database %s is older than source file %s", mdb_path, path); + +#define DICT_LMDB_IMPL_FLAGS (DICT_FLAG_FIXED | DICT_FLAG_MULTI_WRITER) + + dict_lmdb->dict.flags = dict_flags | DICT_LMDB_IMPL_FLAGS; + if ((dict_flags & (DICT_FLAG_TRY0NULL | DICT_FLAG_TRY1NULL)) == 0) + dict_lmdb->dict.flags |= (DICT_FLAG_TRY0NULL | DICT_FLAG_TRY1NULL); + if (dict_flags & DICT_FLAG_FOLD_FIX) + dict_lmdb->dict.fold_buf = vstring_alloc(10); + + if (dict_flags & DICT_FLAG_BULK_UPDATE) + dict_jmp_alloc(&dict_lmdb->dict); + + /* + * The following requests return an error result only if we have serious + * memory corruption problem. + */ + if (slmdb_control(&dict_lmdb->slmdb, + CA_SLMDB_CTL_API_RETRY_LIMIT(DICT_LMDB_API_RETRY_LIMIT), + CA_SLMDB_CTL_BULK_RETRY_LIMIT(DICT_LMDB_BULK_RETRY_LIMIT), + CA_SLMDB_CTL_LONGJMP_FN(dict_lmdb_longjmp), + CA_SLMDB_CTL_NOTIFY_FN(msg_verbose ? + dict_lmdb_notify : (SLMDB_NOTIFY_FN) 0), + CA_SLMDB_CTL_ASSERT_FN(dict_lmdb_assert), + CA_SLMDB_CTL_CB_CONTEXT((void *) dict_lmdb), + CA_SLMDB_CTL_END) != 0) + msg_panic("dict_lmdb_open: slmdb_control: %m"); + + if (msg_verbose) + dict_lmdb_notify((void *) dict_lmdb, MDB_SUCCESS, + slmdb_curr_limit(&dict_lmdb->slmdb)); + + DICT_LMDB_OPEN_RETURN(DICT_DEBUG (&dict_lmdb->dict)); +} + +#endif |