/*++
/* NAME
/*	slmdb 3
/* SUMMARY
/*	Simplified LMDB API
/* SYNOPSIS
/*	#include <slmdb.h>
/*
/*	int	slmdb_init(slmdb, curr_limit, size_incr, hard_limit)
/*	SLMDB	*slmdb;
/*	size_t	curr_limit;
/*	int	size_incr;
/*	size_t	hard_limit;
/*
/*	int	slmdb_open(slmdb, path, open_flags, lmdb_flags, slmdb_flags)
/*	SLMDB	*slmdb;
/*	const char *path;
/*	int	open_flags;
/*	int	lmdb_flags;
/*	int	slmdb_flags;
/*
/*	int	slmdb_close(slmdb)
/*	SLMDB	*slmdb;
/*
/*	int	slmdb_get(slmdb, mdb_key, mdb_value)
/*	SLMDB	*slmdb;
/*	MDB_val	*mdb_key;
/*	MDB_val	*mdb_value;
/*
/*	int	slmdb_put(slmdb, mdb_key, mdb_value, flags)
/*	SLMDB	*slmdb;
/*	MDB_val	*mdb_key;
/*	MDB_val	*mdb_value;
/*	int	flags;
/*
/*	int	slmdb_del(slmdb, mdb_key)
/*	SLMDB	*slmdb;
/*	MDB_val	*mdb_key;
/*
/*	int	slmdb_cursor_get(slmdb, mdb_key, mdb_value, op)
/*	SLMDB	*slmdb;
/*	MDB_val	*mdb_key;
/*	MDB_val	*mdb_value;
/*	MDB_cursor_op op;
/* AUXILIARY FUNCTIONS
/*	int	slmdb_fd(slmdb)
/*	SLMDB	*slmdb;
/*
/*	size_t	slmdb_curr_limit(slmdb)
/*	SLMDB	*slmdb;
/*
/*	int	slmdb_control(slmdb, request, ...)
/*	SLMDB	*slmdb;
/*	int	request;
/* DESCRIPTION
/*	This module simplifies the LMDB API by hiding recoverable
/*	errors from the application.  Details are given in the
/*	section "ERROR RECOVERY".
/*
/*	slmdb_init() performs mandatory initialization before opening
/*	an LMDB database. The result value is an LMDB status code
/*	(zero in case of success).
/*
/*	slmdb_open() opens an LMDB database.  The result value is
/*	an LMDB status code (zero in case of success).
/*
/*	slmdb_close() finalizes an optional bulk-mode transaction
/*	and closes a successfully-opened LMDB database.  The result
/*	value is an LMDB status code (zero in case of success).
/*
/*	slmdb_get() is an mdb_get() wrapper with automatic error
/*	recovery.  The result value is an LMDB status code (zero
/*	in case of success).
/*
/*	slmdb_put() is an mdb_put() wrapper with automatic error
/*	recovery.  The result value is an LMDB status code (zero
/*	in case of success).
/*
/*	slmdb_del() is an mdb_del() wrapper with automatic error
/*	recovery.  The result value is an LMDB status code (zero
/*	in case of success).
/*
/*	slmdb_cursor_get() is an mdb_cursor_get() wrapper with
/*	automatic error recovery.  The result value is an LMDB
/*	status code (zero in case of success). This wrapper supports
/*	only one cursor per database.
/*
/*	slmdb_fd() returns the file descriptor for the specified
/*	database.  This may be used for file status queries or
/*	application-controlled locking.
/*
/*	slmdb_curr_limit() returns the current database size limit
/*	for the specified database.
/*
/*	slmdb_control() specifies optional features. The result is
/*	an LMDB status code (zero in case of success).
/*
/*	Arguments:
/* .IP slmdb
/*	Pointer to caller-provided storage.
/* .IP curr_limit
/*	The initial memory mapping size limit. This limit is
/*	automatically increased when the database becomes full.
/* .IP size_incr
/*	An integer factor by which the memory mapping size limit
/*	is increased when the database becomes full.
/* .IP hard_limit
/*	The upper bound for the memory mapping size limit.
/* .IP path
/*	LMDB database pathname.
/* .IP open_flags
/*	Flags that control file open operations. Do not specify
/*	locking flags here.
/* .IP lmdb_flags
/*	Flags that control the LMDB environment. If MDB_NOLOCK is
/*	specified, then each slmdb_get() or slmdb_cursor_get() call
/*	must be protected with a shared (or exclusive) external lock,
/*	and each slmdb_put() or slmdb_del() call must be protected
/*	with an exclusive external lock. A lock may be released
/*	after the call returns. A writer may atomically downgrade
/*	an exclusive lock to shared, but it must obtain an exclusive
/*	lock before making another slmdb(3) write request.
/* .sp
/*	Note: when a database is opened with MDB_NOLOCK, external
/*	locks such as fcntl() do not protect slmdb(3) requests
/*	within the same process against each other.  If a program
/*	cannot avoid making simultaneous slmdb(3) requests, then
/*	it must synchronize these requests with in-process locks,
/*	in addition to the per-process fcntl(2) locks.
/* .IP slmdb_flags
/*	Bit-wise OR of zero or more of the following:
/* .RS
/* .IP SLMDB_FLAG_BULK
/*	Open the database and create a "bulk" transaction that is
/*	committed when the database is closed. If MDB_NOLOCK is
/*	specified, then the entire transaction must be protected
/*	with a persistent external lock.  All slmdb_get(), slmdb_put()
/*	and slmdb_del() requests will be directed to the "bulk"
/*	transaction.
/* .RE
/* .IP mdb_key
/*	Pointer to caller-provided lookup key storage.
/* .IP mdb_value
/*	Pointer to caller-provided value storage.
/* .IP op
/*	LMDB cursor operation.
/* .IP request
/*	The start of a list of (name, value) pairs, terminated with
/*	CA_SLMDB_CTL_END.  The following text enumerates the symbolic
/*	request names and the corresponding argument types.
/* .RS
/* .IP "CA_SLMDB_CTL_LONGJMP_FN(void (*)(void *, int))"
/*	Call-back function pointer. The function is called to repeat
/*	a failed bulk-mode transaction from the start. The arguments
/*	are the application context and the setjmp() or sigsetjmp()
/*	result value.
/* .IP "CA_SLMDB_CTL_NOTIFY_FN(void (*)(void *, int, ...))"
/*	Call-back function pointer. The function is called to report
/*	successful error recovery. The arguments are the application
/*	context, the MDB error code, and additional arguments that
/*	depend on the error code.  Details are given in the section
/*	"ERROR RECOVERY".
/* .IP "CA_SLMDB_CTL_ASSERT_FN(void (*)(void *, const char *))"
/*	Call-back function pointer.  The function is called to
/*	report an LMDB internal assertion failure. The arguments
/*	are the application context, and text that describes the
/*	problem.
/* .IP "CA_SLMDB_CTL_CB_CONTEXT(void *)"
/*	Application context that is passed in call-back function
/*	calls.
/* .IP "CA_SLMDB_CTL_API_RETRY_LIMIT(int)"
/*	How many times to recover from LMDB errors within the
/*	execution of a single slmdb(3) API call before giving up.
/* .IP "CA_SLMDB_CTL_BULK_RETRY_LIMIT(int)"
/*	How many times to recover from a bulk-mode transaction
/*	before giving up.
/* .RE
/* ERROR RECOVERY
/* .ad
/* .fi
/*	This module automatically repeats failed requests after
/*	recoverable errors, up to the limits specified with
/*	slmdb_control().
/*
/*	Recoverable errors are reported through an optional
/*	notification function specified with slmdb_control().  With
/*	recoverable MDB_MAP_FULL and MDB_MAP_RESIZED errors, the
/*	additional argument is a size_t value with the updated
/*	current database size limit; with recoverable MDB_READERS_FULL
/*	errors there is no additional argument.
/* BUGS
/*	Recovery from MDB_MAP_FULL involves resizing the database
/*	memory mapping.  According to LMDB documentation this
/*	requires that there is no concurrent activity in the same
/*	database by other threads in the same memory address space.
/* SEE ALSO
/*	lmdb(3) API manpage (currently, non-existent).
/* 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
/*--*/

 /*
  * DO NOT include other Postfix-specific header files. This LMDB wrapper
  * must be usable outside Postfix.
  */

