use std::collections::HashMap;
use super::execute::{execute_mut, execute_read, ExecuteOptions};
use super::transaction::Session;
use crate::datatypes::Value;
use crate::graph::dir_graph::DirGraph;
use crate::graph::languages::cypher::plan_cache;
use crate::graph::languages::cypher::plan_cache::instrumentation::{self, CacheStats};
const WRITE: &str = "CREATE (:Item {id: 1})";
const READ: &str = "MATCH (n:Item) RETURN n.id";
fn empty_params() -> HashMap<String, Value> {
HashMap::new()
}
fn events(stats: CacheStats) -> (u64, u64, u64) {
(stats.lookups, stats.hits, stats.insertions)
}
fn seeded() -> DirGraph {
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
let mut graph = DirGraph::new();
execute_mut(&mut graph, WRITE, &opts).expect("seed write");
graph
}
#[test]
fn repeated_read_hits_on_the_second_run() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let graph = seeded();
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
instrumentation::reset();
execute_read(&graph, READ, &opts).expect("cold read");
execute_read(&graph, READ, &opts).expect("warm read");
let stats = instrumentation::totals();
assert_eq!(
events(stats.read),
(2, 1, 1),
"an unchanged graph must serve the second identical read from cache"
);
assert_eq!(
events(stats.mutation),
(0, 0, 0),
"no mutation ran in this case"
);
}
#[test]
fn serial_mutations_perform_no_plan_cache_insertion() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let mut graph = DirGraph::new();
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
const WRITES: u64 = 8;
instrumentation::reset();
for _ in 0..WRITES {
execute_mut(&mut graph, WRITE, &opts).expect("write");
}
let stats = instrumentation::totals();
assert_eq!(
events(stats.mutation),
(WRITES, 0, 0),
"a write must insert nothing: its own bump_version would move the key \
before the entry could ever be read back"
);
assert_eq!(events(stats.read), (0, 0, 0), "no read ran in this case");
}
#[test]
fn a_mutation_between_two_reads_invalidates_the_read_plan() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let mut graph = seeded();
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
execute_read(&graph, READ, &opts).expect("populate the read plan");
instrumentation::reset();
execute_read(&graph, READ, &opts).expect("warm read");
assert_eq!(
events(instrumentation::totals().read),
(1, 1, 0),
"control: an unchanged graph serves this read from cache"
);
execute_mut(&mut graph, WRITE, &opts).expect("write");
instrumentation::reset();
execute_read(&graph, READ, &opts).expect("post-write read");
let stats = instrumentation::totals();
assert_eq!(
events(stats.read),
(1, 0, 1),
"the same read must miss after a write and re-plan against the new \
version"
);
assert_eq!(events(stats.mutation), (0, 0, 0));
}
#[test]
fn transactions_forked_from_one_base_version_no_longer_reuse_a_mutation_plan() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let session = Session::new(seeded());
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
let mut first = session.begin();
let mut second = session.begin();
instrumentation::reset();
execute_mut(first.working_mut().expect("tx1 working"), WRITE, &opts).expect("tx1 write");
execute_mut(second.working_mut().expect("tx2 working"), WRITE, &opts).expect("tx2 write");
let stats = instrumentation::totals();
assert_eq!(
events(stats.mutation),
(2, 0, 0),
"same-base-version forks share a key, but nothing is cached to share"
);
}
#[test]
fn a_mutation_that_errors_before_the_version_bump_no_longer_lets_a_retry_hit() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let mut graph = DirGraph::new();
graph
.interner
.try_register(
crate::graph::schema::InternedKey::from_str("CollisionType"),
"conflicting-existing",
)
.expect("register the colliding name");
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
let colliding = "CREATE (:CollisionType {id: 1})";
instrumentation::reset();
assert!(execute_mut(&mut graph, colliding, &opts).is_err());
assert!(execute_mut(&mut graph, colliding, &opts).is_err());
assert_eq!(
graph.version(),
0,
"a failed write must not bump the version"
);
let stats = instrumentation::totals();
assert_eq!(
events(stats.mutation),
(2, 0, 0),
"an unbumped version no longer helps: a failed write cached nothing"
);
}
#[test]
fn a_write_burst_evicts_no_other_graphs_read_plan() {
let _guard = plan_cache::TEST_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
plan_cache::clear_for_tests();
let params = empty_params();
let opts = ExecuteOptions::eager(¶ms);
let reader = seeded();
execute_read(&reader, READ, &opts).expect("warm the reader's plan");
let mut writer = DirGraph::new();
const BURST: u64 = 600;
instrumentation::reset();
for _ in 0..BURST {
execute_mut(&mut writer, WRITE, &opts).expect("write");
}
let stats = instrumentation::totals();
assert_eq!(
events(stats.mutation),
(BURST, 0, 0),
"a burst of {BURST} writes must leave no mutation-keyed entry behind"
);
assert_eq!(
stats.mutation.evictions, 0,
"a write that inserts nothing cannot evict anything"
);
assert!(
plan_cache::entry_count_for_tests() < plan_cache::CACHE_CAPACITY,
"the cache must not be saturated by a writer; {BURST} writes left {} of \
{} entries resident",
plan_cache::entry_count_for_tests(),
plan_cache::CACHE_CAPACITY
);
instrumentation::reset();
execute_read(&reader, READ, &opts).expect("read after the burst");
assert_eq!(
events(instrumentation::totals().read),
(1, 1, 0),
"{BURST} writes on an unrelated graph must not cost this reader its \
cached plan"
);
}