use arcbox_connect::sandbox_v1::{InspectSandboxRequest, SandboxState};
use arcbox_connect::v1::SandboxResumeCommand;
use arcbox_engine::EngineError;
use crate::cleanup;
use crate::host::SandboxHost;
use crate::locks::SandboxOperationLocks;
pub const SANDBOX_PAUSED_WIRE_CODE: i32 = 423;
pub const REASON_RESUME: &str = "resume";
pub const REASON_AUTO_RESUME: &str = "auto_resume";
#[must_use]
pub fn is_sandbox_paused(error: &EngineError) -> bool {
matches!(
error,
EngineError::Agent {
code: SANDBOX_PAUSED_WIRE_CODE,
..
}
)
}
pub async fn resume<H: SandboxHost>(
host: &H,
operations: &SandboxOperationLocks,
machine: &str,
sandbox_id: &str,
reason: &str,
) -> arcbox_engine::Result<()> {
let _operation = operations.lock(machine, sandbox_id).await;
let mut agent = host.agent(machine)?;
let resumed = {
const RETRY_BUDGET: std::time::Duration = std::time::Duration::from_secs(8);
const RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(250);
let deadline = tokio::time::Instant::now() + RETRY_BUDGET;
loop {
let attempt = agent
.sandbox_resume(SandboxResumeCommand {
id: sandbox_id.to_owned(),
reason: reason.to_owned(),
..Default::default()
})
.await;
match attempt {
Ok(resumed) => break resumed,
Err(EngineError::Agent { code: 503, message })
if tokio::time::Instant::now() < deadline =>
{
tracing::debug!(
sandbox_id,
message,
"sandbox resume hit a retryable condition; retrying"
);
tokio::time::sleep(RETRY_DELAY).await;
}
Err(error) => {
tracing::warn!(machine, sandbox_id, reason, %error, "sandbox resume failed");
return Err(error);
}
}
}
};
cleanup::register_live_sandbox_dns(host, machine, sandbox_id, &resumed.ip_address).await;
Ok(())
}
pub async fn ensure_resumed_for_write<H: SandboxHost>(
host: &H,
operations: &SandboxOperationLocks,
machine: &str,
sandbox_id: &str,
auto_resume: bool,
) -> arcbox_engine::Result<()> {
let mut agent = host.agent(machine)?;
let info = agent
.sandbox_inspect(InspectSandboxRequest {
id: sandbox_id.to_owned(),
..Default::default()
})
.await?;
if matches!(
info.state.as_known(),
Some(SandboxState::Paused | SandboxState::Pausing)
) {
if !auto_resume {
return Err(EngineError::Agent {
code: SANDBOX_PAUSED_WIRE_CODE,
message: format!("sandbox '{sandbox_id}' is paused"),
});
}
resume(host, operations, machine, sandbox_id, REASON_AUTO_RESUME).await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_the_paused_wire_code_triggers_auto_resume() {
assert!(is_sandbox_paused(&EngineError::Agent {
code: 423,
message: "sandbox 'box' is paused".into(),
}));
for code in [400, 404, 409, 412, 416, 500, 503] {
assert!(!is_sandbox_paused(&EngineError::Agent {
code,
message: "other".into(),
}));
}
assert!(!is_sandbox_paused(&EngineError::Machine(
"dead vsock".into()
)));
}
}