#ifdef HAS_LMDB

/* System library. */

#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>

/* Application-specific. */

#include <slmdb.h>

 /*
  * Minimum LMDB patchlevel.
  * 
  * LMDB 0.9.11 allows Postfix daemons to log an LMDB error message instead of
  * falling out of the sky without any explanation. Without such logging,
  * Postfix with LMDB would be too hard to support.
  * 
  * LMDB 0.9.10 fixes an information leak where LMDB wrote chunks of up to 4096
  * bytes of uninitialized heap memory to a database. This was a security
  * violation because it made information persistent that was not meant to be
  * persisted, or it was sharing information that was not meant to be shared.
  * 
  * LMDB 0.9.9 allows Postfix to use external (fcntl()-based) locks, instead of
  * having to use world-writable LMDB lock files.
  * 
  * LMDB 0.9.8 allows Postfix to update the database size limit on-the-fly, so
  * that it can recover from an MDB_MAP_FULL error without having to close
  * the database. It also allows an application to "pick up" a new database
  * size limit on-the-fly, so that it can recover from an MDB_MAP_RESIZED
  * error without having to close the database.
  * 
  * The database size limit that remains is imposed by the hardware memory
  * address space (31 or 47 bits, typically) or file system. The LMDB
  * implementation is supposed to handle databases larger than physical
  * memory. However, this is not necessarily guaranteed for (bulk)
  * transactions larger than physical memory.
  */
