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

transaction: expose API to control transaction behaviour on error #723

Closed
wants to merge 4 commits into from
Closed
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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ if (VERSION_PRERELEASE)
set(VERSION ${VERSION}-${VERSION_PRERELEASE})
endif()

set(LIBPMEMOBJ_REQUIRED_VERSION 1.8)
set(LIBPMEMOBJ_REQUIRED_VERSION 1.9)
set(LIBPMEM_REQUIRED_VERSION 1.7)

set(CMAKE_DISABLE_IN_SOURCE_BUILD ON)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Up-to-date support/maintenance status of branches/releases is available on [pmem

## Requirements: ##
- cmake >= 3.3
- libpmemobj-dev(el) >= 1.8 (https://pmem.io/pmdk/)
- libpmemobj-dev(el) >= 1.9 (https://pmem.io/pmdk/)
- compiler with C++11 support:
- GCC >= 4.8.1 (C++11 is supported in GCC since version 4.8.1, but it does not support expanding variadic template variables in lambda expressions, which is required to build persistent containers and is possible with GCC >= 4.9.0. If you want to build libpmemobj-cpp without testing containers, use flag TEST_XXX=OFF (separate flag for each container))
- clang >= 3.3
Expand Down
203 changes: 154 additions & 49 deletions include/libpmemobj++/transaction.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
#include <libpmemobj++/pool.hpp>
#include <libpmemobj/tx_base.h>

#ifndef LIBPMEMOBJ_CPP_TX_FAILURE_ABORT
#define LIBPMEMOBJ_CPP_TX_FAILURE_EVENT return_error
#else
#define LIBPMEMOBJ_CPP_TX_FAILURE_EVENT abort
#endif

namespace pmem
{

Expand Down Expand Up @@ -52,6 +58,10 @@ namespace obj
* This class also exposes a closure-like transaction API, which is the
* preferred way of handling transactions.
*
* Transactions can be configured using transaction::options structure. It
* allows to change transaction's behavior on failure. See
* transaction::failure_behavior for more details.
*
* This API should NOT be mixed with C transactions API. One issue is that
* C++ callbacks registered using transaction::register_callback() would not
* be called if C++ transaction is created inside C transaction.
Expand All @@ -63,6 +73,37 @@ namespace obj
*/
class transaction {
public:
class manual;

/** Specifies failure event in case of a transaction error */
enum class failure_behavior {
abort = POBJ_TX_FAILURE_ABORT, /**< each transactional function
* will abort the transaction on
* error. */
return_error =
POBJ_TX_FAILURE_RETURN /**< transactional functions will
* throw an exception but will
* leave the transaction in work
* stage. */
};

/**
* options structure which can be used to control transaction
* behavior.
*/
struct options {
/** Controls behavior of a transaction in case of errors. It can
* be failure_behavior::abort or failure_behavior::return_error
* . This setting is inherited by inner transactions.
* 'return_error' is the default behavior (unless
* LIBPMEMOBJ_CPP_TX_FAILURE_ABORT macro is defined). It is not
* possible to start an 'abort' transaction within
* 'return_error' transaction. */
::pmem::obj::transaction::failure_behavior failure_behavior =
::pmem::obj::transaction::failure_behavior::
LIBPMEMOBJ_CPP_TX_FAILURE_EVENT;
};

/**
* C++ manual scope transaction class.
*
Expand Down Expand Up @@ -90,32 +131,59 @@ class transaction {
* new transaction. The list of locks may be empty.
*
* @param[in,out] pop pool object.
* @param[in] opts options for controlling transaction behavior
* @param[in,out] locks locks of obj::mutex or
* obj::shared_mutex type.
*
* @throw pmem::transaction_error when pmemobj_tx_begin
* function or locks adding failed.
*/
template <typename... L>
manual(obj::pool_base &pop, L &... locks)
manual(obj::pool_base &pop, options opts, L &... locks)
{
int ret = 0;

if (pmemobj_tx_stage() == TX_STAGE_NONE) {
bool nested = pmemobj_tx_stage() == TX_STAGE_WORK;

if (nested) {
if (opts.failure_behavior ==
failure_behavior::abort &&
pmemobj_tx_get_failure_behavior() ==
POBJ_TX_FAILURE_RETURN)
throw pmem::transaction_error(
"Cannot start transaction with failure_behavior::abort. Outer transaction was configured with failure_behavior::return_error");

ret = pmemobj_tx_begin(pop.handle(), nullptr,
TX_PARAM_NONE);

} else if (pmemobj_tx_stage() == TX_STAGE_NONE) {
ret = pmemobj_tx_begin(pop.handle(), nullptr,
TX_PARAM_CB,
transaction::c_callback,
nullptr, TX_PARAM_NONE);
} else {
ret = pmemobj_tx_begin(pop.handle(), nullptr,
TX_PARAM_NONE);
throw pmem::transaction_scope_error(
"Cannot start transaction in stage different than WORK or NONE");
}

if (ret != 0)
throw pmem::transaction_error(
"failed to start transaction")
.with_pmemobj_errormsg();

pmemobj_tx_set_failure_behavior(
(pobj_tx_failure_behavior)
opts.failure_behavior);

/*
* Even if opts.failure_behavior is set to return_error
* we have to abort the transaction if there is an
* active exception in the outer most transaction.
*/
should_abort_on_failure = opts.failure_behavior ==
failure_behavior::abort ||
!nested;

auto err = add_lock(locks...);

if (err) {
Expand All @@ -127,6 +195,12 @@ class transaction {
}
}

template <typename... L>
manual(obj::pool_base &pop, L &... locks)
: manual(pop, options{}, locks...)
{
}

/**
* Destructor.
*
Expand All @@ -137,8 +211,12 @@ class transaction {
~manual() noexcept
{
/* normal exit or with an active exception */
if (pmemobj_tx_stage() == TX_STAGE_WORK)
pmemobj_tx_abort(ECANCELED);
if (pmemobj_tx_stage() == TX_STAGE_WORK) {
if (should_abort_on_failure)
pmemobj_tx_abort(ECANCELED);
else
pmemobj_tx_commit();
}

(void)pmemobj_tx_end();
}
Expand All @@ -162,6 +240,9 @@ class transaction {
* Deleted move assignment operator.
*/
manual &operator=(manual &&p) = delete;

private:
bool should_abort_on_failure;
};

/*
Expand Down Expand Up @@ -190,6 +271,30 @@ class transaction {
*/
class automatic {
public:
/**
* RAII constructor with pmem resident locks.
*
* Start pmemobj transaction and add list of locks to
* new transaction. The list of locks may be empty.
*
* This class is only available if the
* `__cpp_lib_uncaught_exceptions` feature macro is
* defined. This is a C++17 feature.
*
* @param[in,out] pop pool object.
* @param[in] opts options for controlling transaction behavior.
* @param[in,out] locks locks of obj::mutex or
* obj::shared_mutex type.
*
* @throw pmem::transaction_error when pmemobj_tx_begin
* function or locks adding failed.
*/
template <typename... L>
automatic(obj::pool_base &pop, options opts, L &... locks)
: tx_worker(pop, opts, locks...)
{
}

/**
* RAII constructor with pmem resident locks.
*
Expand All @@ -209,7 +314,7 @@ class transaction {
*/
template <typename... L>
automatic(obj::pool_base &pop, L &... locks)
: tx_worker(pop, locks...)
: automatic(pop, options{}, locks...)
{
}

Expand Down Expand Up @@ -405,61 +510,61 @@ class transaction {
static void
run(pool_base &pool, std::function<void()> tx, Locks &... locks)
{
int ret = 0;

if (pmemobj_tx_stage() == TX_STAGE_NONE) {
ret = pmemobj_tx_begin(pool.handle(), nullptr,
TX_PARAM_CB,
transaction::c_callback, nullptr,
TX_PARAM_NONE);
} else {
ret = pmemobj_tx_begin(pool.handle(), nullptr,
TX_PARAM_NONE);
}

if (ret != 0)
throw pmem::transaction_error(
"failed to start transaction")
.with_pmemobj_errormsg();

auto err = add_lock(locks...);

if (err) {
pmemobj_tx_abort(err);
(void)pmemobj_tx_end();
throw pmem::transaction_error(
"failed to add a lock to the transaction")
.with_pmemobj_errormsg();
}
run(pool, options{}, tx, locks...);
}

try {
tx();
} catch (manual_tx_abort &) {
(void)pmemobj_tx_end();
throw;
} catch (...) {
/* first exception caught */
if (pmemobj_tx_stage() == TX_STAGE_WORK)
pmemobj_tx_abort(ECANCELED);
/**
* Execute a closure-like transaction and lock `locks`.
*
* The locks have to be persistent memory resident locks. An
* attempt to lock the locks will be made. If any of the
* specified locks is already locked, the method will block.
* The locks are held until the end of the transaction. The
* transaction does not have to be committed manually. Manual
* aborts will end the transaction with an active exception.
*
* If an exception is thrown within the transaction, it gets aborted
* and the exception is rethrown. Therefore extra care has to be taken
* with proper error handling.
*
* The locks are held for the entire duration of the transaction. They
* are released at the end of the scope, so within the `catch` block,
* they are already unlocked. If the cleanup action requires access to
* data within a critical section, the locks have to be manually
* acquired once again.
*
* @param[in,out] pool the pool in which the transaction will take
* place.
* @param[in] tx an std::function<void ()> which will perform
* operations within this transaction.
* @param[in] opts optional options object which affects transaction
* behavior.
* @param[in,out] locks locks to be taken for the duration of
* the transaction.
*
* @throw transaction_error on any error pertaining the execution
* of the transaction.
* @throw manual_tx_abort on manual transaction abort.
*/
template <typename... Locks>
static void
run(pool_base &pool, options opts, std::function<void()> tx,
Locks &... locks)
{
manual worker(pool, opts, locks...);

/* waterfall tx_end for outer tx */
(void)pmemobj_tx_end();
throw;
}
tx();

auto stage = pmemobj_tx_stage();

if (stage == TX_STAGE_WORK) {
pmemobj_tx_commit();
} else if (stage == TX_STAGE_ONABORT) {
(void)pmemobj_tx_end();
throw pmem::transaction_error("transaction aborted");
} else if (stage == TX_STAGE_NONE) {
throw pmem::transaction_error(
"transaction ended prematurely");
}

(void)pmemobj_tx_end();
}

template <typename... Locks>
Expand Down
42 changes: 19 additions & 23 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,17 @@ if (NOT WIN32)
add_test_generic(NAME shared_mutex_posix TRACERS drd helgrind pmemcheck)
endif()

build_test(transaction transaction/transaction.cpp)
add_test_generic(NAME transaction TRACERS none pmemcheck memcheck)
# Test transaction.cpp with both abort and return_error on failure
build_test_ext(NAME transaction_failure_abort SRC_FILES transaction/transaction.cpp BUILD_OPTIONS -DLIBPMEMOBJ_CPP_TX_FAILURE_ABORT)
add_test_generic(NAME transaction_failure_abort TRACERS none pmemcheck memcheck)
build_test_ext(NAME transaction_failure_return SRC_FILES transaction/transaction.cpp)
add_test_generic(NAME transaction_failure_return TRACERS none pmemcheck memcheck)

build_test_ext(NAME transaction_noabort SRC_FILES transaction/transaction_noabort.cpp)
add_test_generic(NAME transaction_noabort TRACERS none pmemcheck memcheck)

build_test_ext(NAME transaction_abort SRC_FILES transaction/transaction_abort.cpp BUILD_OPTIONS -DLIBPMEMOBJ_CPP_TX_FAILURE_ABORT)
add_test_generic(NAME transaction_abort TRACERS none pmemcheck memcheck)

if (VOLATILE_STATE_PRESENT)
build_test(volatile_state volatile_state/volatile_state.cpp)
Expand Down Expand Up @@ -622,13 +631,8 @@ if(TEST_SEGMENT_VECTOR_ARRAY_EXPSIZE)
build_test_ext(NAME segment_vector_array_expsize_ctor_exceptions_notx SRC_FILES vector_ctor_exceptions_notx/vector_ctor_exceptions_notx.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_ARRAY_EXPSIZE)
add_test_generic(NAME segment_vector_array_expsize_ctor_exceptions_notx TRACERS none memcheck)

#[[ XXX:
When we throwing exceptions in segment vector constructor, destructors is called for all constructed element according to standard.
In this way for segment with non-zero capacity in the underlying vectors destructor free_data() methods is called.
The free_data() run nested transaction which is failed to run when internally calls pmemobj_tx_begin(pool.handle(), nullptr, TX_PARAM_NONE).
]]
# build_test_ext(NAME segment_vector_array_expsize_ctor_exceptions_oom SRC_FILES vector_ctor_exceptions_oom/vector_ctor_exceptions_oom.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_ARRAY_EXPSIZE)
# add_test_generic(NAME segment_vector_array_expsize_ctor_exceptions_oom TRACERS none memcheck pmemcheck)
build_test_ext(NAME segment_vector_array_expsize_ctor_exceptions_oom SRC_FILES vector_ctor_exceptions_oom/vector_ctor_exceptions_oom.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_ARRAY_EXPSIZE)
add_test_generic(NAME segment_vector_array_expsize_ctor_exceptions_oom TRACERS none memcheck pmemcheck)

build_test_ext(NAME segment_vector_array_expsize_ctor_move SRC_FILES vector_ctor_move/vector_ctor_move.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_ARRAY_EXPSIZE)
add_test_generic(NAME segment_vector_array_expsize_ctor_move TRACERS none memcheck pmemcheck)
Expand Down Expand Up @@ -695,13 +699,8 @@ if(TEST_SEGMENT_VECTOR_VECTOR_EXPSIZE)
build_test_ext(NAME segment_vector_vector_expsize_ctor_exceptions_notx SRC_FILES vector_ctor_exceptions_notx/vector_ctor_exceptions_notx.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_VECTOR_EXPSIZE)
add_test_generic(NAME segment_vector_vector_expsize_ctor_exceptions_notx TRACERS none memcheck)

#[[ XXX:
When we throwing exceptions in segment vector constructor, destructors is called for all constructed element according to standard.
In this way for segment with non-zero capacity in the underlying vectors destructor free_data() methods is called.
The free_data() run nested transaction which is failed to run when internally calls pmemobj_tx_begin(pool.handle(), nullptr, TX_PARAM_NONE).
]]
# build_test_ext(NAME segment_vector_vector_expsize_ctor_exceptions_oom SRC_FILES vector_ctor_exceptions_oom/vector_ctor_exceptions_oom.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_VECTOR_EXPSIZE)
# add_test_generic(NAME segment_vector_vector_expsize_ctor_exceptions_oom TRACERS none memcheck pmemcheck)
build_test_ext(NAME segment_vector_vector_expsize_ctor_exceptions_oom SRC_FILES vector_ctor_exceptions_oom/vector_ctor_exceptions_oom.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_VECTOR_EXPSIZE)
add_test_generic(NAME segment_vector_vector_expsize_ctor_exceptions_oom TRACERS none memcheck pmemcheck)

build_test_ext(NAME segment_vector_vector_expsize_ctor_move SRC_FILES vector_ctor_move/vector_ctor_move.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_VECTOR_EXPSIZE)
add_test_generic(NAME segment_vector_vector_expsize_ctor_move TRACERS none memcheck pmemcheck)
Expand Down Expand Up @@ -768,13 +767,10 @@ if(TEST_SEGMENT_VECTOR_VECTOR_FIXEDSIZE)
build_test_ext(NAME segment_vector_vector_fixedsize_ctor_exceptions_notx SRC_FILES vector_ctor_exceptions_notx/vector_ctor_exceptions_notx.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_VECTOR_FIXEDSIZE)
add_test_generic(NAME segment_vector_vector_fixedsize_ctor_exceptions_notx TRACERS none memcheck)

#[[ XXX:
When we throwing exceptions in segment vector constructor, destructors is called for all constructed element according to standard.
In this way for segment with non-zero capacity in the underlying vectors destructor free_data() methods is called.
The free_data() run nested transaction which is failed to run when internally calls pmemobj_tx_begin(pool.handle(), nullptr, TX_PARAM_NONE).
]]
# build_test_ext(NAME segment_vector_vector_fixedsize_ctor_exceptions_oom SRC_FILES vector_ctor_exceptions_oom/vector_ctor_exceptions_oom.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_VECTOR_FIXEDSIZE)
# add_test_generic(NAME segment_vector_vector_fixedsize_ctor_exceptions_oom TRACERS none memcheck pmemcheck)
if (TESTS_LONG)
build_test_ext(NAME segment_vector_vector_fixedsize_ctor_exceptions_oom SRC_FILES vector_ctor_exceptions_oom/vector_ctor_exceptions_oom.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_VECTOR_FIXEDSIZE)
add_test_generic(NAME segment_vector_vector_fixedsize_ctor_exceptions_oom TRACERS none memcheck pmemcheck)
endif()

build_test_ext(NAME segment_vector_vector_fixedsize_ctor_move SRC_FILES vector_ctor_move/vector_ctor_move.cpp BUILD_OPTIONS -DSEGMENT_VECTOR_VECTOR_FIXEDSIZE)
add_test_generic(NAME segment_vector_vector_fixedsize_ctor_move TRACERS none memcheck pmemcheck)
Expand Down
Loading