Skip to content
This repository has been archived by the owner on Mar 22, 2023. It is now read-only.

Introduce flat transaction class and change containers' behavior #932

Merged
merged 4 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ More implementation details can be found in [include/libpmemobj++/README.md](inc
Latest releases can be found on the ["releases" tab](https://github.com/pmem/libpmemobj-cpp/releases).
Up-to-date support/maintenance status of branches/releases is available on [pmem.io](https://pmem.io/libpmemobj-cpp).

# Compatibility note #
In libpmemobj 1.12 we introduced a new transaction handler type: [pmem::obj::flat_transaction](https://pmem.io/libpmemobj-cpp/master/doxygen/classpmem_1_1obj_1_1flat__transaction.html). By defining LIBPMEMOBJ_CPP_USE_FLAT_TRANSACTION you can make pmem::obj::transaction to be an alias to pmem::obj::flat_transaction. In 1.12 we have also changed the default behavior of containers' transactional methods. Now, in case of any failure within such method, the outer transaction (if any) will not be immediately aborted. Instead, an exception will be thrown, which will lead to transaction abort only if it's not caught before the outer tx scope ends. To change the behavior to the old one, you can set LIBPMEMOBJ_CPP_FLAT_TX_USE_FAILURE_RETURN macro to 0. Be aware that the old behavior can lead to segfaults in some cases (see tx_nested_struct_example in this [file](examples/transaction/transaction.cpp)).

# How to build #

## Requirements: ##
Expand Down
260 changes: 260 additions & 0 deletions examples/transaction/transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,263 @@ tx_callback_example()
}
//! [tx_callback_example]

//! [tx_flat_example]
#include <libpmemobj++/make_persistent.hpp>
#include <libpmemobj++/persistent_ptr.hpp>
#include <libpmemobj++/pext.hpp>
#include <libpmemobj++/pool.hpp>
#include <libpmemobj++/transaction.hpp>

using namespace pmem::obj;

void
tx_flat_example()
{
/* pool root structure */
struct root {
p<int> count;
};

/* create a pmemobj pool */
auto pop = pool<root>::create("poolfile", "layout", PMEMOBJ_MIN_POOL);
auto proot = pop.root();

try {
flat_transaction::run(pop, [&] {
proot->count++;

try {
flat_transaction::run(pop, [&] {
proot->count++;
throw std::runtime_error("some error");
});
} catch (...) {
/* Transaction is not aborted yet (unlike for
* basic_transaction). */
assert(pmemobj_tx_stage() == TX_STAGE_WORK);
assert(proot->count == 2);
throw;
}
});
} catch (pmem::transaction_error &) {
/* An internal transaction error occurred, outer tx aborted just
* now. Reacquire locks if necessary. */
assert(proot->count == 0);
} catch (...) {
/* Some other exception thrown, outer tx aborted just now.
* Reacquire locks if necessary. */
assert(proot->count == 0);
}
}
//! [tx_flat_example]

//! [tx_nested_struct_example]
#include <libpmemobj++/make_persistent.hpp>
#include <libpmemobj++/persistent_ptr.hpp>
#include <libpmemobj++/pext.hpp>
#include <libpmemobj++/pool.hpp>
#include <libpmemobj++/transaction.hpp>

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);
delete_persistent<T>(ptr);
}

persistent_ptr<T> ptr;
};

/**
* This struct holds two simple_ptr. It presents problems when using
* basic_transaction in case of transactional function abort. */
struct A {
A() : ptr1(), ptr2()
{
}

simple_ptr<int> ptr1;
simple_ptr<char[(1ULL << 30)]> ptr2;
};

/**
* This struct holds two simple_ptr. It presents problems when throwing
* exception from within basic_transaction. */
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"); });

flat_transaction::run(
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 &) {
}
}
//! [tx_nested_struct_example]

//! [manual_flat_tx_example]
#include <libpmemobj++/make_persistent.hpp>
#include <libpmemobj++/mutex.hpp>
#include <libpmemobj++/persistent_ptr.hpp>
#include <libpmemobj++/pext.hpp>
#include <libpmemobj++/pool.hpp>
#include <libpmemobj++/shared_mutex.hpp>
#include <libpmemobj++/transaction.hpp>

using namespace pmem::obj;

int
manual_flat_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();

try {
flat_transaction::manual tx(pop, proot->pmutex);

/* atomically allocate objects */
proot->another_root = make_persistent<root>();

{
flat_transaction::manual inner_tx(pop,
proot->shared_pmutex);

/* atomically modify objects */
proot->count++;

/* OPTIONAL */
// transaction::commit();

/* Even if there is no explicit commit inner_tx will
* not abort. This is true even if
* flat_transaction::manual is destroyed because of an
* active exception. For basic_transaction::manual you
* have to call commit() at each level (as many times as
* there are manual transaction objects). In case of
* a flat_transaction, the commit has to be called only
* once, at the outermost level.
*/
}

/* It's necessary to commit the transaction manually and
* it has to be the last operation in the transaction. */
transaction::commit();
} catch (pmem::transaction_error &) {
/* An internal transaction error occurred, outer tx aborted just
* now. Reacquire locks if necessary, */
} catch (...) {
/* Some other exception thrown, outer tx aborted just now.
* Reacquire locks if necessary. */
}

/* In complex cases with library calls, remember to check the status of
* the last transaction. */
return transaction::error();
}
//! [manual_flat_tx_example]

int
main()
{
Expand All @@ -234,6 +491,9 @@ main()
manual_tx_example();
automatic_tx_example();
tx_callback_example();
tx_flat_example();
tx_nested_struct_example();
manual_flat_tx_example();
} catch (const std::exception &e) {
std::cerr << "Exception " << e.what() << std::endl;
return -1;
Expand Down
8 changes: 4 additions & 4 deletions include/libpmemobj++/container/array.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ struct array {
if (this == &other)
return *this;

transaction::run(pop, [&] {
flat_transaction::run(pop, [&] {
detail::conditional_add_to_tx(
this, 1, POBJ_XADD_ASSUME_INITIALIZED);
std::copy(other.cbegin(), other.cend(), _get_data());
Expand Down Expand Up @@ -151,7 +151,7 @@ struct array {
if (this == &other)
return *this;

transaction::run(pop, [&] {
flat_transaction::run(pop, [&] {
detail::conditional_add_to_tx(
this, 1, POBJ_XADD_ASSUME_INITIALIZED);
detail::conditional_add_to_tx(
Expand Down Expand Up @@ -588,7 +588,7 @@ struct array {
{
auto pop = _get_pool();

transaction::run(pop, [&] {
flat_transaction::run(pop, [&] {
detail::conditional_add_to_tx(
this, 1, POBJ_XADD_ASSUME_INITIALIZED);
std::fill(_get_data(), _get_data() + size(), value);
Expand All @@ -615,7 +615,7 @@ struct array {
if (this == &other)
return;

transaction::run(pop, [&] {
flat_transaction::run(pop, [&] {
detail::conditional_add_to_tx(
this, 1, POBJ_XADD_ASSUME_INITIALIZED);
detail::conditional_add_to_tx(
Expand Down
Loading