#if MDB_VERSION_FULL < MDB_VERINT(0, 9, 11)
#error "This Postfix version requires LMDB version 0.9.11 or later"
#endif

 /*
  * Error recovery.
  * 
  * The purpose of the slmdb(3) API is to hide LMDB quirks (recoverable
  * MAP_FULL, MAP_RESIZED, or MDB_READERS_FULL errors). With these out of the
  * way, applications can pretend that those quirks don't exist, and focus on
  * their own job.
  * 
  * - To recover from a single-transaction LMDB error, each wrapper function
  * uses tail recursion instead of goto. Since LMDB errors are rare, code
  * clarity is more important than speed.
  * 
  * - To recover from a bulk-transaction LMDB error, the error-recovery code
  * triggers a long jump back into the caller to some pre-arranged point (the
  * closest thing that C has to exception handling). The application is then
  * expected to repeat the bulk transaction from scratch.
  * 
  * When any code aborts a bulk transaction, it must reset slmdb->txn to null to
  * avoid a use-after-free problem in slmdb_close().
  */

 /*
  * Our default retry attempt limits. We allow a few retries per slmdb(3) API
  * call for non-bulk transactions. We allow a number of bulk-transaction
  * retries that is proportional to the memory address space.
  */
#define SLMDB_DEF_API_RETRY_LIMIT 30	/* Retries per slmdb(3) API call */
#define SLMDB_DEF_BULK_RETRY_LIMIT \
        (2 * sizeof(size_t) * CHAR_BIT)	/* Retries per bulk-mode transaction */

 /*
  * We increment the recursion counter each time we try to recover from
  * error, and reset the recursion counter when returning to the application
  * from the slmdb(3) API.
  */
#define SLMDB_API_RETURN(slmdb, status) do { \
	(slmdb)->api_retry_count = 0; \
	return (status); \
    } while (0)

 /*
  * With MDB_NOLOCK, the application uses an external lock for inter-process
  * synchronization. Because the caller may release the external lock after
  * an SLMDB API call, each SLMDB API function must use a short-lived
  * transaction unless the transaction is a bulk-mode transaction.
  */

/* slmdb_cursor_close - close cursor and its read transaction */

static void slmdb_cursor_close(SLMDB *slmdb)
{
    MDB_txn *txn;

    /*
     * Close the cursor and its read transaction. We can restore it later
     * from the saved key information.
     */
    txn = mdb_cursor_txn(slmdb->cursor);
    mdb_cursor_close(slmdb->cursor);
    slmdb->cursor = 0;
    mdb_txn_abort(txn);
}

/* slmdb_saved_key_init - initialize saved key info */

static void slmdb_saved_key_init(SLMDB *slmdb)
{
    slmdb->saved_key.mv_data = 0;
    slmdb->saved_key.mv_size = 0;
    slmdb->saved_key_size = 0;
}

/* slmdb_saved_key_free - destroy saved key info */

static void slmdb_saved_key_free(SLMDB *slmdb)
{
    free(slmdb->saved_key.mv_data);
    slmdb_saved_key_init(slmdb);
}

#define HAVE_SLMDB_SAVED_KEY(s) ((s)->saved_key.mv_data != 0)

