Implements MVP for a swactor distributed datastore, addressing large files via content addressing.
191 lines
5.3 KiB
Rust
191 lines
5.3 KiB
Rust
//! Garbage collection scenario tests.
|
|
//!
|
|
//! Verifies the full GC flow: MetadataActor builds a referenced chunk set
|
|
//! from its manifests and sends GcUnreferenced to BlobStoreActor, which
|
|
//! deletes orphaned chunks.
|
|
|
|
mod common;
|
|
|
|
use common::GcHarness;
|
|
|
|
use swactor_datastore::chunking::chunk_blob;
|
|
|
|
#[test]
|
|
fn gc_cleans_up_chunks_after_object_deleted() {
|
|
let h = GcHarness::new();
|
|
|
|
// Store a 200-byte blob (produces 4 chunks at chunk_size=64).
|
|
let data = vec![0xAB; 200];
|
|
let hash = h.put_blob(&data, Some("doomed.bin"));
|
|
|
|
let chunks_before = h.list_chunks();
|
|
assert!(!chunks_before.is_empty(), "chunks should exist after put");
|
|
|
|
// Delete the object from the metadata index.
|
|
h.delete_blob(&hash);
|
|
|
|
// Run enough ticks for GC to fire (gc_interval=3).
|
|
h.gc_ticks(3);
|
|
|
|
// All chunks should be gone — nothing references them anymore.
|
|
let chunks_after = h.list_chunks();
|
|
assert!(
|
|
chunks_after.is_empty(),
|
|
"expected all chunks removed after GC, found {}",
|
|
chunks_after.len()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn gc_preserves_chunks_still_referenced() {
|
|
let h = GcHarness::new();
|
|
|
|
// Store two distinct blobs.
|
|
let data_a = vec![0x11; 200];
|
|
let data_b = vec![0x22; 150];
|
|
let hash_a = h.put_blob(&data_a, Some("keep.bin"));
|
|
let _hash_b = h.put_blob(&data_b, Some("also-keep.bin"));
|
|
|
|
let chunks_before = h.list_chunks();
|
|
|
|
// Delete only blob A.
|
|
h.delete_blob(&hash_a);
|
|
|
|
// GC should clean up A's orphaned chunks but preserve B's.
|
|
h.gc_ticks(3);
|
|
|
|
// Blob B's chunks should all survive.
|
|
let (_, manifest_b, _) = chunk_blob(&data_b, h.chunk_size);
|
|
for chunk_ref in &manifest_b.chunks {
|
|
assert!(
|
|
h.has_chunk(&chunk_ref.hash),
|
|
"blob B chunk {:?} should survive GC",
|
|
chunk_ref.hash
|
|
);
|
|
}
|
|
|
|
// Blob A's chunks should be gone (they don't overlap with B since data differs).
|
|
let (_, manifest_a, _) = chunk_blob(&data_a, h.chunk_size);
|
|
for chunk_ref in &manifest_a.chunks {
|
|
assert!(
|
|
!h.has_chunk(&chunk_ref.hash),
|
|
"blob A chunk {:?} should be removed by GC",
|
|
chunk_ref.hash
|
|
);
|
|
}
|
|
|
|
// Total chunk count should have decreased.
|
|
let chunks_after = h.list_chunks();
|
|
assert!(
|
|
chunks_after.len() < chunks_before.len(),
|
|
"chunk count should decrease after GC removes orphans"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn gc_handles_deduplication_correctly() {
|
|
let h = GcHarness::new();
|
|
|
|
// Two 128-byte blobs sharing the same 64-byte prefix (first chunk is identical).
|
|
let mut data_x = vec![0xCC; 128];
|
|
let mut data_y = vec![0xCC; 128];
|
|
// The first 64 bytes are identical → same first chunk hash.
|
|
// Differ in the second 64 bytes → different second chunk + different content hash.
|
|
data_x[64..].fill(0xAA);
|
|
data_y[64..].fill(0xBB);
|
|
|
|
let hash_x = h.put_blob(&data_x, Some("x.bin"));
|
|
let _hash_y = h.put_blob(&data_y, Some("y.bin"));
|
|
|
|
// Verify the shared chunk exists.
|
|
let (_, manifest_x, _) = chunk_blob(&data_x, h.chunk_size);
|
|
let (_, manifest_y, _) = chunk_blob(&data_y, h.chunk_size);
|
|
let shared_chunk = manifest_x.chunks[0].hash;
|
|
assert_eq!(
|
|
shared_chunk, manifest_y.chunks[0].hash,
|
|
"first chunk should be identical (shared prefix)"
|
|
);
|
|
|
|
// Delete only X.
|
|
h.delete_blob(&hash_x);
|
|
h.gc_ticks(3);
|
|
|
|
// Shared chunk should survive (Y still references it).
|
|
assert!(
|
|
h.has_chunk(&shared_chunk),
|
|
"shared chunk should survive — still referenced by Y"
|
|
);
|
|
|
|
// X's unique second chunk should be gone.
|
|
let x_unique = manifest_x.chunks[1].hash;
|
|
assert!(
|
|
!h.has_chunk(&x_unique),
|
|
"X's unique chunk should be removed by GC"
|
|
);
|
|
|
|
// Y's unique second chunk should survive.
|
|
let y_unique = manifest_y.chunks[1].hash;
|
|
assert!(
|
|
h.has_chunk(&y_unique),
|
|
"Y's unique chunk should survive GC"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn gc_is_no_op_when_nothing_deleted() {
|
|
let h = GcHarness::new();
|
|
|
|
let data = vec![0xFF; 200];
|
|
h.put_blob(&data, Some("survivor.bin"));
|
|
|
|
let chunks_before = h.list_chunks();
|
|
|
|
// GC fires but nothing was deleted — all chunks should survive.
|
|
h.gc_ticks(3);
|
|
|
|
let chunks_after = h.list_chunks();
|
|
assert_eq!(
|
|
chunks_before.len(),
|
|
chunks_after.len(),
|
|
"GC without any deletes should preserve all chunks"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn gc_runs_on_interval_not_every_tick() {
|
|
let h = GcHarness::new();
|
|
|
|
let data = vec![0xDD; 200];
|
|
let hash = h.put_blob(&data, Some("interval-test.bin"));
|
|
|
|
h.delete_blob(&hash);
|
|
|
|
// Send gc_interval - 1 = 2 ticks. GC should NOT have fired yet.
|
|
h.gc_ticks(2);
|
|
|
|
let chunks_mid = h.list_chunks();
|
|
assert!(
|
|
!chunks_mid.is_empty(),
|
|
"chunks should still exist before gc_interval is reached"
|
|
);
|
|
|
|
// One more tick reaches gc_interval=3. GC fires and cleans up.
|
|
h.gc_ticks(1);
|
|
|
|
let chunks_after = h.list_chunks();
|
|
assert!(
|
|
chunks_after.is_empty(),
|
|
"chunks should be cleaned after gc_interval reached"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn gc_with_empty_datastore_is_harmless() {
|
|
let h = GcHarness::new();
|
|
|
|
// GC on an empty store — should not panic.
|
|
h.gc_ticks(3);
|
|
|
|
let chunks = h.list_chunks();
|
|
assert!(chunks.is_empty(), "empty store should remain empty after GC");
|
|
}
|