reddb_engine/transaction/
state.rs1use 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#[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
37pub 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 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}