/* slmdb_saved_key_assign - copy the saved key */

static int slmdb_saved_key_assign(SLMDB *slmdb, MDB_val *key_val)
{

    /*
     * Extend the buffer to fit the key, so that we can avoid malloc()
     * overhead most of the time.
     */
    if (slmdb->saved_key_size < key_val->mv_size) {
	if (slmdb->saved_key.mv_data == 0)
	    slmdb->saved_key.mv_data = malloc(key_val->mv_size);
	else
	    slmdb->saved_key.mv_data =
		realloc(slmdb->saved_key.mv_data, key_val->mv_size);
	if (slmdb->saved_key.mv_data == 0) {
	    slmdb_saved_key_init(slmdb);
	    return (ENOMEM);
	} else {
	    slmdb->saved_key_size = key_val->mv_size;
	}
    }

    /*
     * Copy the key under the cursor.
     */
    memcpy(slmdb->saved_key.mv_data, key_val->mv_data, key_val->mv_size);
    slmdb->saved_key.mv_size = key_val->mv_size;
    return (0);
}

/* slmdb_prepare - LMDB-specific (re)initialization before actual access */

static int slmdb_prepare(SLMDB *slmdb)
{
    int status = 0;

    if (slmdb->open_flags & O_TRUNC) {

        /* Drop the main DB — this dirties the transaction */
        status = mdb_drop(slmdb->txn, slmdb->dbi, 0);

        /* Regardless of success, this txn MUST be aborted */
        mdb_txn_abort(slmdb->txn);
        slmdb->txn = 0;

        if (status != 0)
            return status;

        /*
         * Now reopen the main DB in a fresh transaction.
         * This replaces the old DBI handle.
         */
        MDB_txn *txn2;
        status = mdb_txn_begin(slmdb->env, NULL, 0, &txn2);
        if (status != 0)
            return status;

        status = mdb_dbi_open(txn2, NULL, 0, &slmdb->dbi);
        if (status != 0) {
            mdb_txn_abort(txn2);
            return status;
        }

        /* Commit the DBI re-open unless bulk mode defers it */
        if ((slmdb->slmdb_flags & SLMDB_FLAG_BULK) == 0) {
            status = mdb_txn_commit(txn2);
            if (status != 0)
                return status;
        } else {
            slmdb->txn = txn2;  /* bulk mode keeps txn open */
        }

    } else if ((slmdb->slmdb_flags & SLMDB_FLAG_BULK) == 0) {
        /* Normal case: abort the open transaction */
        mdb_txn_abort(slmdb->txn);
        slmdb->txn = 0;
    }

    slmdb->api_retry_count = 0;
    return status;
}


/* slmdb_recover - recover from LMDB errors */

