diff --git a/README.md b/README.md index 2ab0d54..be157cd 100644 --- a/README.md +++ b/README.md @@ -87,16 +87,16 @@ transaction is aborted automatically. To commit or abort, use `commit()` or `abort()`, after which going out of scope has no further effect. ``` - txn.put(dbi, "lmdb", "great"); + txn->put(dbi, "lmdb", "great"); string_view data; - if(!txn.get(dbi, "lmdb", data)) { + if(!txn->get(dbi, "lmdb", data)) { cout<< "Within RW transaction, found that lmdb = " << data <commit(); ``` LMDB is so fast because it does not copy data unless it really needs to. @@ -129,8 +129,8 @@ For example, to store `double` values for 64 bit IDs: auto txn = env->getRWTransaction(); uint64_t id=12345678901; double score=3.14159; - txn.put(dbi, id, score); - txn.commit(); + txn->put(dbi, id, score); + txn->commit(); ``` Behind the scenes, the `id` and `score` values are wrapped by `MDBInVal` @@ -142,7 +142,7 @@ works similary: uint64_t id=12345678901; MDBOutValue val; - txn.get(dbi, id, val); + txn->get(dbi, id, val); cout << "Score: " << val.get() << "\n"; ``` @@ -170,10 +170,10 @@ struct Coordinate C c{12.0, 13.0}; -txn.put(dbi, MDBInVal::fromStruct(c), 12.0); +txn->put(dbi, MDBInVal::fromStruct(c), 12.0); MDBOutVal res; -txn.get(dbi, MDBInVal::fromStruct(c), res); +txn->get(dbi, MDBInVal::fromStruct(c), res); auto c1 = res.get_struct(); ``` @@ -193,7 +193,7 @@ calls to mdb. This is the usual opening sequence. ``` - auto cursor=txn.getCursor(dbi); + auto cursor=txn->getCursor(dbi); MDBOutVal key, data; int count=0; cout<<"Counting records.. "; cout.flush(); @@ -212,7 +212,7 @@ records in under a second (!). ``` cout<<"Clearing records.. "; cout.flush(); - mdb_drop(txn, dbi, 0); // clear records + mdb_drop(*txn, dbi, 0); // clear records cout<<"Done!"<put(dbi, n, n); } cout <<"Done!"<commit(); cout<<"Done!"<getROTransaction(); MDBOutVal data; - if(!rotxn.get(dbi, "lmdb", data)) { + if(!rotxn->get(dbi, "lmdb", data)) { cout<< "Outside RW transaction, found that lmdb = " << data.get() <openDB("example", MDB_CREATE); auto txn = env->getRWTransaction(); - mdb_drop(txn, dbi, 0); - txn.put(dbi, "lmdb", "great"); + mdb_drop(*txn, dbi, 0); + txn->put(dbi, "lmdb", "great"); MDBOutVal data; - if(!txn.get(dbi, "lmdb", data)) { + if(!txn->get(dbi, "lmdb", data)) { cout<< "Within RW transaction, found that lmdb = " << data.get() <commit(); cout<<"Committed data"<getRWTransaction(); - mdb_drop(txn, dbi, 0); - txn.commit(); + mdb_drop(*txn, dbi, 0); + txn->commit(); } diff --git a/lmdb-safe.cc b/lmdb-safe.cc index eea5864..7397cd3 100644 --- a/lmdb-safe.cc +++ b/lmdb-safe.cc @@ -139,53 +139,107 @@ MDBDbi MDBEnv::openDB(const string_view dbname, int flags) if(!(envflags & MDB_RDONLY)) { auto rwt = getRWTransaction(); - MDBDbi ret = rwt.openDB(dbname, flags); - rwt.commit(); + MDBDbi ret = rwt->openDB(dbname, flags); + rwt->commit(); return ret; } MDBDbi ret; { auto rwt = getROTransaction(); - ret = rwt.openDB(dbname, flags); + ret = rwt->openDB(dbname, flags); } return ret; } -MDBRWTransaction::MDBRWTransaction(MDBEnv* parent, int flags) : d_parent(parent) +MDBRWTransactionImpl::MDBRWTransactionImpl(MDBEnv *parent, MDB_txn *txn): + MDBROTransactionImpl(parent, txn) + { - if(d_parent->getROTX() || d_parent->getRWTX()) + +} + +MDB_txn *MDBRWTransactionImpl::openRWTransaction(MDBEnv *env, MDB_txn *parent, int flags) +{ + MDB_txn *result; + if(env->getROTX() || env->getRWTX()) throw std::runtime_error("Duplicate RW transaction"); for(int tries =0 ; tries < 3; ++tries) { // it might happen twice, who knows - if(int rc=mdb_txn_begin(d_parent->d_env, 0, flags, &d_txn)) { + if(int rc=mdb_txn_begin(env->d_env, parent, flags, &result)) { if(rc == MDB_MAP_RESIZED && tries < 2) { // "If the mapsize is increased by another process (..) mdb_txn_begin() will return MDB_MAP_RESIZED. // call mdb_env_set_mapsize with a size of zero to adopt the new size." - mdb_env_set_mapsize(d_parent->d_env, 0); + mdb_env_set_mapsize(env->d_env, 0); continue; } throw std::runtime_error("Unable to start RW transaction: "+std::string(mdb_strerror(rc))); } break; } - d_parent->incRWTX(); + env->incRWTX(); + return result; } -MDBROTransaction::MDBROTransaction(MDBEnv* parent, int flags) : d_parent(parent) +MDBRWTransactionImpl::MDBRWTransactionImpl(MDBEnv* parent, int flags): + MDBRWTransactionImpl(parent, openRWTransaction(parent, nullptr, flags)) { - if(d_parent->getRWTX()) +} + +MDBRWTransactionImpl::~MDBRWTransactionImpl() +{ + abort(); +} + +void MDBRWTransactionImpl::commit() +{ + closeRORWCursors(); + if (!d_txn) { + return; + } + + if(int rc = mdb_txn_commit(d_txn)) { + throw std::runtime_error("committing: " + std::string(mdb_strerror(rc))); + } + environment().decRWTX(); + d_txn = nullptr; +} + +void MDBRWTransactionImpl::abort() +{ + closeRORWCursors(); + if (!d_txn) { + return; + } + + mdb_txn_abort(d_txn); + // prevent the RO destructor from cleaning up the transaction itself + environment().decRWTX(); + d_txn = nullptr; +} + +MDBROTransactionImpl::MDBROTransactionImpl(MDBEnv *parent, MDB_txn *txn): + d_parent(parent), + d_cursors(), + d_txn(txn) +{ + +} + +MDB_txn *MDBROTransactionImpl::openROTransaction(MDBEnv *env, MDB_txn *parent, int flags) +{ + if(env->getRWTX()) throw std::runtime_error("Duplicate RO transaction"); /* A transaction and its cursors must only be used by a single thread, and a thread may only have a single transaction at a time. If MDB_NOTLS is in use, this does not apply to read-only transactions. */ - + MDB_txn *result = nullptr; for(int tries =0 ; tries < 3; ++tries) { // it might happen twice, who knows - if(int rc=mdb_txn_begin(d_parent->d_env, 0, MDB_RDONLY | flags, &d_txn)) { + if(int rc=mdb_txn_begin(env->d_env, parent, MDB_RDONLY | flags, &result)) { if(rc == MDB_MAP_RESIZED && tries < 2) { // "If the mapsize is increased by another process (..) mdb_txn_begin() will return MDB_MAP_RESIZED. // call mdb_env_set_mapsize with a size of zero to adopt the new size." - mdb_env_set_mapsize(d_parent->d_env, 0); + mdb_env_set_mapsize(env->d_env, 0); continue; } @@ -193,43 +247,125 @@ MDBROTransaction::MDBROTransaction(MDBEnv* parent, int flags) : d_parent(parent) } break; } - d_parent->incROTX(); + env->incROTX(); + + return result; +} + +void MDBROTransactionImpl::closeROCursors() +{ + // we need to move the vector away to ensure that the cursors don’t mess with our iteration. + std::vector buf; + std::swap(d_cursors, buf); + for (auto &cursor: buf) { + cursor->close(); + } +} + +MDBROTransactionImpl::MDBROTransactionImpl(MDBEnv *parent, int flags): + MDBROTransactionImpl(parent, openROTransaction(parent, nullptr, flags)) +{ + +} + +MDBROTransactionImpl::~MDBROTransactionImpl() +{ + // this is safe because C++ will not call overrides of virtual methods in destructors. + commit(); +} + +void MDBROTransactionImpl::abort() +{ + closeROCursors(); + // if d_txn is non-nullptr here, either the transaction object was invalidated earlier (e.g. by moving from it), or it is an RW transaction which has already cleaned up the d_txn pointer (with an abort). + if (d_txn) { + d_parent->decROTX(); + mdb_txn_abort(d_txn); // this appears to work better than abort for r/o database opening + d_txn = nullptr; + } +} + +void MDBROTransactionImpl::commit() +{ + closeROCursors(); + // if d_txn is non-nullptr here, either the transaction object was invalidated earlier (e.g. by moving from it), or it is an RW transaction which has already cleaned up the d_txn pointer (with an abort). + if (d_txn) { + d_parent->decROTX(); + mdb_txn_commit(d_txn); // this appears to work better than abort for r/o database opening + d_txn = nullptr; + } } -void MDBRWTransaction::clear(MDB_dbi dbi) +void MDBRWTransactionImpl::clear(MDB_dbi dbi) { if(int rc = mdb_drop(d_txn, dbi, 0)) { throw runtime_error("Error clearing database: " + MDBError(rc)); } } -MDBRWCursor MDBRWTransaction::getCursor(const MDBDbi& dbi) +MDBRWCursor MDBRWTransactionImpl::getRWCursor(const MDBDbi& dbi) { - return MDBRWCursor(this, dbi); + MDB_cursor *cursor; + int rc= mdb_cursor_open(d_txn, dbi, &cursor); + if(rc) { + throw std::runtime_error("Error creating RO cursor: "+std::string(mdb_strerror(rc))); + } + return MDBRWCursor(d_rw_cursors, cursor); +} + +MDBRWCursor MDBRWTransactionImpl::getCursor(const MDBDbi &dbi) +{ + return getRWCursor(dbi); +} + +MDBRWTransaction MDBRWTransactionImpl::getRWTransaction() +{ + MDB_txn *txn; + if (int rc = mdb_txn_begin(environment(), *this, 0, &txn)) { + throw std::runtime_error(std::string("failed to start child transaction: ")+mdb_strerror(rc)); + } + // we need to increase the counter here because commit/abort on the child transaction will decrease it + environment().incRWTX(); + return MDBRWTransaction(new MDBRWTransactionImpl(&environment(), txn)); +} + +MDBROTransaction MDBRWTransactionImpl::getROTransaction() +{ + return std::move(getRWTransaction()); } MDBROTransaction MDBEnv::getROTransaction() { - return MDBROTransaction(this); + return MDBROTransaction(new MDBROTransactionImpl(this)); } MDBRWTransaction MDBEnv::getRWTransaction() { - return MDBRWTransaction(this); + return MDBRWTransaction(new MDBRWTransactionImpl(this)); } -void MDBRWTransaction::closeCursors() +void MDBRWTransactionImpl::closeRWCursors() { - for(auto& c : d_cursors) - c->close(); - d_cursors.clear(); + decltype(d_rw_cursors) buf; + std::swap(d_rw_cursors, buf); + for (auto &cursor: buf) { + cursor->close(); + } } -MDBROCursor MDBROTransaction::getCursor(const MDBDbi& dbi) +MDBROCursor MDBROTransactionImpl::getCursor(const MDBDbi& dbi) { - return MDBROCursor(this, dbi); + return getROCursor(dbi); } - +MDBROCursor MDBROTransactionImpl::getROCursor(const MDBDbi &dbi) +{ + MDB_cursor *cursor; + int rc= mdb_cursor_open(d_txn, dbi, &cursor); + if(rc) { + throw std::runtime_error("Error creating RO cursor: "+std::string(mdb_strerror(rc))); + } + return MDBROCursor(d_cursors, cursor); +} diff --git a/lmdb-safe.hh b/lmdb-safe.hh index fd76086..5185807 100644 --- a/lmdb-safe.hh +++ b/lmdb-safe.hh @@ -9,6 +9,8 @@ #include #include #include +#include +#include // apple compiler somehow has string_view even in c++11! #if __cplusplus < 201703L && !defined(__APPLE__) @@ -57,8 +59,11 @@ public: MDB_dbi d_dbi; }; -class MDBRWTransaction; -class MDBROTransaction; +class MDBRWTransactionImpl; +class MDBROTransactionImpl; + +using MDBROTransaction = std::unique_ptr; +using MDBRWTransaction = std::unique_ptr; class MDBEnv { @@ -220,35 +225,36 @@ private: class MDBROCursor; -class MDBROTransaction +class MDBROTransactionImpl { +protected: + MDBROTransactionImpl(MDBEnv *parent, MDB_txn *txn); + +private: + static MDB_txn *openROTransaction(MDBEnv *env, MDB_txn *parent, int flags=0); + + MDBEnv* d_parent; + std::vector d_cursors; + +protected: + MDB_txn* d_txn; + + void closeROCursors(); + public: - explicit MDBROTransaction(MDBEnv* parent, int flags=0); + explicit MDBROTransactionImpl(MDBEnv* parent, int flags=0); - MDBROTransaction(MDBROTransaction&& rhs) - { - d_parent = rhs.d_parent; - d_txn = rhs.d_txn; - rhs.d_parent = 0; - rhs.d_txn = 0; - } + MDBROTransactionImpl(const MDBROTransactionImpl& src) = delete; + MDBROTransactionImpl &operator=(const MDBROTransactionImpl& src) = delete; - void reset() - { - // this does not free cursors - mdb_txn_reset(d_txn); - d_parent->decROTX(); - } + // The move constructor/operator cannot be made safe due to Object Slicing with MDBRWTransaction. + MDBROTransactionImpl(MDBROTransactionImpl&& rhs) = delete; + MDBROTransactionImpl &operator=(MDBROTransactionImpl &&rhs) = delete; - void renew() - { - if(d_parent->getROTX()) - throw std::runtime_error("Duplicate RO transaction"); - if(int rc = mdb_txn_renew(d_txn)) - throw std::runtime_error("Renewing RO transaction: "+std::string(mdb_strerror(rc))); - d_parent->incROTX(); - } - + virtual ~MDBROTransactionImpl(); + + virtual void abort(); + virtual void commit(); int get(MDB_dbi dbi, const MDBInVal& key, MDBOutVal& val) { @@ -280,22 +286,21 @@ public: } MDBROCursor getCursor(const MDBDbi&); + MDBROCursor getROCursor(const MDBDbi&); - ~MDBROTransaction() - { - if(d_txn) { - d_parent->decROTX(); - mdb_txn_commit(d_txn); // this appears to work better than abort for r/o database opening - } - } - - operator MDB_txn*&() + operator MDB_txn*() { return d_txn; } - - MDBEnv* d_parent; - MDB_txn* d_txn; + + inline operator bool() const { + return d_txn; + } + + inline MDBEnv &environment() + { + return *d_parent; + } }; /* @@ -304,12 +309,75 @@ public: "If the parent transaction commits, the cursor must not be used again." */ -template +template class MDBGenCursor { +private: + std::vector *d_registry; + MDB_cursor* d_cursor; + +public: + MDBGenCursor(): + d_registry(nullptr), + d_cursor(nullptr) + { + + } + + MDBGenCursor(std::vector ®istry, MDB_cursor *cursor): + d_registry(®istry), + d_cursor(cursor) + { + registry.emplace_back(static_cast(this)); + } + +private: + void move_from(MDBGenCursor *src) + { + if (!d_registry) { + return; + } + + auto iter = std::find(d_registry->begin(), + d_registry->end(), + src); + if (iter != d_registry->end()) { + *iter = static_cast(this); + } else { + d_registry->emplace_back(static_cast(this)); + } + } + +public: + MDBGenCursor(const MDBGenCursor &src) = delete; + + MDBGenCursor(MDBGenCursor &&src) noexcept: + d_registry(src.d_registry), + d_cursor(src.d_cursor) + { + move_from(&src); + src.d_registry = nullptr; + src.d_cursor = nullptr; + } + + MDBGenCursor &operator=(const MDBGenCursor &src) = delete; + + MDBGenCursor &operator=(MDBGenCursor &&src) noexcept + { + d_registry = src.d_registry; + d_cursor = src.d_cursor; + move_from(&src); + src.d_registry = nullptr; + src.d_cursor = nullptr; + return *this; + } + + ~MDBGenCursor() + { + close(); + } + public: - MDBGenCursor(Transaction *t) : d_parent(t) - {} int get(MDBOutVal& key, MDBOutVal& data, MDB_cursor_op op) { int rc = mdb_cursor_get(d_cursor, &key.d_mdbval, &data.d_mdbval, op); @@ -377,101 +445,78 @@ public: return currentlast(key, data, MDB_FIRST); } - operator MDB_cursor*&() + operator MDB_cursor*() { return d_cursor; } - MDB_cursor* d_cursor; - Transaction* d_parent; -}; - -class MDBROCursor : public MDBGenCursor -{ -public: - MDBROCursor(MDBROTransaction* parent, const MDB_dbi& dbi) : MDBGenCursor(parent) + operator bool() const { - int rc= mdb_cursor_open(d_parent->d_txn, dbi, &d_cursor); - if(rc) { - throw std::runtime_error("Error creating RO cursor: "+std::string(mdb_strerror(rc))); - } - } - MDBROCursor(MDBROCursor&& rhs) : MDBGenCursor(rhs.d_parent) - { - d_cursor = rhs.d_cursor; - rhs.d_cursor=0; + return d_cursor; } void close() { - mdb_cursor_close(d_cursor); - d_cursor=0; - } - - ~MDBROCursor() - { - if(d_cursor) + if (d_registry) { + auto iter = std::find(d_registry->begin(), + d_registry->end(), + static_cast(this)); + if (iter != d_registry->end()) { + d_registry->erase(iter); + } + d_registry = nullptr; + } + if (d_cursor) { mdb_cursor_close(d_cursor); + d_cursor = nullptr; + } } +}; + +class MDBROCursor : public MDBGenCursor +{ +public: + MDBROCursor() = default; + using MDBGenCursor::MDBGenCursor; + MDBROCursor(const MDBROCursor &src) = delete; + MDBROCursor(MDBROCursor &&src) = default; + MDBROCursor &operator=(const MDBROCursor &src) = delete; + MDBROCursor &operator=(MDBROCursor &&src) = default; + ~MDBROCursor() = default; }; class MDBRWCursor; -class MDBRWTransaction +class MDBRWTransactionImpl: public MDBROTransactionImpl { +protected: + MDBRWTransactionImpl(MDBEnv* parent, MDB_txn* txn); + +private: + static MDB_txn *openRWTransaction(MDBEnv* env, MDB_txn *parent, int flags); + +private: + std::vector d_rw_cursors; + + void closeRWCursors(); + inline void closeRORWCursors() { + closeROCursors(); + closeRWCursors(); + } + public: - explicit MDBRWTransaction(MDBEnv* parent, int flags=0); + explicit MDBRWTransactionImpl(MDBEnv* parent, int flags=0); - MDBRWTransaction(MDBRWTransaction&& rhs) - { - d_parent = rhs.d_parent; - d_txn = rhs.d_txn; - rhs.d_parent = 0; - rhs.d_txn = 0; - } + MDBRWTransactionImpl(const MDBRWTransactionImpl& rhs) = delete; + MDBRWTransactionImpl(MDBRWTransactionImpl&& rhs) = delete; + MDBRWTransactionImpl &operator=(const MDBRWTransactionImpl& rhs) = delete; + MDBRWTransactionImpl &operator=(MDBRWTransactionImpl&& rhs) = delete; - MDBRWTransaction& operator=(MDBRWTransaction&& rhs) - { - if(d_txn) - abort(); - - d_parent = rhs.d_parent; - d_txn = rhs.d_txn; - rhs.d_parent = 0; - rhs.d_txn = 0; - - return *this; - } - - ~MDBRWTransaction() - { - if(d_txn) { - d_parent->decRWTX(); - closeCursors(); - mdb_txn_abort(d_txn); // XXX check response? - } - } - void closeCursors(); + ~MDBRWTransactionImpl() override; - void commit() - { - closeCursors(); - if(int rc = mdb_txn_commit(d_txn)) { - throw std::runtime_error("committing: " + std::string(mdb_strerror(rc))); - } - d_parent->decRWTX(); - - d_txn=0; - } - - void abort() - { - closeCursors(); - mdb_txn_abort(d_txn); // XXX check error? - d_txn = 0; - d_parent->decRWTX(); - } + void commit() override; + void abort() override; void clear(MDB_dbi dbi); @@ -529,79 +574,35 @@ public: MDBDbi openDB(string_view dbname, int flags) { - return MDBDbi(d_parent->d_env, d_txn, dbname, flags); + return MDBDbi(environment().d_env, d_txn, dbname, flags); } + MDBRWCursor getRWCursor(const MDBDbi&); MDBRWCursor getCursor(const MDBDbi&); - void reportCursor(MDBRWCursor* child) - { - d_cursors.insert(child); - } - void unreportCursor(MDBRWCursor* child) - { - d_cursors.erase(child); - } - - void reportCursorMove(MDBRWCursor* from, MDBRWCursor* to) - { - d_cursors.erase(from); - d_cursors.insert(to); - } - - operator MDB_txn*&() - { - return d_txn; - } - - - - std::set d_cursors; - MDBEnv* d_parent; - MDB_txn* d_txn; + MDBRWTransaction getRWTransaction(); + MDBROTransaction getROTransaction(); }; /* "A cursor in a write-transaction can be closed before its transaction ends, and will otherwise be closed when its transaction ends" This is a problem for us since it may means we are closing the cursor twice, which is bad */ -class MDBRWCursor : public MDBGenCursor +class MDBRWCursor : public MDBGenCursor { public: - MDBRWCursor(MDBRWTransaction* parent, const MDB_dbi& dbi) : MDBGenCursor(parent) - { - int rc= mdb_cursor_open(d_parent->d_txn, dbi, &d_cursor); - if(rc) { - throw std::runtime_error("Error creating RW cursor: "+std::string(mdb_strerror(rc))); - } - d_parent->reportCursor(this); - } - MDBRWCursor(MDBRWCursor&& rhs) : MDBGenCursor(rhs.d_parent) - { - d_cursor = rhs.d_cursor; - rhs.d_cursor=0; - d_parent->reportCursorMove(&rhs, this); - } - - void close() - { - if(d_cursor) - mdb_cursor_close(d_cursor); - d_cursor=0; - } - - ~MDBRWCursor() - { - if(d_cursor) - mdb_cursor_close(d_cursor); - d_parent->unreportCursor(this); - } - + MDBRWCursor() = default; + using MDBGenCursor::MDBGenCursor; + MDBRWCursor(const MDBRWCursor &src) = delete; + MDBRWCursor(MDBRWCursor &&src) = default; + MDBRWCursor &operator=(const MDBRWCursor &src) = delete; + MDBRWCursor &operator=(MDBRWCursor &&src) = default; + ~MDBRWCursor() = default; void put(const MDBOutVal& key, const MDBInVal& data) { - int rc = mdb_cursor_put(d_cursor, - const_cast(&key.d_mdbval), - const_cast(&data.d_mdbval), MDB_CURRENT); + int rc = mdb_cursor_put(*this, + const_cast(&key.d_mdbval), + const_cast(&data.d_mdbval), MDB_CURRENT); if(rc) throw std::runtime_error("mdb_cursor_put: " + std::string(mdb_strerror(rc))); } @@ -610,14 +611,15 @@ public: int put(const MDBOutVal& key, const MDBOutVal& data, int flags=0) { // XXX check errors - return mdb_cursor_put(d_cursor, + return mdb_cursor_put(*this, const_cast(&key.d_mdbval), const_cast(&data.d_mdbval), flags); } int del(int flags=0) { - return mdb_cursor_del(d_cursor, flags); + return mdb_cursor_del(*this, flags); } + }; diff --git a/lmdb-typed.cc b/lmdb-typed.cc index 95206e3..b4ecd38 100644 --- a/lmdb-typed.cc +++ b/lmdb-typed.cc @@ -2,7 +2,7 @@ unsigned int MDBGetMaxID(MDBRWTransaction& txn, MDBDbi& dbi) { - auto cursor = txn.getCursor(dbi); + auto cursor = txn->getRWCursor(dbi); MDBOutVal maxidval, maxcontent; unsigned int maxid{0}; if(!cursor.get(maxidval, maxcontent, MDB_LAST)) { diff --git a/lmdb-typed.hh b/lmdb-typed.hh index a64dd11..b573841 100644 --- a/lmdb-typed.hh +++ b/lmdb-typed.hh @@ -101,12 +101,12 @@ struct LMDBIndexOps explicit LMDBIndexOps(Parent* parent) : d_parent(parent){} void put(MDBRWTransaction& txn, const Class& t, uint32_t id, int flags=0) { - txn.put(d_idx, keyConv(d_parent->getMember(t)), id, flags); + txn->put(d_idx, keyConv(d_parent->getMember(t)), id, flags); } void del(MDBRWTransaction& txn, const Class& t, uint32_t id) { - if(int rc = txn.del(d_idx, keyConv(d_parent->getMember(t)), id)) { + if(int rc = txn->del(d_idx, keyConv(d_parent->getMember(t)), id)) { throw std::runtime_error("Error deleting from index: " + std::string(mdb_strerror(rc))); } } @@ -205,7 +205,7 @@ public: uint32_t size() { MDB_stat stat; - mdb_stat(*d_parent.d_txn, d_parent.d_parent->d_main, &stat); + mdb_stat(**d_parent.d_txn, d_parent.d_parent->d_main, &stat); return stat.ms_entries; } @@ -214,7 +214,7 @@ public: uint32_t size() { MDB_stat stat; - mdb_stat(*d_parent.d_txn, std::get(d_parent.d_parent->d_tuple).d_idx, &stat); + mdb_stat(**d_parent.d_txn, std::get(d_parent.d_parent->d_tuple).d_idx, &stat); return stat.ms_entries; } @@ -222,7 +222,7 @@ public: bool get(uint32_t id, T& t) { MDBOutVal data; - if(d_parent.d_txn->get(d_parent.d_parent->d_main, id, data)) + if((*d_parent.d_txn)->get(d_parent.d_parent->d_main, id, data)) return false; serFromString(data.get(), t); @@ -234,7 +234,7 @@ public: uint32_t get(const typename std::tuple_element::type::type& key, T& out) { MDBOutVal id; - if(!d_parent.d_txn->get(std::get(d_parent.d_parent->d_tuple).d_idx, keyConv(key), id)) { + if(!(*d_parent.d_txn)->get(std::get(d_parent.d_parent->d_tuple).d_idx, keyConv(key), id)) { if(get(id.get(), out)) return id.get(); } @@ -245,7 +245,7 @@ public: template uint32_t cardinality() { - auto cursor = d_parent.d_txn->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); + auto cursor = (*d_parent.d_txn)->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); bool first = true; MDBOutVal key, data; uint32_t count = 0; @@ -284,7 +284,7 @@ public: } if(d_on_index) { - if(d_parent->d_txn->get(d_parent->d_parent->d_main, d_id, d_data)) + if((*d_parent->d_txn)->get(d_parent->d_parent->d_main, d_id, d_data)) throw std::runtime_error("Missing id in constructor"); serFromString(d_data.get(), d_t); } @@ -309,7 +309,7 @@ public: } if(d_on_index) { - if(d_parent->d_txn->get(d_parent->d_parent->d_main, d_id, d_data)) + if((*d_parent->d_txn)->get(d_parent->d_parent->d_main, d_id, d_data)) throw std::runtime_error("Missing id in constructor"); serFromString(d_data.get(), d_t); } @@ -362,7 +362,7 @@ public: } else { if(d_on_index) { - if(d_parent->d_txn->get(d_parent->d_parent->d_main, d_id, data)) + if((*d_parent->d_txn)->get(d_parent->d_parent->d_main, d_id, data)) throw std::runtime_error("Missing id field"); if(filter && !filter(data)) goto next; @@ -419,7 +419,7 @@ public: template iter_t genbegin(MDB_cursor_op op) { - typename Parent::cursor_t cursor = d_parent.d_txn->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); + typename Parent::cursor_t cursor = (*d_parent.d_txn)->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); MDBOutVal out, id; @@ -445,7 +445,7 @@ public: iter_t begin() { - typename Parent::cursor_t cursor = d_parent.d_txn->getCursor(d_parent.d_parent->d_main); + typename Parent::cursor_t cursor = (*d_parent.d_txn)->getCursor(d_parent.d_parent->d_main); MDBOutVal out, id; @@ -466,7 +466,7 @@ public: template iter_t genfind(const typename std::tuple_element::type::type& key, MDB_cursor_op op) { - typename Parent::cursor_t cursor = d_parent.d_txn->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); + typename Parent::cursor_t cursor = (*d_parent.d_txn)->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); std::string keystr = keyConv(key); MDBInVal in(keystr); @@ -498,7 +498,7 @@ public: template std::pair equal_range(const typename std::tuple_element::type::type& key) { - typename Parent::cursor_t cursor = d_parent.d_txn->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); + typename Parent::cursor_t cursor = (*d_parent.d_txn)->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); std::string keyString=keyConv(key); MDBInVal in(keyString); @@ -517,7 +517,7 @@ public: template std::pair prefix_range(const typename std::tuple_element::type::type& key) { - typename Parent::cursor_t cursor = d_parent.d_txn->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); + typename Parent::cursor_t cursor = (*d_parent.d_txn)->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); std::string keyString=keyConv(key); MDBInVal in(keyString); @@ -595,7 +595,7 @@ public: id = MDBGetMaxID(*d_txn, d_parent->d_main) + 1; flags = MDB_APPEND; } - d_txn->put(d_parent->d_main, id, serToString(t), flags); + (*d_txn)->put(d_parent->d_main, id, serToString(t), flags); #define insertMacro(N) std::get(d_parent->d_tuple).put(*d_txn, t, id); insertMacro(0); @@ -626,14 +626,14 @@ public: if(!this->get(id, t)) return; - d_txn->del(d_parent->d_main, id); + (*d_txn)->del(d_parent->d_main, id); clearIndex(id, t); } //! clear database & indexes (by hand!) void clear() { - auto cursor = d_txn->getCursor(d_parent->d_main); + auto cursor = (*d_txn)->getRWCursor(d_parent->d_main); bool first = true; MDBOutVal key, data; while(!cursor.get(key, data, first ? MDB_FIRST : MDB_NEXT)) { @@ -648,13 +648,13 @@ public: //! commit this transaction void commit() { - d_txn->commit(); + (*d_txn)->commit(); } //! abort this transaction void abort() { - d_txn->abort(); + (*d_txn)->abort(); } typedef MDBRWCursor cursor_t; diff --git a/lmdb-various.cc b/lmdb-various.cc index 4ffa457..7377e33 100644 --- a/lmdb-various.cc +++ b/lmdb-various.cc @@ -18,7 +18,7 @@ static void closeTest() auto txn = env->getROTransaction(); for(auto& d : {&main, &dbi, &hyc}) { - auto rocursor = txn.getCursor(*d); + auto rocursor = txn->getCursor(*d); MDBOutVal key, data; if(rocursor.get(key, data, MDB_FIRST)) continue; @@ -41,8 +41,8 @@ try for(int n=0; n < 15; ++n) { auto txn = env->getRWTransaction(); int val = n + 1000*tid; - txn.put(dbi, val, val); - txn.commit(); + txn->put(dbi, val, val); + txn->commit(); cout << "Done with transaction "<getROTransaction(); int val = n + 1000*tid; MDBOutVal res; - if(txn.get(dbi, val, res)) { + if(txn->get(dbi, val, res)) { throw std::runtime_error("no record"); } @@ -85,9 +85,9 @@ void doFill() auto txn = env->getRWTransaction(); for(int j=0; j < 1000000; ++j) { MDBInVal mv(n*1000000+j); - txn.put(dbi, mv, mv, 0); + txn->put(dbi, mv, mv, 0); } - txn.commit(); + txn->commit(); } cout<<"Done filling"<get(dbi, mv, res)) ++count; } cout<openDB(dbname, 0); + auto cursor = txn->getCursor(db); uint32_t count = 0; MDBOutVal key, val; while(!cursor.get(key, val, count ? MDB_NEXT : MDB_FIRST)) { @@ -27,7 +27,7 @@ int main(int argc, char** argv) auto main = env.openDB("", 0); auto txn = env.getROTransaction(); - auto cursor = txn.getCursor(main); + auto cursor = txn->getCursor(main); MDBOutVal key, val; if(cursor.get(key, val, MDB_FIRST)) { diff --git a/multi-example.cc b/multi-example.cc index 045b5e6..540536e 100644 --- a/multi-example.cc +++ b/multi-example.cc @@ -1,6 +1,8 @@ #include "lmdb-safe.hh" using namespace std; +#include + int main() { unlink("./multi"); @@ -8,26 +10,26 @@ int main() auto dbi = env->openDB("qnames", MDB_DUPSORT | MDB_CREATE); auto txn = env->getRWTransaction(); - txn.clear(dbi); + txn->clear(dbi); - txn.put(dbi, "bdb", "old"); - txn.put(dbi, "lmdb", "hot"); - txn.put(dbi, "lmdb", "fast"); - txn.put(dbi, "lmdb", "zooms"); - txn.put(dbi, "lmdb", "c"); - txn.put(dbi, "mdb", "old name"); + txn->put(dbi, "bdb", "old"); + txn->put(dbi, "lmdb", "hot"); + txn->put(dbi, "lmdb", "fast"); + txn->put(dbi, "lmdb", "zooms"); + txn->put(dbi, "lmdb", "c"); + txn->put(dbi, "mdb", "old name"); string_view v1; - if(!txn.get(dbi, "mdb", v1)) { + if(!txn->get(dbi, "mdb", v1)) { cout<commit(); txn = env->getRWTransaction(); - auto cursor = txn.getCursor(dbi); + auto cursor = txn->getRWCursor(dbi); MDBOutVal key, data; diff --git a/rel-example.cc b/rel-example.cc index a2ba275..9754a31 100644 --- a/rel-example.cc +++ b/rel-example.cc @@ -27,7 +27,7 @@ struct Record static unsigned int getMaxID(MDBRWTransaction& txn, MDBDbi& dbi) { - auto cursor = txn.getCursor(dbi); + auto cursor = txn->getRWCursor(dbi); MDBOutVal maxidval, maxcontent; unsigned int maxid{0}; if(!cursor.get(maxidval, maxcontent, MDB_LAST)) { @@ -42,9 +42,9 @@ static void store(MDBRWTransaction& txn, MDBDbi& records, MDBDbi& domainidx, MDB boost::archive::binary_oarchive oa(oss,boost::archive::no_header ); oa << r; - txn.put(records, r.id, oss.str(), MDB_APPEND); - txn.put(domainidx, r.domain_id, r.id); - txn.put(nameidx, r.name, r.id); + txn->put(records, r.id, oss.str(), MDB_APPEND); + txn->put(domainidx, r.domain_id, r.id); + txn->put(nameidx, r.name, r.id); } @@ -122,12 +122,12 @@ int main(int argc, char** argv) store(txn, records, domainidx, nameidx, r); } - txn.commit(); + txn->commit(); auto rotxn = env->getROTransaction(); auto rotxn2 = env->getROTransaction(); - auto rocursor = rotxn.getCursor(nameidx); + auto rocursor = rotxn->getCursor(nameidx); MDBOutVal data; int count = 0; @@ -143,7 +143,7 @@ int main(int argc, char** argv) cout<<"Got something: id="<get(records, data, record)) { Record test; stringstream istr{record.get()}; boost::archive::binary_iarchive oi(istr,boost::archive::no_header ); diff --git a/resize-example.cc b/resize-example.cc index 6904d92..fbd9993 100644 --- a/resize-example.cc +++ b/resize-example.cc @@ -10,42 +10,42 @@ int main(int argc, char** argv) MDBInVal key("counter"); auto rwtxn = env->getRWTransaction(); - rwtxn.put(main, "counter", "1234"); - rwtxn.put(main, MDBInVal::fromStruct(std::make_pair(12,13)), "hoi dan 12,13"); + rwtxn->put(main, "counter", "1234"); + rwtxn->put(main, MDBInVal::fromStruct(std::make_pair(12,13)), "hoi dan 12,13"); - rwtxn.put(main, MDBInVal::fromStruct(std::make_pair(14,15)), - MDBInVal::fromStruct(std::make_pair(20,23))); + rwtxn->put(main, MDBInVal::fromStruct(std::make_pair(14,15)), + MDBInVal::fromStruct(std::make_pair(20,23))); MDBOutVal out; - if(!rwtxn.get(main, MDBInVal::fromStruct(std::make_pair(12,13)), out)) + if(!rwtxn->get(main, MDBInVal::fromStruct(std::make_pair(12,13)), out)) cout << "Got: " << out.get() << endl; else cout << "Got nothing!1"<get(main, MDBInVal::fromStruct(std::make_pair(14,15)), out)) { auto res = out.get_struct>(); cout << "Got: " << res.first<<", "<put(main, 12.12, 7.3); + if(!rwtxn->get(main, 12.12, out)) { cout<<"Got: "<< out.get() <commit(); return 0; if(argc==1) { for(;;) { auto rotxn = env->getROTransaction(); MDBOutVal data; - if(!rotxn.get(main, key, data)) { + if(!rotxn->get(main, key, data)) { cout<<"Counter is "<() << endl; cout <() << endl; cout<() << endl; @@ -73,10 +73,10 @@ int main(int argc, char** argv) cout<<"Did resize"<getRWTransaction(); - txn.put(main, key, MDBInVal(n)); + txn->put(main, key, MDBInVal(n)); for(int k=0; k < 100; ++k) - txn.put(main, MDBInVal(n+1000*k), MDBInVal(n+1000*k)); - txn.commit(); + txn->put(main, MDBInVal(n+1000*k), MDBInVal(n+1000*k)); + txn->commit(); } } } diff --git a/scale-example.cc b/scale-example.cc index 0ee9065..1efaa36 100644 --- a/scale-example.cc +++ b/scale-example.cc @@ -28,7 +28,7 @@ int main(int argc, char** argv) limit = atoi(argv[1]); cout<<"Counting records.. "; cout.flush(); - auto cursor=txn.getCursor(dbi); + auto cursor = txn->getCursor(dbi); MDBOutVal key, data; int count=0; while(!cursor.get(key, data, count ? MDB_NEXT : MDB_FIRST)) { @@ -40,15 +40,15 @@ int main(int argc, char** argv) cout<<"Have "<put(dbi, n, n, MDB_APPEND); } cout <<"Done!"<commit(); cout<<"Done!"<get(main, "lmdb", out) == MDB_NOTFOUND); - REQUIRE(txn.get(main, "lmdb", out) == 0); + txn->put(main, "lmdb", "hot"); + + REQUIRE(txn->get(main, "lmdb", out) == 0); REQUIRE(out.get() == "hot"); - txn.abort(); + txn->abort(); auto rotxn = env.getROTransaction(); - REQUIRE(rotxn.get(main, "lmdb", out) == MDB_NOTFOUND); + REQUIRE(rotxn->get(main, "lmdb", out) == MDB_NOTFOUND); } TEST_CASE("Range tests", "[range]") { @@ -36,20 +36,20 @@ TEST_CASE("Range tests", "[range]") { REQUIRE(1); MDBDbi main = env.openDB("", MDB_CREATE); - + auto txn = env.getRWTransaction(); MDBOutVal out; - REQUIRE(txn.get(main, "lmdb", out) == MDB_NOTFOUND); - - txn.put(main, "bert", "hubert"); - txn.put(main, "bertt", "1975"); - txn.put(main, "berthubert", "lmdb"); - txn.put(main, "bert1", "one"); - txn.put(main, "beru", "not"); - + REQUIRE(txn->get(main, "lmdb", out) == MDB_NOTFOUND); + + txn->put(main, "bert", "hubert"); + txn->put(main, "bertt", "1975"); + txn->put(main, "berthubert", "lmdb"); + txn->put(main, "bert1", "one"); + txn->put(main, "beru", "not"); + { - auto cursor = txn.getCursor(main); + auto cursor = txn->getCursor(main); MDBInVal bert("bert"); MDBOutVal key, val; REQUIRE(cursor.lower_bound(bert, key, val) == 0); @@ -64,14 +64,14 @@ TEST_CASE("Range tests", "[range]") { REQUIRE(key.get() == "berthubert"); REQUIRE(val.get() == "lmdb"); - REQUIRE(cursor.lower_bound("kees", key, val) == MDB_NOTFOUND); + REQUIRE(cursor.lower_bound("kees", key, val) == MDB_NOTFOUND); - txn.commit(); + txn->commit(); } - + auto rotxn = env.getROTransaction(); { - auto cursor = rotxn.getCursor(main); + auto cursor = rotxn->getCursor(main); MDBInVal bert("bert"); MDBOutVal key, val; REQUIRE(cursor.lower_bound(bert, key, val) == 0); @@ -86,7 +86,244 @@ TEST_CASE("Range tests", "[range]") { REQUIRE(key.get() == "berthubert"); REQUIRE(val.get() == "lmdb"); - REQUIRE(cursor.lower_bound("kees", key, val) == MDB_NOTFOUND); + REQUIRE(cursor.lower_bound("kees", key, val) == MDB_NOTFOUND); } - + +} + +TEST_CASE("moving transactions") +{ + unlink("./tests"); + + MDBEnv env("./tests", MDB_NOSUBDIR, 0600); + REQUIRE(1); + + MDBDbi main = env.openDB("", MDB_CREATE); + + auto txn = env.getRWTransaction(); + MDBOutVal out; + + REQUIRE(txn->get(main, "lmdb", out) == MDB_NOTFOUND); + + txn->put(main, "bert", "hubert"); + txn->put(main, "bertt", "1975"); + txn->put(main, "berthubert", "lmdb"); + txn->put(main, "bert1", "one"); + txn->put(main, "beru", "not"); + + auto cursor = txn->getCursor(main); + auto txn2 = std::move(txn); + { + auto cursor2 = std::move(cursor); + } +} + +TEST_CASE("transaction inheritance and moving") +{ + unlink("./tests"); + + MDBEnv env("./tests", MDB_NOSUBDIR, 0600); + MDBDbi main = env.openDB("", MDB_CREATE); + + MDBRWCursor cursor; + { + MDBRWTransaction txn = env.getRWTransaction(); + MDBOutVal out; + + REQUIRE(txn->get(main, "lmdb", out) == MDB_NOTFOUND); + + // lets just keep this cursor to ensure that it invalidates + cursor = txn->getRWCursor(main); + txn->put(main, "bert", "hubert"); + txn->put(main, "bertt", "1975"); + txn->put(main, "berthubert", "lmdb"); + txn->put(main, "bert1", "one"); + txn->put(main, "beru", "not"); + + MDBROTransaction ro_txn(std::move(txn)); + // despite being moved to an ro_txn (which normally commits instead of + // aborting by default) + } + + CHECK(!cursor); +} + +TEST_CASE("nested RW transactions", "[transactions]") +{ + unlink("./tests"); + + MDBEnv env("./tests", MDB_NOSUBDIR, 0600); + REQUIRE(1); + + MDBDbi main = env.openDB("", MDB_CREATE); + + /* bootstrap some data */ + { + auto txn = env.getRWTransaction(); + txn->put(main, "bert", "hubert"); + txn->put(main, "bertt", "1975"); + txn->put(main, "berthubert", "lmdb"); + txn->put(main, "bert1", "one"); + txn->put(main, "beru", "not"); + txn->commit(); + } + + auto main_txn = env.getRWTransaction(); + main_txn->del(main, "bertt", "1975"); + + MDBOutVal dummy{}; + CHECK(main_txn->get(main, "bertt", dummy) == MDB_NOTFOUND); + + { + auto sub_txn = main_txn->getRWTransaction(); + CHECK(sub_txn->get(main, "berthubert", dummy) == 0); + sub_txn->del(main, "berthubert", "lmdb"); + CHECK(sub_txn->get(main, "berthubert", dummy) == MDB_NOTFOUND); + } + + /* check that subtransaction got rolled back */ + CHECK(main_txn->get(main, "berthubert", dummy) == 0); + /* and that the main changes are still there */ + CHECK(main_txn->get(main, "bertt", dummy) == MDB_NOTFOUND); + + { + auto sub_txn = main_txn->getRWTransaction(); + CHECK(sub_txn->get(main, "berthubert", dummy) == 0); + sub_txn->del(main, "berthubert", "lmdb"); + CHECK(sub_txn->get(main, "berthubert", dummy) == MDB_NOTFOUND); + /* this time for real! */ + sub_txn->commit(); + } + + CHECK(main_txn->get(main, "berthubert", dummy) == MDB_NOTFOUND); + CHECK(main_txn->get(main, "bertt", dummy) == MDB_NOTFOUND); +} + + +TEST_CASE("nesting RW -> RO", "[transactions]") +{ + unlink("./tests"); + + MDBEnv env("./tests", MDB_NOSUBDIR, 0600); + REQUIRE(1); + + MDBDbi main = env.openDB("", MDB_CREATE); + + /* bootstrap some data */ + { + auto txn = env.getRWTransaction(); + txn->put(main, "bert", "hubert"); + txn->put(main, "bertt", "1975"); + txn->put(main, "berthubert", "lmdb"); + txn->put(main, "bert1", "one"); + txn->put(main, "beru", "not"); + txn->commit(); + } + + auto main_txn = env.getRWTransaction(); + main_txn->del(main, "bertt", "1975"); + + MDBOutVal dummy{}; + CHECK(main_txn->get(main, "bertt", dummy) == MDB_NOTFOUND); + + { + MDBROTransaction sub_txn = main_txn->getROTransaction(); + CHECK(sub_txn->get(main, "berthubert", dummy) == 0); + } + + /* check that subtransaction got rolled back */ + CHECK(main_txn->get(main, "berthubert", dummy) == 0); + /* and that the main changes are still there */ + CHECK(main_txn->get(main, "bertt", dummy) == MDB_NOTFOUND); + + { + auto sub_txn = main_txn->getRWTransaction(); + CHECK(sub_txn->get(main, "berthubert", dummy) == 0); + sub_txn->del(main, "berthubert", "lmdb"); + CHECK(sub_txn->get(main, "berthubert", dummy) == MDB_NOTFOUND); + { + MDBROTransaction sub_sub_txn = sub_txn->getROTransaction(); + CHECK(sub_sub_txn->get(main, "berthubert", dummy) == MDB_NOTFOUND); + } + /* this time for real! */ + sub_txn->commit(); + } + + CHECK(main_txn->get(main, "berthubert", dummy) == MDB_NOTFOUND); + CHECK(main_txn->get(main, "bertt", dummy) == MDB_NOTFOUND); +} + +TEST_CASE("try to nest twice", "[transactions]") +{ + unlink("./tests"); + + MDBEnv env("./tests", MDB_NOSUBDIR, 0600); + REQUIRE(1); + + MDBDbi main = env.openDB("", MDB_CREATE); + + /* bootstrap some data */ + { + auto txn = env.getRWTransaction(); + txn->put(main, "bert", "hubert"); + txn->put(main, "bertt", "1975"); + txn->put(main, "berthubert", "lmdb"); + txn->put(main, "bert1", "one"); + txn->put(main, "beru", "not"); + txn->commit(); + } + + auto main_txn = env.getRWTransaction(); + main_txn->del(main, "bertt", "1975"); + + MDBOutVal dummy{}; + CHECK(main_txn->get(main, "bertt", dummy) == MDB_NOTFOUND); + + { + auto sub_txn = main_txn->getRWTransaction(); + CHECK(sub_txn->get(main, "berthubert", dummy) == 0); + sub_txn->del(main, "berthubert", "lmdb"); + CHECK(sub_txn->get(main, "berthubert", dummy) == MDB_NOTFOUND); + + CHECK_THROWS_AS( + main_txn->getRWTransaction(), + std::runtime_error + ); + } +} + +TEST_CASE("transaction counter correctness for RW->RW nesting") +{ + unlink("./tests"); + + MDBEnv env("./tests", MDB_NOSUBDIR, 0600); + REQUIRE(1); + + MDBDbi main = env.openDB("", MDB_CREATE); + + { + auto txn = env.getRWTransaction(); + auto sub_txn = txn->getRWTransaction(); + } + + CHECK_NOTHROW(env.getRWTransaction()); + CHECK_NOTHROW(env.getROTransaction()); +} + +TEST_CASE("transaction counter correctness for RW->RO nesting") +{ + unlink("./tests"); + + MDBEnv env("./tests", MDB_NOSUBDIR, 0600); + REQUIRE(1); + + MDBDbi main = env.openDB("", MDB_CREATE); + + { + auto txn = env.getRWTransaction(); + auto sub_txn = txn->getROTransaction(); + } + + CHECK_NOTHROW(env.getRWTransaction()); + CHECK_NOTHROW(env.getROTransaction()); }