/* Copyright (C) CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #include #include #include #include "contrib/cleanup.h" #include "contrib/ucw/lib.h" #include "lib/cache/cdb_lmdb.h" #include "lib/cache/cdb_api.h" #include "lib/utils.h" /* Defines */ #define LMDB_DIR_MODE 0770 #define LMDB_FILE_MODE 0660 /* TODO: we rely on mirrors of these two structs not changing layout * in libknot and knot resolver! */ struct lmdb_env { size_t mapsize; MDB_dbi dbi; MDB_env *env; /** Cached transactions * * - only one of (ro,rw) may be active at once * - non-NULL .ro may be active or reset * - non-NULL .rw is always active */ struct { bool ro_active, ro_curs_active; MDB_txn *ro, *rw; MDB_cursor *ro_curs; } txn; /* Cached part of struct stat for data.mdb. */ dev_t st_dev; ino_t st_ino; off_t st_size; const char *mdb_data_path; /**< path to data.mdb, for convenience */ }; struct libknot_lmdb_env { bool shared; unsigned dbi; void *env; knot_mm_t *pool; }; /** Type-safe conversion helper. * * We keep lmdb_env as a separate type from kr_db_pt, as different implementation of API * would need to define the contents differently. */ static inline struct lmdb_env * db2env(kr_cdb_pt db) { return (struct lmdb_env *)db; } static inline kr_cdb_pt env2db(struct lmdb_env *env) { return (kr_cdb_pt)env; } static int cdb_commit(kr_cdb_pt db, struct kr_cdb_stats *stats); /** @brief Convert LMDB error code. */ static int lmdb_error(int error) { switch (error) { case MDB_SUCCESS: return kr_ok(); case MDB_NOTFOUND: return kr_error(ENOENT); case ENOSPC: case MDB_MAP_FULL: case MDB_TXN_FULL: return kr_error(ENOSPC); default: kr_log_error(CACHE, "LMDB error: %s\n", mdb_strerror(error)); return kr_error(error); } } /** Conversion between knot and lmdb structs for values. */ static inline knot_db_val_t val_mdb2knot(MDB_val v) { return (knot_db_val_t){ .len = v.mv_size, .data = v.mv_data }; } static inline MDB_val val_knot2mdb(knot_db_val_t v) { return (MDB_val){ .mv_size = v.len, .mv_data = v.data }; } /** Refresh mapsize value from file, including env->mapsize. * It's much lighter than reopen_env(). */ static int refresh_mapsize(struct lmdb_env *env) { int ret = cdb_commit(env2db(env), NULL); if (!ret) ret = lmdb_error(mdb_env_set_mapsize(env->env, 0)); if (ret) return ret; MDB_envinfo info; ret = lmdb_error(mdb_env_info(env->env, &info)); if (ret) return ret; env->mapsize = info.me_mapsize; if (env->mapsize != env->st_size) { kr_log_info(CACHE, "suspicious size of cache file '%s'" ": file size %zu != LMDB map size %zu\n", env->mdb_data_path, (size_t)env->st_size, env->mapsize); } return kr_ok(); } static void clear_stale_readers(struct lmdb_env *env) { int cleared; int ret = mdb_reader_check(env->env, &cleared); if (ret != MDB_SUCCESS) { kr_log_error(CACHE, "failed to clear stale reader locks: " "LMDB error %d %s\n", ret, mdb_strerror(ret)); } else if (cleared != 0) { kr_log_info(CACHE, "cleared %d stale reader locks\n", cleared); } } #define FLAG_RENEW (2*MDB_RDONLY) /** mdb_txn_begin or _renew + handle retries in some situations * * The retrying logic is so ugly that it has its own function. * \note this assumes no transactions are active * \return MDB_ errcode, not usual kr_error(...) */ static int txn_get_noresize(struct lmdb_env *env, unsigned int flag, MDB_txn **txn) { if (kr_fails_assert(!env->txn.rw && (!env->txn.ro || !env->txn.ro_active))) return kr_error(1); int attempts = 0; int ret; retry: /* Do a few attempts in case we encounter multiple issues at once. */ if (++attempts > 2) return kr_error(1); if (flag == FLAG_RENEW) { ret = mdb_txn_renew(*txn); } else { ret = mdb_txn_begin(env->env, NULL, flag, txn); } if (unlikely(ret == MDB_MAP_RESIZED)) { kr_log_info(CACHE, "detected size increased by another process\n"); ret = refresh_mapsize(env); if (ret == 0) goto retry; } else if (unlikely(ret == MDB_READERS_FULL)) { clear_stale_readers(env); goto retry; } return ret; } /** Obtain a transaction. (they're cached in env->txn) */ static int txn_get(struct lmdb_env *env, MDB_txn **txn, bool rdonly) { if (kr_fails_assert(env && txn)) return kr_error(EINVAL); if (env->txn.rw) { /* Reuse the *open* RW txn even if only reading is requested. * We leave the management of this to the cdb_commit command. * The user may e.g. want to do some reads between the writes. */ *txn = env->txn.rw; return kr_ok(); } if (!rdonly) { /* avoid two active transactions */ if (env->txn.ro && env->txn.ro_active) { mdb_txn_reset(env->txn.ro); env->txn.ro_active = false; env->txn.ro_curs_active = false; } int ret = txn_get_noresize(env, 0/*RW*/, &env->txn.rw); if (ret == MDB_SUCCESS) { *txn = env->txn.rw; kr_assert(*txn); } return lmdb_error(ret); } /* Get an active RO txn and return it. */ int ret = MDB_SUCCESS; if (!env->txn.ro) { //:unlikely ret = txn_get_noresize(env, MDB_RDONLY, &env->txn.ro); } else if (!env->txn.ro_active) { ret = txn_get_noresize(env, FLAG_RENEW, &env->txn.ro); } if (ret != MDB_SUCCESS) { return lmdb_error(ret); } env->txn.ro_active = true; *txn = env->txn.ro; kr_assert(*txn); return kr_ok(); } static int cdb_commit(kr_cdb_pt db, struct kr_cdb_stats *stats) { struct lmdb_env *env = db2env(db); int ret = kr_ok(); if (env->txn.rw) { if (stats) stats->commit++; ret = lmdb_error(mdb_txn_commit(env->txn.rw)); env->txn.rw = NULL; /* the transaction got freed even in case of errors */ } else if (env->txn.ro && env->txn.ro_active) { mdb_txn_reset(env->txn.ro); env->txn.ro_active = false; env->txn.ro_curs_active = false; } return ret; } /** Obtain a read-only cursor (and a read-only transaction). */ static int txn_curs_get(struct lmdb_env *env, MDB_cursor **curs, struct kr_cdb_stats *stats) { if (kr_fails_assert(env && curs)) return kr_error(EINVAL); if (env->txn.ro_curs_active) goto success; /* Only in a read-only txn; TODO: it's a bit messy/coupled */ if (env->txn.rw) { int ret = cdb_commit(env2db(env), stats); if (ret) return ret; } MDB_txn *txn = NULL; int ret = txn_get(env, &txn, true); if (ret) return ret; if (env->txn.ro_curs) { ret = mdb_cursor_renew(txn, env->txn.ro_curs); } else { ret = mdb_cursor_open(txn, env->dbi, &env->txn.ro_curs); } if (ret) return lmdb_error(ret); env->txn.ro_curs_active = true; success: kr_assert(env->txn.ro_curs_active && env->txn.ro && env->txn.ro_active && !env->txn.rw); *curs = env->txn.ro_curs; kr_assert(*curs); return kr_ok(); } static void txn_free_ro(struct lmdb_env *env) { if (env->txn.ro_curs) { mdb_cursor_close(env->txn.ro_curs); env->txn.ro_curs = NULL; } if (env->txn.ro) { mdb_txn_abort(env->txn.ro); env->txn.ro = NULL; } } /** Abort all transactions. * * This is useful after an error happens, as those (always?) require abortion. * It's possible that _reset() would suffice and marking cursor inactive, * but these errors should be rare so let's close them completely. */ static void txn_abort(struct lmdb_env *env) { txn_free_ro(env); if (env->txn.rw) { mdb_txn_abort(env->txn.rw); env->txn.rw = NULL; /* the transaction got freed even in case of errors */ } } /*! \brief Close the database. */ static void cdb_close_env(struct lmdb_env *env, struct kr_cdb_stats *stats) { if (kr_fails_assert(env && env->env)) return; /* Get rid of any transactions. */ txn_free_ro(env); cdb_commit(env2db(env), stats); mdb_env_sync(env->env, 1); stats->close++; mdb_dbi_close(env->env, env->dbi); mdb_env_close(env->env); free_const(env->mdb_data_path); memset(env, 0, sizeof(*env)); } /** We assume that *env is zeroed and we return it zeroed on errors. */ static int cdb_open_env(struct lmdb_env *env, const char *path, const size_t mapsize, struct kr_cdb_stats *stats) { int ret = mkdir(path, LMDB_DIR_MODE); if (ret && errno != EEXIST) return kr_error(errno); stats->open++; ret = mdb_env_create(&env->env); if (ret != MDB_SUCCESS) return lmdb_error(ret); env->mdb_data_path = kr_absolutize_path(path, "data.mdb"); if (!env->mdb_data_path) { ret = ENOMEM; goto error_sys; } /* Set map size, rounded to page size. */ errno = 0; const long pagesize = sysconf(_SC_PAGESIZE); if (errno) { ret = errno; goto error_sys; } const bool size_requested = mapsize; if (size_requested) { env->mapsize = (mapsize / pagesize) * pagesize; ret = mdb_env_set_mapsize(env->env, env->mapsize); if (ret != MDB_SUCCESS) goto error_mdb; } /* Cache doesn't require durability, we can be * loose with the requirements as a tradeoff for speed. */ const unsigned flags = MDB_WRITEMAP | MDB_MAPASYNC | MDB_NOTLS; ret = mdb_env_open(env->env, path, flags, LMDB_FILE_MODE); if (ret != MDB_SUCCESS) goto error_mdb; mdb_filehandle_t fd = -1; ret = mdb_env_get_fd(env->env, &fd); if (ret != MDB_SUCCESS) goto error_mdb; struct stat st; if (fstat(fd, &st)) { ret = errno; goto error_sys; } env->st_dev = st.st_dev; env->st_ino = st.st_ino; env->st_size = st.st_size; /* Get the real mapsize. Shrinking can be restricted, etc. * Unfortunately this is only reliable when not setting the size explicitly. */ if (!size_requested) { ret = refresh_mapsize(env); if (ret) goto error_sys; } /* Open the database. */ MDB_txn *txn = NULL; ret = mdb_txn_begin(env->env, NULL, 0, &txn); if (ret != MDB_SUCCESS) goto error_mdb; ret = mdb_dbi_open(txn, NULL, 0, &env->dbi); if (ret != MDB_SUCCESS) { mdb_txn_abort(txn); goto error_mdb; } #if !defined(__MACOSX__) && !(defined(__APPLE__) && defined(__MACH__)) if (size_requested) { ret = posix_fallocate(fd, 0, MAX(env->mapsize, env->st_size)); } else { ret = 0; } if (ret == EINVAL || ret == EOPNOTSUPP) { /* POSIX says this can happen when the feature isn't supported by the FS. * We haven't seen this happen on Linux+glibc but it was reported on * Linux+musl and FreeBSD. */ kr_log_info(CACHE, "space pre-allocation failed and ignored; " "your (file)system probably doesn't support it.\n"); } else if (ret != 0) { mdb_txn_abort(txn); goto error_sys; } #endif stats->commit++; ret = mdb_txn_commit(txn); if (ret != MDB_SUCCESS) goto error_mdb; /* Stale RO transactions could have been left behind by a cashing process * (e.g. one whose termination lead to spawning the current one). * According to docs they might hold onto some space until we clear them. */ clear_stale_readers(env); return kr_ok(); error_mdb: ret = lmdb_error(ret); error_sys: free_const(env->mdb_data_path); stats->close++; mdb_env_close(env->env); memset(env, 0, sizeof(*env)); return kr_error(ret); } static int cdb_init(kr_cdb_pt *db, struct kr_cdb_stats *stats, struct kr_cdb_opts *opts, knot_mm_t *pool) { if (!db || !stats || !opts) { return kr_error(EINVAL); } /* Open the database. */ struct lmdb_env *env = calloc(1, sizeof(*env)); if (!env) { return kr_error(ENOMEM); } int ret = cdb_open_env(env, opts->path, opts->maxsize, stats); if (ret != 0) { free(env); return ret; } *db = env2db(env); return 0; } static void cdb_deinit(kr_cdb_pt db, struct kr_cdb_stats *stats) { cdb_close_env(db2env(db), stats); free(db); } static int cdb_count(kr_cdb_pt db, struct kr_cdb_stats *stats) { struct lmdb_env *env = db2env(db); MDB_txn *txn = NULL; int ret = txn_get(env, &txn, true); if (ret != 0) { return ret; } MDB_stat stat; stats->count++; ret = mdb_stat(txn, env->dbi, &stat); if (ret == MDB_SUCCESS) { return stat.ms_entries; } else { txn_abort(env); return lmdb_error(ret); } } static int reopen_env(struct lmdb_env *env, struct kr_cdb_stats *stats, const size_t mapsize) { /* Keep copy as it points to current handle internals. */ const char *path; int ret = mdb_env_get_path(env->env, &path); if (ret != MDB_SUCCESS) { return lmdb_error(ret); } auto_free char *path_copy = strdup(path); cdb_close_env(env, stats); return cdb_open_env(env, path_copy, mapsize, stats); } static int cdb_check_health(kr_cdb_pt db, struct kr_cdb_stats *stats) { struct lmdb_env *env = db2env(db); struct stat st; if (stat(env->mdb_data_path, &st)) { int ret = errno; return kr_error(ret); } if (st.st_dev != env->st_dev || st.st_ino != env->st_ino) { kr_log_debug(CACHE, "cache file has been replaced, reopening\n"); int ret = reopen_env(env, stats, 0); // we accept mapsize from the new file return ret == 0 ? 1 : ret; } /* Cache check through file size works OK without reopening, * contrary to methods based on mdb_env_info(). */ if (st.st_size == env->st_size) return kr_ok(); kr_log_info(CACHE, "detected size change (by another instance?) of file '%s'" ": file size %zu -> file size %zu\n", env->mdb_data_path, (size_t)env->st_size, (size_t)st.st_size); env->st_size = st.st_size; // avoid retrying in cycle even if we fail return refresh_mapsize(env); } /** Obtain exclusive (advisory) lock by creating a file, returning FD or negative kr_error(). * The lock is auto-released by OS in case the process finishes in any way (file remains). */ static int lockfile_get(const char *path) { if (kr_fails_assert(path)) return kr_error(EINVAL); const int fd = open(path, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP); if (fd < 0) return kr_error(errno); struct flock lock_info; memset(&lock_info, 0, sizeof(lock_info)); lock_info.l_type = F_WRLCK; lock_info.l_whence = SEEK_SET; lock_info.l_start = 0; lock_info.l_len = 1; // it's OK for locks to extend beyond the end of the file int err; do { err = fcntl(fd, F_SETLK, &lock_info); } while (err == -1 && errno == EINTR); if (err) { close(fd); return kr_error(errno); } return fd; } /** Release and remove lockfile created by lockfile_get(). Return kr_error(). */ static int lockfile_release(int fd) { if (kr_fails_assert(fd > 0)) // fd == 0 is surely a mistake, in our case at least return kr_error(EINVAL); if (close(fd)) { return kr_error(errno); } else { return kr_ok(); } } static int cdb_clear(kr_cdb_pt db, struct kr_cdb_stats *stats) { struct lmdb_env *env = db2env(db); stats->clear++; /* First try mdb_drop() to clear the DB; this may fail with ENOSPC. */ { MDB_txn *txn = NULL; int ret = txn_get(env, &txn, false); if (ret == kr_ok()) { ret = lmdb_error(mdb_drop(txn, env->dbi, 0)); if (ret == kr_ok()) { ret = cdb_commit(db, stats); } if (ret == kr_ok()) { return ret; } } kr_log_info(CACHE, "clearing error, falling back\n"); } /* Fallback: we'll remove the database files and reopen. * Other instances can continue to use the removed lmdb, * though it's best for them to reopen soon. */ /* We are about to switch to a different file, so end all txns, to be sure. */ txn_free_ro(env); (void) cdb_commit(db, stats); const char *path = NULL; int ret = mdb_env_get_path(env->env, &path); if (ret != MDB_SUCCESS) { return lmdb_error(ret); } auto_free char *mdb_lockfile = kr_strcatdup(2, path, "/lock.mdb"); auto_free char *lockfile = kr_strcatdup(2, path, "/krcachelock"); if (!mdb_lockfile || !lockfile) { return kr_error(ENOMEM); } /* Find if we get a lock on lockfile. */ const int lockfile_fd = lockfile_get(lockfile); if (lockfile_fd < 0) { kr_log_error(CACHE, "clearing failed to get ./krcachelock (%s); retry later\n", kr_strerror(lockfile_fd)); /* As we're out of space (almost certainly - mdb_drop didn't work), * we will retry on the next failing write operation. */ return kr_error(EAGAIN); } /* We acquired lockfile. Now find whether *.mdb are what we have open now. * If they are not we don't want to remove them; most likely they have been * cleaned by another instance. */ ret = cdb_check_health(db, stats); if (ret != 0) { if (ret == 1) // file changed and reopened successfully ret = kr_ok(); // else pass some other error } else { kr_log_debug(CACHE, "clear: identical files, unlinking\n"); // coverity[toctou] unlink(env->mdb_data_path); unlink(mdb_lockfile); ret = reopen_env(env, stats, env->mapsize); } /* Environment updated, release lockfile. */ int lrerr = lockfile_release(lockfile_fd); if (lrerr) { kr_log_error(CACHE, "failed to release ./krcachelock: %s\n", kr_strerror(lrerr)); } return ret; } static int cdb_readv(kr_cdb_pt db, struct kr_cdb_stats *stats, const knot_db_val_t *key, knot_db_val_t *val, int maxcount) { struct lmdb_env *env = db2env(db); MDB_txn *txn = NULL; int ret = txn_get(env, &txn, true); if (ret) { return ret; } for (int i = 0; i < maxcount; ++i) { /* Convert key structs */ MDB_val _key = val_knot2mdb(key[i]); MDB_val _val = val_knot2mdb(val[i]); stats->read++; ret = mdb_get(txn, env->dbi, &_key, &_val); if (ret != MDB_SUCCESS) { if (ret == MDB_NOTFOUND) { stats->read_miss++; } else { txn_abort(env); } ret = lmdb_error(ret); if (ret == kr_error(ENOSPC)) { /* we're likely to be forced to cache clear anyway */ ret = kr_error(ENOENT); } return ret; } /* Update the result. */ val[i] = val_mdb2knot(_val); } return kr_ok(); } static int cdb_write(struct lmdb_env *env, MDB_txn **txn, const knot_db_val_t *key, knot_db_val_t *val, unsigned flags, struct kr_cdb_stats *stats) { /* Convert key structs and write */ MDB_val _key = val_knot2mdb(*key); MDB_val _val = val_knot2mdb(*val); stats->write++; int ret = mdb_put(*txn, env->dbi, &_key, &_val, flags); /* We don't try to recover from MDB_TXN_FULL. */ if (ret != MDB_SUCCESS) { txn_abort(env); return lmdb_error(ret); } /* Update the result. */ val->data = _val.mv_data; val->len = _val.mv_size; return kr_ok(); } static int cdb_writev(kr_cdb_pt db, struct kr_cdb_stats *stats, const knot_db_val_t *key, knot_db_val_t *val, int maxcount) { struct lmdb_env *env = db2env(db); MDB_txn *txn = NULL; int ret = txn_get(env, &txn, false); for (int i = 0; ret == kr_ok() && i < maxcount; ++i) { /* This is LMDB specific optimisation, * if caller specifies value with NULL data and non-zero length, * LMDB will preallocate the entry for caller and leave write * transaction open, caller is responsible for syncing thus committing transaction. */ unsigned mdb_flags = 0; if (val[i].len > 0 && val[i].data == NULL) { mdb_flags |= MDB_RESERVE; } ret = cdb_write(env, &txn, &key[i], &val[i], mdb_flags, stats); } return ret; } static int cdb_remove(kr_cdb_pt db, struct kr_cdb_stats *stats, knot_db_val_t keys[], int maxcount) { struct lmdb_env *env = db2env(db); MDB_txn *txn = NULL; int ret = txn_get(env, &txn, false); int deleted = 0; for (int i = 0; ret == kr_ok() && i < maxcount; ++i) { MDB_val _key = val_knot2mdb(keys[i]); MDB_val val = { 0, NULL }; stats->remove++; ret = lmdb_error(mdb_del(txn, env->dbi, &_key, &val)); if (ret == kr_ok()) deleted++; else if (ret == KNOT_ENOENT) { stats->remove_miss++; ret = kr_ok(); /* skip over non-existing entries */ } else { txn_abort(env); break; } } return ret < 0 ? ret : deleted; } static int cdb_match(kr_cdb_pt db, struct kr_cdb_stats *stats, knot_db_val_t *key, knot_db_val_t keyval[][2], int maxcount) { struct lmdb_env *env = db2env(db); MDB_txn *txn = NULL; int ret = txn_get(env, &txn, true); if (ret != 0) { return ret; } /* LATER(optim.): use txn_curs_get() instead, to save resources. */ MDB_cursor *cur = NULL; ret = mdb_cursor_open(txn, env->dbi, &cur); if (ret != 0) { txn_abort(env); return lmdb_error(ret); } MDB_val cur_key = val_knot2mdb(*key); MDB_val cur_val = { 0, NULL }; stats->match++; ret = mdb_cursor_get(cur, &cur_key, &cur_val, MDB_SET_RANGE); if (ret != MDB_SUCCESS) { mdb_cursor_close(cur); if (ret != MDB_NOTFOUND) { txn_abort(env); } return lmdb_error(ret); } int results = 0; while (ret == MDB_SUCCESS) { /* Retrieve current key and compare with prefix */ if (cur_key.mv_size < key->len || memcmp(cur_key.mv_data, key->data, key->len) != 0) { break; } /* Add to result set */ if (results < maxcount) { keyval[results][0] = val_mdb2knot(cur_key); keyval[results][1] = val_mdb2knot(cur_val); ++results; } else { break; } stats->match++; ret = mdb_cursor_get(cur, &cur_key, &cur_val, MDB_NEXT); } mdb_cursor_close(cur); if (ret != MDB_SUCCESS && ret != MDB_NOTFOUND) { txn_abort(env); return lmdb_error(ret); } else if (results == 0) { stats->match_miss++; } return results; } static int cdb_read_leq(kr_cdb_pt db, struct kr_cdb_stats *stats, knot_db_val_t *key, knot_db_val_t *val) { if (kr_fails_assert(db && key && key->data && val)) return kr_error(EINVAL); struct lmdb_env *env = db2env(db); MDB_cursor *curs = NULL; int ret = txn_curs_get(env, &curs, stats); if (ret) return ret; MDB_val key2_m = val_knot2mdb(*key); MDB_val val2_m = { 0, NULL }; stats->read_leq++; ret = mdb_cursor_get(curs, &key2_m, &val2_m, MDB_SET_RANGE); if (ret) goto failure; /* test for equality //:unlikely */ if (key2_m.mv_size == key->len && memcmp(key2_m.mv_data, key->data, key->len) == 0) { ret = 0; /* equality */ goto success; } stats->read_leq_miss++; /* we must be greater than key; do one step to smaller */ stats->read_leq++; ret = mdb_cursor_get(curs, &key2_m, &val2_m, MDB_PREV); if (ret) goto failure; ret = 1; success: /* finalize the output */ *key = val_mdb2knot(key2_m); *val = val_mdb2knot(val2_m); return ret; failure: if (ret == MDB_NOTFOUND) { stats->read_leq_miss++; } else { txn_abort(env); } return lmdb_error(ret); } static double cdb_usage_percent(kr_cdb_pt db) { knot_db_t *kdb = kr_cdb_pt2knot_db_t(db); const size_t db_size = knot_db_lmdb_get_mapsize(kdb); const size_t db_usage_abs = knot_db_lmdb_get_usage(kdb); const double db_usage = (double)db_usage_abs / db_size * 100.0; free(kdb); return db_usage; } static size_t cdb_get_maxsize(kr_cdb_pt db) { return db2env(db)->mapsize; } /** Conversion between knot and lmdb structs. */ knot_db_t *kr_cdb_pt2knot_db_t(kr_cdb_pt db) { /* this is struct lmdb_env as in resolver/cdb_lmdb.c */ const struct lmdb_env *kres_db = db2env(db); struct libknot_lmdb_env *libknot_db = malloc(sizeof(*libknot_db)); if (libknot_db != NULL) { libknot_db->shared = false; libknot_db->pool = NULL; libknot_db->env = kres_db->env; libknot_db->dbi = kres_db->dbi; } return libknot_db; } const struct kr_cdb_api *kr_cdb_lmdb(void) { static const struct kr_cdb_api api = { "lmdb", cdb_init, cdb_deinit, cdb_count, cdb_clear, cdb_commit, cdb_readv, cdb_writev, cdb_remove, cdb_match, cdb_read_leq, cdb_usage_percent, cdb_get_maxsize, cdb_check_health, }; return &api; }