diff --git a/README.md b/README.md index 29ec7d20..d1575cd6 100644 --- a/README.md +++ b/README.md @@ -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()` получить значение по +заданному ключу, одновременно с количеством дубликатов. diff --git a/lmdb.h b/lmdb.h index 701f4b46..b6a55f11 100644 --- a/lmdb.h +++ b/lmdb.h @@ -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 diff --git a/mdb.c b/mdb.c index 08dccaaa..ce088c72 100644 --- a/mdb.c +++ b/mdb.c @@ -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) diff --git a/mdb_chk.c b/mdb_chk.c index d23ce2cd..1422eea1 100644 --- a/mdb_chk.c +++ b/mdb_chk.c @@ -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) diff --git a/mdbx.c b/mdbx.c index ff2ba67a..71fd224b 100644 --- a/mdbx.c +++ b/mdbx.c @@ -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; +} diff --git a/mdbx.h b/mdbx.h index 3a0eda23..6a266395 100644 --- a/mdbx.h +++ b/mdbx.h @@ -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); /** @} */ diff --git a/reopen.h b/reopen.h index 72f8e3ad..dd214172 100644 --- a/reopen.h +++ b/reopen.h @@ -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