PMDK C++ bindings  1.13.0-git107.g7e59f08f
This is the C++ bindings documentation for PMDK's libpmemobj.
Transactions

Transactional approach to store data on pmem. More...

Classes

class  pmem::obj::basic_transaction
 C++ transaction handler class. More...
 
class  pmem::obj::flat_transaction
 C++ flat transaction handler class. More...
 

Detailed Description

Transactional approach to store data on pmem.

General info about transactions

The heart of the libpmemobj are transactions. A transaction is defined as series of operations on persistent memory objects that either all occur, or nothing occurs. In particular, if the execution of a transaction is interrupted by a power failure or a system crash, it is guaranteed that after system restart, all the changes made as a part of the uncompleted transaction will be rolled back, restoring the consistent state of the memory pool from the moment when the transaction was started.

Note
Any operations not using libpmemobj-cpp API (like int x = 5 or malloc()) used within transaction will not be a part ot the transaction and won't be rolled back on failure! To properly store variables on pmem use Primitives , to allocate data on pmem see Allocation functions.

In C++ bindings (this library) transactions were designed (in comparison to C API) to be as developer-friendly as possible. Even though libpmemobj++ are the bindings you should not mix these two APIs - using libpmemobj (C API) in C++ application will not work!

Let's take a look at the following snippet:

using namespace pmem::obj;
void
general_tx_example()
{
/* pool root structure */
struct root {
mutex pmutex;
shared_mutex shared_pmutex;
p<int> count;
persistent_ptr<root> another_root;
};
/* create a pmemobj pool */
auto pop = pool<root>::create("poolfile", "layout", PMEMOBJ_MIN_POOL);
auto proot = pop.root();
/* typical usage schemes */
try {
/* take locks and start a transaction */
pop,
[&]() {
/* atomically allocate objects */
proot->another_root = make_persistent<root>();
/* atomically modify objects */
proot->count++;
},
proot->pmutex, proot->shared_pmutex);
/* a transaction error occurred, transaction got aborted
* reacquire locks if necessary */
} catch (...) {
/* some other exception got propagated from within the tx
* reacquire locks if necessary */
}
}
static void run(obj::pool_base &pool, std::function< void()> tx, Locks &... locks)
Execute a closure-like transaction and lock locks.
Definition: transaction.hpp:676
static pool< T > create(const std::string &path, const std::string &layout, std::size_t size=PMEMOBJ_MIN_POOL, mode_t mode=DEFAULT_MODE)
Creates a new transactional object store pool.
Definition: pool.hpp:694
Custom transaction error class.
Definition: pexceptions.hpp:109
persistent_ptr transactional allocation functions for objects.
Pmem-resident mutex.
Main libpmemobj namespace.
Definition: allocation_flag.hpp:18
Persistent smart pointer.
Convenience extensions for the resides on pmem property template.
C++ pmemobj pool.
Pmem-resident shared mutex.
C++ pmemobj transactions.

Code above is an example how automatic transaction can look like. After object creation there are a few statements executed within a transaction. Transaction will be committed during tx object's destruction at the end of the scope.

It's worth noticing that pmem::obj::flat_transaction is recommended to use over pmem::obj::basic_transaction. An extra explanation is provided inline an example in pmem::obj::flat_transaction description.

Mentioned above transactions are handled through two internal classes:

In both approaches one of the parameters is the locks. They are held for the entire duration of the transaction and they are released at the end of the scope - so within the catch block, they are already unlocked.

If you want to read more and see example usages of both, you have to see flat or basic transaction documentation, because each implementation may differ.

Lifecycle and stages:

When you are using transaction API a transaction can be in one of the following states:

Moving from one stage to another is possible under some conditions, but in libpmemobj-cpp it's transparent for user, so please focus on relationships between stages. Look at the diagram below:

lifecycle

To be more familiar with functions used in diagram read e.g. pmemobj_tx_begin(3) manpage (C API for libpmemobj, link below in Additional resources).

If you need to read general information about transaction move to the Additional resources section.

Example of flat_transaction

For comparison with the previous snippet, here is a code snippet of pmem::obj::flat_transaction which is listed below with basic explanation inline.

