use std::sync::Arc;
use arcbox_core::Runtime;
use arcbox_engine::EngineError;
use connectrpc::{ConnectError, RequestContext};
use crate::ApiError;
use arcbox_computer::locks::SandboxOperationLocks;
pub(super) use arcbox_computer::resume::{REASON_AUTO_RESUME, REASON_RESUME, is_sandbox_paused};
pub(super) const NO_AUTO_RESUME_HEADER: &str = "x-arcbox-no-auto-resume";
pub(super) fn auto_resume_opted_out(ctx: &RequestContext) -> bool {
ctx.header(NO_AUTO_RESUME_HEADER)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| !matches!(value, "" | "0" | "false"))
}
pub(super) async fn resume(
runtime: &Arc<Runtime>,
operations: &SandboxOperationLocks,
machine: &str,
sandbox_id: &str,
reason: &str,
) -> Result<(), ConnectError> {
arcbox_computer::resume::resume(runtime, operations, machine, sandbox_id, reason)
.await
.map_err(|e| ConnectError::from(ApiError::from(e)))
}
pub(super) async fn with_auto_resume<T, F, Fut>(
runtime: &Arc<Runtime>,
operations: &SandboxOperationLocks,
ctx: &RequestContext,
machine: &str,
sandbox_id: &str,
call: F,
) -> Result<T, ConnectError>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<T, EngineError>>,
{
match call().await {
Ok(value) => Ok(value),
Err(error) if is_sandbox_paused(&error) => {
if auto_resume_opted_out(ctx) {
return Err(ConnectError::from(ApiError::from(error)));
}
resume(runtime, operations, machine, sandbox_id, REASON_AUTO_RESUME).await?;
call()
.await
.map_err(|e| ConnectError::from(ApiError::from(e)))
}
Err(error) => Err(ConnectError::from(ApiError::from(error))),
}
}
pub(super) async fn ensure_resumed_for_write(
runtime: &Arc<Runtime>,
operations: &SandboxOperationLocks,
ctx: &RequestContext,
machine: &str,
sandbox_id: &str,
) -> Result<(), ConnectError> {
arcbox_computer::resume::ensure_resumed_for_write(
runtime,
operations,
machine,
sandbox_id,
!auto_resume_opted_out(ctx),
)
.await
.map_err(|e| ConnectError::from(ApiError::from(e)))
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx_with_header(value: Option<&str>) -> RequestContext {
let mut headers = http::HeaderMap::new();
if let Some(value) = value {
headers.insert(NO_AUTO_RESUME_HEADER, value.parse().unwrap());
}
RequestContext::new(headers)
}
#[test]
fn opt_out_header_is_presence_with_explicit_off_values() {
assert!(!auto_resume_opted_out(&ctx_with_header(None)));
assert!(!auto_resume_opted_out(&ctx_with_header(Some(""))));
assert!(!auto_resume_opted_out(&ctx_with_header(Some("0"))));
assert!(!auto_resume_opted_out(&ctx_with_header(Some("false"))));
assert!(auto_resume_opted_out(&ctx_with_header(Some("1"))));
assert!(auto_resume_opted_out(&ctx_with_header(Some("true"))));
}
}