static int slmdb_recover(SLMDB *slmdb, int status)
{
    MDB_envinfo info;
    int     original_status = status;

    /*
     * This may be needed in non-MDB_NOLOCK mode. Recovery is rare enough
     * that we don't care about a few wasted cycles.
     */
    if (slmdb->cursor != 0)
	slmdb_cursor_close(slmdb);

    /*
     * Limit the number of recovery attempts per slmdb(3) API request.
     */
    if ((slmdb->api_retry_count += 1) >= slmdb->api_retry_limit)
	return (status);

    /*
     * Limit the number of bulk transaction recovery attempts.
     */
    if ((slmdb->slmdb_flags & SLMDB_FLAG_BULK) != 0
	&& (slmdb->bulk_retry_count += 1) > slmdb->bulk_retry_limit)
	return (status);

    /*
     * Try to clear the error condition.
     */
    switch (status) {

	/*
	 * As of LMDB 0.9.8 when a non-bulk update runs into a "map full"
	 * error, we can resize the environment's memory map and clear the
	 * error condition. The caller should retry immediately.
	 */
    case MDB_MAP_FULL:
	/* Can we increase the memory map? Give up if we can't. */
	if (slmdb->curr_limit < slmdb->hard_limit / slmdb->size_incr) {
	    slmdb->curr_limit = slmdb->curr_limit * slmdb->size_incr;
	} else if (slmdb->curr_limit < slmdb->hard_limit) {
	    slmdb->curr_limit = slmdb->hard_limit;
	} else {
	    /* Sorry, we are already maxed out. */
	    break;
	}
	if (slmdb->notify_fn)
	    slmdb->notify_fn(slmdb->cb_context, MDB_MAP_FULL,
			     slmdb->curr_limit);
	status = mdb_env_set_mapsize(slmdb->env, slmdb->curr_limit);
	break;

	/*
	 * When a writer resizes the database, read-only applications must
	 * increase their LMDB memory map size limit, too. Otherwise, they
	 * won't be able to read a table after it grows.
	 * 
	 * As of LMDB 0.9.8 we can import the new memory map size limit into the
	 * database environment by calling mdb_env_set_mapsize() with a zero
	 * size argument. Then we extract the map size limit for later use.
	 * The caller should retry immediately.
	 */
    case MDB_MAP_RESIZED:
	if ((status = mdb_env_set_mapsize(slmdb->env, 0)) == 0) {
	    /* Do not panic. Maps may shrink after bulk update. */
	    mdb_env_info(slmdb->env, &info);
	    slmdb->curr_limit = info.me_mapsize;
	    if (slmdb->notify_fn)
		slmdb->notify_fn(slmdb->cb_context, MDB_MAP_RESIZED,
				 slmdb->curr_limit);
	}
	break;

	/*
	 * What is it with these built-in hard limits that cause systems to
	 * stop when demand is at its highest? When the system is under
	 * stress it should slow down and keep making progress.
	 */
    case MDB_READERS_FULL:
	if (slmdb->notify_fn)
	    slmdb->notify_fn(slmdb->cb_context, MDB_READERS_FULL);
	sleep(1);
	status = 0;
	break;

	/*
	 * We can't solve this problem. The application should terminate with
	 * a fatal run-time error and the program should be re-run later.
	 */
    default:
	break;
    }

    /*
     * If we cleared the error condition for a non-bulk transaction, return a
     * success status. The caller should retry the failed operation
     * immediately.
     */
    if (status == 0 && (slmdb->slmdb_flags & SLMDB_FLAG_BULK) != 0) {

	/*
	 * We cleared the error condition for a	bulk transaction. If the
	 * transaction is not restartable, return the original error. The
	 * caller should terminate with a fatal run-time error, and the
	 * program should be re-run later.
	 */
	if (slmdb->longjmp_fn == 0)
	    return (original_status);

	/*
	 * Rebuild a bulk transaction from scratch, by making a long jump
	 * back into the caller at some pre-arranged point. In MDB_NOLOCK
	 * mode, there is no need to upgrade a lock to "exclusive", because a
	 * failed write transaction has no side effects.
	 */
	if ((status = mdb_txn_begin(slmdb->env, (MDB_txn *) 0,
				    slmdb->lmdb_flags & MDB_RDONLY,
				    &slmdb->txn)) == 0
	    && (status = slmdb_prepare(slmdb)) == 0)
	    slmdb->longjmp_fn(slmdb->cb_context, 1);
    }
    return (status);
}

/* slmdb_txn_begin - mdb_txn_begin() wrapper with LMDB error recovery */

static int slmdb_txn_begin(SLMDB *slmdb, int rdonly, MDB_txn **txn)
{
    int     status;

    if ((status = mdb_txn_begin(slmdb->env, (MDB_txn *) 0, rdonly, txn)) != 0
	&& (status = slmdb_recover(slmdb, status)) == 0)
	status = slmdb_txn_begin(slmdb, rdonly, txn);

    return (status);
}

/* slmdb_get - mdb_get() wrapper with LMDB error recovery */

int     slmdb_get(SLMDB *slmdb, MDB_val *mdb_key, MDB_val *mdb_value)
{
    MDB_txn *txn;
    int     status;

    /*
     * Start a read transaction if there's no bulk-mode txn.
     */
    if (slmdb->txn)
	txn = slmdb->txn;
    else if ((status = slmdb_txn_begin(slmdb, MDB_RDONLY, &txn)) != 0)
	SLMDB_API_RETURN(slmdb, status);

    /*
     * Do the lookup.
     */
    if ((status = mdb_get(txn, slmdb->dbi, mdb_key, mdb_value)) != 0
	&& status != MDB_NOTFOUND) {
	mdb_txn_abort(txn);
	if (txn == slmdb->txn)
	    slmdb->txn = 0;
	if ((status = slmdb_recover(slmdb, status)) == 0)
	    status = slmdb_get(slmdb, mdb_key, mdb_value);
	SLMDB_API_RETURN(slmdb, status);
    }

    /*
     * Close the read txn if it's not the bulk-mode txn.
     */
    if (slmdb->txn == 0)
	mdb_txn_abort(txn);

    SLMDB_API_RETURN(slmdb, status);
}

