use std::collections::BTreeSet;
use std::io;
use crate::error::{Error, Result};
use crate::journal::{self, Journal};
use crate::path::RelPath;
use crate::plan::{Plan, Step};
use crate::progress::{self, Record};
use crate::state::{GC_PREFIX, Marker, PROBE_PREFIX, TX_PREFIX, TxDir, delete_tree, private_dir};
use crate::vfs::{FileId, Probe, Vfs};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Pos {
At(usize),
Both(usize),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum StepState {
Pre,
Post,
Inflight,
}
fn step_state(pos: Pos, s: Step) -> StepState {
match pos {
Pos::At(i) if i > s.from => StepState::Post,
Pos::At(_) => StepState::Pre,
Pos::Both(i) if i == s.from => StepState::Inflight,
Pos::Both(i) if i > s.from => StepState::Post,
Pos::Both(_) => StepState::Pre,
}
}
pub(crate) fn classify(vfs: &dyn Vfs, plan: &Plan) -> io::Result<Vec<Result<Pos, String>>> {
let known: BTreeSet<FileId> = plan.tokens.iter().map(|t| t.id).collect();
let mut out = Vec::with_capacity(plan.tokens.len());
for tok in &plan.tokens {
let mut hits = Vec::new();
let mut foreign = None;
for (i, loc) in tok.locs.iter().enumerate() {
match vfs.probe(&loc.path, loc.parent)? {
Probe::ParentMismatch | Probe::Missing => {}
Probe::Present(m) if m.id == tok.id && m.kind == tok.kind => hits.push(i),
Probe::Present(m) if known.contains(&m.id) => {}
Probe::Present(_) => foreign = Some(loc.path.clone()),
}
}
out.push(match (hits.as_slice(), foreign) {
(_, Some(p)) => Err(format!("unknown entry at {p:?}")),
([i], None) => Ok(Pos::At(*i)),
([i, j], None) if *j == i + 1 && tok.kind == crate::vfs::Kind::File => {
Ok(Pos::Both(*i))
}
([], None) => Err(format!(
"token {:?} not found at any of its locations",
tok.id
)),
(_, None) => Err(format!(
"token {:?} found at several locations {hits:?}",
tok.id
)),
});
}
Ok(out)
}
fn check_shape(plan: &Plan, pos: &[Pos]) -> Result<(), String> {
let mut first_not_post = None;
let mut last_not_pre = None;
for (w, wave) in plan.waves.iter().enumerate() {
for s in wave {
let st = step_state(pos[s.token], *s);
if st != StepState::Post && first_not_post.is_none() {
first_not_post = Some(w);
}
if st != StepState::Pre {
last_not_pre = Some(w);
}
}
}
match (first_not_post, last_not_pre) {
(Some(a), Some(b)) if b > a => Err(format!("waves {a} and {b} both partially applied")),
_ => Ok(()),
}
}
fn check_progress(plan: &Plan, recs: &[Record], pos: &[Pos]) -> Result<(), String> {
let n = plan.waves.len();
let apply_done = recs
.iter()
.filter_map(|r| match r {
Record::ApplyDone(w) => Some(*w as usize),
_ => None,
})
.max();
let undo_min = recs
.iter()
.filter_map(|r| match r {
Record::UndoDone(w) => Some(*w as usize),
_ => None,
})
.min();
if apply_done.is_some_and(|w| w >= n) || undo_min.is_some_and(|w| w >= n) {
return Err("progress names a wave that does not exist".into());
}
let undo_from = undo_min.unwrap_or(n);
for (w, wave) in plan.waves.iter().enumerate() {
for s in wave {
let st = step_state(pos[s.token], *s);
if w >= undo_from && st != StepState::Pre {
return Err(format!("wave {w} logged as undone but not observed undone"));
}
if apply_done.is_some_and(|a| w <= a) && w + 1 < undo_from && st != StepState::Post {
return Err(format!(
"wave {w} logged as applied but not observed applied"
));
}
}
}
Ok(())
}
fn sync_parent(
vfs: &dyn Vfs,
path: &RelPath,
expect: FileId,
done: &mut BTreeSet<RelPath>,
) -> io::Result<()> {
let parent = path.parent().unwrap_or_default();
if done.contains(&parent) {
return Ok(());
}
if let Some(m) = vfs.stat(&parent)?
&& m.id == expect
{
vfs.sync_dir(&parent)?;
done.insert(parent);
}
Ok(())
}
pub(crate) fn barrier(vfs: &dyn Vfs, plan: &Plan, wave: &[Step]) -> io::Result<()> {
let mut done = BTreeSet::new();
for s in wave {
for loc in [plan.src(*s), plan.dst(*s)] {
sync_parent(vfs, &loc.path, loc.parent, &mut done)?;
}
}
Ok(())
}
pub(crate) fn append_progress(vfs: &dyn Vfs, tx: &TxDir, r: Record) -> io::Result<()> {
vfs.append(&tx.progress(), &progress::encode(r))?;
vfs.sync_file(&tx.progress())
}
fn read_progress(vfs: &dyn Vfs, tx: &TxDir) -> io::Result<Vec<Record>> {
match vfs.stat(&tx.progress())? {
Some(_) => Ok(progress::parse(&vfs.read_file(&tx.progress())?)),
None => Ok(Vec::new()),
}
}
fn required(tx: &TxDir, reason: impl Into<String>) -> Error {
Error::RecoveryRequired {
tx: tx.name.clone(),
reason: reason.into(),
}
}
pub(crate) fn preflight(vfs: &dyn Vfs, tx: &TxDir, j: &Journal) -> Result<Vec<Pos>> {
let mut pos = Vec::new();
for r in classify(vfs, &j.plan)? {
pos.push(r.map_err(|e| required(tx, e))?);
}
check_shape(&j.plan, &pos).map_err(|e| required(tx, e))?;
check_progress(&j.plan, &read_progress(vfs, tx)?, &pos).map_err(|e| required(tx, e))?;
Ok(pos)
}
pub(crate) fn rollback(vfs: &dyn Vfs, tx: &TxDir, j: &Journal) -> Result<()> {
let mut pos = preflight(vfs, tx, j)?;
let plan = &j.plan;
let mut done = BTreeSet::new();
for tok in &plan.tokens {
for loc in &tok.locs {
sync_parent(vfs, &loc.path, loc.parent, &mut done)?;
}
}
for d in [tx.staged(), tx.backup(), tx.dir(), private_dir()] {
if !done.contains(&d) && vfs.stat(&d)?.is_some() {
vfs.sync_dir(&d)?;
}
}
vfs.event("stabilized");
tx.set_marker(vfs, Marker::RollingBack)?;
for (w, wave) in plan.waves.iter().enumerate().rev() {
for s in wave {
let (src, dst) = (plan.src(*s), plan.dst(*s));
match pos[s.token] {
Pos::At(i) if i == s.from + 1 => {
vfs.rename_noreplace(&dst.path, Some(dst.parent), &src.path, Some(src.parent))?;
pos[s.token] = Pos::At(s.from);
}
Pos::Both(i) if i == s.from => {
vfs.unlink(&dst.path, Some(dst.parent))?;
pos[s.token] = Pos::At(s.from);
}
Pos::At(i) if i <= s.from => {}
Pos::Both(i) if i < s.from => {}
other => {
return Err(required(
tx,
format!("token {} at {other:?} during undo of wave {w}", s.token),
));
}
}
}
barrier(vfs, plan, wave)?;
append_progress(vfs, tx, Record::UndoDone(w as u32))?;
}
vfs.event("undone");
tx.set_marker(vfs, Marker::RolledBack)?;
tx.gc(vfs)?;
Ok(())
}
#[derive(Debug)]
pub(crate) enum Decision {
Discard,
Committed,
RolledBack,
RollBack(Box<Journal>),
Required(String),
}
pub(crate) fn decide(vfs: &dyn Vfs, tx: &TxDir) -> io::Result<Decision> {
let committed = tx.has_marker(vfs, Marker::Committed)?;
let rolling = tx.has_marker(vfs, Marker::RollingBack)?;
let rolled = tx.has_marker(vfs, Marker::RolledBack)?;
if committed && (rolling || rolled) {
return Ok(Decision::Required(
"both COMMITTED and rollback markers present".into(),
));
}
if committed {
return Ok(Decision::Committed);
}
if rolled {
return Ok(Decision::RolledBack);
}
if vfs.stat(&tx.journal())?.is_none() {
let backup_used = match vfs.stat(&tx.backup())? {
Some(_) => !vfs.read_dir(&tx.backup())?.is_empty(),
None => false,
};
return Ok(if backup_used || rolling {
Decision::Required(
"no journal, but the transaction has progressed past preparation".into(),
)
} else {
Decision::Discard
});
}
let bytes = vfs.read_file(&tx.journal())?;
Ok(match journal::decode(&bytes, &tx.name, vfs.root_id()?) {
Ok(j) => Decision::RollBack(Box::new(j)),
Err(e) => Decision::Required(e),
})
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RecoveryReport {
pub rolled_back: Vec<String>,
pub completed: Vec<String>,
pub discarded: Vec<String>,
pub garbage_removed: usize,
}
pub(crate) fn recover_locked(vfs: &dyn Vfs) -> Result<RecoveryReport> {
let mut report = RecoveryReport::default();
let mut first_err = None;
let mut names: Vec<String> = vfs
.read_dir(&private_dir())?
.into_iter()
.filter_map(|n| n.into_string().ok())
.collect();
names.sort();
for name in names {
if name.starts_with(GC_PREFIX) || name.starts_with(PROBE_PREFIX) {
vfs.sync_dir(&private_dir())?;
delete_tree(vfs, &private_dir().join(&name))?;
vfs.sync_dir(&private_dir())?;
report.garbage_removed += 1;
continue;
}
if !name.starts_with(TX_PREFIX) {
continue;
}
let tx = TxDir { name: name.clone() };
match decide(vfs, &tx)? {
Decision::Discard => {
tx.gc(vfs)?;
report.discarded.push(name);
}
Decision::Committed => {
tx.gc(vfs)?;
report.completed.push(name);
}
Decision::RolledBack => {
tx.gc(vfs)?;
report.rolled_back.push(name);
}
Decision::RollBack(j) => match rollback(vfs, &tx, &j) {
Ok(()) => report.rolled_back.push(name),
Err(e @ Error::RecoveryRequired { .. }) => {
first_err.get_or_insert(e);
}
Err(e) => return Err(e),
},
Decision::Required(reason) => {
first_err.get_or_insert(Error::RecoveryRequired { tx: name, reason });
}
}
}
match first_err {
Some(e) => Err(e),
None => Ok(report),
}
}