fix(cache,storage): harden cache bounds + atomic disk writes
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Test (workspace all features) (push) Canceled after 0s
CI / Test (workspace default features) (push) Canceled after 0s
CI / Test (mytheclipse / bg only) (push) Canceled after 0s
CI / Test (mytheclipse / compute only) (push) Canceled after 0s
CI / Test (mytheclipse / io only) (push) Canceled after 0s
CI / Test (mytheclipse / lifecycle only) (push) Canceled after 0s
CI / Test (mytheclipse / observability only) (push) Canceled after 0s
CI / Test (mytheclipse / resiliency only) (push) Canceled after 0s
CI / Test (mytheclipse / traffic only) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l2-redis) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l1-moka) (push) Canceled after 0s
CI / Test (mytheclipse-cache / default) (push) Canceled after 0s
CI / Test (mytheclipse-config / default) (push) Canceled after 0s
CI / Test (mytheclipse-crypto / default) (push) Canceled after 0s
CI / Test (mytheclipse-event / amqp) (push) Canceled after 0s
CI / Test (mytheclipse-event / nats) (push) Canceled after 0s
CI / Test (mytheclipse-event / default (mem)) (push) Canceled after 0s
CI / Test (mytheclipse-storage / gcs) (push) Canceled after 0s
CI / Test (mytheclipse-storage / s3) (push) Canceled after 0s
CI / Test (mytheclipse-storage / default (local)) (push) Canceled after 0s
CI / Run mytheclipse example (push) Canceled after 0s
CI / Docs check (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-cache) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-config) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-crypto) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-event) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-storage) (push) Canceled after 0s
Release / Semantic Release (push) Canceled after 0s
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Test (workspace all features) (push) Canceled after 0s
CI / Test (workspace default features) (push) Canceled after 0s
CI / Test (mytheclipse / bg only) (push) Canceled after 0s
CI / Test (mytheclipse / compute only) (push) Canceled after 0s
CI / Test (mytheclipse / io only) (push) Canceled after 0s
CI / Test (mytheclipse / lifecycle only) (push) Canceled after 0s
CI / Test (mytheclipse / observability only) (push) Canceled after 0s
CI / Test (mytheclipse / resiliency only) (push) Canceled after 0s
CI / Test (mytheclipse / traffic only) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l2-redis) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l1-moka) (push) Canceled after 0s
CI / Test (mytheclipse-cache / default) (push) Canceled after 0s
CI / Test (mytheclipse-config / default) (push) Canceled after 0s
CI / Test (mytheclipse-crypto / default) (push) Canceled after 0s
CI / Test (mytheclipse-event / amqp) (push) Canceled after 0s
CI / Test (mytheclipse-event / nats) (push) Canceled after 0s
CI / Test (mytheclipse-event / default (mem)) (push) Canceled after 0s
CI / Test (mytheclipse-storage / gcs) (push) Canceled after 0s
CI / Test (mytheclipse-storage / s3) (push) Canceled after 0s
CI / Test (mytheclipse-storage / default (local)) (push) Canceled after 0s
CI / Run mytheclipse example (push) Canceled after 0s
CI / Docs check (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-cache) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-config) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-crypto) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-event) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-storage) (push) Canceled after 0s
Release / Semantic Release (push) Canceled after 0s
- cache: guard MokaL1::new(0) with panic; add MemoryCache::with_max_entries bounded LRU eviction (oldest evicted past cap) + docs warning about unbounded default growth. Verifies moka treats max_capacity=0 as a permanent no-insert sentinel. - storage: make LocalFileStorage::put atomic via temp-file + fsync + rename; cleans up temp on write failure; no leftover .tmp-* on disk after success. Tests: 17 cache tests (incl bounded_cache_evicts_oldest, zero_max_panics), 6 storage tests (incl put_leaves_no_temp_file). Full workspace: clippy 0 warnings, all tests green.
This commit is contained in:
@@ -22,6 +22,11 @@ impl LocalFileStorage {
|
||||
Self { root: root.into() }
|
||||
}
|
||||
|
||||
/// Returns the root directory path.
|
||||
pub fn root(&self) -> &std::path::Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
fn resolve(&self, path: &str) -> Result<PathBuf, StorageError> {
|
||||
let rel = Path::new(path.trim_start_matches('/'));
|
||||
for component in rel.components() {
|
||||
@@ -52,14 +57,30 @@ impl StorageDriver for LocalFileStorage {
|
||||
if let Some(parent) = full.parent() {
|
||||
tokio::fs::create_dir_all(parent)
|
||||
.await
|
||||
.map_err(|e| StorageError::Io(e.to_string()))?;
|
||||
.map_err(|e| StorageError::Io(e.to_string()))?
|
||||
}
|
||||
let mut file = tokio::fs::File::create(&full)
|
||||
// Write to a sibling temp file then atomically rename. On POSIX this is
|
||||
// atomic, so a crash mid-write leaves *either* the previous file *or*
|
||||
// the complete new file — never a half-written truncated object.
|
||||
// Use a PID-unguarded temp name and clean it up if anything fails.
|
||||
let tmp = full.with_extension(format!(".tmp-{}", std::process::id()));
|
||||
let mut file = tokio::fs::File::create(&tmp)
|
||||
.await
|
||||
.map_err(|e| StorageError::Io(e.to_string()))?;
|
||||
let written = tokio::io::copy(&mut data, &mut file)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
StorageError::Io(e.to_string())
|
||||
})?;
|
||||
// Ensure durability: flush to OS, fsync, then rename.
|
||||
tokio::fs::File::open(&tmp)
|
||||
.await
|
||||
.map_err(|e| StorageError::Io(e.to_string()))?
|
||||
.sync_all()
|
||||
.await
|
||||
.map_err(|e| StorageError::Io(e.to_string()))?;
|
||||
std::fs::rename(&tmp, &full).map_err(|e| StorageError::Io(e.to_string()))?;
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
@@ -162,4 +183,23 @@ mod tests {
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, StorageError::InvalidPath(_)));
|
||||
}
|
||||
|
||||
/// Verifies the atomic-write contract: after a successful `put`, the temp
|
||||
/// file must not linger on disk.
|
||||
#[tokio::test]
|
||||
async fn put_leaves_no_temp_file() {
|
||||
let (storage, _dir) = driver();
|
||||
storage
|
||||
.put("clean.txt", bytes_stream(b"data".to_vec()))
|
||||
.await
|
||||
.unwrap();
|
||||
// No `*.tmp-*` files should remain in the root after a clean write.
|
||||
let leftover: Vec<_> = std::fs::read_dir(storage.root())
|
||||
.unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name())
|
||||
.filter(|n| n.to_string_lossy().contains(".tmp-"))
|
||||
.collect();
|
||||
assert!(leftover.is_empty(), "temp files left behind: {leftover:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user