/* slmdb_put - mdb_put() wrapper with LMDB error recovery */

int     slmdb_put(SLMDB *slmdb, MDB_val *mdb_key,
		          MDB_val *mdb_value, int flags)
{
    MDB_txn *txn;
    int     status;

    /*
     * Start a write transaction if there's no bulk-mode txn.
     */
    if (slmdb->txn)
	txn = slmdb->txn;
    else if ((status = slmdb_txn_begin(slmdb, 0, &txn)) != 0)
	SLMDB_API_RETURN(slmdb, status);

    /*
     * Do the update.
     */
    if ((status = mdb_put(txn, slmdb->dbi, mdb_key, mdb_value, flags)) != 0) {
	if (status != MDB_KEYEXIST) {
	    mdb_txn_abort(txn);
	    if (txn == slmdb->txn)
		slmdb->txn = 0;
	    if ((status = slmdb_recover(slmdb, status)) == 0)
		status = slmdb_put(slmdb, mdb_key, mdb_value, flags);
	    SLMDB_API_RETURN(slmdb, status);
	} else {
	    /* Abort non-bulk transaction only. */
	    if (slmdb->txn == 0)
		mdb_txn_abort(txn);
	}
    }

    /*
     * Commit the transaction if it's not the bulk-mode txn.
     */
    if (status == 0 && slmdb->txn == 0 && (status = mdb_txn_commit(txn)) != 0
	&& (status = slmdb_recover(slmdb, status)) == 0)
	status = slmdb_put(slmdb, mdb_key, mdb_value, flags);

    SLMDB_API_RETURN(slmdb, status);
}

/* slmdb_del - mdb_del() wrapper with LMDB error recovery */

int     slmdb_del(SLMDB *slmdb, MDB_val *mdb_key)
{
    MDB_txn *txn;
    int     status;

    /*
     * Start a write transaction if there's no bulk-mode txn.
     */
    if (slmdb->txn)
	txn = slmdb->txn;
    else if ((status = slmdb_txn_begin(slmdb, 0, &txn)) != 0)
	SLMDB_API_RETURN(slmdb, status);

    /*
     * Do the update.
     */
    if ((status = mdb_del(txn, slmdb->dbi, mdb_key, (MDB_val *) 0)) != 0) {
	if (status != MDB_NOTFOUND) {
	    mdb_txn_abort(txn);
	    if (txn == slmdb->txn)
		slmdb->txn = 0;
	    if ((status = slmdb_recover(slmdb, status)) == 0)
		status = slmdb_del(slmdb, mdb_key);
	    SLMDB_API_RETURN(slmdb, status);
	} else {
	    /* Abort non-bulk transaction only. */
	    if (slmdb->txn == 0)
		mdb_txn_abort(txn);
	}
    }

    /*
     * Commit the transaction if it's not the bulk-mode txn.
     */
    if (status == 0 && slmdb->txn == 0 && (status = mdb_txn_commit(txn)) != 0
	&& (status = slmdb_recover(slmdb, status)) == 0)
	status = slmdb_del(slmdb, mdb_key);

    SLMDB_API_RETURN(slmdb, status);
}

/* slmdb_cursor_get - mdb_cursor_get() wrapper with LMDB error recovery */

