use std::path::Path;
use std::sync::{Arc, Mutex};
use crate::storage::cas::CasStore;
use super::manifest::{Manifest, ManifestError};
pub const CURRENT_FILE: &str = "CURRENT";
pub const GENERATIONS_DIR: &str = "generations";
pub const MANIFEST_FILE: &str = "manifest";
pub fn read_current_generation(storage_root: &Path) -> Option<u64> {
let raw = std::fs::read_to_string(storage_root.join(CURRENT_FILE)).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() || !trimmed.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
trimmed.parse::<u64>().ok()
}
pub fn read_generation_manifest(
storage_root: &Path,
generation: u64,
) -> Result<Manifest, ManifestError> {
let manifest_path = storage_root
.join(GENERATIONS_DIR)
.join(generation.to_string())
.join(MANIFEST_FILE);
let bytes = std::fs::read(&manifest_path).map_err(|_| ManifestError::MissingManifest {
generation,
path: manifest_path.to_string_lossy().into_owned(),
})?;
Manifest::from_bytes(&bytes)
}
pub struct GenerationLease {
store: Arc<Mutex<CasStore>>,
manifest: Manifest,
hashes: Vec<[u8; 32]>,
}
impl GenerationLease {
pub fn acquire(store: Arc<Mutex<CasStore>>, manifest: &Manifest) -> Result<Self, LeaseError> {
let hashes = manifest.layer_hashes();
{
let mut s = store.lock().expect("cas store mutex poisoned");
for hash in &hashes {
s.incr(hash);
}
s.record_generation_hold(manifest.generation);
if let Err(persist_error) = s.persist() {
s.release_generation_hold(manifest.generation);
for hash in &hashes {
if let Err(error) = s.decr(hash) {
tracing::warn!(
hash = %crate::storage::cas::blob::hash_to_hex(hash),
%error,
"generation lease rollback: decr underflowed; another handle released this blob concurrently"
);
}
}
return Err(LeaseError::Persist(persist_error));
}
let pruned = hashes.iter().filter(|hash| !s.exists(hash)).count();
if pruned > 0 {
s.release_generation_hold(manifest.generation);
for hash in &hashes {
if let Err(error) = s.decr(hash) {
tracing::warn!(
hash = %crate::storage::cas::blob::hash_to_hex(hash),
%error,
"generation lease rollback (pruned): decr underflowed"
);
}
}
if let Err(error) = s.persist() {
tracing::error!(
generation = manifest.generation,
%error,
"generation lease rollback (pruned): persist FAILED — \
phantom refcounts/hold remain durably pinned; they are \
repaired only by a later successful persist through THIS \
same handle or by process exit (a peer's dead-owner reclaim), \
not by persists through other handles"
);
}
return Err(LeaseError::GenerationPruned {
generation: manifest.generation,
});
}
}
Ok(GenerationLease {
store,
manifest: manifest.clone(),
hashes,
})
}
pub fn generation(&self) -> u64 {
self.manifest.generation
}
pub fn layer_hashes(&self) -> &[[u8; 32]] {
&self.hashes
}
pub fn manifest(&self) -> &Manifest {
&self.manifest
}
}
impl Drop for GenerationLease {
fn drop(&mut self) {
if let Ok(mut store) = self.store.lock() {
for hash in &self.hashes {
let _ = store.decr(hash);
}
store.release_generation_hold(self.manifest.generation);
let _ = store.persist();
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum LeaseError {
#[error("failed to persist lease refcounts: {0}")]
Persist(#[from] crate::storage::cas::CasError),
#[error("invalid manifest for lease: {0}")]
InvalidManifest(#[from] super::manifest::ManifestError),
#[error("no current generation for project: {0}")]
NoCurrentGeneration(String),
#[error(
"generation {generation} was pruned while its lease was being acquired; retry against CURRENT"
)]
GenerationPruned {
generation: u64,
},
#[error("io error: {0}")]
Io(#[from] std::io::Error),
}
#[cfg(test)]
#[path = "lease_test.rs"]
mod tests;