mod log;
mod recovery;
mod types;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use doublets::data::LinkReference;
use crate::error::LinkError;
use crate::link::GenericLink;
use crate::named_types::NamedTypesDecorator;
use crate::storage::{LinksStorage, LinksStorageRef};
pub use log::{FileTransitionLog, TransitionLogStore};
pub use types::{
CommitMode, DoubletLink, GenericDoubletLink, GenericTransition, LogRetentionPolicy, Transition,
TransitionKind,
};
use types::{COMMIT_MARKER_PREFIX, ROLLBACK_MARKER_PREFIX, TRANSITION_NAME_PREFIX};
struct PendingTransaction<T> {
id: u128,
transitions: Vec<GenericTransition<T>>,
auto_commit: bool,
started_ms: i64,
}
type ObservedChange<T> = (T, GenericDoubletLink<T>, GenericDoubletLink<T>);
fn record_observed<T: LinkReference>(
observed: &mut Vec<ObservedChange<T>>,
before: GenericLink<T>,
after: GenericLink<T>,
) {
let zero = T::from_byte(0);
let key = if before.index != zero {
before.index
} else {
after.index
};
if key == zero {
return;
}
let before = GenericDoubletLink::from_link(&before);
let after = GenericDoubletLink::from_link(&after);
match observed.iter_mut().find(|(index, _, _)| *index == key) {
Some(entry) => {
if entry.1.index == zero {
entry.1 = before;
}
entry.2 = after;
}
None => observed.push((key, before, after)),
}
}
#[derive(Debug, Clone)]
pub struct TransactionHandle {
pub id: u128,
pub started_ms: i64,
}
pub struct GenericTransactionsDecorator<T, S, L>
where
T: LinkReference,
S: LinksStorage<T>,
L: TransitionLogStore,
{
inner: S,
log_store: L,
log: Vec<GenericTransition<T>>,
committed: HashSet<u128>,
rolled_back: HashSet<u128>,
applied: HashSet<i64>,
current: Option<PendingTransaction<T>>,
sequence_counter: i64,
applied_sequence: i64,
retention_policy: LogRetentionPolicy,
commit_mode: CommitMode,
replaying: bool,
trace: bool,
}
pub type TransactionsDecorator =
GenericTransactionsDecorator<u32, NamedTypesDecorator, NamedTypesDecorator>;
impl<T, S, L> GenericTransactionsDecorator<T, S, L>
where
T: LinkReference,
S: LinksStorage<T>,
L: TransitionLogStore,
{
pub fn new(
inner: S,
log_store: L,
retention_policy: LogRetentionPolicy,
commit_mode: CommitMode,
trace: bool,
) -> Result<Self, LinkError> {
let mut decorator = Self {
inner,
log_store,
log: Vec::new(),
committed: HashSet::new(),
rolled_back: HashSet::new(),
applied: HashSet::new(),
current: None,
sequence_counter: 0,
applied_sequence: 0,
retention_policy,
commit_mode,
replaying: false,
trace,
};
decorator.recover()?;
Ok(decorator)
}
pub fn make_transitions_database_filename<P: AsRef<Path>>(database_filename: P) -> PathBuf {
let path = database_filename.as_ref();
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default();
let name = format!("{stem}.transitions.links");
match path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.join(name),
_ => PathBuf::from(name),
}
}
pub fn retention_policy(&self) -> &LogRetentionPolicy {
&self.retention_policy
}
pub fn set_retention_policy(&mut self, policy: LogRetentionPolicy) {
self.retention_policy = policy;
}
pub fn commit_mode(&self) -> CommitMode {
self.commit_mode
}
pub fn set_commit_mode(&mut self, mode: CommitMode) {
self.commit_mode = mode;
}
pub fn applied_sequence(&self) -> i64 {
self.applied_sequence
}
pub fn last_logged_sequence(&self) -> i64 {
self.sequence_counter
}
pub fn log(&self) -> Vec<GenericTransition<T>> {
self.log.clone()
}
pub fn inner(&self) -> &S {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut S {
&mut self.inner
}
pub fn log_store(&self) -> &L {
&self.log_store
}
pub fn log_store_mut(&mut self) -> &mut L {
&mut self.log_store
}
pub fn into_inner(self) -> (S, L) {
(self.inner, self.log_store)
}
pub fn flush(&mut self) -> Result<(), LinkError> {
self.inner.flush()?;
self.log_store.flush_log()?;
Ok(())
}
pub fn save(&mut self) -> Result<(), LinkError> {
self.flush()
}
pub fn has_external_changes(&self) -> Result<bool, LinkError> {
self.inner.has_external_changes()
}
pub fn reload(&mut self) -> Result<(), LinkError> {
if self.current.is_some() {
return Err(LinkError::Transaction(
"Cannot reload while a transaction is open.".to_string(),
));
}
self.inner.reload()?;
self.recover()
}
pub fn create(&mut self, source: T, target: T) -> Result<T, LinkError> {
if self.replaying {
return self.inner.create_link(source, target);
}
let owns = self.ensure_open_transaction();
let id = self.inner.create_link(source, target)?;
let after = self
.inner
.get_link(id)
.map(|link| GenericDoubletLink::from_link(&link))
.unwrap_or_else(|| GenericDoubletLink::new(id, source, target));
self.record_transition(TransitionKind::Create, GenericDoubletLink::empty(), after)?;
if owns {
self.commit_current()?;
}
Ok(id)
}
pub fn update(&mut self, id: T, source: T, target: T) -> Result<GenericLink<T>, LinkError> {
if self.replaying {
return self.inner.update_link(id, source, target);
}
let before = self.snapshot(id);
let owns = self.ensure_open_transaction();
let mut observed: Vec<ObservedChange<T>> = Vec::new();
let outcome = self
.inner
.update_link_observed(id, source, target, &mut |before, after| {
record_observed(&mut observed, before, after)
});
let prev = match outcome {
Ok(prev) => prev,
Err(err) => {
if owns {
self.current = None;
}
return Err(err);
}
};
if observed.is_empty() {
let after = self
.inner
.get_link(id)
.map(|link| GenericDoubletLink::from_link(&link))
.unwrap_or_else(|| GenericDoubletLink::new(id, source, target));
self.record_transition(TransitionKind::Update, before, after)?;
} else {
self.record_observed_transitions(&observed)?;
}
if owns {
self.commit_current()?;
}
Ok(prev)
}
pub fn delete(&mut self, id: T) -> Result<GenericLink<T>, LinkError> {
self.delete_observed(id, &mut |_, _| {})
}
pub fn delete_observed(
&mut self,
id: T,
observer: &mut dyn FnMut(GenericLink<T>, GenericLink<T>),
) -> Result<GenericLink<T>, LinkError> {
if self.replaying {
let deleted = self.inner.delete_link(id)?;
observer(deleted, GenericLink::null());
return Ok(deleted);
}
let before = self.snapshot(id);
let owns = self.ensure_open_transaction();
let mut observed: Vec<ObservedChange<T>> = Vec::new();
let outcome = self.inner.delete_link_observed(id, &mut |before, after| {
observer(before, after);
record_observed(&mut observed, before, after)
});
let deleted = match outcome {
Ok(d) => d,
Err(err) => {
if owns {
self.current = None;
}
return Err(err);
}
};
if observed.is_empty() {
self.record_transition(TransitionKind::Delete, before, GenericDoubletLink::empty())?;
} else {
self.record_observed_transitions(&observed)?;
}
if owns {
self.commit_current()?;
}
Ok(deleted)
}
pub fn create_and_update(&mut self, source: T, target: T) -> Result<T, LinkError> {
let owns = self.ensure_open_transaction();
let zero = T::from_byte(0);
let id = self.create(zero, zero)?;
self.update(id, source, target)?;
if owns {
self.commit_current()?;
}
Ok(id)
}
pub fn exists(&self, id: T) -> bool {
self.inner.link_exists(id)
}
pub fn search(&self, source: T, target: T) -> Option<T> {
self.inner.search_link(source, target)
}
pub fn get_or_create(&mut self, source: T, target: T) -> Result<T, LinkError> {
if let Some(existing) = self.inner.search_link(source, target) {
return Ok(existing);
}
self.create(source, target)
}
pub fn ensure_created(&mut self, id: T) -> Result<T, LinkError> {
self.inner.ensure_link_created(id)
}
fn snapshot(&self, id: T) -> GenericDoubletLink<T> {
let zero = T::from_byte(0);
self.inner
.get_link(id)
.map(|link| GenericDoubletLink::from_link(&link))
.unwrap_or_else(|| GenericDoubletLink::new(id, zero, zero))
}
fn ensure_open_transaction(&mut self) -> bool {
if self.current.is_none() {
self.current = Some(PendingTransaction {
id: new_transaction_id(),
transitions: Vec::new(),
auto_commit: true,
started_ms: now_unix_ms(),
});
true
} else {
false
}
}
fn record_observed_transitions(
&mut self,
observed: &[ObservedChange<T>],
) -> Result<(), LinkError> {
let zero = T::from_byte(0);
for (_, before, after) in observed {
let kind = match (before.index != zero, after.index != zero) {
(false, true) => TransitionKind::Create,
(true, false) => TransitionKind::Delete,
_ => TransitionKind::Update,
};
self.record_transition(kind, *before, *after)?;
}
Ok(())
}
fn record_transition(
&mut self,
kind: TransitionKind,
before: GenericDoubletLink<T>,
after: GenericDoubletLink<T>,
) -> Result<(), LinkError> {
self.sequence_counter += 1;
let sequence = self.sequence_counter;
let timestamp_ms = now_unix_ms();
let transaction_id = self.current.as_ref().map(|tx| tx.id).ok_or_else(|| {
LinkError::Transaction(
"internal: missing open transaction while recording transition".to_string(),
)
})?;
let transition = GenericTransition {
transaction_id,
sequence,
timestamp_ms,
kind,
before,
after,
};
if let Some(current) = self.current.as_mut() {
current.transitions.push(transition);
}
self.log.push(transition);
self.write_transition_to_log(&transition)?;
if self.trace {
eprintln!(
"[Transactions] Recorded {:?} seq={} tx={:032x}: ({},{},{}) -> ({},{},{}).",
kind,
sequence,
transaction_id,
before.index,
before.source,
before.target,
after.index,
after.source,
after.target,
);
}
Ok(())
}
fn write_transition_to_log(
&mut self,
transition: &GenericTransition<T>,
) -> Result<(), LinkError> {
self.log_store.append_log_entry(&format!(
"{TRANSITION_NAME_PREFIX}{}",
transition.serialize()
))
}
fn write_marker(&mut self, name: &str) -> Result<(), LinkError> {
self.log_store.append_log_entry(name)
}
pub fn begin_transaction(&mut self) -> Result<TransactionHandle, LinkError> {
if self.current.is_some() {
return Err(LinkError::Transaction(
"Nested transactions are not supported.".to_string(),
));
}
let id = new_transaction_id();
let started_ms = now_unix_ms();
self.current = Some(PendingTransaction {
id,
transitions: Vec::new(),
auto_commit: false,
started_ms,
});
Ok(TransactionHandle { id, started_ms })
}
pub fn commit(&mut self) -> Result<(), LinkError> {
if self.current.is_none() {
return Ok(());
}
self.commit_current()
}
fn commit_current(&mut self) -> Result<(), LinkError> {
let pending = match self.current.take() {
Some(p) => p,
None => return Ok(()),
};
self.committed.insert(pending.id);
self.write_marker(&format!("{COMMIT_MARKER_PREFIX}{:032x}", pending.id))?;
if self.trace {
eprintln!(
"[Transactions] Committed tx {:032x} (mode={:?}, transitions={}).",
pending.id,
self.commit_mode,
pending.transitions.len()
);
}
for transition in &pending.transitions {
self.mark_applied(transition)?;
}
let _ = pending.auto_commit;
let _ = pending.started_ms;
self.enforce_retention()?;
Ok(())
}
pub fn rollback(&mut self) -> Result<(), LinkError> {
let pending = match self.current.take() {
Some(p) => p,
None => return Ok(()),
};
self.rolled_back.insert(pending.id);
self.replaying = true;
for transition in pending.transitions.iter().rev() {
self.try_revert_transition(transition);
}
self.replaying = false;
self.write_marker(&format!("{ROLLBACK_MARKER_PREFIX}{:032x}", pending.id))?;
if self.trace {
eprintln!(
"[Transactions] Rolled back tx {:032x} ({} transitions).",
pending.id,
pending.transitions.len(),
);
}
self.enforce_retention()?;
Ok(())
}
}
impl<T, S, L> GenericTransactionsDecorator<T, S, L>
where
T: LinkReference,
S: LinksStorageRef<T>,
L: TransitionLogStore,
{
pub fn get(&self, id: T) -> Option<&GenericLink<T>> {
self.inner.get_link_ref(id)
}
pub fn all(&self) -> Vec<&GenericLink<T>> {
self.inner.all_link_refs()
}
pub fn query(
&self,
index: Option<T>,
source: Option<T>,
target: Option<T>,
) -> Vec<&GenericLink<T>> {
self.inner.query_link_refs(index, source, target)
}
}
fn insert_ordered<T: LinkReference>(
list: &mut Vec<GenericTransition<T>>,
transition: GenericTransition<T>,
) {
let mut lo = 0usize;
let mut hi = list.len();
while lo < hi {
let mid = (lo + hi) / 2;
if list[mid].sequence < transition.sequence {
lo = mid + 1;
} else {
hi = mid;
}
}
list.insert(lo, transition);
}
static TX_COUNTER: AtomicU64 = AtomicU64::new(0);
fn new_transaction_id() -> u128 {
let count = TX_COUNTER.fetch_add(1, Ordering::Relaxed) as u128;
let now = now_unix_ms() as u128;
(now << 64) | count
}
fn now_unix_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retention_policy_parses_specs() {
assert!(matches!(
LogRetentionPolicy::parse("infinite").unwrap(),
LogRetentionPolicy::Infinite
));
assert!(matches!(
LogRetentionPolicy::parse("sized:1000").unwrap(),
LogRetentionPolicy::Sized {
max_transitions: 1000
}
));
match LogRetentionPolicy::parse("chunked:500:/tmp/x").unwrap() {
LogRetentionPolicy::Chunked {
chunk_size,
archive_directory,
} => {
assert_eq!(chunk_size, 500);
assert_eq!(archive_directory, PathBuf::from("/tmp/x"));
}
_ => panic!("expected Chunked"),
}
assert!(LogRetentionPolicy::parse("garbage").is_err());
}
#[test]
fn transition_round_trips_through_serialize() {
let t = Transition {
transaction_id: 0xabcdef1234567890u128,
sequence: 42,
timestamp_ms: 1234567890,
kind: TransitionKind::Update,
before: DoubletLink::new(1, 2, 3),
after: DoubletLink::new(1, 4, 5),
};
let parsed = Transition::try_parse(&t.serialize()).unwrap();
assert_eq!(t, parsed);
}
#[test]
fn wide_transition_is_rejected_by_a_narrow_address_type() {
let wide = GenericTransition::<u64> {
transaction_id: 7,
sequence: 1,
timestamp_ms: 0,
kind: TransitionKind::Create,
before: GenericDoubletLink::empty(),
after: GenericDoubletLink::new(u32::MAX as u64 + 1, 0, 0),
};
assert!(matches!(
GenericTransition::<u32>::parse(&wide.serialize()),
Err(LinkError::AddressOutOfRange(_))
));
assert!(GenericTransition::<u64>::parse(&wide.serialize()).is_ok());
}
}