Skip to main content

reinhardt_db/orm/two_phase_commit/
transaction_log.rs

1//! Transaction Log
2//!
3//! Persists the state of Two-Phase Commit transactions and enables failure recovery.
4
5use super::{TransactionState, TwoPhaseError};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::{Arc, Mutex};
10
11/// Transaction log entry
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct TransactionLogEntry {
14	/// Transaction ID
15	pub transaction_id: String,
16	/// Current state
17	pub state: TransactionState,
18	/// List of participants
19	pub participants: Vec<String>,
20	/// Timestamp of log entry
21	pub timestamp: chrono::DateTime<chrono::Utc>,
22	/// Additional metadata
23	pub metadata: HashMap<String, String>,
24}
25
26impl TransactionLogEntry {
27	/// Create a new log entry
28	pub fn new(
29		transaction_id: impl Into<String>,
30		state: TransactionState,
31		participants: Vec<String>,
32	) -> Self {
33		Self {
34			transaction_id: transaction_id.into(),
35			state,
36			participants,
37			timestamp: chrono::Utc::now(),
38			metadata: HashMap::new(),
39		}
40	}
41
42	/// Add metadata
43	pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
44		self.metadata.insert(key.into(), value.into());
45		self
46	}
47}
48
49/// Transaction log interface
50pub trait TransactionLog: Send + Sync {
51	/// Write a log entry
52	fn write(&self, entry: &TransactionLogEntry) -> Result<(), TwoPhaseError>;
53
54	/// Read a log entry by transaction ID
55	fn read(&self, transaction_id: &str) -> Result<Option<TransactionLogEntry>, TwoPhaseError>;
56
57	/// Read all log entries
58	fn read_all(&self) -> Result<Vec<TransactionLogEntry>, TwoPhaseError>;
59
60	/// Delete a log entry
61	fn delete(&self, transaction_id: &str) -> Result<(), TwoPhaseError>;
62
63	/// Find transactions by specific state
64	fn find_by_state(
65		&self,
66		state: TransactionState,
67	) -> Result<Vec<TransactionLogEntry>, TwoPhaseError>;
68}
69
70/// In-memory transaction log
71///
72/// Simple memory-based implementation for testing. Persisted logs should be used in production environments.
73#[derive(Debug, Clone)]
74pub struct InMemoryTransactionLog {
75	entries: Arc<Mutex<HashMap<String, TransactionLogEntry>>>,
76}
77
78impl InMemoryTransactionLog {
79	/// Create a new in-memory log
80	pub fn new() -> Self {
81		Self {
82			entries: Arc::new(Mutex::new(HashMap::new())),
83		}
84	}
85}
86
87impl Default for InMemoryTransactionLog {
88	fn default() -> Self {
89		Self::new()
90	}
91}
92
93impl TransactionLog for InMemoryTransactionLog {
94	fn write(&self, entry: &TransactionLogEntry) -> Result<(), TwoPhaseError> {
95		let mut entries = self
96			.entries
97			.lock()
98			.map_err(|_| TwoPhaseError::LogError("Failed to acquire lock".to_string()))?;
99		entries.insert(entry.transaction_id.clone(), entry.clone());
100		Ok(())
101	}
102
103	fn read(&self, transaction_id: &str) -> Result<Option<TransactionLogEntry>, TwoPhaseError> {
104		let entries = self
105			.entries
106			.lock()
107			.map_err(|_| TwoPhaseError::LogError("Failed to acquire lock".to_string()))?;
108		Ok(entries.get(transaction_id).cloned())
109	}
110
111	fn read_all(&self) -> Result<Vec<TransactionLogEntry>, TwoPhaseError> {
112		let entries = self
113			.entries
114			.lock()
115			.map_err(|_| TwoPhaseError::LogError("Failed to acquire lock".to_string()))?;
116		Ok(entries.values().cloned().collect())
117	}
118
119	fn delete(&self, transaction_id: &str) -> Result<(), TwoPhaseError> {
120		let mut entries = self
121			.entries
122			.lock()
123			.map_err(|_| TwoPhaseError::LogError("Failed to acquire lock".to_string()))?;
124		entries.remove(transaction_id);
125		Ok(())
126	}
127
128	fn find_by_state(
129		&self,
130		state: TransactionState,
131	) -> Result<Vec<TransactionLogEntry>, TwoPhaseError> {
132		let entries = self
133			.entries
134			.lock()
135			.map_err(|_| TwoPhaseError::LogError("Failed to acquire lock".to_string()))?;
136		Ok(entries
137			.values()
138			.filter(|e| e.state == state)
139			.cloned()
140			.collect())
141	}
142}
143
144/// File-based transaction log
145///
146/// Persists transaction state to the file system in JSON format.
147#[derive(Debug)]
148pub struct FileTransactionLog {
149	log_dir: PathBuf,
150}
151
152impl FileTransactionLog {
153	/// Create a new file-based log
154	///
155	/// # Examples
156	///
157	/// ```no_run
158	/// use reinhardt_db::orm::two_phase_commit::transaction_log::FileTransactionLog;
159	/// use std::path::PathBuf;
160	///
161	/// let log = FileTransactionLog::new(PathBuf::from("/var/log/2pc"));
162	/// ```
163	pub fn new(log_dir: PathBuf) -> Result<Self, TwoPhaseError> {
164		// Create log directory if it doesn't exist
165		if !log_dir.exists() {
166			std::fs::create_dir_all(&log_dir).map_err(|e| {
167				TwoPhaseError::LogError(format!("Failed to create log directory: {}", e))
168			})?;
169		}
170
171		Ok(Self { log_dir })
172	}
173
174	/// Generate file path from transaction ID
175	fn entry_path(&self, transaction_id: &str) -> PathBuf {
176		self.log_dir.join(format!("{}.json", transaction_id))
177	}
178}
179
180impl TransactionLog for FileTransactionLog {
181	fn write(&self, entry: &TransactionLogEntry) -> Result<(), TwoPhaseError> {
182		let path = self.entry_path(&entry.transaction_id);
183		let json = serde_json::to_string_pretty(entry)
184			.map_err(|e| TwoPhaseError::LogError(format!("Serialization error: {}", e)))?;
185
186		std::fs::write(&path, json)
187			.map_err(|e| TwoPhaseError::LogError(format!("Failed to write log file: {}", e)))?;
188
189		Ok(())
190	}
191
192	fn read(&self, transaction_id: &str) -> Result<Option<TransactionLogEntry>, TwoPhaseError> {
193		let path = self.entry_path(transaction_id);
194
195		if !path.exists() {
196			return Ok(None);
197		}
198
199		let json = std::fs::read_to_string(&path)
200			.map_err(|e| TwoPhaseError::LogError(format!("Failed to read log file: {}", e)))?;
201
202		let entry: TransactionLogEntry = serde_json::from_str(&json)
203			.map_err(|e| TwoPhaseError::LogError(format!("Deserialization error: {}", e)))?;
204
205		Ok(Some(entry))
206	}
207
208	fn read_all(&self) -> Result<Vec<TransactionLogEntry>, TwoPhaseError> {
209		let mut entries = Vec::new();
210
211		let dir_entries = std::fs::read_dir(&self.log_dir)
212			.map_err(|e| TwoPhaseError::LogError(format!("Failed to read log directory: {}", e)))?;
213
214		for entry_result in dir_entries {
215			let entry = entry_result.map_err(|e| {
216				TwoPhaseError::LogError(format!("Failed to read directory entry: {}", e))
217			})?;
218
219			let path = entry.path();
220			if path.extension().and_then(|s| s.to_str()) == Some("json") {
221				let json = std::fs::read_to_string(&path).map_err(|e| {
222					TwoPhaseError::LogError(format!("Failed to read log file: {}", e))
223				})?;
224
225				let log_entry: TransactionLogEntry = serde_json::from_str(&json).map_err(|e| {
226					TwoPhaseError::LogError(format!("Deserialization error: {}", e))
227				})?;
228
229				entries.push(log_entry);
230			}
231		}
232
233		Ok(entries)
234	}
235
236	fn delete(&self, transaction_id: &str) -> Result<(), TwoPhaseError> {
237		let path = self.entry_path(transaction_id);
238
239		if path.exists() {
240			std::fs::remove_file(&path).map_err(|e| {
241				TwoPhaseError::LogError(format!("Failed to delete log file: {}", e))
242			})?;
243		}
244
245		Ok(())
246	}
247
248	fn find_by_state(
249		&self,
250		state: TransactionState,
251	) -> Result<Vec<TransactionLogEntry>, TwoPhaseError> {
252		let all_entries = self.read_all()?;
253		Ok(all_entries
254			.into_iter()
255			.filter(|e| e.state == state)
256			.collect())
257	}
258}
259
260#[cfg(test)]
261mod tests {
262	use super::*;
263
264	#[test]
265	fn test_log_entry_creation() {
266		let entry = TransactionLogEntry::new(
267			"txn_001",
268			TransactionState::Active,
269			vec!["db1".to_string(), "db2".to_string()],
270		);
271
272		assert_eq!(entry.transaction_id, "txn_001");
273		assert_eq!(entry.state, TransactionState::Active);
274		assert_eq!(entry.participants.len(), 2);
275		assert!(entry.metadata.is_empty());
276	}
277
278	#[test]
279	fn test_log_entry_with_metadata() {
280		let entry = TransactionLogEntry::new("txn_002", TransactionState::Prepared, vec![])
281			.with_metadata("user", "alice")
282			.with_metadata("operation", "transfer");
283
284		assert_eq!(entry.metadata.get("user"), Some(&"alice".to_string()));
285		assert_eq!(
286			entry.metadata.get("operation"),
287			Some(&"transfer".to_string())
288		);
289	}
290
291	#[test]
292	fn test_in_memory_log_write_read() {
293		let log = InMemoryTransactionLog::new();
294		let entry =
295			TransactionLogEntry::new("txn_003", TransactionState::Active, vec!["db1".to_string()]);
296
297		log.write(&entry).unwrap();
298		let read_entry = log.read("txn_003").unwrap();
299
300		assert_eq!(read_entry, Some(entry));
301	}
302
303	#[test]
304	fn test_in_memory_log_delete() {
305		let log = InMemoryTransactionLog::new();
306		let entry = TransactionLogEntry::new("txn_004", TransactionState::Active, vec![]);
307
308		log.write(&entry).unwrap();
309		assert!(log.read("txn_004").unwrap().is_some());
310
311		log.delete("txn_004").unwrap();
312		assert!(log.read("txn_004").unwrap().is_none());
313	}
314
315	#[test]
316	fn test_in_memory_log_find_by_state() {
317		let log = InMemoryTransactionLog::new();
318
319		log.write(&TransactionLogEntry::new(
320			"txn_005",
321			TransactionState::Active,
322			vec![],
323		))
324		.unwrap();
325		log.write(&TransactionLogEntry::new(
326			"txn_006",
327			TransactionState::Prepared,
328			vec![],
329		))
330		.unwrap();
331		log.write(&TransactionLogEntry::new(
332			"txn_007",
333			TransactionState::Prepared,
334			vec![],
335		))
336		.unwrap();
337
338		let prepared = log.find_by_state(TransactionState::Prepared).unwrap();
339		assert_eq!(prepared.len(), 2);
340	}
341
342	#[test]
343	fn test_in_memory_log_read_all() {
344		let log = InMemoryTransactionLog::new();
345
346		log.write(&TransactionLogEntry::new(
347			"txn_008",
348			TransactionState::Active,
349			vec![],
350		))
351		.unwrap();
352		log.write(&TransactionLogEntry::new(
353			"txn_009",
354			TransactionState::Prepared,
355			vec![],
356		))
357		.unwrap();
358
359		let all = log.read_all().unwrap();
360		assert_eq!(all.len(), 2);
361	}
362
363	#[test]
364	fn test_file_log_write_read() {
365		let temp_dir = std::env::temp_dir().join("reinhardt_2pc_test");
366		let _ = std::fs::remove_dir_all(&temp_dir); // Clean up if exists
367
368		let log = FileTransactionLog::new(temp_dir.clone()).unwrap();
369		let entry = TransactionLogEntry::new(
370			"txn_file_001",
371			TransactionState::Active,
372			vec!["db1".to_string()],
373		);
374
375		log.write(&entry).unwrap();
376		let read_entry = log.read("txn_file_001").unwrap();
377
378		assert_eq!(
379			read_entry.as_ref().map(|e| &e.transaction_id),
380			Some(&"txn_file_001".to_string())
381		);
382
383		// Cleanup
384		std::fs::remove_dir_all(&temp_dir).unwrap();
385	}
386
387	#[test]
388	fn test_file_log_delete() {
389		let temp_dir = std::env::temp_dir().join("reinhardt_2pc_test_delete");
390		let _ = std::fs::remove_dir_all(&temp_dir);
391
392		let log = FileTransactionLog::new(temp_dir.clone()).unwrap();
393		let entry = TransactionLogEntry::new("txn_file_002", TransactionState::Active, vec![]);
394
395		log.write(&entry).unwrap();
396		assert!(log.read("txn_file_002").unwrap().is_some());
397
398		log.delete("txn_file_002").unwrap();
399		assert!(log.read("txn_file_002").unwrap().is_none());
400
401		// Cleanup
402		std::fs::remove_dir_all(&temp_dir).unwrap();
403	}
404
405	#[test]
406	fn test_file_log_find_by_state() {
407		let temp_dir = std::env::temp_dir().join("reinhardt_2pc_test_state");
408		let _ = std::fs::remove_dir_all(&temp_dir);
409
410		let log = FileTransactionLog::new(temp_dir.clone()).unwrap();
411
412		log.write(&TransactionLogEntry::new(
413			"txn_file_003",
414			TransactionState::Active,
415			vec![],
416		))
417		.unwrap();
418		log.write(&TransactionLogEntry::new(
419			"txn_file_004",
420			TransactionState::Prepared,
421			vec![],
422		))
423		.unwrap();
424		log.write(&TransactionLogEntry::new(
425			"txn_file_005",
426			TransactionState::Prepared,
427			vec![],
428		))
429		.unwrap();
430
431		let prepared = log.find_by_state(TransactionState::Prepared).unwrap();
432		assert_eq!(prepared.len(), 2);
433
434		// Cleanup
435		std::fs::remove_dir_all(&temp_dir).unwrap();
436	}
437}