int     slmdb_cursor_get(SLMDB *slmdb, MDB_val *mdb_key,
			         MDB_val *mdb_value, MDB_cursor_op op)
{
    MDB_txn *txn;
    int     status = 0;

    /*
     * TODO: figure how we would recover a failing bulk transaction.
     */
    if ((slmdb->slmdb_flags & SLMDB_FLAG_BULK) != 0) {
	if (slmdb->assert_fn)
	    slmdb->assert_fn(slmdb->cb_context,
		     "slmdb_cursor_get: bulk transaction is not supported");
	return (MDB_PANIC);
    }

    /*
     * Open a read transaction and cursor if needed.
     */
    if (slmdb->cursor == 0) {
	if ((status = slmdb_txn_begin(slmdb, MDB_RDONLY, &txn)) != 0)
	    SLMDB_API_RETURN(slmdb, status);
	if ((status = mdb_cursor_open(txn, slmdb->dbi, &slmdb->cursor)) != 0) {
	    mdb_txn_abort(txn);
	    if ((status = slmdb_recover(slmdb, status)) == 0)
		status = slmdb_cursor_get(slmdb, mdb_key, mdb_value, op);
	    SLMDB_API_RETURN(slmdb, status);
	}

	/*
	 * Restore the cursor position from the saved key information.
	 */
	if (HAVE_SLMDB_SAVED_KEY(slmdb) && op != MDB_FIRST)
	    status = mdb_cursor_get(slmdb->cursor, &slmdb->saved_key,
				    (MDB_val *) 0, MDB_SET);
    }

    /*
     * Database lookup.
     */
    if (status == 0)
	status = mdb_cursor_get(slmdb->cursor, mdb_key, mdb_value, op);

    /*
     * Save the cursor position if successful. This can fail only with
     * ENOMEM.
     * 
     * Close the cursor read transaction if in MDB_NOLOCK mode, because the
     * caller may release the external lock after we return.
     */
    if (status == 0) {
	status = slmdb_saved_key_assign(slmdb, mdb_key);
	if (slmdb->lmdb_flags & MDB_NOLOCK)
	    slmdb_cursor_close(slmdb);
    }

    /*
     * Handle end-of-database or other error.
     */
    else {
	/* Do not hand-optimize out the slmdb_cursor_close() calls below. */
	if (status == MDB_NOTFOUND) {
	    slmdb_cursor_close(slmdb);
	    if (HAVE_SLMDB_SAVED_KEY(slmdb))
		slmdb_saved_key_free(slmdb);
	} else {
	    slmdb_cursor_close(slmdb);
	    if ((status = slmdb_recover(slmdb, status)) == 0)
		status = slmdb_cursor_get(slmdb, mdb_key, mdb_value, op);
	    SLMDB_API_RETURN(slmdb, status);
	    /* Do not hand-optimize out the above return statement. */
	}
    }
    SLMDB_API_RETURN(slmdb, status);
}

/* slmdb_assert_cb - report LMDB assertion failure */

static void slmdb_assert_cb(MDB_env *env, const char *text)
{
    SLMDB  *slmdb = (SLMDB *) mdb_env_get_userctx(env);

    if (slmdb->assert_fn)
	slmdb->assert_fn(slmdb->cb_context, text);
}

/* slmdb_control - control optional settings */

int     slmdb_control(SLMDB *slmdb, int first,...)
{
    va_list ap;
    int     status = 0;
    int     reqno;
    int     rc;

    va_start(ap, first);
    for (reqno = first; status == 0 && reqno != SLMDB_CTL_END; reqno = va_arg(ap, int)) {
	switch (reqno) {
	case SLMDB_CTL_LONGJMP_FN:
	    slmdb->longjmp_fn = va_arg(ap, SLMDB_LONGJMP_FN);
	    break;
	case SLMDB_CTL_NOTIFY_FN:
	    slmdb->notify_fn = va_arg(ap, SLMDB_NOTIFY_FN);
	    break;
	case SLMDB_CTL_ASSERT_FN:
	    slmdb->assert_fn = va_arg(ap, SLMDB_ASSERT_FN);
	    if ((rc = mdb_env_set_userctx(slmdb->env, (void *) slmdb)) != 0
	     || (rc = mdb_env_set_assert(slmdb->env, slmdb_assert_cb)) != 0)
		status = rc;
	    break;
	case SLMDB_CTL_CB_CONTEXT:
	    slmdb->cb_context = va_arg(ap, void *);
	    break;
	case SLMDB_CTL_API_RETRY_LIMIT:
	    slmdb->api_retry_limit = va_arg(ap, int);
	    break;
	case SLMDB_CTL_BULK_RETRY_LIMIT:
	    slmdb->bulk_retry_limit = va_arg(ap, int);
	    break;
	default:
	    status = errno = EINVAL;
	    break;
	}
    }
    va_end(ap);
    return (status);
}

/* slmdb_close - wrapper with LMDB error recovery */

