use std::collections::{BTreeMap, BTreeSet};
use super::{ApplyContext, ApplyEvent, ApplyOptions, EVENTS_UNWRITABLE, Steps, WriteSet};
use crate::deploy::layout::RelPath;
use crate::deploy::pair::{
BackupCheck, BackupCheckId, BackupNode, BackupPolicy, BackupReport, BackupSignals,
PairWaitStep, ResyncRecord, TickOutcome, UnreadableUnits, compare_files, effective_files,
exempt_unreadable, files_check, files_skipped_check, mode_backup_check,
read_back_backup_check_with, report, unreachable_check, unreadable_units, unreadable_warnings,
wait_step,
};
use crate::deploy::transport::{FileBlob, Scope};
use crate::deploy::verify::{Expected, ReadBackBaseline, ReadBackJudge};
#[derive(Debug, Clone)]
pub(crate) struct Baseline {
pub(crate) last_sync: Option<i64>,
pub(crate) log_since_ms: i64,
pub(crate) read_back: Option<ReadBackBaseline>,
}
struct Content {
checks: Vec<BackupCheck>,
files_checked: usize,
}
struct Phase<'a, 'c, 's> {
ctx: &'a ApplyContext<'c>,
backup: &'a BackupNode<'c>,
steps: &'a mut Steps<'s>,
warnings: &'a mut Vec<String>,
set: &'a WriteSet,
scope: Scope,
master_files: Option<BTreeMap<RelPath, FileBlob>>,
master_unreadable: UnreadableUnits,
expected: Expected,
read_back_baseline: Option<ReadBackBaseline>,
policy: BackupPolicy,
before: Option<i64>,
log_since_ms: i64,
resync: Option<ResyncRecord>,
}
pub(crate) async fn converge_backup(
ctx: &ApplyContext<'_>,
backup: &BackupNode<'_>,
opts: &ApplyOptions,
set: &WriteSet,
baseline: Baseline,
steps: &mut Steps<'_>,
warnings: &mut Vec<String>,
) -> BackupReport {
let declared = ctx.declared;
let Baseline {
last_sync: before,
log_since_ms,
read_back: read_back_baseline,
} = baseline;
let mut phase = Phase {
ctx,
backup,
steps,
warnings,
set,
scope: Scope::from_manifest(
&declared.manifest,
&declared.target.mode,
&declared.excluded_tag_providers(),
),
master_files: None,
master_unreadable: UnreadableUnits::new(),
expected: Expected::from_render(
&declared.target.mode,
&declared.outcome,
&declared.manifest,
),
read_back_baseline,
policy: opts.backup,
before,
log_since_ms,
resync: None,
};
let budget_s = phase.policy.budget.as_secs();
let started = ctx.clock.now();
phase.emit(ApplyEvent::BackupWaitStarted { budget_s });
let (mut last, mut step) = phase.wait_budget().await;
if step != PairWaitStep::Converged {
phase.send_resync_once().await;
if step == PairWaitStep::TimedOut {
(last, step) = phase.wait_budget().await;
}
if step != PairWaitStep::Converged {
last = phase.tick(true).await;
}
}
let waited = ctx.clock.now().saturating_duration_since(started);
let resync = phase.resync.take();
let report = report(&last, before, budget_s, waited.as_secs(), resync);
phase.emit(ApplyEvent::BackupVerdict {
converged: report.converged,
reason: report.reason.map(|reason| reason.as_str()),
});
report
}
impl Phase<'_, '_, '_> {
fn emit(&mut self, event: ApplyEvent) {
if self.steps.emit(event).is_err()
&& !self
.warnings
.iter()
.any(|warning| warning == EVENTS_UNWRITABLE)
{
self.warnings.push(EVENTS_UNWRITABLE.to_string());
}
}
async fn send_resync_once(&mut self) {
if self.resync.is_some() {
return;
}
let record = match self.ctx.deploy.redundancy_resync().await {
Ok(()) => ResyncRecord {
ok: true,
code: None,
},
Err(error) => ResyncRecord {
ok: false,
code: Some(error.code().to_string()),
},
};
self.resync = Some(record.clone());
self.emit(ApplyEvent::BackupResync {
ok: record.ok,
code: record.code,
});
}
async fn wait_budget(&mut self) -> (TickOutcome, PairWaitStep) {
let started = self.ctx.clock.now();
loop {
let tick = self.tick(false).await;
let elapsed = self.ctx.clock.now().saturating_duration_since(started);
match wait_step(&tick, elapsed, &self.policy) {
PairWaitStep::Sleep(pause) => tokio::time::sleep(pause).await,
step => return (tick, step),
}
}
}
async fn tick(&mut self, force: bool) -> TickOutcome {
let signals = match self.backup.status().await {
Ok(status) => Ok(BackupSignals::from_status(self.before, &status)),
Err(error) => Err(error.code().to_string()),
};
let poll = match &signals {
Ok(s) => ApplyEvent::BackupPoll {
peer_connected: s.peer_connected,
sync_pending: s.sync_pending,
advanced: s.advanced(),
},
Err(_) => ApplyEvent::BackupPoll {
peer_connected: false,
sync_pending: false,
advanced: false,
},
};
self.emit(poll);
let exhausted = match &signals {
Ok(_) => self
.backup
.sync_exhausted(self.log_since_ms)
.await
.unwrap_or(false),
Err(_) => false,
};
let content = match &signals {
Ok(s) if force || (s.linked() && s.advanced()) => Some(self.content().await),
_ => None,
};
TickOutcome {
signals,
exhausted,
files_checked: content.as_ref().map_or(0, |c| c.files_checked),
content: content.map(|c| c.checks),
stable_ticks: 0,
}
}
async fn content(&mut self) -> Content {
let mut checks = Vec::with_capacity(3);
checks.push(match self.backup.deployment_mode().await {
Ok(mode) => mode_backup_check(&self.expected, mode.as_deref()),
Err(error) => unreachable_check(BackupCheckId::Mode, error.code()),
});
checks.push(match self.backup.read_back(&self.expected).await {
Ok(read_back) => {
let judge = match &self.read_back_baseline {
Some(baseline) => ReadBackJudge::Baseline(baseline),
None => ReadBackJudge::Strict,
};
read_back_backup_check_with(&self.expected, &read_back, &judge, &mut Vec::new()).0
}
Err(error) => unreachable_check(BackupCheckId::ReadBack, error.code()),
});
let (files, files_checked) = if self.backup.has_files() {
self.files().await
} else {
(files_skipped_check(), 0)
};
checks.push(files);
Content {
checks,
files_checked,
}
}
async fn files(&mut self) -> (BackupCheck, usize) {
let unreachable = |side: &str, code: &str| {
(
unreachable_check(BackupCheckId::Files, &format!("{side} {code}")),
0,
)
};
if self.master_files.is_none() {
match self.ctx.reader.read(&self.scope).await {
Ok(observed) => {
self.master_unreadable = unreadable_units(&observed);
self.master_files = Some(effective_files(observed));
}
Err(error) => return unreachable("master", error.code()),
}
}
let (backup_files, backup_unreadable) = match self.backup.read_files(&self.scope).await {
Some(Ok(observed)) => {
let unreadable = unreadable_units(&observed);
(effective_files(observed), unreadable)
}
Some(Err(error)) => return unreachable("backup", error.code()),
None => return (files_skipped_check(), 0),
};
let Some(master_files) = self.master_files.as_ref() else {
return (files_skipped_check(), 0);
};
let present: BTreeSet<RelPath> = self
.ctx
.declared
.outcome
.tree
.files
.keys()
.cloned()
.collect();
let absent: BTreeSet<RelPath> = self
.set
.deletes
.iter()
.map(|op| op.path.clone())
.filter(|path| !master_files.contains_key(path))
.collect();
let mut comparison = compare_files(master_files, &backup_files, &present, &absent);
exempt_unreadable(
&mut comparison,
&present,
&absent,
&[
("master", &self.master_unreadable),
("backup", &backup_unreadable),
],
);
for warning in unreadable_warnings(&comparison) {
if !self.warnings.contains(&warning) {
self.warnings.push(warning);
}
}
(files_check(&comparison), comparison.checked)
}
}