#![allow(dead_code, unused_imports, unused_variables)]
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use mongreldb_core::encryption::{AesCipher, Cipher};
use mongreldb_core::query::Condition;
use mongreldb_core::result_cache::{
self, IoError, IoErrorKind, PersistentCacheIo, RealPersistentCacheIo, FRAME_FORMAT_VERSION,
FRAME_MAGIC,
};
use mongreldb_core::{Query, Schema, Table};
use tempfile::tempdir;
struct RecordingPersistentCacheIo {
inner: Arc<RealPersistentCacheIo>,
log: Mutex<Vec<(std::thread::ThreadId, &'static str, Vec<u8>)>>,
}
impl RecordingPersistentCacheIo {
fn new(dir: std::path::PathBuf) -> Self {
Self {
inner: Arc::new(RealPersistentCacheIo::new(dir).expect("create dir")),
log: Mutex::new(Vec::new()),
}
}
fn log(&self) -> Vec<(std::thread::ThreadId, &'static str, Vec<u8>)> {
self.log.lock().unwrap().clone()
}
}
impl PersistentCacheIo for RecordingPersistentCacheIo {
fn write_atomic(&self, key: u64, frame: &[u8]) -> Result<(), IoError> {
self.log.lock().unwrap().push((
std::thread::current().id(),
"write_atomic",
frame.to_vec(),
));
self.inner.write_atomic(key, frame)
}
fn remove(&self, key: u64) -> Result<(), IoError> {
self.log
.lock()
.unwrap()
.push((std::thread::current().id(), "remove", Vec::new()));
self.inner.remove(key)
}
fn clear(&self) -> Result<(), IoError> {
self.log
.lock()
.unwrap()
.push((std::thread::current().id(), "clear", Vec::new()));
self.inner.clear()
}
fn load(&self, key: u64) -> Result<Option<Vec<u8>>, IoError> {
self.log
.lock()
.unwrap()
.push((std::thread::current().id(), "load", Vec::new()));
self.inner.load(key)
}
fn exists(&self, key: u64) -> bool {
self.log
.lock()
.unwrap()
.push((std::thread::current().id(), "exists", Vec::new()));
self.inner.exists(key)
}
}
struct GatedPersistentCacheIo {
inner: Arc<RealPersistentCacheIo>,
gate: Arc<std::sync::atomic::AtomicBool>,
block_for: Duration,
}
impl GatedPersistentCacheIo {
fn new(dir: std::path::PathBuf, block_for: Duration) -> Self {
Self {
inner: Arc::new(RealPersistentCacheIo::new(dir).expect("create dir")),
gate: Arc::new(std::sync::atomic::AtomicBool::new(true)),
block_for,
}
}
fn open(&self) {
self.gate.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
impl PersistentCacheIo for GatedPersistentCacheIo {
fn write_atomic(&self, key: u64, frame: &[u8]) -> Result<(), IoError> {
while self.gate.load(std::sync::atomic::Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(2));
}
std::thread::sleep(self.block_for);
self.inner.write_atomic(key, frame)
}
fn remove(&self, key: u64) -> Result<(), IoError> {
self.inner.remove(key)
}
fn clear(&self) -> Result<(), IoError> {
self.inner.clear()
}
fn load(&self, key: u64) -> Result<Option<Vec<u8>>, IoError> {
self.inner.load(key)
}
fn exists(&self, key: u64) -> bool {
self.inner.exists(key)
}
}
fn test_schema() -> Schema {
Schema {
schema_id: 42,
columns: vec![
mongreldb_core::schema::ColumnDef {
id: 1,
name: "id".into(),
ty: mongreldb_core::schema::TypeId::Int64,
flags: mongreldb_core::schema::ColumnFlags::empty()
.with(mongreldb_core::schema::ColumnFlags::PRIMARY_KEY),
default_value: None,
embedding_source: None,
},
mongreldb_core::schema::ColumnDef {
id: 2,
name: "city".into(),
ty: mongreldb_core::schema::TypeId::Bytes,
flags: mongreldb_core::schema::ColumnFlags::empty()
.with(mongreldb_core::schema::ColumnFlags::NULLABLE),
default_value: None,
embedding_source: None,
},
mongreldb_core::schema::ColumnDef {
id: 3,
name: "cost".into(),
ty: mongreldb_core::schema::TypeId::Float64,
flags: mongreldb_core::schema::ColumnFlags::empty()
.with(mongreldb_core::schema::ColumnFlags::NULLABLE),
default_value: None,
embedding_source: None,
},
],
indexes: vec![
mongreldb_core::schema::IndexDef {
name: "city_bm".into(),
column_id: 2,
kind: mongreldb_core::schema::IndexKind::Bitmap,
predicate: None,
options: Default::default(),
},
mongreldb_core::schema::IndexDef {
name: "cost_lr".into(),
column_id: 3,
kind: mongreldb_core::schema::IndexKind::LearnedRange,
predicate: None,
options: Default::default(),
},
],
colocation: vec![],
constraints: Default::default(),
clustered: false,
}
}
fn rows(n: usize) -> Vec<Vec<(u16, mongreldb_core::Value)>> {
(0..n)
.map(|i| {
vec![
(1, mongreldb_core::Value::Int64(i as i64)),
(
2,
mongreldb_core::Value::Bytes(if i % 2 == 0 {
b"alpha".to_vec()
} else {
b"beta!".to_vec()
}),
),
(3, mongreldb_core::Value::Float64(i as f64)),
]
})
.collect()
}
fn alpha_query() -> Query {
Query::new().and(Condition::BitmapEq {
column_id: 2,
value: b"alpha".to_vec(),
})
}
fn rcache_path(dir: &std::path::Path) -> std::path::PathBuf {
dir.join("_rcache")
}
fn first_bin_file(dir: &std::path::Path) -> Option<std::path::PathBuf> {
let entries = std::fs::read_dir(dir).ok()?;
for entry in entries.flatten() {
let p = entry.path();
if p.extension().and_then(|s| s.to_str()) == Some("bin") {
return Some(p);
}
}
None
}
#[test]
fn async_persisted_entry_is_loaded_after_memory_eviction() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let _ = db.flush_persistent_cache(2_000);
db.set_result_cache_max_bytes(1);
let pre_hit = db.lookup_metrics_snapshot().result_cache_disk_hit;
let r = db.query_cached(&q).unwrap();
let post_hit = db.lookup_metrics_snapshot().result_cache_disk_hit;
assert_eq!(r.len(), 100);
assert!(
post_hit > pre_hit,
"result_cache_disk_hit must advance (was {pre_hit}, now {post_hit})"
);
let path = first_bin_file(&rcache).expect("on-disk cache file present");
let bytes = std::fs::read(&path).unwrap();
assert_eq!(&bytes[..FRAME_MAGIC.len()], &FRAME_MAGIC);
}
#[test]
fn async_persisted_entry_survives_table_reopen() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
{
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let _ = db.flush_persistent_cache(2_000);
db.shutdown_persistent_cache(500);
}
let mut db2 = Table::open(&table_dir).unwrap();
let q = alpha_query();
let r = db2.query_cached(&q).unwrap();
assert_eq!(r.len(), 100);
let path = first_bin_file(&rcache).expect("file present after reopen");
let bytes = std::fs::read(&path).unwrap();
assert_eq!(&bytes[..FRAME_MAGIC.len()], &FRAME_MAGIC);
let _ = FRAME_FORMAT_VERSION; }
#[test]
fn encrypted_async_persisted_entry_survives_reopen() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let passphrase = "hunter2";
{
let mut db = Table::create_encrypted(&table_dir, test_schema(), 1, passphrase).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let _ = db.flush_persistent_cache(2_000);
db.shutdown_persistent_cache(500);
}
let mut db = Table::open_encrypted(&table_dir, passphrase).unwrap();
let q = alpha_query();
let r = db.query_cached(&q).unwrap();
assert_eq!(r.len(), 100);
let rcache = rcache_path(&table_dir);
let path = first_bin_file(&rcache).expect("file present");
let bytes = std::fs::read(&path).unwrap();
assert_eq!(&bytes[..FRAME_MAGIC.len()], &FRAME_MAGIC);
let frame = result_cache::decode_frame(&bytes).expect("frame decodes");
use mongreldb_core::result_cache::PersistedFrame;
let _ = std::mem::size_of::<PersistedFrame>(); assert!(
frame.payload.len() > 12,
"encrypted payload must include nonce + ciphertext + tag"
);
}
#[test]
fn production_loader_rejects_schema_mismatch() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let _ = db.flush_persistent_cache(2_000);
let path = first_bin_file(&rcache).expect("file present");
let mut bytes = std::fs::read(&path).unwrap();
let wrong_schema: u64 = 0xDEAD_BEEF_CAFE_BABE;
bytes[16..24].copy_from_slice(&wrong_schema.to_le_bytes());
let body_len = bytes.len() - 4;
let new_crc = crc32c::crc32c(&bytes[..body_len]);
bytes[body_len..].copy_from_slice(&new_crc.to_le_bytes());
std::fs::write(&path, &bytes).unwrap();
let mut db2 = Table::open(&table_dir).unwrap();
let r = db2.query_cached(&q).unwrap();
assert_eq!(r.len(), 100);
assert!(
!path.exists(),
"tampered file should be deleted by the production loader"
);
}
#[test]
fn production_loader_rejects_older_logical_generation() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let _ = db.flush_persistent_cache(2_000);
db.shutdown_persistent_cache(500);
drop(db);
let path = first_bin_file(&rcache).expect("file present");
let mut bytes = std::fs::read(&path).unwrap();
let old_gen: u64 = 0;
bytes[24..32].copy_from_slice(&old_gen.to_le_bytes());
let body_len = bytes.len() - 4;
let new_crc = crc32c::crc32c(&bytes[..body_len]);
bytes[body_len..].copy_from_slice(&new_crc.to_le_bytes());
std::fs::write(&path, &bytes).unwrap();
let mut db2 = Table::open(&table_dir).unwrap();
let r = db2.query_cached(&q).unwrap();
assert_eq!(r.len(), 100);
assert!(
!path.exists(),
"old-generation file should be removed by the loader"
);
}
fn tamper_frame_generation(rcache: &std::path::Path, new_gen: u64) -> std::path::PathBuf {
let path = first_bin_file(rcache).expect("file present");
let mut bytes = std::fs::read(&path).unwrap();
bytes[24..32].copy_from_slice(&new_gen.to_le_bytes());
let body_len = bytes.len() - 4;
let new_crc = crc32c::crc32c(&bytes[..body_len]);
bytes[body_len..].copy_from_slice(&new_crc.to_le_bytes());
std::fs::write(&path, &bytes).unwrap();
path
}
fn read_frame_generation(path: &std::path::Path) -> u64 {
let bytes = std::fs::read(path).unwrap();
u64::from_le_bytes(bytes[24..32].try_into().unwrap())
}
#[test]
fn production_loader_rejects_future_logical_generation() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let _ = db.flush_persistent_cache(2_000);
db.shutdown_persistent_cache(500);
drop(db);
let current_gen = read_frame_generation(&first_bin_file(&rcache).expect("file present"));
let path = tamper_frame_generation(&rcache, current_gen + 100);
let mut db2 = Table::open(&table_dir).unwrap();
db2._set_persist_min_bytes_for_test(0);
let pre_disk_hit = db2.lookup_metrics_snapshot().result_cache_disk_hit;
assert!(
!path.exists(),
"future-generation file should be removed by the loader"
);
let r = db2.query_cached(&q).unwrap();
assert_eq!(r.len(), 100);
let post_disk_hit = db2.lookup_metrics_snapshot().result_cache_disk_hit;
assert_eq!(
post_disk_hit, pre_disk_hit,
"rejected future-generation file must not increment the disk-hit counter"
);
let _ = db2.flush_persistent_cache(2_000);
let new_path = first_bin_file(&rcache).expect("fresh frame republished");
assert_eq!(read_frame_generation(&new_path), current_gen);
}
#[test]
fn production_loader_rejects_future_logical_generation_encrypted() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let passphrase = "hunter2";
let mut db = Table::create_encrypted(&table_dir, test_schema(), 1, passphrase).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let _ = db.flush_persistent_cache(2_000);
db.shutdown_persistent_cache(500);
drop(db);
let current_gen = read_frame_generation(&first_bin_file(&rcache).expect("file present"));
let path = tamper_frame_generation(&rcache, current_gen + 100);
let mut db2 = Table::open_encrypted(&table_dir, passphrase).unwrap();
let pre_disk_hit = db2.lookup_metrics_snapshot().result_cache_disk_hit;
assert!(
!path.exists(),
"future-generation encrypted frame should be removed by the loader"
);
let r = db2.query_cached(&q).unwrap();
assert_eq!(r.len(), 100);
let post_disk_hit = db2.lookup_metrics_snapshot().result_cache_disk_hit;
assert_eq!(
post_disk_hit, pre_disk_hit,
"rejected future-generation file must not increment the disk-hit counter"
);
}
#[test]
fn production_loader_rejects_corrupt_frame() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let _ = db.flush_persistent_cache(2_000);
db.shutdown_persistent_cache(500);
drop(db);
let path = first_bin_file(&rcache).expect("file present");
let mut bytes = std::fs::read(&path).unwrap();
let mid = bytes.len() / 2;
bytes[mid] ^= 0xFF;
std::fs::write(&path, &bytes).unwrap();
let mut db2 = Table::open(&table_dir).unwrap();
let _ = db2.query_cached(&q).unwrap();
assert!(
!path.exists(),
"bad-crc file should be removed by the production loader"
);
drop(db2);
let dir2 = tempdir().unwrap();
let table_dir2 = dir2.path().to_path_buf();
let rcache2 = rcache_path(&table_dir2);
let mut db3 = Table::create(&table_dir2, test_schema(), 1).unwrap();
db3.bulk_load(rows(200)).unwrap();
db3.flush().unwrap();
let _ = db3.query_cached(&q).unwrap();
let _ = db3.flush_persistent_cache(2_000);
db3.shutdown_persistent_cache(500);
drop(db3);
let path2 = first_bin_file(&rcache2).expect("file present");
let truncated = vec![b'M', b'L', b'C', b'P', 0, 0]; std::fs::write(&path2, &truncated).unwrap();
let mut db4 = Table::open(&table_dir2).unwrap();
let _ = db4.query_cached(&q).unwrap();
assert!(
!path2.exists(),
"truncated-header file should be removed by the loader"
);
}
#[test]
fn production_loader_rejects_wrong_encryption_key() {
let key_a = [0x11u8; 32];
let key_b = [0x22u8; 32];
let cipher_a = AesCipher::new(&key_a).unwrap();
let cipher_b = AesCipher::new(&key_b).unwrap();
let plaintext = b"super-secret-payload";
let context = result_cache::PersistContext {
identity: result_cache::PersistentCacheIdentity {
table_id: 1,
schema_id: 1,
logical_generation: 1,
},
key: 7,
entry_generation: 1,
};
let bytes_a =
result_cache::encode_persisted_entry(context, plaintext, Some(&cipher_a)).expect("encode");
let wrong = result_cache::decode_persisted_entry(
context.identity,
context.key,
&bytes_a,
Some(&cipher_b),
);
assert!(wrong.is_err(), "wrong-key decode must be rejected");
let right = result_cache::decode_persisted_entry(
context.identity,
context.key,
&bytes_a,
Some(&cipher_a),
)
.expect("right key");
assert_eq!(right, plaintext);
}
#[test]
fn production_loader_rejects_legacy_unframed_files() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let _ = db.flush_persistent_cache(2_000);
db.shutdown_persistent_cache(500);
drop(db);
let path = first_bin_file(&rcache).expect("file present");
std::fs::write(&path, b"NOT_MLCP_legacy_data").unwrap();
let mut db2 = Table::open(&table_dir).unwrap();
let r = db2.query_cached(&q).unwrap();
assert_eq!(r.len(), 100);
assert!(
!path.exists(),
"legacy unframed file should be deleted by the loader"
);
}
#[test]
fn explicit_maintenance_sync_api_still_writes_mlcp() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
db.shutdown_persistent_cache(500);
let q = alpha_query();
let r = db.query_cached(&q).unwrap();
assert_eq!(r.len(), 100);
assert!(
first_bin_file(&rcache).is_none(),
"query path must not write synchronously after worker shutdown"
);
assert!(db._persist_cached_entry_synchronously_for_maintenance(&q));
let path = first_bin_file(&rcache).expect("maintenance flush wrote a file");
let bytes = std::fs::read(&path).unwrap();
assert_eq!(&bytes[..FRAME_MAGIC.len()], &FRAME_MAGIC);
let frame = result_cache::decode_frame(&bytes).expect("frame decodes");
assert_eq!(frame.header.format_version, FRAME_FORMAT_VERSION);
assert_eq!(frame.header.table_id, 1);
assert_eq!(frame.header.schema_id, 42);
drop(db);
let mut db2 = Table::open(&table_dir).unwrap();
let r2 = db2.query_cached(&q).unwrap();
assert_eq!(r2.len(), 100);
}
fn beta_query() -> Query {
Query::new().and(Condition::BitmapEq {
column_id: 2,
value: b"beta!".to_vec(),
})
}
fn count_bin_files(dir: &std::path::Path) -> usize {
let Ok(entries) = std::fs::read_dir(dir) else {
return 0;
};
entries
.flatten()
.filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("bin"))
.count()
}
#[test]
fn worker_spawn_failure_does_not_perform_query_thread_io() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
Table::_force_persistent_worker_spawn_failure_for_test(true);
let created = Table::create(&table_dir, test_schema(), 1);
Table::_force_persistent_worker_spawn_failure_for_test(false);
let mut db = created.unwrap();
let snap = db.lookup_metrics_snapshot();
assert_eq!(
snap.result_cache_worker_spawn_failures_total, 1,
"spawn failure must be counted"
);
assert_eq!(
snap.result_cache_persist_unavailable_total, 1,
"the unavailable transition must be counted"
);
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let t0 = Instant::now();
let r = db.query_cached(&q).unwrap();
let elapsed = t0.elapsed();
assert_eq!(r.len(), 100);
assert!(
elapsed < Duration::from_millis(50),
"query must return promptly with no writer (took {elapsed:?})"
);
assert!(
first_bin_file(&rcache).is_none(),
"worker absence must not produce a synchronous on-disk frame"
);
let snap = db.lookup_metrics_snapshot();
assert!(
snap.result_cache_persist_skipped_total >= 1,
"skipped publication must be counted"
);
}
#[test]
fn query_after_writer_shutdown_does_not_write_synchronously() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let _ = db.flush_persistent_cache(2_000);
db.shutdown_persistent_cache(500);
let files_before = count_bin_files(&rcache);
let q2 = beta_query();
let r = db.query_cached(&q2).unwrap();
assert_eq!(r.len(), 100);
assert_eq!(
count_bin_files(&rcache),
files_before,
"no new frame may be written after writer shutdown"
);
let snap = db.lookup_metrics_snapshot();
assert!(
snap.result_cache_worker_shutdown_total >= 1,
"worker shutdown must be counted"
);
assert!(
snap.result_cache_persist_skipped_total >= 1,
"post-shutdown persist must be counted as skipped"
);
}
#[test]
fn writer_unavailable_keeps_memory_cache_result() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
Table::_force_persistent_worker_spawn_failure_for_test(true);
let created = Table::create(&table_dir, test_schema(), 1);
Table::_force_persistent_worker_spawn_failure_for_test(false);
let mut db = created.unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let pre = db.lookup_metrics_snapshot().result_cache_memory_hit;
let r1 = db.query_cached(&q).unwrap();
let r2 = db.query_cached(&q).unwrap();
let post = db.lookup_metrics_snapshot().result_cache_memory_hit;
assert_eq!(r1.len(), 100);
assert_eq!(r2.len(), 100);
assert!(
post > pre,
"second query must hit the in-memory cache (was {pre}, now {post})"
);
}
#[test]
fn writer_unavailable_increments_skip_metric() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
Table::_force_persistent_worker_spawn_failure_for_test(true);
let created = Table::create(&table_dir, test_schema(), 1);
Table::_force_persistent_worker_spawn_failure_for_test(false);
let mut db = created.unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let (r, trace) = mongreldb_core::trace::QueryTrace::capture(|| db.query_cached(&q));
assert_eq!(r.unwrap().len(), 100);
assert!(
trace.result_cache_persist_skipped,
"query trace must record the skipped persist ({trace})"
);
assert_eq!(
trace.result_cache_persist_skip_reason,
Some("worker_spawn_failed"),
"trace must carry the disabled reason label"
);
let snap = db.lookup_metrics_snapshot();
assert!(snap.result_cache_persist_skipped_total >= 1);
assert!(snap.result_cache_persist_unavailable_total >= 1);
assert!(snap.result_cache_worker_spawn_failures_total >= 1);
}
#[test]
fn no_io_on_query_thread() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let recording = Arc::new(RecordingPersistentCacheIo::new(rcache.clone()));
use mongreldb_core::result_cache::{
spawn_persistent_cache_worker, PersistentResultCacheWriter, StalenessGuard, WorkerConfig,
WriterLimits, WriterStalenessGuard,
};
let writer = Arc::new(PersistentResultCacheWriter::for_test(
WriterLimits::default(),
));
let staleness: Arc<dyn StalenessGuard> = Arc::new(WriterStalenessGuard::new(writer.clone()));
let (tx, rx) = std::sync::mpsc::channel();
let config = WorkerConfig {
writer: writer.clone(),
io: recording.clone(),
cipher: None,
staleness,
max_staleness_retries: 8,
completion: Some(tx),
};
let _worker = spawn_persistent_cache_worker(config);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let query_thread = std::thread::current().id();
let r = db.query_cached(&q).unwrap();
assert_eq!(r.len(), 100);
let _ = db.flush_persistent_cache(2_000);
let _ = rx.recv_timeout(Duration::from_millis(500)).ok();
let log = recording.log();
let query_thread_io: Vec<_> = log
.iter()
.filter(|(tid, op, _)| {
*tid == query_thread && matches!(*op, "write_atomic" | "load" | "exists")
})
.collect();
assert!(
query_thread_io.is_empty(),
"query thread must not perform persistent I/O; saw {query_thread_io:?}"
);
}
#[test]
fn blocked_table_writer_does_not_block_query() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let gated = Arc::new(GatedPersistentCacheIo::new(
rcache.clone(),
Duration::from_millis(50),
));
use mongreldb_core::result_cache::{
spawn_persistent_cache_worker, PersistentResultCacheWriter, StalenessGuard, WorkerConfig,
WriterLimits, WriterStalenessGuard,
};
let writer = Arc::new(PersistentResultCacheWriter::for_test(
WriterLimits::default(),
));
let staleness: Arc<dyn StalenessGuard> = Arc::new(WriterStalenessGuard::new(writer.clone()));
let (tx, rx) = std::sync::mpsc::channel();
let gated_clone = gated.clone();
let config = WorkerConfig {
writer: writer.clone(),
io: gated_clone as Arc<dyn PersistentCacheIo>,
cipher: None,
staleness,
max_staleness_retries: 8,
completion: Some(tx),
};
let _worker = spawn_persistent_cache_worker(config);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let t0 = Instant::now();
let r = db.query_cached(&q).unwrap();
let elapsed = t0.elapsed();
assert_eq!(r.len(), 100);
assert!(
elapsed < Duration::from_millis(20),
"query must not block on a stalled writer (took {elapsed:?})"
);
gated.open();
let _ = db.flush_persistent_cache(2_000);
let _ = rx.recv_timeout(Duration::from_millis(500)).ok();
}
#[test]
fn bounded_shutdown_returns_by_deadline() {
let dir = tempdir().unwrap();
let table_dir = dir.path().to_path_buf();
let rcache = rcache_path(&table_dir);
let gated = Arc::new(GatedPersistentCacheIo::new(
rcache.clone(),
Duration::from_secs(5),
));
use mongreldb_core::result_cache::{
spawn_persistent_cache_worker, PersistentResultCacheWriter, StalenessGuard, WorkerConfig,
WriterLimits, WriterStalenessGuard,
};
let writer = Arc::new(PersistentResultCacheWriter::for_test(
WriterLimits::default(),
));
let staleness: Arc<dyn StalenessGuard> = Arc::new(WriterStalenessGuard::new(writer.clone()));
let (tx, _rx) = std::sync::mpsc::channel();
let gated_clone = gated.clone();
let config = WorkerConfig {
writer: writer.clone(),
io: gated_clone as Arc<dyn PersistentCacheIo>,
cipher: None,
staleness,
max_staleness_retries: 8,
completion: Some(tx),
};
let _worker = spawn_persistent_cache_worker(config);
let mut db = Table::create(&table_dir, test_schema(), 1).unwrap();
db.bulk_load(rows(200)).unwrap();
db.flush().unwrap();
db._set_persist_min_bytes_for_test(0);
let q = alpha_query();
let _ = db.query_cached(&q).unwrap();
let t0 = Instant::now();
db.shutdown_persistent_cache(200);
let elapsed = t0.elapsed();
assert!(
elapsed < Duration::from_millis(500),
"shutdown must return by its deadline (took {elapsed:?})"
);
gated.open();
writer.shutdown();
}
mod crc32c {
pub fn crc32c(buf: &[u8]) -> u32 {
let mut h: u32 = 0xFFFF_FFFF;
for &b in buf {
h ^= b as u32;
for _ in 0..8 {
let mask = 0u32.wrapping_sub(h & 1);
h = (h >> 1) ^ (0x82F6_3B78 & mask);
}
}
!h
}
}