mdbx: Merge branch 'devel' branch.

Change-Id: I7430c4078a4cba86b885db25643ddb57bf5fc6a8
This commit is contained in:
Leo Yuriev 2017-02-09 11:56:48 +03:00
commit 284712a9d4
7 changed files with 427 additions and 120 deletions

132
README.md
View File

@ -3,37 +3,36 @@ libmdbx
Extended LMDB, aka "Расширенная LMDB".
*The Future will Positive. Всё будет хорошо.*
[![Build Status](https://travis-ci.org/ReOpen/libmdbx.svg?branch=master)](https://travis-ci.org/ReOpen/libmdbx)
English version by Google [is here](https://translate.googleusercontent.com/translate_c?act=url&depth=1&hl=ru&ie=UTF8&prev=_t&rurl=translate.google.com&sl=ru&tl=en&u=https://github.com/ReOpen/libmdbx/tree/master).
English version by Google [is here](https://translate.googleusercontent.com/translate_c?act=url&ie=UTF8&sl=ru&tl=en&u=https://github.com/ReOpen/libmdbx/tree/master).
## Кратко
_libmdbx_ - это встраиваемый key-value движок хранения со
специфическим набором возможностей, которые при правильном
применении позволяют создавать уникальные решения с чемпионской
производительностью, идеально сочетаясь с технологией
[MRAM](https://en.wikipedia.org/wiki/Magnetoresistive_random-access_memory).
_libmdbx_ обновляет совместно используемый набор данных, никак
не мешая при этом параллельным операциям чтения, не применяя
атомарных операций к самим данным, и обеспечивая
согласованность при аварийной остановке в любой момент. Поэтому
_libmdbx_ позволяя строить системы с линейным масштабированием
производительности чтения/поиска по ядрам CPU и амортизационной
стоимостью любых операций Olog(N).
_libmdbx_ - это встраиваемый key-value движок хранения со специфическим
набором возможностей, которые при правильном применении позволяют
создавать уникальные решения с чемпионской производительностью, идеально
сочетаясь с технологией [MRAM](https://en.wikipedia.org/wiki/Magnetoresistive_random-access_memory).
_libmdbx_ обновляет совместно используемый набор данных, никак не мешая
при этом параллельным операциям чтения, не применяя атомарных операций к
самим данным, и обеспечивая согласованность при аварийной остановке в
любой момент. Поэтому _libmdbx_ позволяя строить системы с линейным
масштабированием производительности чтения/поиска по ядрам CPU и
амортизационной стоимостью любых операций Olog(N).
### История
_libmdbx_ является потомком "Lightning Memory-Mapped Database",
известной под аббревиатурой
[LMDB](https://en.wikipedia.org/wiki/Lightning_Memory-Mapped_Database).
Изначально доработка производилась в составе проекта
[ReOpenLDAP](https://github.com/ReOpen/ReOpenLDAP). Примерно за
год работы внесенные изменения приобрели самостоятельную
ценность. Осенью 2015 доработанный движок был выделен в
отдельный проект, который был [представлен на конференции
Highload++ 2015](http://www.highload.ru/2015/abstracts/1831.html).
[ReOpenLDAP](https://github.com/ReOpen/ReOpenLDAP). Примерно за год
работы внесенные изменения приобрели самостоятельную ценность. Осенью
2015 доработанный движок был выделен в отдельный проект, который был
[представлен на конференции Highload++
2015](http://www.highload.ru/2015/abstracts/1831.html).
Характеристики и ключевые особенности
@ -283,48 +282,85 @@ RECLAIM` в _libmdbx_.
формированием сильных точек фиксации.
4. Возможность автоматического формирования контрольных точек
(сброса данных на диск) при накоплении заданного объёма
изменений, устанавливаемого функцией
`mdbx_env_set_syncbytes()`.
(сброса данных на диск) при накоплении заданного объёма изменений,
устанавливаемого функцией `mdbx_env_set_syncbytes()`.
5. Возможность получить отставание текущей транзакции чтения от
последней версии данных в БД посредством
`mdbx_txn_straggler()`.
последней версии данных в БД посредством `mdbx_txn_straggler()`.
6. Утилита mdbx_chk для проверки БД и функция
`mdbx_env_pgwalk()` для обхода всех страниц БД.
6. Утилита mdbx_chk для проверки БД и функция `mdbx_env_pgwalk()` для
обхода всех страниц БД.
7. Управление отладкой и получение отладочных сообщений
посредством `mdbx_setup_debug()`.
7. Управление отладкой и получение отладочных сообщений посредством
`mdbx_setup_debug()`.
8. Возможность связать с каждой завершаемой транзакцией до 3
дополнительных маркеров посредством `mdbx_canary_put()`, и
прочитать их в транзакции чтения посредством
`mdbx_canary_get()`.
дополнительных маркеров посредством `mdbx_canary_put()`, и прочитать их
в транзакции чтения посредством `mdbx_canary_get()`.
9. Возможность узнать есть ли за текущей позицией курсора
строка данных посредством `mdbx_cursor_eof()`.
9. Возможность узнать есть ли за текущей позицией курсора строка данных
посредством `mdbx_cursor_eof()`.
10. Возможность явно запросить обновление существующей записи,
без создания новой посредством флажка `MDB_CURRENT` для
`mdb_put()`.
10. Возможность явно запросить обновление существующей записи, без
создания новой посредством флажка `MDB_CURRENT` для `mdbx_put()`.
11. Возможность обновить или удалить запись с получением
предыдущего значения данных посредством `mdbx_replace()`.
11. Возможность обновить или удалить запись с получением предыдущего
значения данных посредством `mdbx_replace()`.
12. Поддержка ключей нулевого размера.
12. Поддержка ключей и значений нулевой длины. Включая сортированные
дубликаты, в том числе вне зависимости от порядка их добавления или
обновления.
13. Исправленный вариант `mdb_cursor_count()`, возвращающий
корректное количество дубликатов для всех типов таблиц и любого
положения курсора.
13. Исправленный вариант `mdbx_cursor_count()`, возвращающий корректное
количество дубликатов для всех типов таблиц и любого положения курсора.
14. Возможность открыть БД в эксклюзивном режиме посредством
`mdbx_env_open_ex()`, например в целях её проверки.
15. Возможность закрыть БД в "грязном" состоянии (без сброса
данных и формирования сильной точки фиксации) посредством
`mdbx_env_close_ex()`.
15. Возможность закрыть БД в "грязном" состоянии (без сброса данных и
формирования сильной точки фиксации) посредством `mdbx_env_close_ex()`.
16. Возможность получить посредством `mdbx_env_info()`
дополнительную информацию, включая номер самой старой версии БД
(снимка данных), который используется одним из читателей.
16. Возможность получить посредством `mdbx_env_info()` дополнительную
информацию, включая номер самой старой версии БД (снимка данных),
который используется одним из читателей.
17. Функция `mdbx_del()` не игнорирует дополнительный (уточняющий)
аргумент `data` для таблиц без дубликатов (без флажка `MDB_DUPSORT`), а
при его ненулевом значении всегда использует его для сверки с удаляемой
записью.
18. Возможность открыть dbi-таблицу, одновременно с установкой
компараторов для ключей и данных, посредством `mdbx_dbi_open_ex()`.
19. Возможность посредством `mdbx_is_dirty()` определить находятся ли
некоторый ключ или данные в "грязной" странице БД. Таким образом избегаю
лишнего копирования данных перед выполнением модифицирующих операций
(значения в размещенные "грязных" страницах могут быть перезаписаны при
изменениях, иначе они будут неизменны).
20. Корректное обновление текущей записи, в том числе сортированного
дубликата, при использовании режима `MDB_CURRENT` в `mdbx_cursor_put()`.
21. Все курсоры, как в транзакциях только для чтения, так и в пишущих,
могут быть переиспользованы посредством `mdbx_cursor_renew()` и ДОЛЖНЫ
ОСВОБОЖДАТЬСЯ ЯВНО.
>
> ## _ВАЖНО_, Обратите внимание!
>
> Это единственное изменение в API, которое значимо меняет
> семантику управления курсорами и может приводить к утечкам
> памяти. Следует отметить, что это изменение вынужденно.
> Так устраняется неоднозначность с массой тяжких последствий:
>
> - обращение к уже освобожденной памяти;
> - попытки повторного освобождения памяти;
> - memory corruption and segfaults.
22. Дополнительный код ошибки `MDBX_EMULTIVAL`, который возвращается из
`mdbx_put()` и `mdbx_replace()` при попытке выполнять неоднозначное
обновление или удаления одного из нескольких значений с одним ключом,
т.е. когда невозможно однозначно идентифицировать одно целевое значение
из нескольких.
23. Возможность посредством `mdbx_get_ex()` получить значение по
заданному ключу, одновременно с количеством дубликатов.

18
lmdb.h
View File

@ -1,7 +1,7 @@
/** @file lmdb.h
* @brief Extended Lightning memory-mapped database library
*
* @mainpage Extended Lightning Memory-Mapped Database Manager (MDBX)
* @mainpage Extended Lightning Memory-Mapped Database (MDBX)
*
* @section intro_sec Introduction
* MDBX is a Btree-based database management library modeled loosely on the
@ -1387,12 +1387,20 @@ int mdb_put(MDB_txn *txn, MDB_dbi dbi, MDB_val *key, MDB_val *data,
/** @brief Delete items from a database.
*
* This function removes key/data pairs from the database.
*
* MDBX-mode:
* The data parameter is NOT ignored regardless the database does
* support sorted duplicate data items or not. If the data parameter
* is non-NULL only the matching data item will be deleted.
*
* LMDB-compatible mode:
* If the database does not support sorted duplicate data items
* (#MDB_DUPSORT) the data parameter is ignored.
* If the database supports sorted duplicates and the data parameter
* is NULL, all of the duplicate data items for the key will be
* deleted. Otherwise, if the data parameter is non-NULL
* only the matching data item will be deleted.
*
* This function will return #MDB_NOTFOUND if the specified key/data
* pair is not in the database.
* @param[in] txn A transaction handle returned by #mdb_txn_begin()
@ -1414,6 +1422,13 @@ int mdb_del(MDB_txn *txn, MDB_dbi dbi, MDB_val *key, MDB_val *data);
* A cursor cannot be used when its database handle is closed. Nor
* when its transaction has ended, except with #mdb_cursor_renew().
* It can be discarded with #mdb_cursor_close().
*
* MDBX-mode:
* A cursor must be closed explicitly always, before
* or after its transaction ends. It can be reused with
* #mdb_cursor_renew() before finally closing it.
*
* LMDB-compatible mode:
* A cursor in a write-transaction can be closed before its transaction
* ends, and will otherwise be closed when its transaction ends.
* A cursor in a read-only transaction must be closed explicitly, before
@ -1421,6 +1436,7 @@ int mdb_del(MDB_txn *txn, MDB_dbi dbi, MDB_val *key, MDB_val *data);
* #mdb_cursor_renew() before finally closing it.
* @note Earlier documentation said that cursors in every transaction
* were closed when the transaction committed or aborted.
*
* @param[in] txn A transaction handle returned by #mdb_txn_begin()
* @param[in] dbi A database handle returned by #mdb_dbi_open()
* @param[out] cursor Address where the new #MDB_cursor handle will be stored

149
mdb.c
View File

@ -70,6 +70,10 @@
# warning "ReOpenMDBX required at least GLIBC 2.12."
#endif
#if MDB_DEBUG
# undef NDEBUG
#endif
#include "./reopen.h"
#include "./barriers.h"
@ -934,6 +938,8 @@ struct MDB_xcursor;
*/
struct MDB_cursor {
#define MDBX_MC_SIGNATURE (0xFE05D5B1^MDBX_MODE_SALT)
#define MDBX_MC_READY4CLOSE (0x2817A047^MDBX_MODE_SALT)
#define MDBX_MC_WAIT4EOT (0x90E297A7^MDBX_MODE_SALT)
unsigned mc_signature;
/** Next cursor on this DB in this txn */
MDB_cursor *mc_next;
@ -1178,7 +1184,6 @@ static void mdb_xcursor_init1(MDB_cursor *mc, MDB_node *node);
static void mdb_xcursor_init2(MDB_cursor *mc, MDB_xcursor *src_mx, int force);
static int mdb_drop0(MDB_cursor *mc, int subs);
static void mdb_default_cmp(MDB_txn *txn, MDB_dbi dbi);
static int mdb_reader_check0(MDB_env *env, int rlocked, int *dead);
/** @cond */
@ -2687,7 +2692,7 @@ mdb_cursor_shadow(MDB_txn *src, MDB_txn *dst)
* @return 0 on success, non-zero on failure.
*/
static void
mdb_cursors_close(MDB_txn *txn, unsigned merge)
mdb_cursors_eot(MDB_txn *txn, unsigned merge)
{
MDB_cursor **cursors = txn->mt_cursors, *mc, *next, *bk;
MDB_xcursor *mx;
@ -2695,6 +2700,8 @@ mdb_cursors_close(MDB_txn *txn, unsigned merge)
for (i = txn->mt_numdbs; --i >= 0; ) {
for (mc = cursors[i]; mc; mc = next) {
mdb_ensure(NULL, mc->mc_signature == MDBX_MC_SIGNATURE
|| mc->mc_signature == MDBX_MC_WAIT4EOT);
next = mc->mc_next;
if ((bk = mc->mc_backup) != NULL) {
if (merge) {
@ -2707,16 +2714,30 @@ mdb_cursors_close(MDB_txn *txn, unsigned merge)
if ((mx = mc->mc_xcursor) != NULL)
mx->mx_cursor.mc_txn = bk->mc_txn;
} else {
/* Abort nested txn */
/* Abort nested txn, but save current cursor's stage */
unsigned stage = mc->mc_signature;
*mc = *bk;
mc->mc_signature = stage;
if ((mx = mc->mc_xcursor) != NULL)
*mx = *(MDB_xcursor *)(bk+1);
}
#if MDBX_MODE_ENABLED
bk->mc_signature = 0;
free(bk);
}
if (mc->mc_signature == MDBX_MC_WAIT4EOT) {
mc->mc_signature = 0;
free(mc);
} else {
mc->mc_signature = MDBX_MC_READY4CLOSE;
}
#else
mc = bk;
}
/* Only malloced cursors are permanently tracked. */
mc->mc_signature = 0;
free(mc);
#endif
}
cursors[i] = NULL;
}
@ -3163,7 +3184,7 @@ mdb_txn_end(MDB_txn *txn, unsigned mode)
pgno_t *pghead = env->me_pghead;
if (!(mode & MDB_END_UPDATE)) /* !(already closed cursors) */
mdb_cursors_close(txn, 0);
mdb_cursors_eot(txn, 0);
if (!(env->me_flags & MDB_WRITEMAP)) {
mdb_dlist_free(txn);
}
@ -3761,7 +3782,7 @@ mdb_txn_commit(MDB_txn *txn)
parent->mt_flags = txn->mt_flags;
/* Merge our cursors into parent's and close them */
mdb_cursors_close(txn, 1);
mdb_cursors_eot(txn, 1);
/* Update parent's DB table. */
memcpy(parent->mt_dbs, txn->mt_dbs, txn->mt_numdbs * sizeof(MDB_db));
@ -3880,7 +3901,7 @@ mdb_txn_commit(MDB_txn *txn)
goto fail;
}
mdb_cursors_close(txn, 0);
mdb_cursors_eot(txn, 0);
if (!txn->mt_u.dirty_list[0].mid &&
!(txn->mt_flags & (MDB_TXN_DIRTY|MDB_TXN_SPILLS)))
@ -6753,11 +6774,25 @@ mdb_cursor_put(MDB_cursor *mc, MDB_val *key, MDB_val *data,
mdb_debug("==> put db %d key [%s], size %zu, data size %zu",
DDBI(mc), DKEY(key), key ? key->mv_size : 0, data->mv_size);
dkey.mv_size = 0;
int dupdata_flag = 0;
if (flags & MDB_CURRENT) {
if (unlikely(!(mc->mc_flags & C_INITIALIZED)))
return EINVAL;
#if MDBX_MODE_ENABLED
if (F_ISSET(mc->mc_db->md_flags, MDB_DUPSORT)) {
MDB_node *leaf = NODEPTR(mc->mc_pg[mc->mc_top], mc->mc_ki[mc->mc_top]);
if (F_ISSET(leaf->mn_flags, F_DUPDATA)) {
mdb_cassert(mc, mc->mc_xcursor != NULL
&& (mc->mc_xcursor->mx_cursor.mc_flags & C_INITIALIZED));
if (mc->mc_xcursor->mx_db.md_entries > 1) {
rc = mdbx_cursor_del(mc, 0);
if (rc != MDB_SUCCESS)
return rc;
flags -= MDB_CURRENT;
}
}
}
#endif /* MDBX_MODE_ENABLED */
rc = MDB_SUCCESS;
} else if (mc->mc_db->md_root == P_INVALID) {
/* new database, cursor has nothing to point to */
@ -6897,8 +6932,11 @@ more:
/* Was a single item before, must convert now */
if (!F_ISSET(leaf->mn_flags, F_DUPDATA)) {
/* Just overwrite the current item */
if (flags == MDB_CURRENT)
if (flags & MDB_CURRENT) {
if ((flags & MDB_NODUPDATA) && !mc->mc_dbx->md_dcmp(data, &olddata))
return MDB_KEYEXIST;
goto current;
}
/* does data match? */
if (!mc->mc_dbx->md_dcmp(data, &olddata)) {
@ -6909,6 +6947,7 @@ more:
}
/* Back up original data item */
dupdata_flag = 1;
dkey.mv_size = olddata.mv_size;
dkey.mv_data = memcpy(fp+1, olddata.mv_data, olddata.mv_size);
@ -7137,7 +7176,7 @@ new_sub:
* size limits on dupdata. The actual data fields of the child
* DB are all zero size. */
if (do_sub) {
int xflags, new_dupdata;
int xflags;
size_t ecount;
put_sub:
xdata.mv_size = 0;
@ -7153,9 +7192,8 @@ put_sub:
}
if (sub_root)
mc->mc_xcursor->mx_cursor.mc_pg[0] = sub_root;
new_dupdata = (int)dkey.mv_size;
/* converted, write the original data first */
if (dkey.mv_size) {
if (dupdata_flag) {
rc = mdb_cursor_put(&mc->mc_xcursor->mx_cursor, &dkey, &xdata, xflags);
if (unlikely(rc))
goto bad_sub;
@ -7175,7 +7213,7 @@ put_sub:
if (!(m2->mc_flags & C_INITIALIZED)) continue;
if (m2->mc_pg[i] == mp) {
if (m2->mc_ki[i] == mc->mc_ki[i]) {
mdb_xcursor_init2(m2, mx, new_dupdata);
mdb_xcursor_init2(m2, mx, dupdata_flag);
} else if (!insert_key && m2->mc_ki[i] < nkeys) {
XCURSOR_REFRESH(m2, mp, m2->mc_ki[i]);
}
@ -7761,9 +7799,7 @@ mdb_xcursor_init2(MDB_cursor *mc, MDB_xcursor *src_mx, int new_dupdata)
mx->mx_cursor.mc_flags |= C_INITIALIZED;
mx->mx_cursor.mc_ki[0] = 0;
mx->mx_dbflag = DB_VALID|DB_USRVALID|DB_DUPDATA;
#if UINT_MAX < SIZE_MAX
mx->mx_dbx.md_cmp = src_mx->mx_dbx.md_cmp;
#endif
} else if (!(mx->mx_cursor.mc_flags & C_INITIALIZED)) {
return;
}
@ -7849,16 +7885,31 @@ mdb_cursor_renew(MDB_txn *txn, MDB_cursor *mc)
if (unlikely(!mc || !txn))
return EINVAL;
if (unlikely(txn->mt_signature != MDBX_MT_SIGNATURE
|| mc->mc_signature != MDBX_MC_SIGNATURE))
if (unlikely(txn->mt_signature != MDBX_MT_SIGNATURE))
return MDB_VERSION_MISMATCH;
if (unlikely(mc->mc_signature != MDBX_MC_SIGNATURE
&& mc->mc_signature != MDBX_MC_READY4CLOSE))
return EINVAL;
if (unlikely(!TXN_DBI_EXIST(txn, mc->mc_dbi, DB_VALID)))
return EINVAL;
if (unlikely((mc->mc_flags & C_UNTRACK) || txn->mt_cursors))
if (unlikely(mc->mc_backup))
return EINVAL;
if (unlikely((mc->mc_flags & C_UNTRACK) || txn->mt_cursors)) {
#if MDBX_MODE_ENABLED
MDB_cursor **prev = &mc->mc_txn->mt_cursors[mc->mc_dbi];
while (*prev && *prev != mc) prev = &(*prev)->mc_next;
if (*prev == mc)
*prev = mc->mc_next;
mc->mc_signature = MDBX_MC_READY4CLOSE;
#else
return EINVAL;
#endif
}
if (unlikely(txn->mt_flags & MDB_TXN_BLOCKED))
return MDB_BAD_TXN;
@ -7894,16 +7945,13 @@ mdb_cursor_count(MDB_cursor *mc, size_t *countp)
return MDB_NOTFOUND;
}
if (mc->mc_xcursor == NULL || IS_LEAF2(mp)) {
*countp = 1;
} else {
*countp = 1;
if (mc->mc_xcursor != NULL) {
MDB_node *leaf = NODEPTR(mp, mc->mc_ki[mc->mc_top]);
if (!F_ISSET(leaf->mn_flags, F_DUPDATA))
*countp = 1;
else if (unlikely(!(mc->mc_xcursor->mx_cursor.mc_flags & C_INITIALIZED)))
return EINVAL;
else
if (F_ISSET(leaf->mn_flags, F_DUPDATA)) {
mdb_cassert(mc, mc->mc_xcursor && (mc->mc_xcursor->mx_cursor.mc_flags & C_INITIALIZED));
*countp = mc->mc_xcursor->mx_db.md_entries;
}
}
#else
if (unlikely(mc->mc_xcursor == NULL))
@ -7932,12 +7980,12 @@ void
mdb_cursor_close(MDB_cursor *mc)
{
if (mc) {
mdb_ensure(NULL, mc->mc_signature == MDBX_MC_SIGNATURE);
mdb_ensure(NULL, mc->mc_signature == MDBX_MC_SIGNATURE
|| mc->mc_signature == MDBX_MC_READY4CLOSE);
if (!mc->mc_backup) {
/* Remove from txn, if tracked.
* A read-only txn (!C_UNTRACK) may have been freed already,
* so do not peek inside it. Only write txns track cursors.
*/
* so do not peek inside it. Only write txns track cursors. */
if ((mc->mc_flags & C_UNTRACK) && mc->mc_txn->mt_cursors) {
MDB_cursor **prev = &mc->mc_txn->mt_cursors[mc->mc_dbi];
while (*prev && *prev != mc) prev = &(*prev)->mc_next;
@ -7946,6 +7994,10 @@ mdb_cursor_close(MDB_cursor *mc)
}
mc->mc_signature = 0;
free(mc);
} else {
/* cursor closed before nested txn ends */
mdb_cassert(mc, mc->mc_signature == MDBX_MC_SIGNATURE);
mc->mc_signature = MDBX_MC_WAIT4EOT;
}
}
}
@ -8743,10 +8795,12 @@ mdb_del(MDB_txn *txn, MDB_dbi dbi,
if (unlikely(txn->mt_flags & (MDB_TXN_RDONLY|MDB_TXN_BLOCKED)))
return (txn->mt_flags & MDB_TXN_RDONLY) ? EACCES : MDB_BAD_TXN;
#if ! MDBX_MODE_ENABLED
if (!F_ISSET(txn->mt_dbs[dbi].md_flags, MDB_DUPSORT)) {
/* must ignore any data */
data = NULL;
}
#endif
return mdb_del0(txn, dbi, key, data, 0);
}
@ -8758,7 +8812,7 @@ mdb_del0(MDB_txn *txn, MDB_dbi dbi,
MDB_cursor mc;
MDB_xcursor mx;
MDB_cursor_op op;
MDB_val rdata, *xdata;
MDB_val rdata;
int rc, exact = 0;
DKBUF;
@ -8769,13 +8823,12 @@ mdb_del0(MDB_txn *txn, MDB_dbi dbi,
if (data) {
op = MDB_GET_BOTH;
rdata = *data;
xdata = &rdata;
data = &rdata;
} else {
op = MDB_SET;
xdata = NULL;
flags |= MDB_NODUPDATA;
}
rc = mdb_cursor_set(&mc, key, xdata, op, &exact);
rc = mdb_cursor_set(&mc, key, data, op, &exact);
if (likely(rc == 0)) {
/* let mdb_page_split know about this cursor if needed:
* delete will trigger a rebalance; if it needs to move
@ -9254,7 +9307,7 @@ mdb_put(MDB_txn *txn, MDB_dbi dbi,
MDB_node *leaf = NODEPTR(mc.mc_pg[mc.mc_top], mc.mc_ki[mc.mc_top]);
if (F_ISSET(leaf->mn_flags, F_DUPDATA)) {
mdb_tassert(txn, XCURSOR_INITED(&mc) && mc.mc_xcursor->mx_db.md_entries > 1);
rc = MDB_KEYEXIST;
rc = MDBX_EMULTIVAL;
}
}
}
@ -10002,6 +10055,21 @@ mdb_env_info(MDB_env *env, MDB_envinfo *arg)
return mdbx_env_info(env, (MDBX_envinfo*) arg, sizeof(MDB_envinfo));
}
static MDB_cmp_func*
mdbx_default_keycmp(unsigned flags)
{
return (flags & MDB_REVERSEKEY) ? mdb_cmp_memnr :
(flags & MDB_INTEGERKEY) ? mdb_cmp_int_a2 : mdb_cmp_memn;
}
static MDB_cmp_func*
mdbx_default_datacmp(unsigned flags)
{
return !(flags & MDB_DUPSORT) ? 0 :
((flags & MDB_INTEGERDUP) ? mdb_cmp_int_ua :
((flags & MDB_REVERSEDUP) ? mdb_cmp_memnr : mdb_cmp_memn));
}
/** Set the default comparison functions for a database.
* Called immediately after a database is opened to set the defaults.
* The user can then override them with #mdb_set_compare() or
@ -10012,16 +10080,9 @@ mdb_env_info(MDB_env *env, MDB_envinfo *arg)
static void
mdb_default_cmp(MDB_txn *txn, MDB_dbi dbi)
{
unsigned f = txn->mt_dbs[dbi].md_flags;
txn->mt_dbxs[dbi].md_cmp =
(f & MDB_REVERSEKEY) ? mdb_cmp_memnr :
(f & MDB_INTEGERKEY) ? mdb_cmp_int_a2 : mdb_cmp_memn;
txn->mt_dbxs[dbi].md_dcmp =
!(f & MDB_DUPSORT) ? 0 :
((f & MDB_INTEGERDUP) ? mdb_cmp_int_ua :
((f & MDB_REVERSEDUP) ? mdb_cmp_memnr : mdb_cmp_memn));
unsigned flags = txn->mt_dbs[dbi].md_flags;
txn->mt_dbxs[dbi].md_cmp = mdbx_default_keycmp(flags);
txn->mt_dbxs[dbi].md_dcmp = mdbx_default_datacmp(flags);
}
int mdb_dbi_open(MDB_txn *txn, const char *name, unsigned flags, MDB_dbi *dbi)

View File

@ -264,7 +264,7 @@ static int pgvisitor(size_t pgno, unsigned pgnumber, void* ctx, const char* dbi,
problem_add("page", pgno, "illegal header-length", "%zu < %i < %zu",
sizeof(long), header_bytes, stat.base.ms_psize - sizeof(long));
if (payload_bytes < 1) {
if (nentries > 0) {
if (nentries > 1) {
problem_add("page", pgno, "zero size-of-entry", "payload %i bytes, %i entries",
payload_bytes, nentries);
if ((size_t) header_bytes + unused_bytes < page_size) {
@ -487,9 +487,7 @@ static int process_db(MDB_dbi dbi, char *name, visitor *handler, int silent)
goto bailout;
}
if (key.mv_size == 0) {
problem_add("entry", record_count, "key with zero length", NULL);
} else if (key.mv_size > maxkeysize) {
if (key.mv_size > maxkeysize) {
problem_add("entry", record_count, "key length exceeds max-key-size",
"%zu > %zu", key.mv_size, maxkeysize);
} else if ((flags & MDB_INTEGERKEY)

220
mdbx.c
View File

@ -118,7 +118,7 @@ mdbx_env_set_syncbytes(MDB_env *env, size_t bytes)
return MDB_VERSION_MISMATCH;
env->me_sync_threshold = bytes;
return env->me_map ? mdb_env_sync(env, 0) : 0;
return env->me_map ? mdb_env_sync(env, 0) : MDB_SUCCESS;
}
void __cold
@ -245,8 +245,6 @@ mdb_env_walk(mdb_walk_ctx_t *ctx, const char* dbi, pgno_t pg, int flags, int dee
}
assert(IS_LEAF(mp));
if (node->mn_ksize < 1)
return MDB_CORRUPTED;
if (node->mn_flags & F_BIGDATA) {
MDB_page *omp;
pgno_t *opg;
@ -281,8 +279,6 @@ mdb_env_walk(mdb_walk_ctx_t *ctx, const char* dbi, pgno_t pg, int flags, int dee
MDB_db *db = NODEDATA(node);
char* name = NULL;
if (NODEDSZ(node) < 1)
return MDB_CORRUPTED;
if (! (node->mn_flags & F_DUPDATA)) {
name = NODEKEY(node);
int namelen = (char*) db - name;
@ -367,16 +363,16 @@ int mdbx_cursor_eof(MDB_cursor *mc)
return MDB_VERSION_MISMATCH;
if ((mc->mc_flags & C_INITIALIZED) == 0)
return 1;
return MDBX_RESULT_TRUE;
if (mc->mc_snum == 0)
return 1;
return MDBX_RESULT_TRUE;
if ((mc->mc_flags & C_EOF)
&& mc->mc_ki[mc->mc_top] >= NUMKEYS(mc->mc_pg[mc->mc_top]))
return 1;
return MDBX_RESULT_TRUE;
return 0;
return MDBX_RESULT_FALSE;
}
static int mdbx_is_samedata(const MDB_val* a, const MDB_val* b) {
@ -399,6 +395,8 @@ static int mdbx_is_samedata(const MDB_val* a, const MDB_val* b) {
* когда посредством old_data из записей с одинаковым ключом для
* удаления/обновления выбирается конкретная. Для выбора этого сценария
* во flags следует одновременно указать MDB_CURRENT и MDB_NOOVERWRITE.
* Именно эта комбинация выбрана, так как она лишена смысла, и этим позволяет
* идентифицировать запрос такого сценария.
*
* Функция может быть замещена соответствующими операциями с курсорами
* после двух доработок (TODO):
@ -411,7 +409,7 @@ int mdbx_replace(MDB_txn *txn, MDB_dbi dbi,
MDB_cursor mc;
MDB_xcursor mx;
if (unlikely(!key || !old_data || !txn))
if (unlikely(!key || !old_data || !txn || old_data == new_data))
return EINVAL;
if (unlikely(txn->mt_signature != MDBX_MT_SIGNATURE))
@ -438,17 +436,50 @@ int mdbx_replace(MDB_txn *txn, MDB_dbi dbi,
int rc;
MDB_val present_key = *key;
if (F_ISSET(flags, MDB_CURRENT | MDB_NOOVERWRITE)
&& (txn->mt_dbs[dbi].md_flags & MDB_DUPSORT)) {
if (F_ISSET(flags, MDB_CURRENT | MDB_NOOVERWRITE)) {
/* в old_data значение для выбора конкретного дубликата */
if (unlikely(!(txn->mt_dbs[dbi].md_flags & MDB_DUPSORT))) {
rc = EINVAL;
goto bailout;
}
/* убираем лишний бит, он был признаком запрошенного режима */
flags -= MDB_NOOVERWRITE;
rc = mdbx_cursor_get(&mc, &present_key, old_data, MDB_GET_BOTH);
if (rc != MDB_SUCCESS)
goto bailout;
/* если данные совпадают, то ничего делать не надо */
if (new_data && mdbx_is_samedata(old_data, new_data))
goto bailout;
if (new_data) {
/* обновление конкретного дубликата */
if (mdbx_is_samedata(old_data, new_data))
/* если данные совпадают, то ничего делать не надо */
goto bailout;
#if 0 /* LY: исправлено в mdbx_cursor_put(), здесь в качестве памятки */
MDB_node *leaf = NODEPTR(mc.mc_pg[mc.mc_top], mc.mc_ki[mc.mc_top]);
if (F_ISSET(leaf->mn_flags, F_DUPDATA)
&& mc.mc_xcursor->mx_db.md_entries > 1) {
/* Если у ключа больше одного значения, то
* сначала удаляем найденое "старое" значение.
*
* Этого можно не делать, так как MDBX уже
* обучен корректно обрабатывать такие ситуации.
*
* Однако, следует помнить, что в LMDB при
* совпадении размера данных, значение будет
* просто перезаписано с нарушением
* упорядоченности, что сломает поиск. */
rc = mdbx_cursor_del(&mc, 0);
if (rc != MDB_SUCCESS)
goto bailout;
flags -= MDB_CURRENT;
}
#endif
}
} else {
/* в old_data буфер получения предыдущего значения */
/* в old_data буфер для сохранения предыдущего значения */
if (unlikely(new_data && old_data->iov_base == new_data->iov_base))
return EINVAL;
MDB_val present_data;
rc = mdbx_cursor_get(&mc, &present_key, &present_data, MDB_SET_KEY);
if (unlikely(rc != MDB_SUCCESS)) {
@ -468,14 +499,20 @@ int mdbx_replace(MDB_txn *txn, MDB_dbi dbi,
MDB_node *leaf = NODEPTR(page, mc.mc_ki[mc.mc_top]);
if (F_ISSET(leaf->mn_flags, F_DUPDATA)) {
mdb_tassert(txn, XCURSOR_INITED(&mc) && mc.mc_xcursor->mx_db.md_entries > 1);
rc = MDB_KEYEXIST;
goto bailout;
if (mc.mc_xcursor->mx_db.md_entries > 1) {
rc = MDBX_EMULTIVAL;
goto bailout;
}
}
/* если данные совпадают, то ничего делать не надо */
if (new_data && mdbx_is_samedata(&present_data, new_data)) {
*old_data = *new_data;
goto bailout;
}
/* В оригинальной LMDB фладок MDB_CURRENT здесь приведет
* к замене данных без учета MDB_DUPSORT сортировки,
* но здесь это в любом случае допустимо, так как мы
* проверили что для ключа есть только одно значение. */
} else if ((flags & MDB_NODUPDATA) && mdbx_is_samedata(&present_data, new_data)) {
/* если данные совпадают и установлен MDB_NODUPDATA */
rc = MDB_KEYEXIST;
@ -494,7 +531,7 @@ int mdbx_replace(MDB_txn *txn, MDB_dbi dbi,
if (unlikely(old_data->iov_len < present_data.iov_len)) {
old_data->iov_base = NULL;
old_data->iov_len = present_data.iov_len;
rc = -1;
rc = MDBX_RESULT_TRUE;
goto bailout;
}
memcpy(old_data->iov_base, present_data.iov_base, present_data.iov_len);
@ -514,3 +551,148 @@ bailout:
txn->mt_cursors[dbi] = mc.mc_next;
return rc;
}
int
mdbx_get_ex(MDB_txn *txn, MDB_dbi dbi,
MDB_val *key, MDB_val *data, int* values_count)
{
DKBUF;
mdb_debug("===> get db %u key [%s]", dbi, DKEY(key));
if (unlikely(!key || !data || !txn))
return EINVAL;
if (unlikely(txn->mt_signature != MDBX_MT_SIGNATURE))
return MDB_VERSION_MISMATCH;
if (unlikely(!TXN_DBI_EXIST(txn, dbi, DB_USRVALID)))
return EINVAL;
if (unlikely(txn->mt_flags & MDB_TXN_BLOCKED))
return MDB_BAD_TXN;
MDB_cursor mc;
MDB_xcursor mx;
mdb_cursor_init(&mc, txn, dbi, &mx);
int exact = 0;
int rc = mdb_cursor_set(&mc, key, data, MDB_SET_KEY, &exact);
if (unlikely(rc != MDB_SUCCESS)) {
if (rc == MDB_NOTFOUND && values_count)
*values_count = 0;
return rc;
}
if (values_count) {
*values_count = 1;
if (mc.mc_xcursor != NULL) {
MDB_node *leaf = NODEPTR(mc.mc_pg[mc.mc_top], mc.mc_ki[mc.mc_top]);
if (F_ISSET(leaf->mn_flags, F_DUPDATA)) {
mdb_tassert(txn, mc.mc_xcursor == &mx
&& (mx.mx_cursor.mc_flags & C_INITIALIZED));
*values_count = mx.mx_db.md_entries;
}
}
}
return MDB_SUCCESS;
}
/* Функция сообщает находится ли указанный адрес в "грязной" странице у
* заданной пишущей транзакции. В конечном счете это позволяет избавиться от
* лишнего копирования данных из НЕ-грязных страниц.
*
* "Грязные" страницы - это те, которые уже были изменены в ходе пишущей
* транзакции. Соответственно, какие-либо дальнейшие изменения могут привести
* к перезаписи таких страниц. Поэтому все функции, выполняющие изменения, в
* качестве аргументов НЕ должны получать указатели на данные в таких
* страницах. В свою очередь "НЕ грязные" страницы перед модификацией будут
* скопированы.
*
* Другими словами, данные из "грязных" страниц должны быть либо скопированы
* перед передачей в качестве аргументов для дальнейших модификаций, либо
* отвергнуты на стадии проверки корректности аргументов.
*
* Таким образом, функция позволяет как избавится от лишнего копирования,
* так и выполнить более полную проверку аргументов.
*
* ВАЖНО: Передаваемый указатель должен указывать на начало данных. Только
* так гарантируется что актуальный заголовок страницы будет физически
* расположен в той-же странице памяти, в том числе для многостраничных
* P_OVERFLOW страниц с длинными данными. */
int mdbx_is_dirty(const MDB_txn *txn, const void* ptr)
{
if (unlikely(!txn))
return EINVAL;
if(unlikely(txn->mt_signature != MDBX_MT_SIGNATURE))
return MDB_VERSION_MISMATCH;
if (unlikely(txn->mt_flags & MDB_TXN_RDONLY))
return MDB_BAD_TXN;
const MDB_env *env = txn->mt_env;
const uintptr_t mask = ~(uintptr_t) (env->me_psize - 1);
const MDB_page *page = (const MDB_page *) ((uintptr_t) ptr & mask);
/* LY: Тут не всё хорошо с абсолютной достоверностью результата,
* так как флажок P_DIRTY в LMDB может означать не совсем то,
* что было исходно задумано, детали см в логике кода mdb_page_touch().
*
* Более того, в режиме БЕЗ WRITEMAP грязные страницы выделяются через
* malloc(), т.е. находятся вне mmap-диаппазона.
*
* Тем не менее, однозначно страница "не грязная" если:
* - адрес находится внутри mmap-диаппазона и в заголовке страницы
* нет флажка P_DIRTY, то однозначно страница "не грязная".
* - адрес вне mmap-диаппазона и его нет среди списка "грязных" страниц.
*/
if (env->me_map < (char*) page) {
const size_t used_size = env->me_psize * txn->mt_next_pgno;
if (env->me_map + used_size > (char*) page) {
/* страница внутри диапазона */
if (page->mp_flags & P_DIRTY)
return MDBX_RESULT_TRUE;
return MDBX_RESULT_FALSE;
}
/* Гипотетически здесь возможна ситуация, когда указатель адресует что-то
* в пределах mmap, но за границей распределенных страниц. Это тяжелая
* ошибка, которой не возможно добиться без каких-то мега-нарушений.
* Поэтому не проверяем этот случай кроме как assert-ом, ибо бестолку. */
mdb_tassert(txn, env->me_map + env->me_mapsize > (char*) page);
}
/* Страница вне mmap-диаппазона */
if (env->me_flags & MDB_WRITEMAP)
/* Если MDB_WRITEMAP, то результат уже ясен. */
return MDBX_RESULT_FALSE;
/* Смотрим список грязных страниц у заданной транзакции. */
MDB_ID2 *list = txn->mt_u.dirty_list;
if (list) {
unsigned i, n = list[0].mid;
for (i = 1; i <= n; i++) {
const MDB_page *dirty = list[i].mptr;
if (dirty == page)
return MDBX_RESULT_TRUE;
}
}
/* При вложенных транзакциях, страница может быть в dirty-списке
* родительской транзакции, но в этом случае она будет скопирована перед
* изменением в текущей транзакции, т.е. относительно заданной транзакции
* проверяемый адрес "не грязный". */
return MDBX_RESULT_FALSE;
}
int mdbx_dbi_open_ex(MDB_txn *txn, const char *name, unsigned flags,
MDB_dbi *pdbi, MDB_cmp_func *keycmp, MDB_cmp_func *datacmp)
{
int rc = mdbx_dbi_open(txn, name, flags, pdbi);
if (likely(rc == MDB_SUCCESS)) {
MDB_dbi dbi = *pdbi;
unsigned flags = txn->mt_dbs[dbi].md_flags;
txn->mt_dbxs[dbi].md_cmp = keycmp ? keycmp : mdbx_default_keycmp(flags);
txn->mt_dbxs[dbi].md_dcmp = datacmp ? datacmp : mdbx_default_datacmp(flags);
}
return rc;
}

16
mdbx.h
View File

@ -219,12 +219,26 @@ typedef struct mdbx_canary {
int mdbx_canary_put(MDB_txn *txn, const mdbx_canary* canary);
size_t mdbx_canary_get(MDB_txn *txn, mdbx_canary* canary);
/** Returns 1 when no more data available or cursor not positioned,
/* Returns 1 when no more data available or cursor not positioned,
* 0 otherwise or less that zero in error case. */
int mdbx_cursor_eof(MDB_cursor *mc);
#define MDBX_EMULTIVAL (MDB_LAST_ERRCODE - 42)
#define MDBX_RESULT_FALSE MDB_SUCCESS
#define MDBX_RESULT_TRUE (-1)
int mdbx_replace(MDB_txn *txn, MDB_dbi dbi,
MDB_val *key, MDB_val *new_data, MDB_val *old_data, unsigned flags);
/* Same as mdbx_get(), but:
* 1) if values_count is not NULL, then returns the count
* of multi-values/duplicates for a given key.
* 2) updates the key for pointing to the actual key's data inside DB. */
int mdbx_get_ex(MDB_txn *txn, MDB_dbi dbi, MDB_val *key, MDB_val *data, int* values_count);
int mdbx_is_dirty(const MDB_txn *txn, const void* ptr);
int mdbx_dbi_open_ex(MDB_txn *txn, const char *name, unsigned flags,
MDB_dbi *dbi, MDB_cmp_func *keycmp, MDB_cmp_func *datacmp);
/** @} */

View File

@ -57,7 +57,7 @@
#endif /* __must_check_result */
#ifndef __hot
# if defined(NDEBUG) && (defined(__GNUC__) && !defined(__clang__))
# if defined(__OPTIMIZE__) && (defined(__GNUC__) && !defined(__clang__))
# define __hot __attribute__((hot, optimize("O3")))
# elif defined(__GNUC__)
/* cland case, just put frequently used functions in separate section */
@ -68,7 +68,7 @@
#endif /* __hot */
#ifndef __cold
# if defined(NDEBUG) && (defined(__GNUC__) && !defined(__clang__))
# if defined(__OPTIMIZE__) && (defined(__GNUC__) && !defined(__clang__))
# define __cold __attribute__((cold, optimize("Os")))
# elif defined(__GNUC__)
/* cland case, just put infrequently used functions in separate section */
@ -79,7 +79,7 @@
#endif /* __cold */
#ifndef __flatten
# if defined(NDEBUG) && (defined(__GNUC__) || defined(__clang__))
# if defined(__OPTIMIZE__) && (defined(__GNUC__) || defined(__clang__))
# define __flatten __attribute__((flatten))
# else
# define __flatten