use super::machine::PreparedResource;
use super::*;
pub(super) type WaveResult = (usize, f64, Result<transport::ExecOutput, String>, String);
struct Attempt {
output: Result<transport::ExecOutput, String>,
script: String,
retryable: bool,
}
pub(super) fn execute_wave_io(
cfg: &ApplyConfig,
prepared: &[PreparedResource],
machine: &Machine,
) -> Vec<WaveResult> {
std::thread::scope(|s| {
let handles: Vec<(usize, _)> = prepared
.iter()
.map(|prep| {
(
prep.change_idx,
s.spawn(move || run_prepared(cfg, prep, machine)),
)
})
.collect();
join_wave_results(handles)
})
}
pub(super) fn join_wave_results(
handles: Vec<(usize, std::thread::ScopedJoinHandle<'_, WaveResult>)>,
) -> Vec<WaveResult> {
handles
.into_iter()
.map(|(idx, handle)| match handle.join() {
Ok(result) => result,
Err(panic_payload) => {
let msg = extract_panic_message(panic_payload);
eprintln!("error: wave execution thread panicked: {msg}");
(idx, 0.0, Err(format!("thread panic: {msg}")), String::new())
}
})
.collect()
}
fn run_prepared(cfg: &ApplyConfig, prep: &PreparedResource, machine: &Machine) -> WaveResult {
let start = Instant::now();
let mut done = 0u32;
loop {
let attempt = attempt_prepared(cfg, prep, machine);
if !should_retry(cfg, &attempt, done) {
return (
prep.change_idx,
start.elapsed().as_secs_f64(),
attempt.output,
attempt.script,
);
}
done += 1;
let backoff = std::time::Duration::from_secs(1u64 << (done - 1).min(4));
eprintln!(
" retry {}/{} for {} (backoff {:?})",
done, cfg.retry, prep.resource_id, backoff
);
std::thread::sleep(backoff);
}
}
fn should_retry(cfg: &ApplyConfig, attempt: &Attempt, done: u32) -> bool {
if done >= cfg.retry || !attempt.retryable {
return false;
}
if cfg.config.policy.failure == FailurePolicy::StopOnFirst {
return false;
}
match &attempt.output {
Ok(out) => !out.success(),
Err(_) => true,
}
}
fn attempt_prepared(cfg: &ApplyConfig, prep: &PreparedResource, machine: &Machine) -> Attempt {
if let Some(ref pre_hook) = prep.resolved.pre_apply {
if let Some(err) =
super::output_verify::run_pre_apply_hook(machine, pre_hook, cfg.timeout_secs)
{
return Attempt {
output: Err(err),
script: String::new(),
retryable: false,
};
}
}
let (output, script) = exec_prepared(cfg, prep, machine);
Attempt {
output,
script,
retryable: true,
}
}
fn exec_prepared(
cfg: &ApplyConfig,
prep: &PreparedResource,
machine: &Machine,
) -> (Result<transport::ExecOutput, String>, String) {
if prep.use_copia {
return (
copia_apply_file(machine, &prep.resolved, cfg.timeout_secs),
String::new(),
);
}
let script = match codegen::apply_script(&prep.resolved) {
Ok(script) => script,
Err(e) => return (Err(e), String::new()),
};
if cfg.trace {
eprintln!("[TRACE] {} script:\n{}", prep.resource_id, script);
}
let out = transport::exec_script_retry(
machine,
&script,
cfg.timeout_secs,
cfg.config.policy.ssh_retries,
);
(out, script)
}
fn extract_panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"thread panicked".to_string()
}
}