use std::sync::Arc;
use crate::persistent_artrie::core::concurrency::EpochManager;
use crate::persistent_artrie::core::eviction::EvictionCoordinator;
use crate::persistent_artrie::core::key_encoding::KeyEncoding;
use crate::persistent_artrie::core::overlay::atomic_ptr::AtomicNodePtr;
use crate::persistent_artrie::core::overlay::faulter::OverlayFaulter;
use crate::persistent_artrie::core::overlay::node::{Child, OverlayNode};
use crate::persistent_artrie::core::swizzled_ptr::SwizzledPtr;
use crate::value::DictionaryValue;
pub(crate) const DEFAULT_MAX_FAULTIN_RETRIES: usize = 16;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OverlayEvictOutcome {
Evicted,
RootCasLost,
NotEvictable,
}
pub(crate) trait OverlayEvictable<K: KeyEncoding, V: DictionaryValue, S>:
OverlayFaulter<K, V>
{
#[allow(dead_code)]
fn overlay_root_slot(&self) -> Option<&AtomicNodePtr<K, V>>;
fn overlay_epoch_manager(&self) -> &EpochManager;
#[allow(dead_code)]
fn overlay_eviction_coordinator(&self) -> Option<Arc<EvictionCoordinator>>;
#[inline]
fn note_faultin_cas(&self) {}
#[allow(dead_code)]
fn evict_overlay_node_at_path(
&self,
path: &[K::Unit],
disk_ptr: SwizzledPtr,
) -> OverlayEvictOutcome {
if path.is_empty() {
return OverlayEvictOutcome::NotEvictable;
}
if disk_ptr.disk_location().is_none() {
return OverlayEvictOutcome::NotEvictable;
}
let root_slot = match self.overlay_root_slot() {
Some(r) => r,
None => return OverlayEvictOutcome::NotEvictable,
};
let _epoch = self.overlay_epoch_manager().enter_read();
let old_root = match root_slot.load() {
Some(r) => r,
None => return OverlayEvictOutcome::NotEvictable,
};
let mut spine: super::OverlaySpine<K, V> = Vec::with_capacity(path.len());
let mut current = Arc::clone(&old_root);
for &edge in path {
let child = match current.find_child(edge) {
Some(c) => c,
None => return OverlayEvictOutcome::NotEvictable, };
let child_arc = match child.as_in_mem() {
Some(a) => Arc::clone(a),
None => return OverlayEvictOutcome::NotEvictable,
};
spine.push((Arc::clone(¤t), edge));
current = child_arc;
}
if current.durable_stamp() != disk_ptr.to_raw() {
return OverlayEvictOutcome::NotEvictable;
}
let mut new_child: Option<Arc<OverlayNode<K, V>>> = None;
for (ancestor, edge) in spine.into_iter().rev() {
let rebuilt = match new_child.take() {
Some(c) => ancestor.with_child(edge, Child::InMem(c)),
None => ancestor.with_child(edge, Child::OnDisk(disk_ptr.clone())),
};
new_child = Some(Arc::new(rebuilt));
}
let new_root = match new_child {
Some(r) => r,
None => return OverlayEvictOutcome::NotEvictable,
};
match root_slot.compare_exchange(&old_root, new_root) {
Ok(_) => OverlayEvictOutcome::Evicted,
Err(_actual) => OverlayEvictOutcome::RootCasLost,
}
}
fn find_leaf_faulting(
&self,
root_slot: &AtomicNodePtr<K, V>,
key: &[K::Unit],
max_faultin_retries: usize,
) -> crate::persistent_artrie::core::error::Result<Option<Arc<OverlayNode<K, V>>>> {
fn walk_no_fault<K: KeyEncoding, V: DictionaryValue>(
root: &Arc<OverlayNode<K, V>>,
key: &[K::Unit],
) -> Option<Arc<OverlayNode<K, V>>> {
let mut current = Arc::clone(root);
for &edge in key {
let child = current.find_child(edge)?;
let child_arc = child.as_in_mem()?;
let next = Arc::clone(child_arc);
current = next;
}
if current.is_final() {
Some(current)
} else {
None
}
}
let mut captured_answer: Option<Option<Arc<OverlayNode<K, V>>>> = None;
for _attempt in 0..=max_faultin_retries {
let _epoch = self.overlay_epoch_manager().enter_read();
let old_root = match root_slot.load() {
Some(r) => r,
None => return Ok(None), };
let mut spine: super::OverlaySpine<K, V> = Vec::with_capacity(key.len());
let mut current = Arc::clone(&old_root);
let mut faulted = false;
let mut idx = 0usize;
while idx < key.len() {
let edge = key[idx];
let child = match current.find_child(edge) {
Some(c) => c,
None => return Ok(None), };
match child {
Child::InMem(child_arc) => {
let next = Arc::clone(child_arc);
spine.push((Arc::clone(¤t), edge));
current = next;
idx += 1;
}
Child::OnDisk(ptr) if !ptr.is_null() => {
let loaded = match self.fault_overlay_slot(ptr) {
Some(node) => node,
None => return Ok(None),
};
let mut captured_current = Arc::clone(&loaded);
let mut captured_idx = idx + 1;
let answer = loop {
if captured_idx == key.len() {
break if captured_current.is_final() {
Some(captured_current)
} else {
None
};
}
let captured_edge = key[captured_idx];
let Some(captured_child) = captured_current.find_child(captured_edge)
else {
break None;
};
captured_current = match captured_child {
Child::InMem(child_arc) => Arc::clone(child_arc),
Child::OnDisk(nested_ptr) if !nested_ptr.is_null() => {
match self.fault_overlay_slot(nested_ptr) {
Some(node) => node,
None => break None,
}
}
Child::OnDisk(_) => break None,
};
captured_idx += 1;
};
captured_answer = Some(answer);
let mut new_child =
Arc::new(current.with_child(edge, Child::InMem(loaded)));
for (ancestor, anc_edge) in spine.iter().rev() {
new_child =
Arc::new(ancestor.with_child(*anc_edge, Child::InMem(new_child)));
}
let _ = root_slot.compare_exchange(&old_root, new_child);
self.note_faultin_cas();
faulted = true;
break;
}
Child::OnDisk(_) => return Ok(None),
}
}
if faulted {
continue;
}
return Ok(if current.is_final() {
Some(current)
} else {
None
});
}
if let Some(answer) = captured_answer {
return Ok(answer);
}
let final_root = match root_slot.load() {
Some(r) => r,
None => return Ok(None),
};
Ok(walk_no_fault(&final_root, key))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::persistent_artrie::core::key_encoding::{ByteKey, CharKey};
use crate::persistent_artrie::core::swizzled_ptr::NodeType;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Barrier;
use std::thread;
struct ForcedPublicationLoss<K: KeyEncoding> {
root: AtomicNodePtr<K, ()>,
epoch: EpochManager,
loaded: Arc<OverlayNode<K, ()>>,
loader_entered: Barrier,
writer_published: Barrier,
sibling: K::Unit,
cas_attempts: AtomicUsize,
}
impl<K: KeyEncoding> OverlayFaulter<K, ()> for ForcedPublicationLoss<K> {
fn fault_overlay_slot(&self, _slot: &SwizzledPtr) -> Option<Arc<OverlayNode<K, ()>>> {
self.loader_entered.wait();
self.writer_published.wait();
Some(Arc::clone(&self.loaded))
}
}
impl<K: KeyEncoding> OverlayEvictable<K, (), ()> for ForcedPublicationLoss<K> {
fn overlay_root_slot(&self) -> Option<&AtomicNodePtr<K, ()>> {
Some(&self.root)
}
fn overlay_epoch_manager(&self) -> &EpochManager {
&self.epoch
}
fn overlay_eviction_coordinator(&self) -> Option<Arc<EvictionCoordinator>> {
None
}
fn note_faultin_cas(&self) {
self.cas_attempts.fetch_add(1, Ordering::Relaxed);
}
}
fn assert_publication_loss_preserves_committed_read<K: KeyEncoding>(
first: K::Unit,
second: K::Unit,
sibling: K::Unit,
node_type: NodeType,
) {
let disk_child = SwizzledPtr::on_disk(7, 11, node_type);
let root =
Arc::new(OverlayNode::<K, ()>::new().with_child(first, Child::OnDisk(disk_child)));
let committed_leaf = Arc::new(OverlayNode::<K, ()>::new().as_final());
let loaded =
Arc::new(OverlayNode::<K, ()>::new().with_child(second, Child::InMem(committed_leaf)));
let fixture = Arc::new(ForcedPublicationLoss {
root: AtomicNodePtr::new(root),
epoch: EpochManager::new(),
loaded,
loader_entered: Barrier::new(2),
writer_published: Barrier::new(2),
sibling,
cas_attempts: AtomicUsize::new(0),
});
let max_retries = DEFAULT_MAX_FAULTIN_RETRIES;
let reader = {
let fixture = Arc::clone(&fixture);
thread::spawn(move || {
<ForcedPublicationLoss<K> as OverlayEvictable<K, (), ()>>::find_leaf_faulting(
&fixture,
&fixture.root,
&[first, second],
max_retries,
)
.expect("faulting read")
})
};
let writer = {
let fixture = Arc::clone(&fixture);
thread::spawn(move || {
for _ in 0..=max_retries {
fixture.loader_entered.wait();
let old_root = fixture.root.load().expect("published root");
let sibling_leaf = Arc::new(OverlayNode::<K, ()>::new().as_final());
let advanced =
Arc::new(old_root.with_child(fixture.sibling, Child::InMem(sibling_leaf)));
fixture.root.store(advanced);
fixture.writer_published.wait();
}
})
};
let found = reader.join().expect("reader thread");
writer.join().expect("writer thread");
assert!(
found.is_some_and(|leaf| leaf.is_final()),
"a root-CAS loss must not turn the loaded committed term into absence"
);
assert_eq!(
fixture.cas_attempts.load(Ordering::Relaxed),
max_retries + 1,
"the test must force every bounded install-CAS attempt to lose"
);
let published = fixture.root.load().expect("final published root");
assert!(matches!(
published.find_child(first),
Some(Child::OnDisk(_))
));
assert!(matches!(
published.find_child(sibling),
Some(Child::InMem(_))
));
}
#[test]
fn byte_read_never_misses_committed_after_forced_faultin_publication_loss() {
assert_publication_loss_preserves_committed_read::<ByteKey>(
b'a',
b'b',
b'z',
NodeType::Node4,
);
}
#[test]
fn char_read_never_misses_committed_after_forced_faultin_publication_loss() {
assert_publication_loss_preserves_committed_read::<CharKey>(
'λ' as u32,
'雪' as u32,
'☃' as u32,
NodeType::CharNode4,
);
}
}