Skip to main content

postproject_core/
transaction.rs

1//! Backend-neutral transaction state semantics.
2
3use crate::{Error, ErrorKind, Result, TransactionId};
4
5/// The state of an explicit domain transaction.
6#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
7pub enum TransactionState {
8    /// Mutations may still be staged.
9    Open,
10    /// All staged mutations were atomically persisted.
11    Committed,
12    /// Staged mutations were discarded.
13    RolledBack,
14}
15
16/// A small state machine shared by storage-backed domain transactions.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub struct TransactionLifecycle {
19    id: TransactionId,
20    state: TransactionState,
21}
22
23impl TransactionLifecycle {
24    /// Creates a new open transaction with a stable identity.
25    #[must_use]
26    pub fn new() -> Self {
27        Self {
28            id: TransactionId::new(),
29            state: TransactionState::Open,
30        }
31    }
32
33    /// Creates an open transaction with an existing identity.
34    #[must_use]
35    pub const fn with_id(id: TransactionId) -> Self {
36        Self {
37            id,
38            state: TransactionState::Open,
39        }
40    }
41
42    /// Returns the transaction identity.
43    #[must_use]
44    pub const fn id(self) -> TransactionId {
45        self.id
46    }
47
48    /// Returns the current state.
49    #[must_use]
50    pub const fn state(self) -> TransactionState {
51        self.state
52    }
53
54    /// Ensures that a mutation may be staged.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`ErrorKind::Conflict`] if the transaction is already closed.
59    pub fn ensure_open(self) -> Result<()> {
60        if self.state == TransactionState::Open {
61            Ok(())
62        } else {
63            Err(self.closed_error())
64        }
65    }
66
67    /// Marks the transaction committed after the backend commit succeeds.
68    ///
69    /// # Errors
70    ///
71    /// Returns [`ErrorKind::Conflict`] if the transaction is already closed.
72    pub fn mark_committed(&mut self) -> Result<()> {
73        self.ensure_open()?;
74        self.state = TransactionState::Committed;
75        Ok(())
76    }
77
78    /// Marks the transaction rolled back after staged work is discarded.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`ErrorKind::Conflict`] if the transaction is already closed.
83    pub fn mark_rolled_back(&mut self) -> Result<()> {
84        self.ensure_open()?;
85        self.state = TransactionState::RolledBack;
86        Ok(())
87    }
88
89    fn closed_error(self) -> Error {
90        Error::new(
91            ErrorKind::Conflict,
92            format!("transaction {} is already {:?}", self.id, self.state),
93        )
94    }
95}
96
97impl Default for TransactionLifecycle {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn commit_closes_transaction() {
109        let mut lifecycle = TransactionLifecycle::new();
110        lifecycle
111            .mark_committed()
112            .expect("open transaction commits");
113
114        assert_eq!(lifecycle.state(), TransactionState::Committed);
115        assert_eq!(
116            lifecycle
117                .mark_rolled_back()
118                .expect_err("committed transaction stays closed")
119                .kind(),
120            ErrorKind::Conflict
121        );
122    }
123
124    #[test]
125    fn rollback_closes_transaction() {
126        let mut lifecycle = TransactionLifecycle::new();
127        lifecycle
128            .mark_rolled_back()
129            .expect("open transaction rolls back");
130
131        assert_eq!(lifecycle.state(), TransactionState::RolledBack);
132        assert_eq!(
133            lifecycle
134                .mark_committed()
135                .expect_err("rolled-back transaction stays closed")
136                .kind(),
137            ErrorKind::Conflict
138        );
139    }
140}