use std::path::PathBuf;
use crate::blob_store::recompute_file_digest;
use crate::metadata_store::{ProvisionalInstallInsert, RabsMetadataStore, StoreError};
use rabs_protocol::generation::AttemptId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RecoverySummary {
pub removed: usize,
pub marked_dirty: usize,
}
impl RecoverySummary {
#[must_use]
pub fn has_dirty(&self) -> bool {
self.marked_dirty > 0
}
}
pub fn record_installed_output(
store: &mut dyn RabsMetadataStore,
pin_key: &str,
consumer_worker: &str,
consumer_attempt: AttemptId,
path: &std::path::Path,
installed_seq: u64,
) -> Result<(), ProvisionalInstallError> {
let object =
recompute_file_digest(path).map_err(|_| ProvisionalInstallError::UnreadablePath {
path: path.as_os_str().as_encoded_bytes().to_vec(),
})?;
store.insert_provisional_install(&ProvisionalInstallInsert {
pin_key: pin_key.to_owned(),
consumer_worker: consumer_worker.to_owned(),
consumer_attempt: consumer_attempt.0,
installed_path: path.as_os_str().as_encoded_bytes().to_vec(),
object,
installed_seq,
})?;
Ok(())
}
pub fn recover_after_lineage_failure(
store: &mut dyn RabsMetadataStore,
root_pin_keys: &[String],
) -> Result<RecoverySummary, ProvisionalInstallError> {
let mut visited = std::collections::BTreeSet::new();
let mut frontier: Vec<String> = root_pin_keys.to_vec();
while let Some(key) = frontier.pop() {
if visited.insert(key.clone()) {
frontier.extend(store.list_provisional_pin_descendants(&key)?);
}
}
let pin_keys: Vec<String> = visited.into_iter().collect();
let rows = store.list_provisional_installs_for_pins(&pin_keys)?;
let mut summary = RecoverySummary::default();
for row in rows {
if row.state != "installed" {
continue; }
let os_string: std::ffi::OsString =
std::os::unix::ffi::OsStringExt::from_vec(row.installed_path.clone());
let path: PathBuf = PathBuf::from(os_string);
let outcome = match std::fs::metadata(&path) {
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
Ok(true)
}
Err(_) => Err(()),
Ok(_) => match recompute_file_digest(&path) {
Ok(current) if current == row.object => fs_remove(&path),
_ => Err(()),
},
};
match outcome {
Ok(removed_now) => {
store.set_provisional_install_state(
&row.pin_key,
&row.consumer_attempt_hex,
&row.installed_path,
"removed",
)?;
summary.removed += 1;
let _ = removed_now;
}
Err(()) => {
store.set_provisional_install_state(
&row.pin_key,
&row.consumer_attempt_hex,
&row.installed_path,
"dirty",
)?;
summary.marked_dirty += 1;
}
}
}
Ok(summary)
}
fn fs_remove(path: &std::path::Path) -> Result<bool, ()> {
match std::fs::remove_file(path) {
Ok(()) => Ok(true),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(_) => Err(()),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProvisionalInstallError {
Store(StoreError),
UnreadablePath {
path: Vec<u8>,
},
}
impl From<StoreError> for ProvisionalInstallError {
fn from(value: StoreError) -> Self {
Self::Store(value)
}
}
impl std::fmt::Display for ProvisionalInstallError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Store(e) => write!(f, "store error: {e:?}"),
Self::UnreadablePath { path } => {
write!(
f,
"cannot hash installed path {:?}",
String::from_utf8_lossy(path)
)
}
}
}
}
impl std::error::Error for ProvisionalInstallError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata_store::{RusqliteEngine, SqlMetadataStore};
use crate::provisional_pins::{
ProducerContracts, ProvisionalIdentity, ProvisionalReader, TerminalGate, authorize_reader,
descendant_terminal_gate, invalidate_lineage, open_provisional_pin, resolve_for_reader,
};
use rabs_protocol::authority::{ClusterId, CoordinatorAuthority};
use rabs_protocol::generation::{ActionGenerationId, ExecutionLeaseId};
use rabs_protocol::raw_bytes::RawBytes;
use rabs_protocol::result_identity::{DigestAlgorithm, ObjectId, OutputRole, TypedDigest};
fn unique_tmp(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"m019-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn fixture() -> SqlMetadataStore<RusqliteEngine> {
let engine = RusqliteEngine::open_in_memory().unwrap();
SqlMetadataStore::open(engine).unwrap()
}
fn identity(attempt_tag: u128) -> ProvisionalIdentity {
let mut bytes = [0u8; 32];
bytes[0] = 10;
bytes[31] = 10;
ProvisionalIdentity {
authority: CoordinatorAuthority {
cluster_id: ClusterId("cluster-1".to_owned()),
credential_generation: 1,
term: 101,
incarnation_id: rabs_protocol::authority::CoordinatorIncarnationId(0xAA01),
},
action_key: TypedDigest {
algorithm: DigestAlgorithm::Sha256V1,
domain: "rabs.action-key.sha256.v1",
bytes,
},
generation: ActionGenerationId(0x50),
attempt: AttemptId(attempt_tag),
lease: ExecutionLeaseId(attempt_tag + 1),
role: OutputRole::ProvisionalMetadata,
virtual_path: RawBytes::new(b"target/debug/deps/libfeat.rmeta".to_vec()),
}
}
fn contracts() -> ProducerContracts {
let mut bytes = [0u8; 32];
bytes[0] = 200;
ProducerContracts {
toolchain: TypedDigest {
algorithm: DigestAlgorithm::Sha256V1,
domain: "rabs.action-key.sha256.v1",
bytes,
},
events: TypedDigest {
algorithm: DigestAlgorithm::Sha256V1,
domain: "rabs.action-key.sha256.v1",
bytes: [1u8; 32],
},
}
}
fn pin_with_installing_dependent(
store: &mut SqlMetadataStore<RusqliteEngine>,
dir: &std::path::Path,
file_name: &str,
contents: &[u8],
) -> (ProvisionalIdentity, String, PathBuf) {
let producer = identity(30);
open_provisional_pin(
store,
&producer,
&ObjectId(contracts().toolchain),
&contracts(),
)
.unwrap();
authorize_reader(
store,
&producer,
&ProvisionalReader::DependentAttempt {
worker: "worker-b".to_owned(),
attempt: AttemptId(31),
},
)
.unwrap();
resolve_for_reader(
store,
&producer,
&ProvisionalReader::DependentAttempt {
worker: "worker-b".to_owned(),
attempt: AttemptId(31),
},
)
.unwrap();
let path = dir.join(file_name);
std::fs::write(&path, contents).unwrap();
record_installed_output(
store,
&producer.pin_key(),
"worker-b",
AttemptId(31),
&path,
7,
)
.unwrap();
(producer.clone(), producer.pin_key(), path)
}
#[test]
fn m019_exact_match_is_removed_and_journal_closed() {
let mut store = fixture();
let dir = unique_tmp("exact");
let (producer, pin_key, path) =
pin_with_installing_dependent(&mut store, &dir, "out.rmeta", b"installed-bytes");
assert_eq!(
recover_after_lineage_failure(&mut store, std::slice::from_ref(&pin_key)).unwrap(),
RecoverySummary {
removed: 1,
marked_dirty: 0
}
);
assert!(!path.exists());
let rows = store
.list_provisional_installs_for_pins(&[pin_key])
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].state, "removed");
assert_eq!(
recover_after_lineage_failure(&mut store, &[producer.pin_key()]).unwrap(),
RecoverySummary::default()
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn m019_user_overwrite_is_marked_dirty_and_preserved() {
let mut store = fixture();
let dir = unique_tmp("dirty");
let (_producer, pin_key, path) =
pin_with_installing_dependent(&mut store, &dir, "out.rmeta", b"installed-bytes");
std::fs::write(&path, b"user-edited-content").unwrap();
let summary =
recover_after_lineage_failure(&mut store, std::slice::from_ref(&pin_key)).unwrap();
assert_eq!(
summary,
RecoverySummary {
removed: 0,
marked_dirty: 1
}
);
assert!(summary.has_dirty());
assert!(path.exists());
assert_eq!(std::fs::read(&path).unwrap(), b"user-edited-content");
let dirty = store.list_provisional_installs_by_state("dirty").unwrap();
assert_eq!(dirty.len(), 1);
assert_eq!(dirty[0].installed_path, path.as_os_str().as_encoded_bytes());
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn m019_already_gone_paths_bookkeep_without_error() {
let mut store = fixture();
let dir = unique_tmp("gone");
let (_producer, pin_key, path) =
pin_with_installing_dependent(&mut store, &dir, "out.rmeta", b"installed-bytes");
std::fs::remove_file(&path).unwrap();
assert_eq!(
recover_after_lineage_failure(&mut store, &[pin_key]).unwrap(),
RecoverySummary {
removed: 1,
marked_dirty: 0
}
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn m019_cascade_reaches_descendant_pins_installs() {
let engine = RusqliteEngine::open_in_memory().unwrap();
let mut store = SqlMetadataStore::open(engine).unwrap();
let dir = unique_tmp("cascade");
let a = identity(30);
let b = identity(31);
open_provisional_pin(
&mut store,
&a,
&ObjectId(contracts().toolchain),
&contracts(),
)
.unwrap();
authorize_reader(
&mut store,
&a,
&ProvisionalReader::DependentAttempt {
worker: "worker-b".to_owned(),
attempt: AttemptId(31),
},
)
.unwrap();
resolve_for_reader(
&mut store,
&a,
&ProvisionalReader::DependentAttempt {
worker: "worker-b".to_owned(),
attempt: AttemptId(31),
},
)
.unwrap();
open_provisional_pin(&mut store, &b, &ObjectId(contracts().events), &contracts()).unwrap();
let b_path = dir.join("b-output.rmeta");
std::fs::write(&b_path, b"b-early-bytes").unwrap();
record_installed_output(
&mut store,
&b.pin_key(), "worker-b",
AttemptId(31),
&b_path,
9,
)
.unwrap();
invalidate_lineage(&mut store, &a, "producer generation failed").unwrap();
let summary = recover_after_lineage_failure(&mut store, &[a.pin_key()]).unwrap();
assert_eq!(
summary,
RecoverySummary {
removed: 1,
marked_dirty: 0
}
);
assert!(!b_path.exists());
let gate = descendant_terminal_gate(&mut store, "worker-b", AttemptId(31)).unwrap();
assert!(matches!(gate, TerminalGate::Refused { .. }));
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn m019_recording_requires_readable_path() {
let mut store = fixture();
let missing = unique_tmp("missing").join("does-not-exist.rmeta");
let err = record_installed_output(
&mut store,
&identity(30).pin_key(),
"worker-x",
AttemptId(40),
&missing,
1,
)
.unwrap_err();
assert_eq!(
err,
ProvisionalInstallError::UnreadablePath {
path: missing.as_os_str().as_encoded_bytes().to_vec()
}
);
std::fs::remove_dir_all(missing.parent().unwrap()).unwrap();
}
}