int     slmdb_close(SLMDB *slmdb)
{
    int     status = 0;

    /*
     * Finish an open bulk transaction. If slmdb_recover() returns after a
     * bulk-transaction error, then it was unable to clear the error
     * condition, or unable to restart the bulk transaction.
     */
    if ((slmdb->slmdb_flags & SLMDB_FLAG_BULK) != 0 && slmdb->txn != 0
	&& (status = mdb_txn_commit(slmdb->txn)) != 0)
	status = slmdb_recover(slmdb, status);

    /*
     * Clean up after an unfinished sequence() operation.
     */
    if (slmdb->cursor != 0)
	slmdb_cursor_close(slmdb);

    mdb_env_close(slmdb->env);

    /*
     * Clean up the saved key information.
     */
    if (HAVE_SLMDB_SAVED_KEY(slmdb))
	slmdb_saved_key_free(slmdb);

    SLMDB_API_RETURN(slmdb, status);
}

/* slmdb_init - mandatory initialization */

int     slmdb_init(SLMDB *slmdb, size_t curr_limit, int size_incr,
		           size_t hard_limit)
{

    /*
     * This is a separate operation to keep the slmdb_open() API simple.
     * Don't allocate resources here. Just store control information,
     */
    slmdb->curr_limit = curr_limit;
    slmdb->size_incr = size_incr;
    slmdb->hard_limit = hard_limit;

    return (MDB_SUCCESS);
}

/* slmdb_open - open wrapped LMDB database */

int     slmdb_open(SLMDB *slmdb, const char *path, int open_flags,
		           int lmdb_flags, int slmdb_flags)
{
    struct stat st;
    MDB_env *env;
    MDB_txn *txn;
    MDB_dbi dbi;
    int     db_fd;
    int     status;

    /*
     * Create LMDB environment.
     */
    if ((status = mdb_env_create(&env)) != 0)
	return (status);

    /*
     * Make sure that the memory map has room to store and commit an initial
     * "drop" transaction as well as fixed database metadata. We have no way
     * to recover from errors before the first application-level I/O request.
     */
#define SLMDB_FUDGE      10240

    if (slmdb->curr_limit < SLMDB_FUDGE)
	slmdb->curr_limit = SLMDB_FUDGE;
    if (stat(path, &st) == 0
	&& st.st_size > slmdb->curr_limit - SLMDB_FUDGE) {
	if (st.st_size > slmdb->hard_limit)
	    slmdb->hard_limit = st.st_size;
	if (st.st_size < slmdb->hard_limit - SLMDB_FUDGE)
	    slmdb->curr_limit = st.st_size + SLMDB_FUDGE;
	else
	    slmdb->curr_limit = slmdb->hard_limit;
    }

    /*
     * mdb_open() requires a txn, but since the default DB always exists in
     * an LMDB environment, we usually don't need to do anything else with
     * the txn. It is currently used for truncate and for bulk transactions.
     */
    if ((status = mdb_env_set_mapsize(env, slmdb->curr_limit)) != 0
	|| (status = mdb_env_open(env, path, lmdb_flags, 0644)) != 0
	|| (status = mdb_txn_begin(env, (MDB_txn *) 0,
				   lmdb_flags & MDB_RDONLY, &txn)) != 0
	|| (status = mdb_open(txn, (const char *) 0, 0, &dbi)) != 0
	|| (status = mdb_env_get_fd(env, &db_fd)) != 0) {
	mdb_env_close(env);
	return (status);
    }

    /*
     * Bundle up.
     */
    slmdb->open_flags = open_flags;
    slmdb->lmdb_flags = lmdb_flags;
    slmdb->slmdb_flags = slmdb_flags;
    slmdb->env = env;
    slmdb->dbi = dbi;
    slmdb->db_fd = db_fd;
    slmdb->cursor = 0;
    slmdb_saved_key_init(slmdb);
    slmdb->api_retry_count = 0;
    slmdb->bulk_retry_count = 0;
    slmdb->api_retry_limit = SLMDB_DEF_API_RETRY_LIMIT;
    slmdb->bulk_retry_limit = SLMDB_DEF_BULK_RETRY_LIMIT;
    slmdb->longjmp_fn = 0;
    slmdb->notify_fn = 0;
    slmdb->assert_fn = 0;
    slmdb->cb_context = 0;
    slmdb->txn = txn;

    if ((status = slmdb_prepare(slmdb)) != 0)
	mdb_env_close(env);

    return (status);
}

#endif
