1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//! Transaction management
//!
//! For general information and examples, see
//! [Transaction control](https://www.tarantool.io/en/doc/latest/book/box/atomic_index/#atomic-atomic-execution).
//!
//! Observe the following rules when working with transactions:
//!
//! 👉 **Rule #1**
//! The requests in a transaction must be sent to a server as a single block.
//! It is not enough to enclose them between begin and commit or rollback.
//! To ensure they are sent as a single block: put them in a function, or put them all on one line, or use a delimiter
//! so that multi-line requests are handled together.
//!
//! 👉 **Rule #2**
//! All database operations in a transaction should use the same storage engine.
//! It is not safe to access tuple sets that are defined with `{engine='vinyl'}` and also access tuple sets that are
//! defined with `{engine='memtx'}`, in the same transaction.
//!
//! 👉 **Rule #3**
//! Requests which cause changes to the data definition – create, alter, drop, truncate – are only allowed with
//! Tarantool version 2.1 or later. Data-definition requests which change an index or change a format, such as
//! `space_object:create_index()` and `space_object:format()`, are not allowed inside transactions except as the first
//! request.
//!
//! See also:
//! - [Transaction control](https://www.tarantool.io/en/doc/latest/book/box/atomic/)
//! - [Lua reference: Functions for transaction management](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_txn_management/)
//! - [C API reference: Module txn](https://www.tarantool.io/en/doc/latest/dev_guide/reference_capi/txn/)

use crate::error::TarantoolError;
use crate::ffi::tarantool as ffi;

/// Transaction-related error cases
#[derive(Debug, thiserror::Error)]
pub enum TransactionError<E> {
    #[error("transaction has already been started")]
    AlreadyStarted,

    #[error("failed to commit: {0}")]
    FailedToCommit(TarantoolError),

    #[error("failed to rollback: {0}")]
    FailedToRollback(TarantoolError),

    #[error("transaction rolled-back: {0}")]
    RolledBack(E),
}

/// Executes a transaction in the current fiber.
///
/// A transaction is attached to caller fiber, therefore one fiber can have
/// only one active transaction.
///
/// - `f` - function will be invoked within transaction
///
/// Returns result of function `f` execution. Depending on the function result:
/// - will **commit** - if function completes successfully
/// - will **rollback** - if function completes with any error
pub fn transaction<T, E, F>(f: F) -> Result<T, TransactionError<E>>
where
    F: FnOnce() -> Result<T, E>,
{
    if unsafe { ffi::box_txn_begin() } < 0 {
        return Err(TransactionError::AlreadyStarted);
    }

    let result = f();
    match &result {
        Ok(_) => {
            if unsafe { ffi::box_txn_commit() } < 0 {
                let error = TarantoolError::last();
                return Err(TransactionError::FailedToCommit(error));
            }
        }
        Err(_) => {
            if unsafe { ffi::box_txn_rollback() } < 0 {
                let error = TarantoolError::last();
                return Err(TransactionError::FailedToRollback(error));
            }
        }
    }
    result.map_err(TransactionError::RolledBack)
}