Skip to main content

reddb_engine/transaction/
state.rs

1//! Per-connection transaction state and lifecycle transitions.
2//!
3//! This module owns the connection-to-transaction and transaction-local
4//! tenant maps. Runtime dispatch supplies commit-time validation and pending
5//! write finalization, while transaction and savepoint state changes stay
6//! behind this interface.
7
8use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10
11use parking_lot::RwLock;
12
13use crate::transaction::snapshot::{
14    Snapshot, SnapshotManager, SnapshotRetentionGuard, TxnContext, Xid,
15};
16use crate::transaction::IsolationLevel;
17
18/// A requested savepoint is absent from the active transaction.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct SavepointError {
21    pub name: String,
22}
23
24impl std::fmt::Display for SavepointError {
25    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(formatter, "savepoint {} does not exist", self.name)
27    }
28}
29
30impl std::error::Error for SavepointError {}
31
32pub struct RollbackToSavepoint {
33    pub savepoint_xid: Xid,
34    pub aborted_xids: Vec<Xid>,
35}
36
37/// Volatile connection state for a host transaction dispatcher.
38///
39/// The host serializes lifecycle calls per connection and validates transaction
40/// control before calling this layer. Commit preparation supplies write-conflict
41/// validation and durable publication; this state machine performs neither I/O
42/// nor recovery, and its visibility transitions alone do not acknowledge a write.
43pub struct TransactionState {
44    snapshot_manager: Arc<SnapshotManager>,
45    contexts: RwLock<HashMap<u64, TxnContext>>,
46    local_tenants: RwLock<HashMap<u64, Option<String>>>,
47}
48
49impl TransactionState {
50    pub fn new() -> Self {
51        Self {
52            snapshot_manager: Arc::new(SnapshotManager::new()),
53            contexts: RwLock::new(HashMap::new()),
54            local_tenants: RwLock::new(HashMap::new()),
55        }
56    }
57
58    pub fn begin(&self, connection_id: u64, isolation: IsolationLevel) -> Xid {
59        let xid = self.snapshot_manager.begin();
60        if isolation == IsolationLevel::Serializable {
61            self.snapshot_manager.begin_serializable(xid);
62        }
63        let context = TxnContext {
64            xid,
65            isolation,
66            snapshot: self.snapshot_manager.snapshot(xid),
67            savepoints: Vec::new(),
68            released_sub_xids: Vec::new(),
69        };
70        self.contexts.write().insert(connection_id, context);
71        xid
72    }
73
74    pub fn commit<E, Publication>(
75        &self,
76        connection_id: u64,
77        prepare: impl FnOnce(&TxnContext) -> Result<Publication, E>,
78    ) -> Result<Option<TxnContext>, E> {
79        self.local_tenants.write().remove(&connection_id);
80        let Some(context) = self.contexts.write().remove(&connection_id) else {
81            return Ok(None);
82        };
83
84        // A writer can return its publication guard after validation and WAL
85        // append. Keep it through the transaction's visibility transition.
86        let _publication = match prepare(&context) {
87            Ok(publication) => publication,
88            Err(error) => {
89                self.rollback_context(&context);
90                return Err(error);
91            }
92        };
93
94        for (_, xid) in &context.savepoints {
95            self.snapshot_manager.commit(*xid);
96        }
97        for xid in &context.released_sub_xids {
98            self.snapshot_manager.commit(*xid);
99        }
100        self.snapshot_manager.commit(context.xid);
101        Ok(Some(context))
102    }
103
104    pub fn rollback(&self, connection_id: u64) -> Option<TxnContext> {
105        self.local_tenants.write().remove(&connection_id);
106        let context = self.contexts.write().remove(&connection_id)?;
107        self.rollback_context(&context);
108        Some(context)
109    }
110
111    fn rollback_context(&self, context: &TxnContext) {
112        for (_, xid) in &context.savepoints {
113            self.snapshot_manager.rollback(*xid);
114        }
115        for xid in &context.released_sub_xids {
116            self.snapshot_manager.rollback(*xid);
117        }
118        self.snapshot_manager.rollback(context.xid);
119    }
120
121    pub fn has_savepoint(&self, connection_id: u64, name: &str) -> bool {
122        self.contexts
123            .read()
124            .get(&connection_id)
125            .is_some_and(|context| {
126                context
127                    .savepoints
128                    .iter()
129                    .any(|(existing, _)| existing == name)
130            })
131    }
132
133    pub fn savepoint(&self, connection_id: u64, name: &str) -> Option<Xid> {
134        let mut contexts = self.contexts.write();
135        let context = contexts.get_mut(&connection_id)?;
136        let xid = self.snapshot_manager.begin();
137        context.savepoints.push((name.to_string(), xid));
138        Some(xid)
139    }
140
141    pub fn release_savepoint(
142        &self,
143        connection_id: u64,
144        name: &str,
145    ) -> Result<Option<usize>, SavepointError> {
146        let mut contexts = self.contexts.write();
147        let Some(context) = contexts.get_mut(&connection_id) else {
148            return Ok(None);
149        };
150        let position = context
151            .savepoints
152            .iter()
153            .position(|(savepoint_name, _)| savepoint_name == name)
154            .ok_or_else(|| SavepointError {
155                name: name.to_string(),
156            })?;
157        let released = context.savepoints.len() - position;
158        context.released_sub_xids.extend(
159            context
160                .savepoints
161                .split_off(position)
162                .into_iter()
163                .map(|(_, xid)| xid),
164        );
165        Ok(Some(released))
166    }
167
168    pub fn rollback_to_savepoint(
169        &self,
170        connection_id: u64,
171        name: &str,
172    ) -> Result<Option<RollbackToSavepoint>, SavepointError> {
173        let mut contexts = self.contexts.write();
174        let Some(context) = contexts.get_mut(&connection_id) else {
175            return Ok(None);
176        };
177        let position = context
178            .savepoints
179            .iter()
180            .position(|(savepoint_name, _)| savepoint_name == name)
181            .ok_or_else(|| SavepointError {
182                name: name.to_string(),
183            })?;
184        let savepoint_xid = context.savepoints[position].1;
185        let aborted_xids = context
186            .savepoints
187            .split_off(position)
188            .into_iter()
189            .map(|(_, xid)| xid)
190            .collect::<Vec<_>>();
191        drop(contexts);
192
193        for xid in &aborted_xids {
194            self.snapshot_manager.rollback(*xid);
195        }
196        Ok(Some(RollbackToSavepoint {
197            savepoint_xid,
198            aborted_xids,
199        }))
200    }
201
202    pub fn context(&self, connection_id: u64) -> Option<TxnContext> {
203        self.contexts.read().get(&connection_id).cloned()
204    }
205
206    pub fn in_transaction(&self, connection_id: u64) -> bool {
207        self.contexts.read().contains_key(&connection_id)
208    }
209
210    pub fn current_snapshot(&self, connection_id: u64) -> Snapshot {
211        if let Some(context) = self.context(connection_id) {
212            if context.isolation == IsolationLevel::ReadCommitted {
213                return self.snapshot_manager.fresh_read_snapshot();
214            }
215            return context.snapshot;
216        }
217        self.snapshot_manager.fresh_read_snapshot()
218    }
219
220    pub fn retained_current_snapshot(
221        &self,
222        connection_id: u64,
223    ) -> (Snapshot, SnapshotRetentionGuard) {
224        if let Some(context) = self.context(connection_id) {
225            if context.isolation != IsolationLevel::ReadCommitted {
226                return self.snapshot_manager.retain_snapshot(context.snapshot);
227            }
228        }
229        self.snapshot_manager.fresh_retained_snapshot()
230    }
231
232    pub fn writer_xid(&self, connection_id: u64) -> Option<Xid> {
233        self.context(connection_id)
234            .map(|context| context.writer_xid())
235    }
236
237    pub fn own_xids(&self, connection_id: u64) -> HashSet<Xid> {
238        let mut xids = HashSet::new();
239        if let Some(context) = self.context(connection_id) {
240            xids.insert(context.xid);
241            xids.extend(context.savepoints.into_iter().map(|(_, xid)| xid));
242            xids.extend(context.released_sub_xids);
243        }
244        xids
245    }
246
247    pub fn set_local_tenant(&self, connection_id: u64, tenant: Option<String>) {
248        self.local_tenants.write().insert(connection_id, tenant);
249    }
250
251    pub fn local_tenant(&self, connection_id: u64) -> Option<Option<String>> {
252        self.local_tenants.read().get(&connection_id).cloned()
253    }
254
255    pub fn snapshot_manager(&self) -> Arc<SnapshotManager> {
256        Arc::clone(&self.snapshot_manager)
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use crate::transaction::IsolationLevel;
263
264    use super::TransactionState;
265
266    #[test]
267    fn begin_commit_and_rollback_update_snapshot_state() {
268        let state = TransactionState::new();
269
270        let committed_xid = state.begin(11, IsolationLevel::SnapshotIsolation);
271        assert!(state.in_transaction(11));
272        assert_eq!(state.writer_xid(11), Some(committed_xid));
273        assert!(state.snapshot_manager().is_active(committed_xid));
274
275        let committed = state
276            .commit(11, |_| Ok::<_, ()>(()))
277            .expect("commit preparation should succeed")
278            .expect("connection should have an active transaction");
279        assert_eq!(committed.xid, committed_xid);
280        assert!(!state.in_transaction(11));
281        assert!(!state.snapshot_manager().is_active(committed_xid));
282        assert!(!state.snapshot_manager().is_aborted(committed_xid));
283
284        let aborted_xid = state.begin(11, IsolationLevel::ReadCommitted);
285        let aborted = state
286            .rollback(11)
287            .expect("connection should have an active transaction");
288        assert_eq!(aborted.xid, aborted_xid);
289        assert!(!state.in_transaction(11));
290        assert!(state.snapshot_manager().is_aborted(aborted_xid));
291    }
292
293    #[test]
294    fn savepoint_release_and_rollback_follow_nested_sequence() {
295        let state = TransactionState::new();
296        let parent_xid = state.begin(21, IsolationLevel::SnapshotIsolation);
297        let outer_xid = state
298            .savepoint(21, "outer")
299            .expect("transaction should accept a savepoint");
300        let released_xid = state
301            .savepoint(21, "released")
302            .expect("transaction should accept a nested savepoint");
303
304        assert_eq!(
305            state
306                .release_savepoint(21, "released")
307                .expect("savepoint should exist"),
308            Some(1)
309        );
310        assert_eq!(state.writer_xid(21), Some(outer_xid));
311
312        let nested_xid = state
313            .savepoint(21, "nested")
314            .expect("transaction should accept another nested savepoint");
315        let rolled_back = state
316            .rollback_to_savepoint(21, "outer")
317            .expect("savepoint should exist")
318            .expect("connection should have an active transaction");
319        assert_eq!(rolled_back.savepoint_xid, outer_xid);
320        assert_eq!(rolled_back.aborted_xids, vec![outer_xid, nested_xid]);
321        assert_eq!(state.writer_xid(21), Some(parent_xid));
322        assert!(state.snapshot_manager().is_aborted(outer_xid));
323        assert!(state.snapshot_manager().is_aborted(nested_xid));
324
325        state
326            .commit(21, |_| Ok::<_, ()>(()))
327            .expect("commit preparation should succeed")
328            .expect("connection should have an active transaction");
329        assert!(!state.snapshot_manager().is_active(parent_xid));
330        assert!(!state.snapshot_manager().is_active(released_xid));
331        assert!(!state.snapshot_manager().is_aborted(released_xid));
332    }
333
334    #[test]
335    fn connections_keep_transaction_and_local_tenant_state_isolated() {
336        let state = TransactionState::new();
337        let first_xid = state.begin(31, IsolationLevel::SnapshotIsolation);
338        let second_xid = state.begin(32, IsolationLevel::ReadCommitted);
339        let first_savepoint = state
340            .savepoint(31, "first_only")
341            .expect("first connection should have an active transaction");
342
343        assert_eq!(state.writer_xid(31), Some(first_savepoint));
344        assert_eq!(state.writer_xid(32), Some(second_xid));
345        assert_eq!(state.context(31).expect("first context").xid, first_xid);
346        assert_eq!(state.context(32).expect("second context").xid, second_xid);
347
348        state.set_local_tenant(31, Some("tenant-a".to_string()));
349        state.set_local_tenant(32, Some("tenant-b".to_string()));
350        assert_eq!(
351            state.local_tenant(31).flatten().as_deref(),
352            Some("tenant-a")
353        );
354        assert_eq!(
355            state.local_tenant(32).flatten().as_deref(),
356            Some("tenant-b")
357        );
358
359        state
360            .rollback(31)
361            .expect("first transaction should roll back");
362        assert!(!state.in_transaction(31));
363        assert!(state.in_transaction(32));
364        assert_eq!(state.local_tenant(31), None);
365        assert_eq!(
366            state.local_tenant(32).flatten().as_deref(),
367            Some("tenant-b")
368        );
369
370        state
371            .commit(32, |_| Ok::<_, ()>(()))
372            .expect("commit preparation should succeed")
373            .expect("connection should have an active transaction");
374        assert_eq!(state.local_tenant(32), None);
375    }
376}