Skip to main content

reinhardt_core/signals/
transaction.rs

1//! Transaction Support - Signals tied to database transaction lifecycle
2//!
3//! This module provides signal support for database transaction lifecycle events,
4//! allowing receivers to be notified of transaction begin, commit, and rollback events.
5//!
6//! # Examples
7//!
8//! ```rust,no_run
9//! use reinhardt_core::signals::transaction::{on_commit, on_rollback, TransactionSignals};
10//!
11//! # #[tokio::main]
12//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
13//! // Connect to transaction commit
14//! on_commit().connect(|ctx| async move {
15//!     println!("Transaction committed: {:?}", ctx);
16//!     Ok(())
17//! });
18//!
19//! // Use TransactionSignals for manual control
20//! let tx_signals = TransactionSignals::new("tx_1");
21//! tx_signals.send_begin().await?;
22//! tx_signals.send_commit().await?;
23//! # Ok(())
24//! # }
25//! ```
26
27use super::core::SignalName;
28use super::error::SignalError;
29use super::registry::get_signal;
30use super::signal::Signal;
31use serde::{Deserialize, Serialize};
32
33/// Transaction context passed to signal receivers
34///
35/// # Examples
36///
37/// ```
38/// use reinhardt_core::signals::transaction::TransactionContext;
39///
40/// let ctx = TransactionContext::new("tx_123");
41/// assert_eq!(ctx.transaction_id, "tx_123");
42/// assert_eq!(ctx.savepoint_depth, 0);
43/// ```
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct TransactionContext {
46	/// Unique transaction identifier
47	pub transaction_id: String,
48	/// Savepoint depth for nested transactions
49	pub savepoint_depth: usize,
50	/// Optional savepoint name
51	pub savepoint_name: Option<String>,
52	/// Whether this is a nested transaction
53	pub is_nested: bool,
54}
55
56impl TransactionContext {
57	/// Create a new transaction context
58	///
59	/// # Examples
60	///
61	/// ```
62	/// use reinhardt_core::signals::transaction::TransactionContext;
63	///
64	/// let ctx = TransactionContext::new("my_transaction");
65	/// assert_eq!(ctx.transaction_id, "my_transaction");
66	/// assert!(!ctx.is_nested);
67	/// ```
68	pub fn new(transaction_id: impl Into<String>) -> Self {
69		Self {
70			transaction_id: transaction_id.into(),
71			savepoint_depth: 0,
72			savepoint_name: None,
73			is_nested: false,
74		}
75	}
76
77	/// Create a nested transaction context
78	///
79	/// # Examples
80	///
81	/// ```
82	/// use reinhardt_core::signals::transaction::TransactionContext;
83	///
84	/// let ctx = TransactionContext::nested("tx_1", 1, "sp_1");
85	/// assert!(ctx.is_nested);
86	/// assert_eq!(ctx.savepoint_depth, 1);
87	/// assert_eq!(ctx.savepoint_name, Some("sp_1".to_string()));
88	/// ```
89	pub fn nested(
90		transaction_id: impl Into<String>,
91		depth: usize,
92		savepoint_name: impl Into<String>,
93	) -> Self {
94		Self {
95			transaction_id: transaction_id.into(),
96			savepoint_depth: depth,
97			savepoint_name: Some(savepoint_name.into()),
98			is_nested: true,
99		}
100	}
101
102	/// Enter a savepoint (increase nesting depth)
103	///
104	/// # Examples
105	///
106	/// ```
107	/// use reinhardt_core::signals::transaction::TransactionContext;
108	///
109	/// let mut ctx = TransactionContext::new("tx_1");
110	/// ctx.enter_savepoint("checkpoint_1");
111	/// assert_eq!(ctx.savepoint_depth, 1);
112	/// assert_eq!(ctx.savepoint_name, Some("checkpoint_1".to_string()));
113	/// ```
114	pub fn enter_savepoint(&mut self, name: impl Into<String>) {
115		self.savepoint_depth += 1;
116		self.savepoint_name = Some(name.into());
117		self.is_nested = true;
118	}
119
120	/// Exit a savepoint (decrease nesting depth)
121	///
122	/// # Examples
123	///
124	/// ```
125	/// use reinhardt_core::signals::transaction::TransactionContext;
126	///
127	/// let mut ctx = TransactionContext::nested("tx_1", 2, "sp_2");
128	/// ctx.exit_savepoint();
129	/// assert_eq!(ctx.savepoint_depth, 1);
130	/// ```
131	pub fn exit_savepoint(&mut self) {
132		if self.savepoint_depth > 0 {
133			self.savepoint_depth -= 1;
134		}
135		if self.savepoint_depth == 0 {
136			self.savepoint_name = None;
137			self.is_nested = false;
138		}
139	}
140}
141
142/// Transaction signal manager
143///
144/// Manages transaction lifecycle signals for a specific transaction
145///
146/// # Examples
147///
148/// ```
149/// use reinhardt_core::signals::transaction::TransactionSignals;
150///
151/// let signals = TransactionSignals::new("transaction_1");
152/// ```
153pub struct TransactionSignals {
154	context: TransactionContext,
155}
156
157impl TransactionSignals {
158	/// Create a new transaction signal manager
159	///
160	/// # Examples
161	///
162	/// ```
163	/// use reinhardt_core::signals::transaction::TransactionSignals;
164	///
165	/// let signals = TransactionSignals::new("tx_001");
166	/// ```
167	pub fn new(transaction_id: impl Into<String>) -> Self {
168		Self {
169			context: TransactionContext::new(transaction_id),
170		}
171	}
172
173	/// Create a nested transaction signal manager
174	///
175	/// # Examples
176	///
177	/// ```
178	/// use reinhardt_core::signals::transaction::TransactionSignals;
179	///
180	/// let signals = TransactionSignals::nested("tx_001", 1, "savepoint_1");
181	/// ```
182	pub fn nested(
183		transaction_id: impl Into<String>,
184		depth: usize,
185		savepoint_name: impl Into<String>,
186	) -> Self {
187		Self {
188			context: TransactionContext::nested(transaction_id, depth, savepoint_name),
189		}
190	}
191
192	/// Get the transaction context
193	///
194	/// # Examples
195	///
196	/// ```
197	/// use reinhardt_core::signals::transaction::TransactionSignals;
198	///
199	/// let signals = TransactionSignals::new("tx_001");
200	/// let ctx = signals.context();
201	/// assert_eq!(ctx.transaction_id, "tx_001");
202	/// ```
203	pub fn context(&self) -> &TransactionContext {
204		&self.context
205	}
206
207	/// Send transaction begin signal
208	///
209	/// # Examples
210	///
211	/// ```rust,no_run
212	/// use reinhardt_core::signals::transaction::TransactionSignals;
213	///
214	/// # #[tokio::main]
215	/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
216	/// let signals = TransactionSignals::new("tx_001");
217	/// signals.send_begin().await?;
218	/// # Ok(())
219	/// # }
220	/// ```
221	pub async fn send_begin(&self) -> Result<(), SignalError> {
222		on_begin().send(self.context.clone()).await
223	}
224
225	/// Send transaction commit signal
226	///
227	/// # Examples
228	///
229	/// ```rust,no_run
230	/// use reinhardt_core::signals::transaction::TransactionSignals;
231	///
232	/// # #[tokio::main]
233	/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
234	/// let signals = TransactionSignals::new("tx_001");
235	/// signals.send_commit().await?;
236	/// # Ok(())
237	/// # }
238	/// ```
239	pub async fn send_commit(&self) -> Result<(), SignalError> {
240		on_commit().send(self.context.clone()).await
241	}
242
243	/// Send transaction rollback signal
244	///
245	/// # Examples
246	///
247	/// ```rust,no_run
248	/// use reinhardt_core::signals::transaction::TransactionSignals;
249	///
250	/// # #[tokio::main]
251	/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
252	/// let signals = TransactionSignals::new("tx_001");
253	/// signals.send_rollback().await?;
254	/// # Ok(())
255	/// # }
256	/// ```
257	pub async fn send_rollback(&self) -> Result<(), SignalError> {
258		on_rollback().send(self.context.clone()).await
259	}
260
261	/// Enter a savepoint and send signal
262	///
263	/// # Examples
264	///
265	/// ```rust,no_run
266	/// use reinhardt_core::signals::transaction::TransactionSignals;
267	///
268	/// # #[tokio::main]
269	/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
270	/// let mut signals = TransactionSignals::new("tx_001");
271	/// signals.enter_savepoint("checkpoint_1").await?;
272	/// # Ok(())
273	/// # }
274	/// ```
275	pub async fn enter_savepoint(&mut self, name: impl Into<String>) -> Result<(), SignalError> {
276		self.context.enter_savepoint(name);
277		on_savepoint().send(self.context.clone()).await
278	}
279
280	/// Exit a savepoint and send signal
281	///
282	/// # Examples
283	///
284	/// ```rust,no_run
285	/// use reinhardt_core::signals::transaction::TransactionSignals;
286	///
287	/// # #[tokio::main]
288	/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
289	/// let mut signals = TransactionSignals::nested("tx_001", 1, "sp_1");
290	/// signals.exit_savepoint().await?;
291	/// # Ok(())
292	/// # }
293	/// ```
294	pub async fn exit_savepoint(&mut self) -> Result<(), SignalError> {
295		self.context.exit_savepoint();
296		on_savepoint_release().send(self.context.clone()).await
297	}
298}
299
300/// Get the transaction begin signal
301///
302/// # Examples
303///
304/// ```
305/// use reinhardt_core::signals::transaction::on_begin;
306///
307/// let signal = on_begin();
308/// ```
309pub fn on_begin() -> Signal<TransactionContext> {
310	get_signal::<TransactionContext>(SignalName::custom("transaction_begin"))
311}
312
313/// Get the transaction commit signal
314///
315/// # Examples
316///
317/// ```
318/// use reinhardt_core::signals::transaction::on_commit;
319///
320/// let signal = on_commit();
321/// ```
322pub fn on_commit() -> Signal<TransactionContext> {
323	get_signal::<TransactionContext>(SignalName::custom("transaction_commit"))
324}
325
326/// Get the transaction rollback signal
327///
328/// # Examples
329///
330/// ```
331/// use reinhardt_core::signals::transaction::on_rollback;
332///
333/// let signal = on_rollback();
334/// ```
335pub fn on_rollback() -> Signal<TransactionContext> {
336	get_signal::<TransactionContext>(SignalName::custom("transaction_rollback"))
337}
338
339/// Get the savepoint create signal
340///
341/// # Examples
342///
343/// ```
344/// use reinhardt_core::signals::transaction::on_savepoint;
345///
346/// let signal = on_savepoint();
347/// ```
348pub fn on_savepoint() -> Signal<TransactionContext> {
349	get_signal::<TransactionContext>(SignalName::custom("transaction_savepoint"))
350}
351
352/// Get the savepoint release signal
353///
354/// # Examples
355///
356/// ```
357/// use reinhardt_core::signals::transaction::on_savepoint_release;
358///
359/// let signal = on_savepoint_release();
360/// ```
361pub fn on_savepoint_release() -> Signal<TransactionContext> {
362	get_signal::<TransactionContext>(SignalName::custom("transaction_savepoint_release"))
363}
364
365#[cfg(test)]
366mod tests {
367	use super::*;
368	use parking_lot::Mutex;
369	use std::sync::Arc;
370	use std::sync::atomic::{AtomicUsize, Ordering};
371
372	#[test]
373	fn test_transaction_context_creation() {
374		let ctx = TransactionContext::new("tx_1");
375		assert_eq!(ctx.transaction_id, "tx_1");
376		assert_eq!(ctx.savepoint_depth, 0);
377		assert_eq!(ctx.savepoint_name, None);
378		assert!(!ctx.is_nested);
379	}
380
381	#[test]
382	fn test_transaction_context_nested() {
383		let ctx = TransactionContext::nested("tx_1", 2, "sp_2");
384		assert_eq!(ctx.transaction_id, "tx_1");
385		assert_eq!(ctx.savepoint_depth, 2);
386		assert_eq!(ctx.savepoint_name, Some("sp_2".to_string()));
387		assert!(ctx.is_nested);
388	}
389
390	#[test]
391	fn test_transaction_context_enter_savepoint() {
392		let mut ctx = TransactionContext::new("tx_1");
393		ctx.enter_savepoint("checkpoint_1");
394		assert_eq!(ctx.savepoint_depth, 1);
395		assert_eq!(ctx.savepoint_name, Some("checkpoint_1".to_string()));
396		assert!(ctx.is_nested);
397	}
398
399	#[test]
400	fn test_transaction_context_exit_savepoint() {
401		let mut ctx = TransactionContext::nested("tx_1", 2, "sp_2");
402		ctx.exit_savepoint();
403		assert_eq!(ctx.savepoint_depth, 1);
404
405		ctx.exit_savepoint();
406		assert_eq!(ctx.savepoint_depth, 0);
407		assert_eq!(ctx.savepoint_name, None);
408		assert!(!ctx.is_nested);
409	}
410
411	#[tokio::test]
412	async fn test_on_commit_signal() {
413		let counter = Arc::new(AtomicUsize::new(0));
414		let counter_clone = Arc::clone(&counter);
415
416		on_commit().connect(move |_ctx| {
417			let counter = Arc::clone(&counter_clone);
418			async move {
419				counter.fetch_add(1, Ordering::SeqCst);
420				Ok(())
421			}
422		});
423
424		let ctx = TransactionContext::new("tx_1");
425		on_commit().send(ctx).await.unwrap();
426
427		assert_eq!(counter.load(Ordering::SeqCst), 1);
428	}
429
430	#[tokio::test]
431	async fn test_on_rollback_signal() {
432		let counter = Arc::new(AtomicUsize::new(0));
433		let counter_clone = Arc::clone(&counter);
434
435		on_rollback().connect(move |_ctx| {
436			let counter = Arc::clone(&counter_clone);
437			async move {
438				counter.fetch_add(1, Ordering::SeqCst);
439				Ok(())
440			}
441		});
442
443		let ctx = TransactionContext::new("tx_1");
444		on_rollback().send(ctx).await.unwrap();
445
446		assert_eq!(counter.load(Ordering::SeqCst), 1);
447	}
448
449	#[tokio::test]
450	#[serial_test::serial]
451	async fn test_transaction_signals_flow() {
452		// Clean up before test
453		on_begin().disconnect_all();
454		on_commit().disconnect_all();
455
456		let events = Arc::new(Mutex::new(Vec::new()));
457
458		let e1 = events.clone();
459		on_begin().connect(move |ctx| {
460			let e = e1.clone();
461			async move {
462				e.lock().push(format!("begin:{}", ctx.transaction_id));
463				Ok(())
464			}
465		});
466
467		let e2 = events.clone();
468		on_commit().connect(move |ctx| {
469			let e = e2.clone();
470			async move {
471				e.lock().push(format!("commit:{}", ctx.transaction_id));
472				Ok(())
473			}
474		});
475
476		let signals = TransactionSignals::new("tx_test");
477		signals.send_begin().await.unwrap();
478		signals.send_commit().await.unwrap();
479
480		let event_log = events.lock();
481		assert_eq!(event_log.len(), 2);
482		assert_eq!(event_log[0], "begin:tx_test");
483		assert_eq!(event_log[1], "commit:tx_test");
484
485		// Clean up after test
486		on_begin().disconnect_all();
487		on_commit().disconnect_all();
488	}
489
490	#[tokio::test]
491	#[serial_test::serial]
492	async fn test_savepoint_signals() {
493		// Clean up before test
494		on_savepoint().disconnect_all();
495		on_savepoint_release().disconnect_all();
496
497		let counter = Arc::new(AtomicUsize::new(0));
498
499		let c1 = counter.clone();
500		on_savepoint().connect(move |_| {
501			let c = c1.clone();
502			async move {
503				c.fetch_add(1, Ordering::SeqCst);
504				Ok(())
505			}
506		});
507
508		let c2 = counter.clone();
509		on_savepoint_release().connect(move |_| {
510			let c = c2.clone();
511			async move {
512				c.fetch_add(10, Ordering::SeqCst);
513				Ok(())
514			}
515		});
516
517		let mut signals = TransactionSignals::new("tx_1");
518		signals.enter_savepoint("sp_1").await.unwrap();
519		signals.exit_savepoint().await.unwrap();
520
521		assert_eq!(counter.load(Ordering::SeqCst), 11); // 1 + 10
522
523		// Clean up after test
524		on_savepoint().disconnect_all();
525		on_savepoint_release().disconnect_all();
526	}
527
528	#[tokio::test]
529	#[serial_test::serial]
530	async fn test_nested_transaction_signals() {
531		// Clean up before test
532		on_savepoint().disconnect_all();
533
534		let events = Arc::new(Mutex::new(Vec::new()));
535
536		let e = events.clone();
537		on_savepoint().connect(move |ctx| {
538			let e = e.clone();
539			async move {
540				e.lock().push(format!(
541					"savepoint:{}:depth:{}",
542					ctx.savepoint_name.as_deref().unwrap_or(""),
543					ctx.savepoint_depth
544				));
545				Ok(())
546			}
547		});
548
549		let mut signals = TransactionSignals::new("tx_nested");
550		signals.enter_savepoint("level_1").await.unwrap();
551		signals.enter_savepoint("level_2").await.unwrap();
552
553		let event_log = events.lock();
554		assert_eq!(event_log.len(), 2);
555		assert_eq!(event_log[0], "savepoint:level_1:depth:1");
556		assert_eq!(event_log[1], "savepoint:level_2:depth:2");
557
558		// Clean up after test
559		on_savepoint().disconnect_all();
560	}
561}