use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::{Cursor, DataProvider, Graph, NeighborResult, NodeId, QueryParams};
pub type CacheKey = (NodeId, Option<Cursor>);
#[derive(Debug, Clone, PartialEq)]
enum Slot { InFlight(u64), Failed(String) }
#[derive(Default)]
pub struct NeighborCache {
ready: HashMap<String, NeighborResult>,
slots: HashMap<String, Slot>,
wanted: Vec<CacheKey>,
dirty: bool,
failures: Vec<(CacheKey, String)>,
new_arrivals: Vec<CacheKey>,
next_epoch: u64,
settled: HashMap<String, u64>,
discard_before: Option<u64>,
}
fn key_str(k: &CacheKey) -> String {
match &k.1 {
Some(Cursor(c)) => format!("{}\u{1}{}\u{1}1{}", k.0.len(), k.0, c),
None => format!("{}\u{1}{}\u{1}0", k.0.len(), k.0),
}
}
impl NeighborCache {
pub fn new() -> Self { Self::default() }
pub fn get(&self, k: &CacheKey) -> Option<&NeighborResult> { self.ready.get(&key_str(k)) }
pub fn error(&self, k: &CacheKey) -> Option<&str> {
match self.slots.get(&key_str(k)) {
Some(Slot::Failed(e)) => Some(e.as_str()),
_ => None,
}
}
pub fn mark_wanted(&mut self, k: CacheKey) {
let ks = key_str(&k);
if self.ready.contains_key(&ks) || self.slots.contains_key(&ks) { return; }
if self.wanted.iter().any(|w| key_str(w) == ks) { return; }
self.wanted.push(k);
}
pub fn take_wanted(&mut self) -> Vec<(CacheKey, u64)> {
let out: Vec<CacheKey> = self.wanted.drain(..).collect();
out.into_iter()
.map(|k| {
self.next_epoch += 1;
self.slots.insert(key_str(&k), Slot::InFlight(self.next_epoch));
(k, self.next_epoch)
})
.collect()
}
fn is_stale(&self, ks: &str, epoch: u64) -> bool {
if matches!(self.discard_before, Some(floor) if epoch <= floor) {
return true;
}
if let Some(Slot::InFlight(e)) = self.slots.get(ks) {
if *e > epoch {
return true;
}
}
matches!(self.settled.get(ks), Some(s) if *s >= epoch)
}
pub fn fill(&mut self, k: CacheKey, epoch: u64, mut res: NeighborResult) {
let ks = key_str(&k);
if self.is_stale(&ks, epoch) { return; }
res.pending = false;
self.slots.remove(&ks);
self.ready.insert(ks.clone(), res);
self.settled.insert(ks, epoch);
self.new_arrivals.push(k);
self.dirty = true;
}
pub fn fail(&mut self, k: CacheKey, epoch: u64, err: String) {
let ks = key_str(&k);
if !matches!(self.slots.get(&ks), Some(Slot::InFlight(e)) if *e == epoch) {
return;
}
self.slots.insert(ks.clone(), Slot::Failed(err.clone()));
self.settled.insert(ks, epoch);
self.failures.push((k, err));
self.dirty = true;
}
pub fn cancel(&mut self, k: &CacheKey) {
let ks = key_str(k);
self.wanted.retain(|w| key_str(w) != ks);
if matches!(self.slots.get(&ks), Some(Slot::InFlight(_))) {
self.slots.remove(&ks);
}
}
pub fn is_in_flight(&self, k: &CacheKey) -> bool {
matches!(self.slots.get(&key_str(k)), Some(Slot::InFlight(_)))
}
pub fn take_dirty(&mut self) -> bool { std::mem::take(&mut self.dirty) }
pub fn take_failures(&mut self) -> Vec<(CacheKey, String)> {
std::mem::take(&mut self.failures)
}
pub fn take_new_arrivals(&mut self) -> Vec<CacheKey> {
std::mem::take(&mut self.new_arrivals)
}
pub fn clear_failures(&mut self) {
self.slots.retain(|_, s| !matches!(s, Slot::Failed(_)));
}
pub fn reset(&mut self) {
self.ready.clear();
self.slots.clear();
self.wanted.clear();
self.dirty = false;
self.failures.clear();
self.new_arrivals.clear();
self.settled.clear();
self.discard_before = Some(self.next_epoch);
}
pub fn is_loading(&self) -> bool {
!self.wanted.is_empty() || self.slots.values().any(|s| matches!(s, Slot::InFlight(_)))
}
pub fn ready_results(&self) -> impl Iterator<Item = &NeighborResult> { self.ready.values() }
}
pub struct CachingProvider {
base: Graph,
cache: Rc<RefCell<NeighborCache>>,
}
impl CachingProvider {
pub fn new(base: Graph, cache: Rc<RefCell<NeighborCache>>) -> Self {
Self { base, cache }
}
pub fn cache(&self) -> Rc<RefCell<NeighborCache>> { self.cache.clone() }
}
impl DataProvider for CachingProvider {
fn load(&self) -> Graph { self.base.clone() }
fn neighbors(&self, focus: &NodeId, params: &QueryParams) -> NeighborResult {
let k: CacheKey = (focus.clone(), params.cursor.clone());
if let Some(hit) = self.cache.borrow().get(&k) {
return hit.clone();
}
if self.cache.borrow().error(&k).is_some() {
return NeighborResult {
nodes: vec![], edges: vec![], aggregates: vec![], next: None, pending: false,
};
}
self.cache.borrow_mut().mark_wanted(k);
NeighborResult {
nodes: vec![], edges: vec![], aggregates: vec![], next: None, pending: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Node;
fn key(id: &str) -> CacheKey { (id.to_string(), None) }
fn result_with(id: &str) -> NeighborResult {
NeighborResult {
nodes: vec![Node { id: id.into(), label: None, attrs: Default::default() }],
edges: vec![], aggregates: vec![], next: None, pending: false,
}
}
#[test]
fn miss_queues_the_key_and_reports_loading() {
let mut c = NeighborCache::new();
assert!(c.get(&key("a")).is_none());
c.mark_wanted(key("a"));
assert!(c.is_loading());
assert_eq!(c.take_wanted(), vec![(key("a"), 1)]);
}
#[test]
fn in_flight_key_is_not_queued_twice() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
c.mark_wanted(key("a")); assert_eq!(c.take_wanted().len(), 1);
c.mark_wanted(key("a")); assert!(c.take_wanted().is_empty(), "in-flight key must not re-queue");
}
#[test]
fn fill_stores_result_clears_pending_and_sets_dirty() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.fill(key("a"), epoch, result_with("a"));
assert!(c.take_dirty());
assert!(!c.take_dirty(), "dirty is consumed");
assert!(!c.is_loading());
assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "a");
}
#[test]
fn failure_is_recorded_and_does_not_retry_loop() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.fail(key("a"), epoch, "boom".into());
assert!(c.take_dirty());
assert!(!c.is_loading());
assert_eq!(c.error(&key("a")), Some("boom"));
c.mark_wanted(key("a"));
assert!(c.take_wanted().is_empty(), "failed key must not auto-retry");
}
#[test]
fn distinct_cursors_are_distinct_cache_entries() {
let mut c = NeighborCache::new();
let k2 = ("a".to_string(), Some(Cursor("off:8".into())));
c.fill(key("a"), 0, result_with("a"));
assert!(c.get(&k2).is_none(), "cursor is part of the key");
}
#[test]
fn ready_results_yields_every_filled_entry() {
let mut c = NeighborCache::new();
c.fill(key("a"), 0, result_with("n1"));
c.fill(("b".to_string(), Some(Cursor("off:8".into()))), 0, result_with("n2"));
let mut ids: Vec<String> =
c.ready_results().map(|r| r.nodes[0].id.clone()).collect();
ids.sort();
assert_eq!(ids, vec!["n1".to_string(), "n2".to_string()]);
}
#[test]
fn provider_returns_pending_on_miss_and_queues_it() {
let cache = Rc::new(RefCell::new(NeighborCache::new()));
let p = CachingProvider::new(Graph::default(), cache.clone());
let r = p.neighbors(&"a".to_string(), &QueryParams { limit: 8, cursor: None });
assert!(r.pending);
assert!(r.nodes.is_empty());
assert_eq!(cache.borrow_mut().take_wanted(), vec![(key("a"), 1)]);
}
#[test]
fn provider_returns_cached_result_once_filled() {
let cache = Rc::new(RefCell::new(NeighborCache::new()));
let p = CachingProvider::new(Graph::default(), cache.clone());
cache.borrow_mut().fill(key("a"), 0, result_with("n1"));
let r = p.neighbors(&"a".to_string(), &QueryParams { limit: 8, cursor: None });
assert!(!r.pending);
assert_eq!(r.nodes[0].id, "n1");
}
#[test]
fn provider_does_not_queue_a_hit() {
let cache = Rc::new(RefCell::new(NeighborCache::new()));
let p = CachingProvider::new(Graph::default(), cache.clone());
cache.borrow_mut().fill(key("a"), 0, result_with("n1"));
let _ = p.neighbors(&"a".to_string(), &QueryParams { limit: 8, cursor: None });
assert!(cache.borrow_mut().take_wanted().is_empty());
}
#[test]
fn provider_reports_a_failed_key_as_resolved_and_empty_not_pending() {
let cache = Rc::new(RefCell::new(NeighborCache::new()));
let p = CachingProvider::new(Graph::default(), cache.clone());
cache.borrow_mut().mark_wanted(key("a"));
let epoch = cache.borrow_mut().take_wanted()[0].1;
cache.borrow_mut().fail(key("a"), epoch, "boom".into());
let r = p.neighbors(&"a".to_string(), &QueryParams { limit: 8, cursor: None });
assert!(!r.pending, "a failed key must not look pending");
assert!(r.nodes.is_empty());
assert!(r.next.is_none());
assert!(cache.borrow_mut().take_wanted().is_empty(), "must not queue a failed key");
}
#[test]
fn clear_failures_lets_a_failed_key_be_queued_again() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.fail(key("a"), epoch, "boom".into());
c.mark_wanted(key("a"));
assert!(c.take_wanted().is_empty(), "no auto-retry before an explicit clear");
c.clear_failures();
assert_eq!(c.error(&key("a")), None);
c.mark_wanted(key("a"));
let requeued = c.take_wanted();
assert_eq!(requeued.len(), 1, "retry re-queues the key");
assert_eq!(requeued[0].0, key("a"));
}
#[test]
fn clear_failures_leaves_ready_and_in_flight_alone() {
let mut c = NeighborCache::new();
c.fill(key("ready"), 0, result_with("n1"));
c.mark_wanted(key("inflight"));
let _ = c.take_wanted();
c.mark_wanted(key("bad"));
let epoch = c.take_wanted()[0].1;
c.fail(key("bad"), epoch, "boom".into());
c.clear_failures();
assert!(c.get(&key("ready")).is_some(), "ready entries survive");
assert!(c.is_loading(), "the in-flight slot survives");
c.mark_wanted(key("inflight"));
assert!(c.take_wanted().is_empty(), "still in flight, so still not re-queued");
}
#[test]
fn fill_never_trusts_pending_from_the_wire() {
let cache = Rc::new(RefCell::new(NeighborCache::new()));
let mut res = result_with("n1");
res.pending = true; cache.borrow_mut().fill(key("a"), 0, res);
assert!(!cache.borrow().get(&key("a")).unwrap().pending, "pending is client-side only");
let p = CachingProvider::new(Graph::default(), cache.clone());
let r = p.neighbors(&"a".to_string(), &QueryParams { limit: 8, cursor: None });
assert!(!r.pending);
assert_eq!(r.nodes[0].id, "n1");
}
#[test]
fn new_arrivals_drain_once_and_report_each_filled_key() {
let mut c = NeighborCache::new();
let k2 = ("b".to_string(), Some(Cursor("off:8".into())));
c.fill(key("a"), 0, result_with("n1"));
c.fill(k2.clone(), 0, result_with("n2"));
let mut drained = c.take_new_arrivals();
drained.sort_by(|x, y| x.0.cmp(&y.0));
assert_eq!(drained, vec![key("a"), k2]);
assert!(c.take_new_arrivals().is_empty(), "draining consumes the arrivals");
}
#[test]
fn ids_containing_the_separator_do_not_collide_with_cursor_keys() {
let mut c = NeighborCache::new();
let k1 = ("a\u{1}b".to_string(), None);
let k2 = ("a".to_string(), Some(Cursor("b".into())));
c.fill(k1.clone(), 0, result_with("from-k1"));
c.fill(k2.clone(), 0, result_with("from-k2"));
assert_eq!(c.get(&k1).unwrap().nodes[0].id, "from-k1");
assert_eq!(c.get(&k2).unwrap().nodes[0].id, "from-k2");
}
#[test]
fn failures_are_drainable_once() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.fail(key("a"), epoch, "boom".into());
let drained = c.take_failures();
assert_eq!(drained.len(), 1);
assert_eq!(drained[0].0, key("a"), "structured key is preserved");
assert_eq!(drained[0].1, "boom");
assert!(c.take_failures().is_empty(), "draining consumes the failures");
assert_eq!(c.error(&key("a")), Some("boom"));
c.mark_wanted(key("a"));
assert!(c.take_wanted().is_empty());
}
#[test]
fn failure_with_a_cursor_keeps_its_cursor_in_the_drained_key() {
let mut c = NeighborCache::new();
let k = ("a".to_string(), Some(Cursor("grp:X:0".into())));
c.mark_wanted(k.clone());
let epoch = c.take_wanted()[0].1;
c.fail(k.clone(), epoch, "nope".into());
let drained = c.take_failures();
assert_eq!(drained[0].0, k);
}
#[test]
fn cancel_clears_the_in_flight_slot_and_stops_reporting_loading() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let _ = c.take_wanted();
assert!(c.is_loading());
c.cancel(&key("a"));
assert!(!c.is_loading(), "a cancelled fetch must not keep the spinner up");
assert!(!c.is_in_flight(&key("a")));
}
#[test]
fn fail_after_cancel_records_nothing() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.cancel(&key("a"));
c.fail(key("a"), epoch, "AbortError".into());
assert_eq!(c.error(&key("a")), None, "no failure is recorded");
assert!(c.take_failures().is_empty(), "nothing is reported to the host");
assert!(!c.take_dirty(), "a cancellation is not a repaint-worthy change");
}
#[test]
fn a_cancelled_key_can_be_requested_again() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.cancel(&key("a"));
c.fail(key("a"), epoch, "AbortError".into());
c.mark_wanted(key("a"));
let requeued = c.take_wanted();
assert_eq!(requeued.len(), 1, "cancellation is not sticky");
assert_eq!(requeued[0].0, key("a"));
}
#[test]
fn fill_after_cancel_still_stores_the_result() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.cancel(&key("a"));
c.fill(key("a"), epoch, result_with("n1"));
assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "n1");
assert!(!c.is_in_flight(&key("a")));
}
#[test]
fn cancel_drops_a_key_queued_but_not_yet_drained() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
c.cancel(&key("a"));
assert!(c.take_wanted().is_empty(), "a queued key is dequeued, not fetched");
assert!(!c.is_loading());
}
#[test]
fn cancel_of_an_unknown_key_is_a_no_op() {
let mut c = NeighborCache::new();
c.cancel(&key("ghost"));
assert!(!c.is_loading());
c.mark_wanted(key("ghost"));
let epoch = c.take_wanted()[0].1;
c.fail(key("ghost"), epoch, "boom".into());
assert_eq!(c.error(&key("ghost")), Some("boom"));
}
#[test]
fn is_in_flight_is_true_only_between_take_wanted_and_settle() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
assert!(!c.is_in_flight(&key("a")), "queued is not yet in flight");
let epoch = c.take_wanted()[0].1;
assert!(c.is_in_flight(&key("a")));
c.fill(key("a"), epoch, result_with("n1"));
assert!(!c.is_in_flight(&key("a")));
}
#[test]
fn cache_keys_are_hashable_so_hosts_can_index_by_them() {
use std::collections::HashMap;
let mut m: HashMap<CacheKey, u32> = HashMap::new();
m.insert(key("a"), 1);
m.insert(("a".to_string(), Some(Cursor("off:8".into()))), 2);
assert_eq!(m.get(&key("a")), Some(&1));
assert_eq!(m.len(), 2, "the cursor is part of the identity");
}
#[test]
fn cancel_requeue_cancel_does_not_poison_the_key() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch1 = c.take_wanted()[0].1; c.cancel(&key("a"));
c.mark_wanted(key("a"));
let epoch2 = c.take_wanted()[0].1; c.cancel(&key("a"));
c.fail(key("a"), epoch1, "AbortError".into()); c.fail(key("a"), epoch2, "AbortError".into());
assert_eq!(c.error(&key("a")), None, "neither cancelled attempt records a failure");
c.mark_wanted(key("a"));
assert_eq!(c.take_wanted().len(), 1, "the key is not poisoned and can be requested again");
}
#[test]
fn genuine_failure_on_a_re_requested_attempt_after_cancel_is_recorded() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch1 = c.take_wanted()[0].1; c.cancel(&key("a"));
c.mark_wanted(key("a"));
let epoch2 = c.take_wanted()[0].1; assert!(c.is_loading());
c.fail(key("a"), epoch1, "boom".into()); assert!(c.is_loading(), "P2 is still outstanding");
assert_eq!(c.error(&key("a")), None);
c.fail(key("a"), epoch2, "boom".into()); assert!(!c.is_loading(), "no orphaned slot: is_loading must go quiet");
assert_eq!(c.error(&key("a")), Some("boom"));
}
#[test]
fn cancel_of_an_already_failed_key_leaves_the_failure_intact() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.fail(key("a"), epoch, "boom".into());
c.cancel(&key("a"));
assert_eq!(c.error(&key("a")), Some("boom"), "cancel must not erase a recorded failure");
c.mark_wanted(key("a"));
assert!(c.take_wanted().is_empty(), "must not re-queue a failed key via cancel's back door");
}
#[test]
fn stale_fill_from_a_superseded_attempt_is_discarded() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch1 = c.take_wanted()[0].1; c.cancel(&key("a"));
c.mark_wanted(key("a"));
let epoch2 = c.take_wanted()[0].1;
c.fill(key("a"), epoch1, result_with("stale")); assert!(c.get(&key("a")).is_none(), "a stale fill must not overwrite the live attempt");
assert!(c.is_in_flight(&key("a")), "P2 is still outstanding");
c.fill(key("a"), epoch2, result_with("live"));
assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "live");
}
#[test]
fn cancel_of_a_ready_key_is_a_no_op() {
let mut c = NeighborCache::new();
c.fill(key("a"), 0, result_with("n1"));
c.cancel(&key("a"));
assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "n1", "a ready result is untouched by cancel");
c.mark_wanted(key("a"));
assert!(c.take_wanted().is_empty(), "a ready key is still a hit, not re-queued");
}
#[test]
fn absent_cursor_and_empty_cursor_are_distinct_cache_entries() {
let mut c = NeighborCache::new();
let empty_cursor_key = ("a".to_string(), Some(Cursor(String::new())));
c.fill(key("a"), 0, result_with("no-cursor"));
c.fill(empty_cursor_key.clone(), 0, result_with("empty-cursor"));
assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "no-cursor");
assert_eq!(c.get(&empty_cursor_key).unwrap().nodes[0].id, "empty-cursor");
}
#[test]
fn late_stale_success_does_not_overwrite_a_newer_settled_success() {
let mut c = NeighborCache::new();
c.mark_wanted(key("b"));
let epoch1 = c.take_wanted()[0].1; c.cancel(&key("b"));
c.mark_wanted(key("b"));
let epoch2 = c.take_wanted()[0].1;
c.fill(key("b"), epoch2, result_with("new")); let _ = c.take_new_arrivals();
c.fill(key("b"), epoch1, result_with("old")); assert_eq!(
c.get(&key("b")).unwrap().nodes[0].id,
"new",
"a stale success must not overwrite a newer one"
);
assert!(
c.take_new_arrivals().is_empty(),
"a discarded stale fill must not be reported as a new arrival"
);
}
#[test]
fn late_stale_success_does_not_erase_a_recorded_failure() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch1 = c.take_wanted()[0].1; c.cancel(&key("a"));
c.mark_wanted(key("a"));
let epoch2 = c.take_wanted()[0].1;
c.fail(key("a"), epoch2, "boom".into()); let _ = c.take_failures();
assert_eq!(c.error(&key("a")), Some("boom"));
c.fill(key("a"), epoch1, result_with("late")); assert_eq!(
c.error(&key("a")),
Some("boom"),
"a stale success must not erase a recorded failure"
);
assert!(c.get(&key("a")).is_none(), "the failed key must not also look like a hit");
}
#[test]
fn fill_after_cancel_with_nothing_settled_since_still_stores() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.cancel(&key("a"));
c.fill(key("a"), epoch, result_with("n1"));
assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "n1");
}
#[test]
fn reset_clears_everything_the_cache_holds() {
let mut c = NeighborCache::new();
c.mark_wanted(key("ready"));
let e_ready = c.take_wanted()[0].1;
c.fill(key("ready"), e_ready, result_with("n1"));
c.mark_wanted(key("bad"));
let e_bad = c.take_wanted()[0].1;
c.fail(key("bad"), e_bad, "boom".into());
c.mark_wanted(key("inflight"));
let _ = c.take_wanted();
c.mark_wanted(key("queued"));
c.reset();
assert!(c.get(&key("ready")).is_none(), "ready entries describe the old graph");
assert_eq!(c.error(&key("bad")), None, "failures are not carried across a load");
assert!(!c.is_loading(), "no in-flight slots and nothing queued survive");
assert!(!c.take_dirty(), "the dirty flag is cleared");
assert!(c.take_failures().is_empty(), "undrained failures are dropped");
assert!(c.take_new_arrivals().is_empty(), "undrained arrivals are dropped");
assert_eq!(c.ready_results().count(), 0);
}
#[test]
fn a_key_settled_before_a_reset_can_be_requested_again() {
let mut c = NeighborCache::new();
c.mark_wanted(key("w0"));
let epoch = c.take_wanted()[0].1;
c.fill(key("w0"), epoch, result_with("old-neighbor"));
assert!(c.get(&key("w0")).is_some());
c.reset();
c.mark_wanted(key("w0"));
let requeued = c.take_wanted();
assert_eq!(requeued.len(), 1, "a reused id must fetch again, not hit");
assert_eq!(requeued[0].0, key("w0"));
c.fill(key("w0"), requeued[0].1, result_with("new-neighbor"));
assert_eq!(c.get(&key("w0")).unwrap().nodes[0].id, "new-neighbor");
}
#[test]
fn a_key_that_failed_before_a_reset_is_not_a_permanent_dead_end() {
let mut c = NeighborCache::new();
c.mark_wanted(key("w0"));
let epoch = c.take_wanted()[0].1;
c.fail(key("w0"), epoch, "boom".into());
c.mark_wanted(key("w0"));
assert!(c.take_wanted().is_empty(), "failed keys do not auto-retry pre-reset");
c.reset();
c.mark_wanted(key("w0"));
assert_eq!(c.take_wanted().len(), 1, "the new dataset's node is fetchable");
}
#[test]
fn a_fetch_outstanding_across_a_reset_cannot_settle_into_the_new_dataset() {
let mut c = NeighborCache::new();
c.mark_wanted(key("w0"));
let old_epoch = c.take_wanted()[0].1;
c.reset();
c.fill(key("w0"), old_epoch, result_with("old-neighbor"));
assert!(c.get(&key("w0")).is_none(), "an old-dataset answer must not land");
assert!(c.take_new_arrivals().is_empty(), "nor be reported as an arrival");
c.fail(key("w0"), old_epoch, "boom".into());
assert_eq!(c.error(&key("w0")), None, "nor poison the reused id with its failure");
}
#[test]
fn epochs_keep_climbing_across_a_reset() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let before = c.take_wanted()[0].1;
c.reset();
c.mark_wanted(key("a"));
let after = c.take_wanted()[0].1;
assert!(after > before, "epochs are monotonic across a reset ({after} > {before})");
}
#[test]
fn reset_on_a_fresh_cache_imposes_no_floor_on_later_attempts() {
let mut c = NeighborCache::new();
c.reset();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.fill(key("a"), epoch, result_with("n1"));
assert_eq!(c.get(&key("a")).unwrap().nodes[0].id, "n1");
}
#[test]
fn duplicate_fill_with_the_same_epoch_is_discarded() {
let mut c = NeighborCache::new();
c.mark_wanted(key("a"));
let epoch = c.take_wanted()[0].1;
c.fill(key("a"), epoch, result_with("first"));
let _ = c.take_new_arrivals();
c.fill(key("a"), epoch, result_with("duplicate"));
assert_eq!(
c.get(&key("a")).unwrap().nodes[0].id,
"first",
"a duplicate delivery of the same attempt does not overwrite"
);
assert!(
c.take_new_arrivals().is_empty(),
"a duplicate settle is not reported as a new arrival"
);
}
}