621 lines
22 KiB
C++
621 lines
22 KiB
C++
/**
|
|
* @file transactional.hpp
|
|
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
|
|
*
|
|
* @section LICENSE
|
|
*
|
|
* Copyright © 2018-2019 Sébastien Rouault.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* any later version. Please see https://gnu.org/licenses/gpl.html
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* @section DESCRIPTION
|
|
*
|
|
* Transactional memory library management and use.
|
|
**/
|
|
|
|
#pragma once
|
|
|
|
// External headers
|
|
extern "C" {
|
|
#include <dlfcn.h>
|
|
#include <limits.h>
|
|
}
|
|
|
|
// Internal headers
|
|
namespace STM {
|
|
#include <tm.hpp>
|
|
}
|
|
#include "common.hpp"
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
namespace Exception {
|
|
|
|
/** Exception tree.
|
|
**/
|
|
EXCEPTION(Path, Any, "path exception");
|
|
EXCEPTION(PathResolve, Path, "unable to resolve the given path");
|
|
EXCEPTION(Module, Any, "transaction library exception");
|
|
EXCEPTION(ModuleLoading, Module, "unable to load a transaction library");
|
|
EXCEPTION(ModuleSymbol, Module, "symbol not found in loaded libraries");
|
|
EXCEPTION(Transaction, Any, "transaction manager exception");
|
|
EXCEPTION(TransactionAlign, Transaction, "incorrect alignment detected before transactional operation");
|
|
EXCEPTION(TransactionReadOnly, Transaction, "tried to write/alloc/free using a read-only transaction");
|
|
EXCEPTION(TransactionCreate, Transaction, "shared memory region creation failed");
|
|
EXCEPTION(TransactionBegin, Transaction, "transaction begin failed");
|
|
EXCEPTION(TransactionAlloc, Transaction, "memory allocation failed (insufficient memory)");
|
|
EXCEPTION(TransactionRetry, Transaction, "transaction aborted and can be retried");
|
|
EXCEPTION(TransactionNotLastSegment, Transaction, "trying to deallocate the first segment");
|
|
EXCEPTION(Shared, Any, "operation in shared memory exception");
|
|
EXCEPTION(SharedAlign, Shared, "address in shared memory is not properly aligned for the specified type");
|
|
EXCEPTION(SharedOverflow, Shared, "index is past array length");
|
|
EXCEPTION(SharedDoubleAlloc, Shared, "(probable) double allocation detected before transactional operation");
|
|
EXCEPTION(SharedDoubleFree, Shared, "double free detected before transactional operation");
|
|
|
|
}
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
/** Transactional library management class.
|
|
**/
|
|
class TransactionalLibrary final: private NonCopyable {
|
|
friend class TransactionalMemory;
|
|
private:
|
|
/** Function types.
|
|
**/
|
|
using FnCreate = decltype(&STM::tm_create);
|
|
using FnDestroy = decltype(&STM::tm_destroy);
|
|
using FnStart = decltype(&STM::tm_start);
|
|
using FnSize = decltype(&STM::tm_size);
|
|
using FnAlign = decltype(&STM::tm_align);
|
|
using FnBegin = decltype(&STM::tm_begin);
|
|
using FnEnd = decltype(&STM::tm_end);
|
|
using FnRead = decltype(&STM::tm_read);
|
|
using FnWrite = decltype(&STM::tm_write);
|
|
using FnAlloc = decltype(&STM::tm_alloc);
|
|
using FnFree = decltype(&STM::tm_free);
|
|
private:
|
|
void* module; // Module opaque handler
|
|
FnCreate tm_create; // Module's initialization function
|
|
FnDestroy tm_destroy; // Module's cleanup function
|
|
FnStart tm_start; // Module's start address query function
|
|
FnSize tm_size; // Module's size query function
|
|
FnAlign tm_align; // Module's alignment query function
|
|
FnBegin tm_begin; // Module's transaction begin function
|
|
FnEnd tm_end; // Module's transaction end function
|
|
FnRead tm_read; // Module's shared memory read function
|
|
FnWrite tm_write; // Module's shared memory write function
|
|
FnAlloc tm_alloc; // Module's shared memory allocation function
|
|
FnFree tm_free; // Module's shared memory freeing function
|
|
private:
|
|
/** Solve a symbol from its name, and bind it to the given function.
|
|
* @param name Name of the symbol to resolve
|
|
* @param func Target function to bind (optional, to use template parameter deduction)
|
|
**/
|
|
template<class Signature> auto solve(char const* name) const {
|
|
auto res = ::dlsym(module, name);
|
|
if (unlikely(!res))
|
|
throw Exception::ModuleSymbol{};
|
|
return *reinterpret_cast<Signature*>(&res);
|
|
}
|
|
template<class Signature> void solve(char const* name, Signature& func) const {
|
|
func = solve<Signature>(name);
|
|
}
|
|
public:
|
|
/** Loader constructor.
|
|
* @param path Path to the library to load
|
|
**/
|
|
TransactionalLibrary(char const* path) {
|
|
{ // Resolve path and load module
|
|
char resolved[PATH_MAX];
|
|
if (unlikely(!realpath(path, resolved)))
|
|
throw Exception::PathResolve{};
|
|
module = ::dlopen(resolved, RTLD_NOW | RTLD_LOCAL);
|
|
if (unlikely(!module))
|
|
throw Exception::ModuleLoading{};
|
|
}
|
|
{ // Bind module's 'tm_*' symbols
|
|
solve("tm_create", tm_create);
|
|
solve("tm_destroy", tm_destroy);
|
|
solve("tm_start", tm_start);
|
|
solve("tm_size", tm_size);
|
|
solve("tm_align", tm_align);
|
|
solve("tm_begin", tm_begin);
|
|
solve("tm_end", tm_end);
|
|
solve("tm_read", tm_read);
|
|
solve("tm_write", tm_write);
|
|
solve("tm_alloc", tm_alloc);
|
|
solve("tm_free", tm_free);
|
|
}
|
|
}
|
|
/** Unloader destructor.
|
|
**/
|
|
~TransactionalLibrary() noexcept {
|
|
::dlclose(module); // Close loaded module
|
|
}
|
|
};
|
|
|
|
/** One shared memory region management class.
|
|
**/
|
|
class TransactionalMemory final: private NonCopyable {
|
|
private:
|
|
/** Check whether the given alignment is a power of 2
|
|
**/
|
|
constexpr static bool is_power_of_two(size_t align) noexcept {
|
|
return align != 0 && (align & (align - 1)) == 0;
|
|
}
|
|
public:
|
|
/** Opaque shared memory region handle class.
|
|
**/
|
|
using Shared = STM::shared_t;
|
|
/** Transaction class alias.
|
|
**/
|
|
using TX = STM::tx_t;
|
|
private:
|
|
TransactionalLibrary const& tl; // Bound transactional library
|
|
Shared shared; // Handle of the shared memory region used
|
|
void* start_addr; // Shared memory region first segment's start address
|
|
size_t start_size; // Shared memory region first segment's size (in bytes)
|
|
size_t alignment; // Shared memory region alignment (in bytes)
|
|
public:
|
|
/** Bind constructor.
|
|
* @param library Transactional library to use
|
|
* @param align Shared memory region required alignment
|
|
* @param size Size of the shared memory region to allocate
|
|
**/
|
|
TransactionalMemory(TransactionalLibrary const& library, size_t align, size_t size): tl{library}, start_size{size}, alignment{align} {
|
|
if (unlikely(assert_mode && (!is_power_of_two(align) || size % align != 0)))
|
|
throw Exception::TransactionAlign{};
|
|
bounded_run(max_side_time, [&]() {
|
|
shared = tl.tm_create(size, align);
|
|
if (unlikely(shared == STM::invalid_shared))
|
|
throw Exception::TransactionCreate{};
|
|
start_addr = tl.tm_start(shared);
|
|
}, "The transactional library takes too long creating the shared memory");
|
|
}
|
|
/** Unbind destructor.
|
|
**/
|
|
~TransactionalMemory() noexcept {
|
|
bounded_run(max_side_time, [&]() {
|
|
tl.tm_destroy(shared);
|
|
}, "The transactional library takes too long destroying the shared memory");
|
|
}
|
|
public:
|
|
/** [thread-safe] Return the start address of the first shared segment.
|
|
* @return Address of the first allocated shared region
|
|
**/
|
|
auto get_start() const noexcept {
|
|
return start_addr;
|
|
}
|
|
/** [thread-safe] Return the size of the first shared segment.
|
|
* @return Size in the first allocated shared region (in bytes)
|
|
**/
|
|
auto get_size() const noexcept {
|
|
return start_size;
|
|
}
|
|
/** [thread-safe] Get the shared memory region global alignment.
|
|
* @return Global alignment (in bytes)
|
|
**/
|
|
auto get_align() const noexcept {
|
|
return alignment;
|
|
}
|
|
public:
|
|
/** [thread-safe] Begin a new transaction on the shared memory region.
|
|
* @param ro Whether the transaction is read-only
|
|
* @return Opaque transaction ID, 'STM::invalid_tx' on failure
|
|
**/
|
|
auto begin(bool ro) const noexcept {
|
|
return tl.tm_begin(shared, ro);
|
|
}
|
|
/** [thread-safe] End the given transaction.
|
|
* @param tx Opaque transaction ID
|
|
* @return Whether the whole transaction is a success
|
|
**/
|
|
auto end(TX tx) const noexcept {
|
|
return tl.tm_end(shared, tx);
|
|
}
|
|
/** [thread-safe] Read operation in the given transaction, source in the shared region and target in a private region.
|
|
* @param tx Transaction to use
|
|
* @param source Source start address
|
|
* @param size Source/target range
|
|
* @param target Target start address
|
|
* @return Whether the whole transaction can continue
|
|
**/
|
|
auto read(TX tx, void const* source, size_t size, void* target) const noexcept {
|
|
return tl.tm_read(shared, tx, source, size, target);
|
|
}
|
|
/** [thread-safe] Write operation in the given transaction, source in a private region and target in the shared region.
|
|
* @param tx Transaction to use
|
|
* @param source Source start address
|
|
* @param size Source/target range
|
|
* @param target Target start address
|
|
* @return Whether the whole transaction can continue
|
|
**/
|
|
auto write(TX tx, void const* source, size_t size, void* target) const noexcept {
|
|
return tl.tm_write(shared, tx, source, size, target);
|
|
}
|
|
/** [thread-safe] Memory allocation operation in the given transaction, throw if no memory available.
|
|
* @param tx Transaction to use
|
|
* @param size Size to allocate
|
|
* @param target Target start address
|
|
* @return Allocation status
|
|
**/
|
|
auto alloc(TX tx, size_t size, void** target) const noexcept {
|
|
return tl.tm_alloc(shared, tx, size, target);
|
|
}
|
|
/** [thread-safe] Memory freeing operation in the given transaction.
|
|
* @param tx Transaction to use
|
|
* @param target Target start address
|
|
* @return Whether the whole transaction can continue
|
|
**/
|
|
auto free(TX tx, void* target) const noexcept {
|
|
return tl.tm_free(shared, tx, target);
|
|
}
|
|
};
|
|
|
|
/** One transaction over a shared memory region management class.
|
|
**/
|
|
class Transaction final: private NonCopyable {
|
|
public:
|
|
/** Transaction mode class.
|
|
**/
|
|
enum class Mode: bool {
|
|
read_write = false,
|
|
read_only = true
|
|
};
|
|
private:
|
|
TransactionalMemory const& tm; // Bound transactional memory
|
|
STM::tx_t tx; // Opaque transaction handle
|
|
bool aborted; // Transaction was aborted
|
|
bool is_ro; // Whether the transaction is read-only (solely for assertion)
|
|
public:
|
|
/** Deleted copy constructor/assignment.
|
|
**/
|
|
Transaction(Transaction const&) = delete;
|
|
Transaction& operator=(Transaction const&) = delete;
|
|
/** Begin constructor.
|
|
* @param tm Transactional memory to bind
|
|
* @param ro Whether the transaction is read-only
|
|
**/
|
|
Transaction(TransactionalMemory const& tm, Mode ro): tm{tm}, tx{tm.begin(static_cast<bool>(ro))}, aborted{false}, is_ro{static_cast<bool>(ro)} {
|
|
if (unlikely(tx == STM::invalid_tx))
|
|
throw Exception::TransactionBegin{};
|
|
}
|
|
/** End destructor.
|
|
**/
|
|
~Transaction() noexcept(false) {
|
|
if (likely(!aborted)) {
|
|
if (unlikely(!tm.end(tx)))
|
|
throw Exception::TransactionRetry{};
|
|
}
|
|
}
|
|
public:
|
|
/** [thread-safe] Return the bound transactional memory instance.
|
|
* @return Bound transactional memory instance
|
|
**/
|
|
auto const& get_tm() const noexcept {
|
|
return tm;
|
|
}
|
|
public:
|
|
/** [thread-safe] Read operation in the bound transaction, source in the shared region and target in a private region.
|
|
* @param source Source start address
|
|
* @param size Source/target range
|
|
* @param target Target start address
|
|
**/
|
|
void read(void const* source, size_t size, void* target) {
|
|
if (unlikely(!tm.read(tx, source, size, target))) {
|
|
aborted = true;
|
|
throw Exception::TransactionRetry{};
|
|
}
|
|
}
|
|
/** [thread-safe] Write operation in the bound transaction, source in a private region and target in the shared region.
|
|
* @param source Source start address
|
|
* @param size Source/target range
|
|
* @param target Target start address
|
|
**/
|
|
void write(void const* source, size_t size, void* target) {
|
|
if (unlikely(assert_mode && is_ro))
|
|
throw Exception::TransactionReadOnly{};
|
|
if (unlikely(!tm.write(tx, source, size, target))) {
|
|
aborted = true;
|
|
throw Exception::TransactionRetry{};
|
|
}
|
|
}
|
|
/** [thread-safe] Memory allocation operation in the bound transaction, throw if no memory available.
|
|
* @param size Size to allocate
|
|
* @return Target start address
|
|
**/
|
|
void* alloc(size_t size) {
|
|
if (unlikely(assert_mode && is_ro))
|
|
throw Exception::TransactionReadOnly{};
|
|
void* target;
|
|
switch (tm.alloc(tx, size, &target)) {
|
|
case STM::Alloc::success:
|
|
return target;
|
|
case STM::Alloc::nomem:
|
|
throw Exception::TransactionAlloc{};
|
|
default: // STM::Alloc::abort
|
|
aborted = true;
|
|
throw Exception::TransactionRetry{};
|
|
}
|
|
}
|
|
/** [thread-safe] Memory freeing operation in the bound transaction.
|
|
* @param target Target start address
|
|
**/
|
|
void free(void* target) {
|
|
if (unlikely(assert_mode && is_ro))
|
|
throw Exception::TransactionReadOnly{};
|
|
if (unlikely(!tm.free(tx, target))) {
|
|
aborted = true;
|
|
throw Exception::TransactionRetry{};
|
|
}
|
|
}
|
|
};
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
/** Shared read/write helper class.
|
|
* @param Type Specified type (array)
|
|
**/
|
|
template<class Type> class Shared {
|
|
protected:
|
|
Transaction& tx; // Bound transaction
|
|
Type* address; // Address in shared memory
|
|
public:
|
|
/** Binding constructor.
|
|
* @param tx Bound transaction
|
|
* @param address Address to bind to
|
|
**/
|
|
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type*>(address)} {
|
|
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
|
|
throw Exception::SharedAlign{};
|
|
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type) != 0))
|
|
throw Exception::SharedAlign{};
|
|
}
|
|
public:
|
|
/** Get the address in shared memory.
|
|
* @return Address in shared memory
|
|
**/
|
|
auto get() const noexcept {
|
|
return address;
|
|
}
|
|
public:
|
|
/** Read operation.
|
|
* @return Private copy of the content at the shared address
|
|
**/
|
|
Type read() const {
|
|
Type res;
|
|
tx.read(address, sizeof(Type), &res);
|
|
return res;
|
|
}
|
|
operator Type() const {
|
|
return read();
|
|
}
|
|
/** Write operation.
|
|
* @param source Private content to write at the shared address
|
|
**/
|
|
void write(Type const& source) const {
|
|
tx.write(&source, sizeof(Type), address);
|
|
}
|
|
void operator=(Type const& source) const {
|
|
return write(source);
|
|
}
|
|
public:
|
|
/** Address of the first byte after the entry.
|
|
* @return First byte after the entry
|
|
**/
|
|
void* after() const noexcept {
|
|
return address + 1;
|
|
}
|
|
};
|
|
template<class Type> class Shared<Type*> {
|
|
protected:
|
|
Transaction& tx; // Bound transaction
|
|
Type** address; // Address in shared memory
|
|
public:
|
|
/** Binding constructor.
|
|
* @param tx Bound transaction
|
|
* @param address Address to bind to
|
|
**/
|
|
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type**>(address)} {
|
|
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
|
|
throw Exception::SharedAlign{};
|
|
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type*) != 0))
|
|
throw Exception::SharedAlign{};
|
|
}
|
|
public:
|
|
/** Get the address in shared memory.
|
|
* @return Address in shared memory
|
|
**/
|
|
auto get() const noexcept {
|
|
return address;
|
|
}
|
|
public:
|
|
/** Read operation.
|
|
* @return Private copy of the content at the shared address
|
|
**/
|
|
Type* read() const {
|
|
Type* res;
|
|
tx.read(address, sizeof(Type*), &res);
|
|
return res;
|
|
}
|
|
operator Type*() const {
|
|
return read();
|
|
}
|
|
/** Write operation.
|
|
* @param source Private content to write at the shared address
|
|
**/
|
|
void write(Type* source) const {
|
|
tx.write(&source, sizeof(Type*), address);
|
|
}
|
|
void operator=(Type* source) const {
|
|
return write(source);
|
|
}
|
|
/** Allocate and write operation.
|
|
* @param size Size to allocate (defaults to size of the underlying class)
|
|
* @return Private copy of the just-written content at the shared address
|
|
**/
|
|
Type* alloc(size_t size = 0) const {
|
|
if (unlikely(assert_mode && read() != nullptr))
|
|
throw Exception::SharedDoubleAlloc{};
|
|
auto addr = tx.alloc(size > 0 ? size: sizeof(Type));
|
|
write(reinterpret_cast<Type*>(addr));
|
|
return reinterpret_cast<Type*>(addr);
|
|
}
|
|
/** Free and write operation.
|
|
**/
|
|
void free() const {
|
|
if (unlikely(assert_mode && read() == nullptr))
|
|
throw Exception::SharedDoubleFree{};
|
|
tx.free(read());
|
|
write(nullptr);
|
|
}
|
|
public:
|
|
/** Address of the first byte after the entry.
|
|
* @return First byte after the entry
|
|
**/
|
|
void* after() const noexcept {
|
|
return address + 1;
|
|
}
|
|
};
|
|
template<class Type> class Shared<Type[]> {
|
|
protected:
|
|
Transaction& tx; // Bound transaction
|
|
Type* address; // Address of the first element in shared memory
|
|
public:
|
|
/** Binding constructor.
|
|
* @param tx Bound transaction
|
|
* @param address Address to bind to
|
|
**/
|
|
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type*>(address)} {
|
|
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
|
|
throw Exception::SharedAlign{};
|
|
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type) != 0))
|
|
throw Exception::SharedAlign{};
|
|
}
|
|
public:
|
|
/** Get the address in shared memory.
|
|
* @return Address in shared memory
|
|
**/
|
|
auto get() const noexcept {
|
|
return address;
|
|
}
|
|
public:
|
|
/** Read operation.
|
|
* @param index Index to read
|
|
* @return Private copy of the content at the shared address
|
|
**/
|
|
Type read(size_t index) const {
|
|
Type res;
|
|
tx.read(address + index, sizeof(Type), &res);
|
|
return res;
|
|
}
|
|
/** Write operation.
|
|
* @param index Index to write
|
|
* @param source Private content to write at the shared address
|
|
**/
|
|
void write(size_t index, Type const& source) const {
|
|
tx.write(tx, &source, sizeof(Type), address + index);
|
|
}
|
|
public:
|
|
/** Reference a cell.
|
|
* @param index Cell to reference
|
|
* @return Shared on that cell
|
|
**/
|
|
Shared<Type> operator[](size_t index) const {
|
|
return Shared<Type>{tx, address + index};
|
|
}
|
|
/** Address of the first byte after the entry.
|
|
* @param length Length of the array
|
|
* @return First byte after the entry
|
|
**/
|
|
void* after(size_t length) const noexcept {
|
|
return address + length;
|
|
}
|
|
};
|
|
template<class Type, size_t n> class Shared<Type[n]> {
|
|
protected:
|
|
Transaction& tx; // Bound transaction
|
|
Type* address; // Address of the first element in shared memory
|
|
public:
|
|
/** Binding constructor.
|
|
* @param tx Bound transaction
|
|
* @param address Address to bind to
|
|
**/
|
|
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type*>(address)} {
|
|
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
|
|
throw Exception::SharedAlign{};
|
|
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type) != 0))
|
|
throw Exception::SharedAlign{};
|
|
}
|
|
public:
|
|
/** Get the address in shared memory.
|
|
* @return Address in shared memory
|
|
**/
|
|
auto get() const noexcept {
|
|
return address;
|
|
}
|
|
public:
|
|
/** Read operation.
|
|
* @param index Index to read
|
|
* @return Private copy of the content at the shared address
|
|
**/
|
|
Type read(size_t index) const {
|
|
if (unlikely(assert_mode && index >= n))
|
|
throw Exception::SharedOverflow{};
|
|
Type res;
|
|
tx.read(address + index, sizeof(Type), &res);
|
|
return res;
|
|
}
|
|
/** Write operation.
|
|
* @param index Index to write
|
|
* @param source Private content to write at the shared address
|
|
**/
|
|
void write(size_t index, Type const& source) const {
|
|
if (unlikely(assert_mode && index >= n))
|
|
throw Exception::SharedOverflow{};
|
|
tx.write(tx, &source, sizeof(Type), address + index);
|
|
}
|
|
public:
|
|
/** Reference a cell.
|
|
* @param index Cell to reference
|
|
* @return Shared on that cell
|
|
**/
|
|
Shared<Type> operator[](size_t index) const {
|
|
if (unlikely(assert_mode && index >= n))
|
|
throw Exception::SharedOverflow{};
|
|
return Shared<Type>{tx, address + index};
|
|
}
|
|
/** Address of the first byte after the array.
|
|
* @return First byte after the array
|
|
**/
|
|
void* after() const noexcept {
|
|
return address + n;
|
|
}
|
|
};
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
/** Repeat a given transaction until it commits.
|
|
* @param tm Transactional memory
|
|
* @param mode Transactional mode
|
|
* @param func Transaction closure (Transaction& -> ...)
|
|
* @return Returned value (or void) when the transaction committed
|
|
**/
|
|
template<class Func> static auto transactional(TransactionalMemory const& tm, Transaction::Mode mode, Func&& func) {
|
|
do {
|
|
try {
|
|
Transaction tx{tm, mode};
|
|
return func(tx);
|
|
} catch (Exception::TransactionRetry const&) {
|
|
continue;
|
|
}
|
|
} while (true);
|
|
}
|