using namespace pmem::obj;
template <typename T>
struct simple_ptr {
simple_ptr()
{
assert(pmemobj_tx_stage() == TX_STAGE_WORK);
ptr = make_persistent<T>();
}
~simple_ptr()
{
assert(pmemobj_tx_stage() == TX_STAGE_WORK);
try {
delete_persistent<T>(ptr);
std::cerr << e.what() << std::endl;
std::terminate();
std::cerr << e.what() << std::endl;
std::terminate();
}
}
persistent_ptr<T> ptr;
};
struct A {
A() : ptr1(), ptr2()
{
}
simple_ptr<int> ptr1;
simple_ptr<char[(1ULL << 30)]> ptr2;
};
struct B {
B() : ptr1(), ptr2()
{
auto pop = pool_base(pmemobj_pool_by_ptr(this));
// It would result in a crash!
// basic_transaction::run(pop, [&]{ throw
// std::runtime_error("Error"); });
pop, [&] { throw std::runtime_error("Error"); });
}
simple_ptr<int> ptr1;
simple_ptr<int> ptr2;
};
void
tx_nested_struct_example()
{
/* pool root structure */
struct root {
persistent_ptr<A> ptrA;
persistent_ptr<B> ptrB;
};
/* create a pmemobj pool */
auto pop = pool<root>::create("poolfile", "layout", PMEMOBJ_MIN_POOL);
auto proot = pop.root();
auto create_a = [&] { proot->ptrA = make_persistent<A>(); };
auto create_b = [&] { proot->ptrB = make_persistent<B>(); };
try {
// It would result in a crash!
// basic_transaction::run(pop, create_a);
flat_transaction::run(pop, create_a);
/* To see why flat_transaction is necessary let's
* consider what happens when calling A ctor. The call stack
* will look like this:
*
* | ptr2 ctor |
* |-----------|
* | ptr1 ctor |
* |-----------|
* | A ctor |
*
* Since ptr2 is a pointer to some huge array of elements,
* calling ptr2 ctor will most likely result in make_persistent
* throwing an exception (due to out of memory). This exception
* will, in turn, cause stack unwinding - already constructed
* elements must be destroyed (in this example ptr1 destructor
* will be called).
*
* If we'd use basic_transaction the allocation failure, apart
* from throwing an exception, would also cause the transaction
* to abort (by default, in basic_transaction, all transactional
* functions failures cause tx abort). This is problematic since
* the ptr1 destructor, which is called during stack unwinding,
* expects the transaction to be in WORK stage (and the actual
* stage is ABORTED). As a result the application will fail on
* assert (and probably crash in NDEBUG mode).
*
* Now, consider what will happen if we'd use flat_transaction
* instead. In this case, make_persistent failure will not abort
* the transaction, it will only result in an exception. This
* means that the transaction is still in WORK stage during
* stack unwinding. Only after it completes, the transaction is
* aborted (it's happening at the outermost level, when exiting
* create_a lambda).
*/
} catch (std::runtime_error &) {
}
try {
basic_transaction::run(pop, create_b);
flat_transaction::run(pop, create_b);
/* Running create_b can be done both within basic and flat
* transaction. However, note that the transaction used in the B
* constructor MUST be a flat_transaction. This is because
* flat_transaction does not abort immediately when catching an
* exception. Instead it passes it to the outermost transaction
* - the abort is performed at that outermost level. In case of
* a basic_transaction the abort would be done within the B ctor
* and it would result in the same problems as with the previous
* example.
*/
} catch (std::runtime_error &) {
}
}
static void run(obj::pool_base &pool, std::function< void()> tx, Locks &... locks)
Execute a closure-like transaction and lock locks.
Definition: transaction.hpp:810
Custom transaction error class.
Definition: pexceptions.hpp:156
Custom transaction error class.
Definition: pexceptions.hpp:167

If you read the inline comments you should be able to notice the difference between pmem::obj::flat_transaction and pmem::obj::basic_transaction. For more examples please look at the examples directory in libpmemobj-cpp repository.

Additional resources