1mod log;
70mod recovery;
72mod types;
73
74use std::collections::HashSet;
75use std::path::{Path, PathBuf};
76use std::sync::atomic::{AtomicU64, Ordering};
77use std::time::{SystemTime, UNIX_EPOCH};
78
79use doublets::data::LinkReference;
80
81use crate::error::LinkError;
82use crate::link::GenericLink;
83use crate::named_types::NamedTypesDecorator;
84use crate::storage::{LinksStorage, LinksStorageRef};
85
86pub use log::{FileTransitionLog, TransitionLogStore};
87pub use types::{
88 CommitMode, DoubletLink, GenericDoubletLink, GenericTransition, LogRetentionPolicy, Transition,
89 TransitionKind,
90};
91use types::{COMMIT_MARKER_PREFIX, ROLLBACK_MARKER_PREFIX, TRANSITION_NAME_PREFIX};
92
93struct PendingTransaction<T> {
96 id: u128,
97 transitions: Vec<GenericTransition<T>>,
98 auto_commit: bool,
99 started_ms: i64,
100}
101
102type ObservedChange<T> = (T, GenericDoubletLink<T>, GenericDoubletLink<T>);
105
106fn record_observed<T: LinkReference>(
115 observed: &mut Vec<ObservedChange<T>>,
116 before: GenericLink<T>,
117 after: GenericLink<T>,
118) {
119 let zero = T::from_byte(0);
120 let key = if before.index != zero {
121 before.index
122 } else {
123 after.index
124 };
125 if key == zero {
126 return;
127 }
128 let before = GenericDoubletLink::from_link(&before);
129 let after = GenericDoubletLink::from_link(&after);
130 match observed.iter_mut().find(|(index, _, _)| *index == key) {
131 Some(entry) => {
132 if entry.1.index == zero {
133 entry.1 = before;
134 }
135 entry.2 = after;
136 }
137 None => observed.push((key, before, after)),
138 }
139}
140
141#[derive(Debug, Clone)]
143pub struct TransactionHandle {
144 pub id: u128,
145 pub started_ms: i64,
146}
147
148pub struct GenericTransactionsDecorator<T, S, L>
151where
152 T: LinkReference,
153 S: LinksStorage<T>,
154 L: TransitionLogStore,
155{
156 inner: S,
157 log_store: L,
158 log: Vec<GenericTransition<T>>,
159 committed: HashSet<u128>,
160 rolled_back: HashSet<u128>,
161 applied: HashSet<i64>,
162 current: Option<PendingTransaction<T>>,
163 sequence_counter: i64,
164 applied_sequence: i64,
165 retention_policy: LogRetentionPolicy,
166 commit_mode: CommitMode,
167 replaying: bool,
168 trace: bool,
169}
170
171pub type TransactionsDecorator =
173 GenericTransactionsDecorator<u32, NamedTypesDecorator, NamedTypesDecorator>;
174
175impl<T, S, L> GenericTransactionsDecorator<T, S, L>
176where
177 T: LinkReference,
178 S: LinksStorage<T>,
179 L: TransitionLogStore,
180{
181 pub fn new(
185 inner: S,
186 log_store: L,
187 retention_policy: LogRetentionPolicy,
188 commit_mode: CommitMode,
189 trace: bool,
190 ) -> Result<Self, LinkError> {
191 let mut decorator = Self {
192 inner,
193 log_store,
194 log: Vec::new(),
195 committed: HashSet::new(),
196 rolled_back: HashSet::new(),
197 applied: HashSet::new(),
198 current: None,
199 sequence_counter: 0,
200 applied_sequence: 0,
201 retention_policy,
202 commit_mode,
203 replaying: false,
204 trace,
205 };
206 decorator.recover()?;
207 Ok(decorator)
208 }
209
210 pub fn make_transitions_database_filename<P: AsRef<Path>>(database_filename: P) -> PathBuf {
212 let path = database_filename.as_ref();
213 let stem = path
214 .file_stem()
215 .and_then(|s| s.to_str())
216 .unwrap_or_default();
217 let name = format!("{stem}.transitions.links");
218 match path.parent() {
219 Some(parent) if !parent.as_os_str().is_empty() => parent.join(name),
220 _ => PathBuf::from(name),
221 }
222 }
223
224 pub fn retention_policy(&self) -> &LogRetentionPolicy {
225 &self.retention_policy
226 }
227
228 pub fn set_retention_policy(&mut self, policy: LogRetentionPolicy) {
229 self.retention_policy = policy;
230 }
231
232 pub fn commit_mode(&self) -> CommitMode {
233 self.commit_mode
234 }
235
236 pub fn set_commit_mode(&mut self, mode: CommitMode) {
237 self.commit_mode = mode;
238 }
239
240 pub fn applied_sequence(&self) -> i64 {
241 self.applied_sequence
242 }
243
244 pub fn last_logged_sequence(&self) -> i64 {
245 self.sequence_counter
246 }
247
248 pub fn log(&self) -> Vec<GenericTransition<T>> {
250 self.log.clone()
251 }
252
253 pub fn inner(&self) -> &S {
254 &self.inner
255 }
256
257 pub fn inner_mut(&mut self) -> &mut S {
258 &mut self.inner
259 }
260
261 pub fn log_store(&self) -> &L {
262 &self.log_store
263 }
264
265 pub fn log_store_mut(&mut self) -> &mut L {
266 &mut self.log_store
267 }
268
269 pub fn into_inner(self) -> (S, L) {
270 (self.inner, self.log_store)
271 }
272
273 pub fn flush(&mut self) -> Result<(), LinkError> {
275 self.inner.flush()?;
276 self.log_store.flush_log()?;
277 Ok(())
278 }
279
280 pub fn save(&mut self) -> Result<(), LinkError> {
283 self.flush()
284 }
285
286 pub fn has_external_changes(&self) -> Result<bool, LinkError> {
289 self.inner.has_external_changes()
290 }
291
292 pub fn reload(&mut self) -> Result<(), LinkError> {
295 if self.current.is_some() {
296 return Err(LinkError::Transaction(
297 "Cannot reload while a transaction is open.".to_string(),
298 ));
299 }
300 self.inner.reload()?;
301 self.recover()
302 }
303
304 pub fn create(&mut self, source: T, target: T) -> Result<T, LinkError> {
307 if self.replaying {
308 return self.inner.create_link(source, target);
309 }
310 let owns = self.ensure_open_transaction();
311 let id = self.inner.create_link(source, target)?;
312 let after = self
313 .inner
314 .get_link(id)
315 .map(|link| GenericDoubletLink::from_link(&link))
316 .unwrap_or_else(|| GenericDoubletLink::new(id, source, target));
317 self.record_transition(TransitionKind::Create, GenericDoubletLink::empty(), after)?;
318 if owns {
319 self.commit_current()?;
320 }
321 Ok(id)
322 }
323
324 pub fn update(&mut self, id: T, source: T, target: T) -> Result<GenericLink<T>, LinkError> {
325 if self.replaying {
326 return self.inner.update_link(id, source, target);
327 }
328 let before = self.snapshot(id);
329 let owns = self.ensure_open_transaction();
330 let mut observed: Vec<ObservedChange<T>> = Vec::new();
331 let outcome = self
332 .inner
333 .update_link_observed(id, source, target, &mut |before, after| {
334 record_observed(&mut observed, before, after)
335 });
336 let prev = match outcome {
337 Ok(prev) => prev,
338 Err(err) => {
339 if owns {
340 self.current = None;
341 }
342 return Err(err);
343 }
344 };
345 if observed.is_empty() {
346 let after = self
347 .inner
348 .get_link(id)
349 .map(|link| GenericDoubletLink::from_link(&link))
350 .unwrap_or_else(|| GenericDoubletLink::new(id, source, target));
351 self.record_transition(TransitionKind::Update, before, after)?;
352 } else {
353 self.record_observed_transitions(&observed)?;
354 }
355 if owns {
356 self.commit_current()?;
357 }
358 Ok(prev)
359 }
360
361 pub fn delete(&mut self, id: T) -> Result<GenericLink<T>, LinkError> {
362 self.delete_observed(id, &mut |_, _| {})
363 }
364
365 pub fn delete_observed(
373 &mut self,
374 id: T,
375 observer: &mut dyn FnMut(GenericLink<T>, GenericLink<T>),
376 ) -> Result<GenericLink<T>, LinkError> {
377 if self.replaying {
378 let deleted = self.inner.delete_link(id)?;
379 observer(deleted, GenericLink::null());
380 return Ok(deleted);
381 }
382 let before = self.snapshot(id);
383 let owns = self.ensure_open_transaction();
384 let mut observed: Vec<ObservedChange<T>> = Vec::new();
385 let outcome = self.inner.delete_link_observed(id, &mut |before, after| {
386 observer(before, after);
387 record_observed(&mut observed, before, after)
388 });
389 let deleted = match outcome {
390 Ok(d) => d,
391 Err(err) => {
392 if owns {
393 self.current = None;
394 }
395 return Err(err);
396 }
397 };
398 if observed.is_empty() {
399 self.record_transition(TransitionKind::Delete, before, GenericDoubletLink::empty())?;
400 } else {
401 self.record_observed_transitions(&observed)?;
402 }
403 if owns {
404 self.commit_current()?;
405 }
406 Ok(deleted)
407 }
408
409 pub fn create_and_update(&mut self, source: T, target: T) -> Result<T, LinkError> {
414 let owns = self.ensure_open_transaction();
415 let zero = T::from_byte(0);
416 let id = self.create(zero, zero)?;
417 self.update(id, source, target)?;
418 if owns {
419 self.commit_current()?;
420 }
421 Ok(id)
422 }
423
424 pub fn exists(&self, id: T) -> bool {
425 self.inner.link_exists(id)
426 }
427
428 pub fn search(&self, source: T, target: T) -> Option<T> {
429 self.inner.search_link(source, target)
430 }
431
432 pub fn get_or_create(&mut self, source: T, target: T) -> Result<T, LinkError> {
433 if let Some(existing) = self.inner.search_link(source, target) {
434 return Ok(existing);
435 }
436 self.create(source, target)
437 }
438
439 pub fn ensure_created(&mut self, id: T) -> Result<T, LinkError> {
440 self.inner.ensure_link_created(id)
443 }
444
445 fn snapshot(&self, id: T) -> GenericDoubletLink<T> {
447 let zero = T::from_byte(0);
448 self.inner
449 .get_link(id)
450 .map(|link| GenericDoubletLink::from_link(&link))
451 .unwrap_or_else(|| GenericDoubletLink::new(id, zero, zero))
452 }
453
454 fn ensure_open_transaction(&mut self) -> bool {
455 if self.current.is_none() {
456 self.current = Some(PendingTransaction {
457 id: new_transaction_id(),
458 transitions: Vec::new(),
459 auto_commit: true,
460 started_ms: now_unix_ms(),
461 });
462 true
463 } else {
464 false
465 }
466 }
467
468 fn record_observed_transitions(
483 &mut self,
484 observed: &[ObservedChange<T>],
485 ) -> Result<(), LinkError> {
486 let zero = T::from_byte(0);
487 for (_, before, after) in observed {
488 let kind = match (before.index != zero, after.index != zero) {
489 (false, true) => TransitionKind::Create,
490 (true, false) => TransitionKind::Delete,
491 _ => TransitionKind::Update,
492 };
493 self.record_transition(kind, *before, *after)?;
494 }
495 Ok(())
496 }
497
498 fn record_transition(
499 &mut self,
500 kind: TransitionKind,
501 before: GenericDoubletLink<T>,
502 after: GenericDoubletLink<T>,
503 ) -> Result<(), LinkError> {
504 self.sequence_counter += 1;
505 let sequence = self.sequence_counter;
506 let timestamp_ms = now_unix_ms();
507 let transaction_id = self.current.as_ref().map(|tx| tx.id).ok_or_else(|| {
508 LinkError::Transaction(
509 "internal: missing open transaction while recording transition".to_string(),
510 )
511 })?;
512 let transition = GenericTransition {
513 transaction_id,
514 sequence,
515 timestamp_ms,
516 kind,
517 before,
518 after,
519 };
520 if let Some(current) = self.current.as_mut() {
521 current.transitions.push(transition);
522 }
523 self.log.push(transition);
524 self.write_transition_to_log(&transition)?;
525 if self.trace {
526 eprintln!(
527 "[Transactions] Recorded {:?} seq={} tx={:032x}: ({},{},{}) -> ({},{},{}).",
528 kind,
529 sequence,
530 transaction_id,
531 before.index,
532 before.source,
533 before.target,
534 after.index,
535 after.source,
536 after.target,
537 );
538 }
539 Ok(())
540 }
541
542 fn write_transition_to_log(
543 &mut self,
544 transition: &GenericTransition<T>,
545 ) -> Result<(), LinkError> {
546 self.log_store.append_log_entry(&format!(
547 "{TRANSITION_NAME_PREFIX}{}",
548 transition.serialize()
549 ))
550 }
551
552 fn write_marker(&mut self, name: &str) -> Result<(), LinkError> {
553 self.log_store.append_log_entry(name)
554 }
555
556 pub fn begin_transaction(&mut self) -> Result<TransactionHandle, LinkError> {
559 if self.current.is_some() {
560 return Err(LinkError::Transaction(
561 "Nested transactions are not supported.".to_string(),
562 ));
563 }
564 let id = new_transaction_id();
565 let started_ms = now_unix_ms();
566 self.current = Some(PendingTransaction {
567 id,
568 transitions: Vec::new(),
569 auto_commit: false,
570 started_ms,
571 });
572 Ok(TransactionHandle { id, started_ms })
573 }
574
575 pub fn commit(&mut self) -> Result<(), LinkError> {
576 if self.current.is_none() {
577 return Ok(());
578 }
579 self.commit_current()
580 }
581
582 fn commit_current(&mut self) -> Result<(), LinkError> {
583 let pending = match self.current.take() {
584 Some(p) => p,
585 None => return Ok(()),
586 };
587 self.committed.insert(pending.id);
588 self.write_marker(&format!("{COMMIT_MARKER_PREFIX}{:032x}", pending.id))?;
589 if self.trace {
590 eprintln!(
591 "[Transactions] Committed tx {:032x} (mode={:?}, transitions={}).",
592 pending.id,
593 self.commit_mode,
594 pending.transitions.len()
595 );
596 }
597 for transition in &pending.transitions {
598 self.mark_applied(transition)?;
599 }
600 let _ = pending.auto_commit;
601 let _ = pending.started_ms;
602 self.enforce_retention()?;
603 Ok(())
604 }
605
606 pub fn rollback(&mut self) -> Result<(), LinkError> {
607 let pending = match self.current.take() {
608 Some(p) => p,
609 None => return Ok(()),
610 };
611 self.rolled_back.insert(pending.id);
612 self.replaying = true;
613 for transition in pending.transitions.iter().rev() {
614 self.try_revert_transition(transition);
615 }
616 self.replaying = false;
617 self.write_marker(&format!("{ROLLBACK_MARKER_PREFIX}{:032x}", pending.id))?;
618 if self.trace {
619 eprintln!(
620 "[Transactions] Rolled back tx {:032x} ({} transitions).",
621 pending.id,
622 pending.transitions.len(),
623 );
624 }
625 self.enforce_retention()?;
626 Ok(())
627 }
628}
629
630impl<T, S, L> GenericTransactionsDecorator<T, S, L>
633where
634 T: LinkReference,
635 S: LinksStorageRef<T>,
636 L: TransitionLogStore,
637{
638 pub fn get(&self, id: T) -> Option<&GenericLink<T>> {
639 self.inner.get_link_ref(id)
640 }
641
642 pub fn all(&self) -> Vec<&GenericLink<T>> {
643 self.inner.all_link_refs()
644 }
645
646 pub fn query(
647 &self,
648 index: Option<T>,
649 source: Option<T>,
650 target: Option<T>,
651 ) -> Vec<&GenericLink<T>> {
652 self.inner.query_link_refs(index, source, target)
653 }
654}
655
656fn insert_ordered<T: LinkReference>(
659 list: &mut Vec<GenericTransition<T>>,
660 transition: GenericTransition<T>,
661) {
662 let mut lo = 0usize;
663 let mut hi = list.len();
664 while lo < hi {
665 let mid = (lo + hi) / 2;
666 if list[mid].sequence < transition.sequence {
667 lo = mid + 1;
668 } else {
669 hi = mid;
670 }
671 }
672 list.insert(lo, transition);
673}
674
675static TX_COUNTER: AtomicU64 = AtomicU64::new(0);
676
677fn new_transaction_id() -> u128 {
678 let count = TX_COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
681 let now = now_unix_ms() as u128;
682 (now << 64) | count
683}
684
685fn now_unix_ms() -> i64 {
686 SystemTime::now()
687 .duration_since(UNIX_EPOCH)
688 .map(|d| d.as_millis() as i64)
689 .unwrap_or(0)
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695
696 #[test]
697 fn retention_policy_parses_specs() {
698 assert!(matches!(
699 LogRetentionPolicy::parse("infinite").unwrap(),
700 LogRetentionPolicy::Infinite
701 ));
702 assert!(matches!(
703 LogRetentionPolicy::parse("sized:1000").unwrap(),
704 LogRetentionPolicy::Sized {
705 max_transitions: 1000
706 }
707 ));
708 match LogRetentionPolicy::parse("chunked:500:/tmp/x").unwrap() {
709 LogRetentionPolicy::Chunked {
710 chunk_size,
711 archive_directory,
712 } => {
713 assert_eq!(chunk_size, 500);
714 assert_eq!(archive_directory, PathBuf::from("/tmp/x"));
715 }
716 _ => panic!("expected Chunked"),
717 }
718 assert!(LogRetentionPolicy::parse("garbage").is_err());
719 }
720
721 #[test]
722 fn transition_round_trips_through_serialize() {
723 let t = Transition {
724 transaction_id: 0xabcdef1234567890u128,
725 sequence: 42,
726 timestamp_ms: 1234567890,
727 kind: TransitionKind::Update,
728 before: DoubletLink::new(1, 2, 3),
729 after: DoubletLink::new(1, 4, 5),
730 };
731 let parsed = Transition::try_parse(&t.serialize()).unwrap();
732 assert_eq!(t, parsed);
733 }
734
735 #[test]
736 fn wide_transition_is_rejected_by_a_narrow_address_type() {
737 let wide = GenericTransition::<u64> {
738 transaction_id: 7,
739 sequence: 1,
740 timestamp_ms: 0,
741 kind: TransitionKind::Create,
742 before: GenericDoubletLink::empty(),
743 after: GenericDoubletLink::new(u32::MAX as u64 + 1, 0, 0),
744 };
745 assert!(matches!(
746 GenericTransition::<u32>::parse(&wide.serialize()),
747 Err(LinkError::AddressOutOfRange(_))
748 ));
749 assert!(GenericTransition::<u64>::parse(&wide.serialize()).is_ok());
750 }
751}