crates/mimir-room/tests/room/cache.rs
use mimir_ingest::Consumer;
use mimir_room::{Room, Scope};
use mimir_schema::{EdgeKind, Origin, Provenance};
use super::common::*;
#[tokio::test(flavor = "current_thread")]
async fn second_open_is_served_from_the_cache_until_new_events_land() {
let _g = serial().await;
Room::clear_cache();
Room::set_cache_capacity(16);
let tmp = tempfile::tempdir().unwrap();
let log = fixture(tmp.path()).await;
let first = Room::open(&log, &Scope::current()).await.unwrap();
assert!(!first.cached);
let again = Room::open(&log, &Scope::current()).await.unwrap();
assert!(again.cached && again.room.same_snapshot(&first.room));
// the same state spelled out explicitly is the same key
let explicit = Scope { repos: [(ROOM.to_string(), Some(3)), (PLAIN.to_string(), Some(1))].into(), universe: false };
assert!(Room::open(&log, &explicit).await.unwrap().cached);
// another scope is another snapshot
let other = Room::open(&log, &Scope::repo(ROOM, Some(2))).await.unwrap();
assert!(!other.cached && !other.room.same_snapshot(&first.room));
// derived matrices stay with the snapshot
first.room.dependents(&["k a"], None).unwrap();
assert_eq!(again.room.stats().cached_matrices, (2, 1), "kind union + Jikes matrix, one transpose");
// ---- source events of a later seq never touch a pinned snapshot
let src = tmp.path().join("src-plain");
commit(&src, 2);
let step = Step {
owner: "p@0.1.0:lib",
nodes: vec![node("p p1", "fn", "p", Origin::Local, true), node("p p2", "fn", "p", Origin::Local, true), node("p p3", "fn", "p", Origin::Local, true)],
edges: vec![edge("p p1", "p p2", EdgeKind::Calls), edge("p p3", "p p1", EdgeKind::Calls)],
binary: None,
ring: vec![],
};
let log = run(log, tmp.path(), PLAIN, step).await;
let replay = Room::open(&log, &explicit).await.unwrap();
assert!(replay.cached && replay.room.same_snapshot(&first.room), "plain@1 + room@3 is unaffected by plain's seq 2");
let fresh = Room::open(&log, &Scope::current()).await.unwrap();
assert!(!fresh.cached, "current now means plain@2: another snapshot");
assert_eq!(fresh.room.scope().repos[PLAIN], 2);
assert!(fresh.room.ordinal("p p3").is_some() && first.room.ordinal("p p3").is_none());
let d = fresh.room.dependents(&["p p2"], None).unwrap();
assert_eq!(fresh.room.monikers(&d.value.reached), vec!["p p1", "p p3"]);
// ---- a binary re-ingest lands AT room's seq 3: room@3 is stale, room@2 is not
let r3 = Room::open(&log, &Scope::repo(ROOM, Some(3))).await.unwrap();
let b_was = r3.room.node(r3.room.ordinal("k b").unwrap()).provenance;
assert_eq!(b_was, Some(Provenance::SourceOnly));
let mut c = Consumer::boot(log, Scripted(vec![])).await.unwrap();
c.reingest_binaries(&repo_spec(tmp.path(), ROOM)).await.unwrap();
let log = c.into_parts().0;
assert!(Room::open(&log, &Scope::repo(ROOM, Some(2))).await.unwrap().cached, "the release is at seq 3");
let r3b = Room::open(&log, &Scope::repo(ROOM, Some(3))).await.unwrap();
assert!(!r3b.cached);
assert_eq!(r3b.room.node(r3b.room.ordinal("k b").unwrap()).provenance, Some(Provenance::Both), "relinked");
// ---- a ring write invalidates ring-reading snapshots only
let p1 = Scope::repo(PLAIN, Some(1));
let p1u = Scope::repo(PLAIN, Some(1)).with_universe(true);
assert!(!Room::open(&log, &p1).await.unwrap().cached);
assert!(!Room::open(&log, &p1u).await.unwrap().cached);
commit(&src, 3);
let step = Step {
owner: "p@0.1.0:lib",
nodes: vec![node("p p1", "fn", "p", Origin::Local, true)],
edges: vec![],
binary: None,
ring: vec![("dep3@1.0.0:lib", vec![node("dep3", "crate", "dep3", Origin::Library, true)], vec![])],
};
let log = run(log, tmp.path(), PLAIN, step).await;
assert!(Room::open(&log, &p1).await.unwrap().cached, "ring off, no linked ring items: the ring is not read");
assert!(!Room::open(&log, &p1u).await.unwrap().cached, "ring on: rebuilt");
// ---- ring definitions: folded once per manifest snapshot, then cached
Room::clear_cache();
Room::clear_ring_cache();
let note = |r: &Room| r.reach(&["k a"], &[EdgeKind::Calls], None, None).unwrap().evidence.notes.join(" | ");
let solo = Scope::repo(ROOM, None);
let a = Room::open(&log, &solo).await.unwrap();
assert!(note(&a.room).contains("ring folded for this build"), "{}", note(&a.room));
Room::clear_cache();
let b = Room::open(&log, &solo).await.unwrap();
assert!(!b.cached && note(&b.room).contains("cached ring definitions"), "{}", note(&b.room));
let lookup = |r: &Room| r.stats().inputs.into_iter().find(|i| i.table.contains("ring definitions")).unwrap();
assert_eq!((lookup(&b.room).events_read, lookup(&b.room).open_rows), (0, 2), "dep h, rt unwind found without folding the ring");
assert_eq!(b.room.node(b.room.ordinal("dep h").unwrap()).provenance, Some(Provenance::Both));
// the old seq-1 state is still reachable by time travel
assert!(Room::at(&log, &p1).await.unwrap().ordinal("p p3").is_none());
}
fn commit(src: &std::path::Path, n: usize) {
std::fs::write(src.join("src/lib.rs"), format!("// commit {n}\n")).unwrap();
nornir_git::gitio::commit_all(src, &format!("c{n}")).unwrap();
}
async fn run(log: mimir_ingest::EventLog, tmp: &std::path::Path, repo: &str, step: Step) -> mimir_ingest::EventLog {
let mut c = Consumer::boot(log, Scripted(vec![step])).await.unwrap();
let rep = c.run(&repo_spec(tmp, repo)).await.unwrap();
assert!(rep.iter().all(|r| r.status == "ok"), "{rep:?}");
c.into_parts().0
}
/// Panes asking at once after a restart folded the same room side by side
/// (dwarf 2026-10-15: 114 s to the first answer). Opens of one room at once
/// fold it ONCE; `peek` answers from the cache and never folds.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn opens_of_one_room_at_once_fold_it_once_and_peek_never_folds() {
let _g = serial().await;
Room::clear_cache();
Room::set_cache_capacity(16);
let tmp = tempfile::tempdir().unwrap();
let log = fixture(tmp.path()).await;
assert!(Room::peek(&log, &Scope::current()).await.unwrap().is_none(), "nothing built yet: peek folds nothing");
assert!(Room::peek(&log, &Scope::current()).await.unwrap().is_none(), "and still nothing after a peek");
let barrier = std::sync::Arc::new(tokio::sync::Barrier::new(6));
let opens: Vec<_> = (0..6)
.map(|_| {
let (log, barrier) = (log.clone(), barrier.clone());
tokio::spawn(async move {
barrier.wait().await;
Room::open(&log, &Scope::current()).await.unwrap()
})
})
.collect();
let mut rooms = Vec::new();
for h in opens {
rooms.push(h.await.unwrap());
}
assert_eq!(rooms.iter().filter(|o| !o.cached).count(), 1, "one fold for six opens at once");
assert!(rooms.iter().all(|o| o.room.same_snapshot(&rooms[0].room) && o.room.version() == rooms[0].room.version()));
let peeked = Room::peek(&log, &Scope::current()).await.unwrap().expect("built now");
assert!(peeked.same_snapshot(&rooms[0].room));
// another fold of the same scope is another version
Room::clear_cache();
let again = Room::open(&log, &Scope::current()).await.unwrap();
assert!(!again.cached && again.room.version() != rooms[0].room.version());
}
/// **At most ONE ring-view room is kept.** A ring-view room (dependency ring
/// on) is the big one — 1.9 GB kept for dwarf's 21 repos, beside 360 MB for
/// the current room — so a second ring view in the cache evicts the older one
/// whatever the count cap says. Ring-off rooms keep the count cap.
#[tokio::test(flavor = "current_thread")]
async fn a_second_ring_view_evicts_the_first_and_leaves_the_others() {
let _g = serial().await;
Room::clear_cache();
Room::set_cache_capacity(16);
let tmp = tempfile::tempdir().unwrap();
let log = fixture(tmp.path()).await;
let plain = Scope::repo(PLAIN, Some(1));
let room = Scope::repo(ROOM, Some(3));
let ring_a = Scope::repo(PLAIN, Some(1)).with_universe(true);
let ring_b = Scope::repo(ROOM, Some(3)).with_universe(true);
for s in [&plain, &room, &ring_a] {
assert!(!Room::open(&log, s).await.unwrap().cached);
}
assert!(Room::open(&log, &ring_a).await.unwrap().cached, "one ring view is kept");
// the second ring view: the first goes
assert!(!Room::open(&log, &ring_b).await.unwrap().cached);
assert!(Room::open(&log, &ring_b).await.unwrap().cached, "the newer ring view is kept");
assert!(!Room::open(&log, &ring_a).await.unwrap().cached, "the older ring view was evicted");
// … which in turn put ring_b out; the ring-off rooms were never touched
assert!(Room::open(&log, &plain).await.unwrap().cached && Room::open(&log, &room).await.unwrap().cached, "ring-off rooms stay");
assert!(!Room::open(&log, &ring_b).await.unwrap().cached, "ring_a's rebuild evicted ring_b");
Room::set_cache_capacity(4);
}
/// RED before: an at-head ring fold left the other ring views of its log
/// in the cache until the new fold landed — no ask could find them (the
/// stamp moved), yet for the six minutes of a live ring refold they were a
/// second dependency universe in memory (dwarf 2026-10-22: OOM-killed at
/// 38.7 GB after each consume). The fold now starts with them gone.
#[tokio::test(flavor = "current_thread")]
async fn an_at_head_fold_drops_the_old_entries_of_its_room_first() {
let _g = serial().await;
Room::clear_cache();
Room::set_cache_capacity(16);
let tmp = tempfile::tempdir().unwrap();
let log = fixture(tmp.path()).await;
let behind = Scope { repos: [(ROOM.to_string(), Some(2)), (PLAIN.to_string(), Some(1))].into(), universe: true };
assert!(!Room::open(&log, &behind).await.unwrap().cached);
assert_eq!(Room::cached_count(), 1);
let head = Scope::current().with_universe(true);
assert!(!Room::open(&log, &head).await.unwrap().cached);
assert_eq!(Room::entries_at_last_fold(), 0, "the fold shared memory with no older entry of its room");
assert_eq!(Room::cached_count(), 1);
Room::set_cache_capacity(4);
}
/// RED before: a ring view kept its fold (a second copy of the dependency
/// universe records) although a consume that dumps dependencies moves the
/// manifest and refuses every advance, and stayed in memory between
/// consumes. Now it keeps no fold by default, and a server puts it out
/// once nobody asked for it in the idle time.
#[tokio::test(flavor = "current_thread")]
async fn a_ring_view_keeps_no_fold_and_goes_when_idle() {
let _g = serial().await;
Room::clear_cache();
Room::set_ring_keeps_fold(false);
Room::set_cache_capacity(16);
let tmp = tempfile::tempdir().unwrap();
let log = fixture(tmp.path()).await;
let ws = Scope::current();
let ring = Scope::current().with_universe(true);
assert!(!Room::open(&log, &ws).await.unwrap().cached);
assert!(!Room::open(&log, &ring).await.unwrap().cached);
assert!(Room::kept_universe_scope(&log).await.unwrap().is_some());
assert_eq!(Room::evict_idle_ring(std::time::Duration::from_secs(3600)), 0, "asked just now: kept");
assert_eq!(Room::evict_idle_ring(std::time::Duration::ZERO), 1, "idle past the limit: out");
assert!(Room::open(&log, &ws).await.unwrap().cached, "the workspace room stays");
assert!(!Room::open(&log, &ring).await.unwrap().cached, "the next ring ask folds again");
Room::set_cache_capacity(4);
}