This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 09:30:21 +02:00
1 parent 1dea7f55c8
commit c819f0a335
151 files changed
+7610 -4264

No files matched your search

+311 -274
View File
@@ -1,293 +1,330 @@
#[tokio::test]
async fn graceful_shutdown_flush_moves_hot_objects_and_releases_resident_bytes() {
let dir = tempfile::tempdir().unwrap();
let mut settings = cold_settings();
settings.max_entries = 8;
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/one", "first-payload").await;
fill(&cache, &settings, "/two", "second-payload").await;
assert_eq!(cache.view().entries, 2);
assert!(cache.view().resident_bytes > 0);
assert_eq!(cache.view().cold_entries, 0);
#[tokio::test]
async fn graceful_shutdown_flush_moves_hot_objects_and_releases_resident_bytes() {
let dir = tempfile::tempdir().unwrap();
let mut settings = cold_settings();
settings.max_entries = 8;
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/one", "first-payload").await;
fill(&cache, &settings, "/two", "second-payload").await;
assert_eq!(cache.view().entries, 2);
assert!(cache.view().resident_bytes > 0);
assert_eq!(cache.view().cold_entries, 0);
assert_eq!(cache.flush_hot_to_cold().await, 2);
let view = cache.view();
assert_eq!(view.entries, 0);
assert_eq!(view.resident_bytes, 0);
assert_eq!(view.hot_logical_bytes, 0);
assert_eq!(view.transient_bytes, 0, "persisted ColdEntry must not retain the body allocation");
assert_eq!(view.cold_entries, 2);
shutdown.cancel();
assert_eq!(cache.flush_hot_to_cold().await, 2);
let view = cache.view();
assert_eq!(view.entries, 0);
assert_eq!(view.resident_bytes, 0);
assert_eq!(view.hot_logical_bytes, 0);
assert_eq!(
view.transient_bytes, 0,
"persisted ColdEntry must not retain the body allocation"
);
assert_eq!(view.cold_entries, 2);
shutdown.cancel();
let restart_shutdown = CancellationToken::new();
let restarted = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
restart_shutdown.clone(),
let restart_shutdown = CancellationToken::new();
let restarted = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
restart_shutdown.clone(),
);
assert_eq!(restarted.view().cold_entries, 2);
restart_shutdown.cancel();
}
#[tokio::test]
async fn managed_update_flush_ignores_shutdown_toggle_when_cold_tier_is_enabled() {
let dir = tempfile::tempdir().unwrap();
let mut settings = cold_settings();
settings.max_entries = 8;
settings.cold_on_shutdown = false;
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/update", "update-payload").await;
assert_eq!(cache.flush_hot_to_cold().await, 0);
assert_eq!(cache.view().entries, 1);
assert_eq!(cache.flush_hot_to_cold_for_update().await, 1);
let view = cache.view();
assert_eq!(view.entries, 0);
assert_eq!(view.resident_bytes, 0);
assert_eq!(view.cold_entries, 1);
shutdown.cancel();
}
#[tokio::test]
async fn graceful_shutdown_flush_can_be_disabled() {
let dir = tempfile::tempdir().unwrap();
let mut settings = cold_settings();
settings.max_entries = 8;
settings.cold_on_shutdown = false;
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/one", "first-payload").await;
assert_eq!(cache.flush_hot_to_cold().await, 0);
assert_eq!(cache.view().entries, 1);
assert_eq!(cache.view().cold_entries, 0);
shutdown.cancel();
}
#[tokio::test]
async fn cold_cache_uses_two_hex_character_shards() {
let dir = tempfile::tempdir().unwrap();
let settings = cold_settings();
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/one", "first-payload").await;
fill(&cache, &settings, "/two", "second-payload").await;
wait_for_cold(&cache, 1).await;
let root = dir.path().join(&settings.cold_path);
let mut shards = Vec::new();
for item in std::fs::read_dir(&root).unwrap() {
let item = item.unwrap();
let name = item.file_name().to_string_lossy().to_string();
assert!(
item.path().is_dir(),
"cold root must contain shard directories only"
);
assert_eq!(restarted.view().cold_entries, 2);
restart_shutdown.cancel();
assert_eq!(name.len(), 2);
assert!(name.bytes().all(|b| b.is_ascii_hexdigit()));
shards.push(item.path());
}
#[tokio::test]
async fn managed_update_flush_ignores_shutdown_toggle_when_cold_tier_is_enabled() {
let dir = tempfile::tempdir().unwrap();
let mut settings = cold_settings();
settings.max_entries = 8;
settings.cold_on_shutdown = false;
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/update", "update-payload").await;
assert_eq!(cache.flush_hot_to_cold().await, 0);
assert_eq!(cache.view().entries, 1);
assert_eq!(cache.flush_hot_to_cold_for_update().await, 1);
let view = cache.view();
assert_eq!(view.entries, 0);
assert_eq!(view.resident_bytes, 0);
assert_eq!(view.cold_entries, 1);
shutdown.cancel();
}
#[tokio::test]
async fn graceful_shutdown_flush_can_be_disabled() {
let dir = tempfile::tempdir().unwrap();
let mut settings = cold_settings();
settings.max_entries = 8;
settings.cold_on_shutdown = false;
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/one", "first-payload").await;
assert_eq!(cache.flush_hot_to_cold().await, 0);
assert_eq!(cache.view().entries, 1);
assert_eq!(cache.view().cold_entries, 0);
shutdown.cancel();
}
#[tokio::test]
async fn cold_cache_uses_two_hex_character_shards() {
let dir = tempfile::tempdir().unwrap();
let settings = cold_settings();
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/one", "first-payload").await;
fill(&cache, &settings, "/two", "second-payload").await;
wait_for_cold(&cache, 1).await;
let root = dir.path().join(&settings.cold_path);
let mut shards = Vec::new();
for item in std::fs::read_dir(&root).unwrap() {
let item = item.unwrap();
let name = item.file_name().to_string_lossy().to_string();
assert!(item.path().is_dir(), "cold root must contain shard directories only");
assert_eq!(name.len(), 2);
assert!(name.bytes().all(|b| b.is_ascii_hexdigit()));
shards.push(item.path());
}
assert!(!shards.is_empty());
assert!(shards.iter().any(|shard| {
std::fs::read_dir(shard).unwrap().filter_map(|entry| entry.ok()).any(|entry| {
assert!(!shards.is_empty());
assert!(shards.iter().any(|shard| {
std::fs::read_dir(shard)
.unwrap()
.filter_map(|entry| entry.ok())
.any(|entry| {
let name = entry.file_name().to_string_lossy().to_string();
name.ends_with(".json") || name.ends_with(".body")
})
}));
shutdown.cancel();
}
}));
shutdown.cancel();
}
#[tokio::test]
async fn legacy_flat_cold_cache_is_not_migrated() {
let dir = tempfile::tempdir().unwrap();
let settings = cold_settings();
let root = dir.path().join(&settings.cold_path);
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("legacy.body"), b"old").unwrap();
std::fs::write(root.join("legacy.json"), b"{}").unwrap();
let shutdown = CancellationToken::new();
let cache = Cache::start(11, Some(dir.path().to_path_buf()), settings, shutdown.clone());
assert_eq!(cache.view().cold_entries, 0);
assert!(!root.join("legacy.body").exists());
assert!(!root.join("legacy.json").exists());
shutdown.cancel();
}
#[tokio::test]
async fn legacy_flat_cold_cache_is_not_migrated() {
let dir = tempfile::tempdir().unwrap();
let settings = cold_settings();
let root = dir.path().join(&settings.cold_path);
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("legacy.body"), b"old").unwrap();
std::fs::write(root.join("legacy.json"), b"{}").unwrap();
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings,
shutdown.clone(),
);
assert_eq!(cache.view().cold_entries, 0);
assert!(!root.join("legacy.body").exists());
assert!(!root.join("legacy.json").exists());
shutdown.cancel();
}
#[tokio::test]
async fn manual_cold_cleanup_reports_removed_objects_and_freed_bytes() {
let dir = tempfile::tempdir().unwrap();
let mut settings = cold_settings();
settings.max_entries = 1;
let shutdown = CancellationToken::new();
let cache = Cache::start(11, Some(dir.path().to_path_buf()), settings.clone(), shutdown.clone());
fill(&cache, &settings, "/one", "first-payload").await;
fill(&cache, &settings, "/two", "second-payload").await;
fill(&cache, &settings, "/three", "third-payload").await;
wait_for_cold(&cache, 2).await;
let job = cache.start_cold_cleanup(ColdCleanupRequest { unused_for_seconds: None, oldest: Some(1), hits_zero: false }).unwrap();
for _ in 0..100 {
let current = cache.cold_cleanup_job(job.id).unwrap();
if current.status == "done" {
assert_eq!(current.removed_entries, 1);
assert!(current.freed_bytes > 0);
assert_eq!(cache.view().cold_entries, 1);
shutdown.cancel();
return;
}
tokio::time::sleep(Duration::from_millis(10)).await;
#[tokio::test]
async fn manual_cold_cleanup_reports_removed_objects_and_freed_bytes() {
let dir = tempfile::tempdir().unwrap();
let mut settings = cold_settings();
settings.max_entries = 1;
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/one", "first-payload").await;
fill(&cache, &settings, "/two", "second-payload").await;
fill(&cache, &settings, "/three", "third-payload").await;
wait_for_cold(&cache, 2).await;
let job = cache
.start_cold_cleanup(ColdCleanupRequest {
unused_for_seconds: None,
oldest: Some(1),
hits_zero: false,
})
.unwrap();
for _ in 0..100 {
let current = cache.cold_cleanup_job(job.id).unwrap();
if current.status == "done" {
assert_eq!(current.removed_entries, 1);
assert!(current.freed_bytes > 0);
assert_eq!(cache.view().cold_entries, 1);
shutdown.cancel();
return;
}
panic!("cold cleanup did not finish");
tokio::time::sleep(Duration::from_millis(10)).await;
}
panic!("cold cleanup did not finish");
}
#[tokio::test]
async fn manual_cold_cleanup_is_single_flight() {
let dir = tempfile::tempdir().unwrap();
let settings = cold_settings();
let shutdown = CancellationToken::new();
let cache = Cache::start(11, Some(dir.path().to_path_buf()), settings, shutdown.clone());
let _held = cache.cleanup_slots.clone().try_acquire_owned().unwrap();
let result = cache.start_cold_cleanup(ColdCleanupRequest {
unused_for_seconds: None,
oldest: Some(1),
hits_zero: false,
});
assert!(result.is_err());
shutdown.cancel();
}
#[tokio::test]
async fn manual_cold_cleanup_is_single_flight() {
let dir = tempfile::tempdir().unwrap();
let settings = cold_settings();
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings,
shutdown.clone(),
);
let _held = cache.cleanup_slots.clone().try_acquire_owned().unwrap();
let result = cache.start_cold_cleanup(ColdCleanupRequest {
unused_for_seconds: None,
oldest: Some(1),
hits_zero: false,
});
assert!(result.is_err());
shutdown.cancel();
}
#[tokio::test]
async fn cold_cleanup_skips_an_entry_while_a_read_is_pinned() {
let dir = tempfile::tempdir().unwrap();
let mut settings = cold_settings();
settings.max_entries = 1;
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/one", "first-payload").await;
fill(&cache, &settings, "/two", "second-payload").await;
wait_for_cold(&cache, 1).await;
#[tokio::test]
async fn cold_cleanup_skips_an_entry_while_a_read_is_pinned() {
let dir = tempfile::tempdir().unwrap();
let mut settings = cold_settings();
settings.max_entries = 1;
let shutdown = CancellationToken::new();
let cache = Cache::start(
11,
Some(dir.path().to_path_buf()),
settings.clone(),
shutdown.clone(),
);
fill(&cache, &settings, "/one", "first-payload").await;
fill(&cache, &settings, "/two", "second-payload").await;
wait_for_cold(&cache, 1).await;
let key = {
let mut cold = cache.cold.lock().unwrap();
let key = *cold.entries.keys().next().unwrap();
cold.entries.get_mut(&key).unwrap().pinned_reads = 1;
key
};
let request = ColdCleanupRequest {
unused_for_seconds: None,
oldest: Some(1),
hits_zero: false,
};
let result = cache
.remove_cold_candidates(Some(&request), None, None, 1)
.await;
assert_eq!(result.removed_entries, 0);
assert!(cache.cold.lock().unwrap().entries.contains_key(&key));
let key = {
let mut cold = cache.cold.lock().unwrap();
let key = *cold.entries.keys().next().unwrap();
cold.entries.get_mut(&key).unwrap().pinned_reads = 1;
key
};
let request = ColdCleanupRequest {
unused_for_seconds: None,
oldest: Some(1),
hits_zero: false,
};
let result = cache
.remove_cold_candidates(Some(&request), None, None, 1)
.await;
assert_eq!(result.removed_entries, 0);
assert!(cache.cold.lock().unwrap().entries.contains_key(&key));
cache.cold.lock().unwrap().entries.get_mut(&key).unwrap().pinned_reads = 0;
let result = cache
.remove_cold_candidates(Some(&request), None, None, 1)
.await;
assert_eq!(result.removed_entries, 1);
assert!(!cache.cold.lock().unwrap().entries.contains_key(&key));
shutdown.cancel();
}
cache
.cold
.lock()
.unwrap()
.entries
.get_mut(&key)
.unwrap()
.pinned_reads = 0;
let result = cache
.remove_cold_candidates(Some(&request), None, None, 1)
.await;
assert_eq!(result.removed_entries, 1);
assert!(!cache.cold.lock().unwrap().entries.contains_key(&key));
shutdown.cancel();
}
#[test]
fn old_cold_read_guard_does_not_unpin_a_replacement_with_the_same_key() {
let cache = Cache::new(1);
let key = [7_u8; 32];
let template = ColdEntry {
key,
base_key: key,
vary: Vec::new(),
revision: 1,
scope: "app".to_owned(),
scheme: "https".to_owned(),
authority: "example.test".to_owned(),
domain: "example.test".to_owned(),
path: "/asset".to_owned(),
target: "https://example.test/asset".to_owned(),
tags: Vec::new(),
created_at: SystemTime::now(),
headers: HeaderMap::new(),
body_path: PathBuf::from("old.body"),
meta_path: PathBuf::from("old.json"),
body_bytes: 1,
body_sha256: "00".repeat(32),
disk_bytes: 2,
stored_at: SystemTime::now(),
last_access: SystemTime::now(),
age_at_store_seconds: 0,
resident_age_at_store_seconds: 0,
ttl: 60,
stale_while_revalidate: 0,
stale_if_error: 0,
must_revalidate: false,
proxy_revalidate: false,
origin_etag: None,
origin_last_modified: None,
hits: 0,
transformed: false,
sequence: 1,
generation: 1,
skip_compression: false,
requires_revalidation: false,
pinned_reads: 1,
};
cache
.cold
.lock()
.unwrap()
.entries
.insert(key, template.clone());
let old_guard = ColdReadGuard {
cache: &cache,
key,
generation: 1,
};
#[test]
fn old_cold_read_guard_does_not_unpin_a_replacement_with_the_same_key() {
let cache = Cache::new(1);
let key = [7_u8; 32];
let template = ColdEntry {
key,
base_key: key,
vary: Vec::new(),
revision: 1,
scope: "app".to_owned(),
scheme: "https".to_owned(),
authority: "example.test".to_owned(),
domain: "example.test".to_owned(),
path: "/asset".to_owned(),
target: "https://example.test/asset".to_owned(),
tags: Vec::new(),
created_at: SystemTime::now(),
headers: HeaderMap::new(),
body_path: PathBuf::from("old.body"),
meta_path: PathBuf::from("old.json"),
body_bytes: 1,
body_sha256: "00".repeat(32),
disk_bytes: 2,
stored_at: SystemTime::now(),
last_access: SystemTime::now(),
age_at_store_seconds: 0,
resident_age_at_store_seconds: 0,
ttl: 60,
stale_while_revalidate: 0,
stale_if_error: 0,
must_revalidate: false,
proxy_revalidate: false,
origin_etag: None,
origin_last_modified: None,
hits: 0,
transformed: false,
sequence: 1,
generation: 1,
skip_compression: false,
requires_revalidation: false,
pinned_reads: 1,
};
cache
.cold
.lock()
.unwrap()
.entries
.insert(key, template.clone());
let old_guard = ColdReadGuard {
cache: &cache,
key,
generation: 1,
};
let mut replacement = template;
replacement.sequence = 2;
replacement.generation = 2;
replacement.pinned_reads = 1;
cache.cold.lock().unwrap().entries.insert(key, replacement);
drop(old_guard);
let mut replacement = template;
replacement.sequence = 2;
replacement.generation = 2;
replacement.pinned_reads = 1;
cache.cold.lock().unwrap().entries.insert(key, replacement);
drop(old_guard);
let cold = cache.cold.lock().unwrap();
let replacement = cold.entries.get(&key).unwrap();
assert_eq!(replacement.generation, 2);
assert_eq!(replacement.pinned_reads, 1);
}
let cold = cache.cold.lock().unwrap();
let replacement = cold.entries.get(&key).unwrap();
assert_eq!(replacement.generation, 2);
assert_eq!(replacement.pinned_reads, 1);
}
#[test]
fn cold_path_cannot_escape_data_directory() {
let mut settings = Settings::default();
settings.cold_path = "../outside".into();
assert!(settings.validate().is_err());
settings.cold_path = "/tmp/outside".into();
assert!(settings.validate().is_err());
settings.cold_path = "cache/cold".into();
assert!(settings.validate().is_ok());
}
#[test]
fn cold_path_cannot_escape_data_directory() {
let mut settings = Settings::default();
settings.cold_path = "../outside".into();
assert!(settings.validate().is_err());
settings.cold_path = "/tmp/outside".into();
assert!(settings.validate().is_err());
settings.cold_path = "cache/cold".into();
assert!(settings.validate().is_ok());
}