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 self.update_observed(id, source, target, &mut |_, _| {})
326 }
327
328 pub fn update_observed(
334 &mut self,
335 id: T,
336 source: T,
337 target: T,
338 observer: &mut dyn FnMut(GenericLink<T>, GenericLink<T>),
339 ) -> Result<GenericLink<T>, LinkError> {
340 if self.replaying {
341 return self
342 .inner
343 .update_link_observed(id, source, target, observer);
344 }
345 let before = self.snapshot(id);
346 let owns = self.ensure_open_transaction();
347 let mut observed: Vec<ObservedChange<T>> = Vec::new();
348 let outcome = self
349 .inner
350 .update_link_observed(id, source, target, &mut |before, after| {
351 observer(before, after);
352 record_observed(&mut observed, before, after)
353 });
354 let prev = match outcome {
355 Ok(prev) => prev,
356 Err(err) => {
357 if owns {
358 self.current = None;
359 }
360 return Err(err);
361 }
362 };
363 if observed.is_empty() {
364 let after = self
365 .inner
366 .get_link(id)
367 .map(|link| GenericDoubletLink::from_link(&link))
368 .unwrap_or_else(|| GenericDoubletLink::new(id, source, target));
369 self.record_transition(TransitionKind::Update, before, after)?;
370 } else {
371 self.record_observed_transitions(&observed)?;
372 }
373 if owns {
374 self.commit_current()?;
375 }
376 Ok(prev)
377 }
378
379 pub fn delete(&mut self, id: T) -> Result<GenericLink<T>, LinkError> {
380 self.delete_observed(id, &mut |_, _| {})
381 }
382
383 pub fn delete_observed(
391 &mut self,
392 id: T,
393 observer: &mut dyn FnMut(GenericLink<T>, GenericLink<T>),
394 ) -> Result<GenericLink<T>, LinkError> {
395 if self.replaying {
396 let deleted = self.inner.delete_link(id)?;
397 observer(deleted, GenericLink::null());
398 return Ok(deleted);
399 }
400 let before = self.snapshot(id);
401 let owns = self.ensure_open_transaction();
402 let mut observed: Vec<ObservedChange<T>> = Vec::new();
403 let outcome = self.inner.delete_link_observed(id, &mut |before, after| {
404 observer(before, after);
405 record_observed(&mut observed, before, after)
406 });
407 let deleted = match outcome {
408 Ok(d) => d,
409 Err(err) => {
410 if owns {
411 self.current = None;
412 }
413 return Err(err);
414 }
415 };
416 if observed.is_empty() {
417 self.record_transition(TransitionKind::Delete, before, GenericDoubletLink::empty())?;
418 } else {
419 self.record_observed_transitions(&observed)?;
420 }
421 if owns {
422 self.commit_current()?;
423 }
424 Ok(deleted)
425 }
426
427 pub fn create_and_update(&mut self, source: T, target: T) -> Result<T, LinkError> {
432 let owns = self.ensure_open_transaction();
433 let zero = T::from_byte(0);
434 let id = self.create(zero, zero)?;
435 self.update(id, source, target)?;
436 if owns {
437 self.commit_current()?;
438 }
439 Ok(id)
440 }
441
442 pub fn exists(&self, id: T) -> bool {
443 self.inner.link_exists(id)
444 }
445
446 pub fn search(&self, source: T, target: T) -> Option<T> {
447 self.inner.search_link(source, target)
448 }
449
450 pub fn get_or_create(&mut self, source: T, target: T) -> Result<T, LinkError> {
451 if let Some(existing) = self.inner.search_link(source, target) {
452 return Ok(existing);
453 }
454 self.create(source, target)
455 }
456
457 pub fn ensure_created(&mut self, id: T) -> Result<T, LinkError> {
458 self.inner.ensure_link_created(id)
461 }
462
463 fn snapshot(&self, id: T) -> GenericDoubletLink<T> {
465 let zero = T::from_byte(0);
466 self.inner
467 .get_link(id)
468 .map(|link| GenericDoubletLink::from_link(&link))
469 .unwrap_or_else(|| GenericDoubletLink::new(id, zero, zero))
470 }
471
472 fn ensure_open_transaction(&mut self) -> bool {
473 if self.current.is_none() {
474 self.current = Some(PendingTransaction {
475 id: new_transaction_id(),
476 transitions: Vec::new(),
477 auto_commit: true,
478 started_ms: now_unix_ms(),
479 });
480 true
481 } else {
482 false
483 }
484 }
485
486 fn record_observed_transitions(
501 &mut self,
502 observed: &[ObservedChange<T>],
503 ) -> Result<(), LinkError> {
504 let zero = T::from_byte(0);
505 for (_, before, after) in observed {
506 let kind = match (before.index != zero, after.index != zero) {
507 (false, true) => TransitionKind::Create,
508 (true, false) => TransitionKind::Delete,
509 _ => TransitionKind::Update,
510 };
511 self.record_transition(kind, *before, *after)?;
512 }
513 Ok(())
514 }
515
516 fn record_transition(
517 &mut self,
518 kind: TransitionKind,
519 before: GenericDoubletLink<T>,
520 after: GenericDoubletLink<T>,
521 ) -> Result<(), LinkError> {
522 self.sequence_counter += 1;
523 let sequence = self.sequence_counter;
524 let timestamp_ms = now_unix_ms();
525 let transaction_id = self.current.as_ref().map(|tx| tx.id).ok_or_else(|| {
526 LinkError::Transaction(
527 "internal: missing open transaction while recording transition".to_string(),
528 )
529 })?;
530 let transition = GenericTransition {
531 transaction_id,
532 sequence,
533 timestamp_ms,
534 kind,
535 before,
536 after,
537 };
538 if let Some(current) = self.current.as_mut() {
539 current.transitions.push(transition);
540 }
541 self.log.push(transition);
542 self.write_transition_to_log(&transition)?;
543 if self.trace {
544 eprintln!(
545 "[Transactions] Recorded {:?} seq={} tx={:032x}: ({},{},{}) -> ({},{},{}).",
546 kind,
547 sequence,
548 transaction_id,
549 before.index,
550 before.source,
551 before.target,
552 after.index,
553 after.source,
554 after.target,
555 );
556 }
557 Ok(())
558 }
559
560 fn write_transition_to_log(
561 &mut self,
562 transition: &GenericTransition<T>,
563 ) -> Result<(), LinkError> {
564 self.log_store.append_log_entry(&format!(
565 "{TRANSITION_NAME_PREFIX}{}",
566 transition.serialize()
567 ))
568 }
569
570 fn write_marker(&mut self, name: &str) -> Result<(), LinkError> {
571 self.log_store.append_log_entry(name)
572 }
573
574 pub fn begin_transaction(&mut self) -> Result<TransactionHandle, LinkError> {
577 if self.current.is_some() {
578 return Err(LinkError::Transaction(
579 "Nested transactions are not supported.".to_string(),
580 ));
581 }
582 let id = new_transaction_id();
583 let started_ms = now_unix_ms();
584 self.current = Some(PendingTransaction {
585 id,
586 transitions: Vec::new(),
587 auto_commit: false,
588 started_ms,
589 });
590 Ok(TransactionHandle { id, started_ms })
591 }
592
593 pub fn commit(&mut self) -> Result<(), LinkError> {
594 if self.current.is_none() {
595 return Ok(());
596 }
597 self.commit_current()
598 }
599
600 fn commit_current(&mut self) -> Result<(), LinkError> {
601 let pending = match self.current.take() {
602 Some(p) => p,
603 None => return Ok(()),
604 };
605 self.committed.insert(pending.id);
606 self.write_marker(&format!("{COMMIT_MARKER_PREFIX}{:032x}", pending.id))?;
607 if self.trace {
608 eprintln!(
609 "[Transactions] Committed tx {:032x} (mode={:?}, transitions={}).",
610 pending.id,
611 self.commit_mode,
612 pending.transitions.len()
613 );
614 }
615 for transition in &pending.transitions {
616 self.mark_applied(transition)?;
617 }
618 let _ = pending.auto_commit;
619 let _ = pending.started_ms;
620 self.enforce_retention()?;
621 Ok(())
622 }
623
624 pub fn rollback(&mut self) -> Result<(), LinkError> {
625 let pending = match self.current.take() {
626 Some(p) => p,
627 None => return Ok(()),
628 };
629 self.rolled_back.insert(pending.id);
630 self.replaying = true;
631 for transition in pending.transitions.iter().rev() {
632 self.try_revert_transition(transition);
633 }
634 self.replaying = false;
635 self.write_marker(&format!("{ROLLBACK_MARKER_PREFIX}{:032x}", pending.id))?;
636 if self.trace {
637 eprintln!(
638 "[Transactions] Rolled back tx {:032x} ({} transitions).",
639 pending.id,
640 pending.transitions.len(),
641 );
642 }
643 self.enforce_retention()?;
644 Ok(())
645 }
646}
647
648impl<T, S, L> GenericTransactionsDecorator<T, S, L>
651where
652 T: LinkReference,
653 S: LinksStorageRef<T>,
654 L: TransitionLogStore,
655{
656 pub fn get(&self, id: T) -> Option<&GenericLink<T>> {
657 self.inner.get_link_ref(id)
658 }
659
660 pub fn all(&self) -> Vec<&GenericLink<T>> {
661 self.inner.all_link_refs()
662 }
663
664 pub fn query(
665 &self,
666 index: Option<T>,
667 source: Option<T>,
668 target: Option<T>,
669 ) -> Vec<&GenericLink<T>> {
670 self.inner.query_link_refs(index, source, target)
671 }
672}
673
674fn insert_ordered<T: LinkReference>(
677 list: &mut Vec<GenericTransition<T>>,
678 transition: GenericTransition<T>,
679) {
680 let mut lo = 0usize;
681 let mut hi = list.len();
682 while lo < hi {
683 let mid = (lo + hi) / 2;
684 if list[mid].sequence < transition.sequence {
685 lo = mid + 1;
686 } else {
687 hi = mid;
688 }
689 }
690 list.insert(lo, transition);
691}
692
693static TX_COUNTER: AtomicU64 = AtomicU64::new(0);
694
695fn new_transaction_id() -> u128 {
696 let count = TX_COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
699 let now = now_unix_ms() as u128;
700 (now << 64) | count
701}
702
703fn now_unix_ms() -> i64 {
704 SystemTime::now()
705 .duration_since(UNIX_EPOCH)
706 .map(|d| d.as_millis() as i64)
707 .unwrap_or(0)
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713
714 #[test]
715 fn retention_policy_parses_specs() {
716 assert!(matches!(
717 LogRetentionPolicy::parse("infinite").unwrap(),
718 LogRetentionPolicy::Infinite
719 ));
720 assert!(matches!(
721 LogRetentionPolicy::parse("sized:1000").unwrap(),
722 LogRetentionPolicy::Sized {
723 max_transitions: 1000
724 }
725 ));
726 match LogRetentionPolicy::parse("chunked:500:/tmp/x").unwrap() {
727 LogRetentionPolicy::Chunked {
728 chunk_size,
729 archive_directory,
730 } => {
731 assert_eq!(chunk_size, 500);
732 assert_eq!(archive_directory, PathBuf::from("/tmp/x"));
733 }
734 _ => panic!("expected Chunked"),
735 }
736 assert!(LogRetentionPolicy::parse("garbage").is_err());
737 }
738
739 #[test]
740 fn transition_round_trips_through_serialize() {
741 let t = Transition {
742 transaction_id: 0xabcdef1234567890u128,
743 sequence: 42,
744 timestamp_ms: 1234567890,
745 kind: TransitionKind::Update,
746 before: DoubletLink::new(1, 2, 3),
747 after: DoubletLink::new(1, 4, 5),
748 };
749 let parsed = Transition::try_parse(&t.serialize()).unwrap();
750 assert_eq!(t, parsed);
751 }
752
753 #[test]
754 fn wide_transition_is_rejected_by_a_narrow_address_type() {
755 let wide = GenericTransition::<u64> {
756 transaction_id: 7,
757 sequence: 1,
758 timestamp_ms: 0,
759 kind: TransitionKind::Create,
760 before: GenericDoubletLink::empty(),
761 after: GenericDoubletLink::new(u32::MAX as u64 + 1, 0, 0),
762 };
763 assert!(matches!(
764 GenericTransition::<u32>::parse(&wide.serialize()),
765 Err(LinkError::AddressOutOfRange(_))
766 ));
767 assert!(GenericTransition::<u64>::parse(&wide.serialize()).is_ok());
768 }
769}