Skip to main content

a3s_box_runtime/local_execution/
backend.rs

1//! Injectable process/runtime boundary for local execution orchestration.
2
3use std::path::PathBuf;
4
5use a3s_box_core::{
6    ExecutionId, ExecutionManagerError, ExecutionManagerResult, ExecutionState, KillOutcome,
7};
8use async_trait::async_trait;
9use chrono::{DateTime, Utc};
10
11use crate::BoxRecord;
12
13/// Runtime evidence persisted after an execution becomes ready.
14#[derive(Debug, Clone)]
15pub struct LocalExecutionHandle {
16    pub started_at: DateTime<Utc>,
17    pub pid: Option<u32>,
18    pub pid_start_time: Option<u64>,
19    pub exec_socket_path: PathBuf,
20    pub console_log: PathBuf,
21    pub anonymous_volumes: Vec<String>,
22}
23
24impl LocalExecutionHandle {
25    pub(crate) fn validate(&self, execution_id: &ExecutionId) -> ExecutionManagerResult<()> {
26        if self.pid.is_none() && self.pid_start_time.is_some() {
27            return Err(ExecutionManagerError::Internal(format!(
28                "backend returned a PID start time without a PID for {execution_id}"
29            )));
30        }
31        if self.exec_socket_path.as_os_str().is_empty() {
32            return Err(ExecutionManagerError::Internal(format!(
33                "backend returned an empty exec socket path for {execution_id}"
34            )));
35        }
36        if self.console_log.as_os_str().is_empty() {
37            return Err(ExecutionManagerError::Internal(format!(
38                "backend returned an empty console log path for {execution_id}"
39            )));
40        }
41        Ok(())
42    }
43}
44
45/// One backend observation used during inspection and restart recovery.
46#[derive(Debug, Clone)]
47pub struct LocalExecutionObservation {
48    pub state: ExecutionState,
49    pub handle: Option<LocalExecutionHandle>,
50    pub exit_code: Option<i32>,
51}
52
53impl LocalExecutionObservation {
54    pub(crate) fn validate(&self, execution_id: &ExecutionId) -> ExecutionManagerResult<()> {
55        match self.state {
56            ExecutionState::Running | ExecutionState::Paused => {
57                self.handle.as_ref().ok_or_else(|| {
58                    ExecutionManagerError::Internal(format!(
59                        "backend returned {:?} without runtime evidence for {execution_id}",
60                        self.state
61                    ))
62                })?;
63            }
64            ExecutionState::Created
65            | ExecutionState::Creating
66            | ExecutionState::Stopped
67            | ExecutionState::Failed => {}
68        }
69        if let Some(handle) = &self.handle {
70            handle.validate(execution_id)?;
71        }
72        Ok(())
73    }
74}
75
76/// Backend operations invoked outside the durable state lock.
77///
78/// Implementations must key all host/runtime paths by [`BoxRecord::id`]. The
79/// external sandbox ID in managed metadata is an untrusted diagnostic label.
80#[async_trait]
81pub trait LocalExecutionBackend: Send + Sync {
82    async fn start(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle>;
83
84    async fn inspect(
85        &self,
86        record: &BoxRecord,
87    ) -> ExecutionManagerResult<LocalExecutionObservation>;
88
89    async fn pause(
90        &self,
91        record: &BoxRecord,
92        keep_memory: bool,
93    ) -> ExecutionManagerResult<LocalExecutionHandle>;
94
95    async fn resume(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle>;
96
97    /// Make a stopped, storage-retained rootfs available for a filesystem
98    /// snapshot without starting the execution runtime.
99    async fn prepare_quiescent_rootfs(&self, _record: &BoxRecord) -> ExecutionManagerResult<()> {
100        Ok(())
101    }
102
103    /// Release any transient rootfs mount created by
104    /// [`Self::prepare_quiescent_rootfs`] while retaining guest data.
105    async fn cleanup_quiescent_rootfs(&self, _record: &BoxRecord) -> ExecutionManagerResult<()> {
106        Ok(())
107    }
108
109    /// Stop the current runtime while preserving execution-owned storage for
110    /// the replacement generation.
111    async fn stop_for_restart(
112        &self,
113        record: &BoxRecord,
114        _timeout_secs: Option<u64>,
115    ) -> ExecutionManagerResult<KillOutcome> {
116        self.kill(record).await
117    }
118
119    async fn kill(&self, record: &BoxRecord) -> ExecutionManagerResult<KillOutcome>;
120}