Skip to main content

a3s_box_runtime/local_execution/
router.rs

1//! Durable dispatch between the retained Box backend and the A3S OCI SDK.
2
3use std::sync::Arc;
4
5use a3s_box_core::{
6    pty::PtyRequest, ExecOutput, ExecRequest, ExecutionEventBatch, ExecutionEventsRequest,
7    ExecutionIsolation, ExecutionManagerError, ExecutionManagerResult, ExecutionProcess,
8    ExecutionProcessInventory, ExecutionResourceUpdate, ExecutionStats, FileRequest, FileResponse,
9    FilesystemRequest, FilesystemResponse, KillOutcome, OperationId,
10};
11use async_trait::async_trait;
12
13use super::{
14    LocalExecutionBackend, LocalExecutionHandle, LocalExecutionObservation,
15    LocalExecutionTermination,
16};
17use crate::{BoxRecord, ManagedRuntimeRoute};
18
19/// Explicit cutover policy applied only while creating a new Box record.
20///
21/// Once selected, the exact route is persisted in the record and this policy
22/// is no longer consulted for lifecycle, recovery, or cleanup operations.
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
24pub enum OciMigrationPolicy {
25    /// Keep both isolation choices on their current Box-owned implementation.
26    #[default]
27    LegacyOnly,
28    /// Route Sandbox to OCI while retaining the current MicroVM implementation.
29    SandboxViaOci,
30    /// Route both Sandbox and MicroVM through the public OCI SDK.
31    AllViaOci,
32}
33
34impl OciMigrationPolicy {
35    const fn route(self, isolation: ExecutionIsolation) -> ManagedRuntimeRoute {
36        match (self, isolation) {
37            (Self::LegacyOnly, _) | (Self::SandboxViaOci, ExecutionIsolation::Microvm) => {
38                ManagedRuntimeRoute::BoxVm
39            }
40            (Self::SandboxViaOci, ExecutionIsolation::Sandbox) | (Self::AllViaOci, _) => {
41                ManagedRuntimeRoute::OciSdk
42            }
43        }
44    }
45}
46
47/// One fail-closed backend router that supports mixed legacy and OCI records.
48///
49/// The router never attempts the alternate backend after an error. A policy
50/// selects only new records; explicit record metadata owns every later call.
51#[derive(Clone)]
52pub struct LocalExecutionBackendRouter {
53    legacy: Arc<dyn LocalExecutionBackend>,
54    oci: Arc<dyn LocalExecutionBackend>,
55    policy: OciMigrationPolicy,
56}
57
58impl LocalExecutionBackendRouter {
59    /// Compose the two implementations behind one immutable creation policy.
60    pub fn new(
61        legacy: Arc<dyn LocalExecutionBackend>,
62        oci: Arc<dyn LocalExecutionBackend>,
63        policy: OciMigrationPolicy,
64    ) -> Self {
65        Self {
66            legacy,
67            oci,
68            policy,
69        }
70    }
71
72    /// Policy used only for records created by this router.
73    #[must_use]
74    pub const fn policy(&self) -> OciMigrationPolicy {
75        self.policy
76    }
77
78    fn route_for_record(&self, record: &BoxRecord) -> ExecutionManagerResult<ManagedRuntimeRoute> {
79        resolved_runtime_route(record)
80    }
81
82    fn backend_for_record(
83        &self,
84        record: &BoxRecord,
85    ) -> ExecutionManagerResult<&Arc<dyn LocalExecutionBackend>> {
86        match self.route_for_record(record)? {
87            ManagedRuntimeRoute::BoxVm => Ok(&self.legacy),
88            ManagedRuntimeRoute::OciSdk => Ok(&self.oci),
89            ManagedRuntimeRoute::Unspecified => unreachable!("route inference is exhaustive"),
90        }
91    }
92}
93
94/// Resolve records written before the route field was introduced using the
95/// same durable evidence for both router dispatch and concrete backend checks.
96pub(super) fn resolved_runtime_route(
97    record: &BoxRecord,
98) -> ExecutionManagerResult<ManagedRuntimeRoute> {
99    let metadata = record.managed_execution.as_ref().ok_or_else(|| {
100        ExecutionManagerError::Internal(format!(
101            "execution {} has no managed lifecycle metadata for backend routing",
102            record.id
103        ))
104    })?;
105    metadata
106        .validate()
107        .map_err(|error| ExecutionManagerError::Internal(error.to_string()))?;
108    match metadata.runtime_route {
109        ManagedRuntimeRoute::BoxVm => Ok(ManagedRuntimeRoute::BoxVm),
110        ManagedRuntimeRoute::OciSdk => Ok(ManagedRuntimeRoute::OciSdk),
111        ManagedRuntimeRoute::Unspecified if metadata.oci_runtime.is_some() => {
112            Ok(ManagedRuntimeRoute::OciSdk)
113        }
114        // OCI handles deliberately store no Box-owned exec socket. This
115        // preserves stopped pre-routing OCI records after their live binding
116        // has been cleared during teardown.
117        ManagedRuntimeRoute::Unspecified if record.exec_socket_path.as_os_str().is_empty() => {
118            Ok(ManagedRuntimeRoute::OciSdk)
119        }
120        ManagedRuntimeRoute::Unspecified => Ok(ManagedRuntimeRoute::BoxVm),
121    }
122}
123
124#[async_trait]
125impl LocalExecutionBackend for LocalExecutionBackendRouter {
126    fn route_for_create(&self, record: &BoxRecord) -> ExecutionManagerResult<ManagedRuntimeRoute> {
127        if record.managed_execution.is_none() {
128            return Err(ExecutionManagerError::Internal(format!(
129                "new execution {} has no managed lifecycle metadata for backend routing",
130                record.id
131            )));
132        }
133        Ok(self.policy.route(record.isolation))
134    }
135
136    async fn preflight(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
137        self.backend_for_record(record)?.preflight(record).await
138    }
139
140    async fn start(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
141        self.backend_for_record(record)?.start(record).await
142    }
143
144    async fn inspect(
145        &self,
146        record: &BoxRecord,
147    ) -> ExecutionManagerResult<LocalExecutionObservation> {
148        self.backend_for_record(record)?.inspect(record).await
149    }
150
151    async fn pause(
152        &self,
153        record: &BoxRecord,
154        keep_memory: bool,
155    ) -> ExecutionManagerResult<LocalExecutionHandle> {
156        self.backend_for_record(record)?
157            .pause(record, keep_memory)
158            .await
159    }
160
161    async fn resume(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
162        self.backend_for_record(record)?.resume(record).await
163    }
164
165    async fn preflight_resource_update(
166        &self,
167        record: &BoxRecord,
168        update: &ExecutionResourceUpdate,
169    ) -> ExecutionManagerResult<()> {
170        self.backend_for_record(record)?
171            .preflight_resource_update(record, update)
172            .await
173    }
174
175    async fn update_resources(
176        &self,
177        record: &BoxRecord,
178        operation_id: &OperationId,
179        update: &ExecutionResourceUpdate,
180    ) -> ExecutionManagerResult<()> {
181        self.backend_for_record(record)?
182            .update_resources(record, operation_id, update)
183            .await
184    }
185
186    async fn list_processes(
187        &self,
188        record: &BoxRecord,
189    ) -> ExecutionManagerResult<ExecutionProcessInventory> {
190        self.backend_for_record(record)?
191            .list_processes(record)
192            .await
193    }
194
195    async fn stats(&self, record: &BoxRecord) -> ExecutionManagerResult<ExecutionStats> {
196        self.backend_for_record(record)?.stats(record).await
197    }
198
199    async fn events(
200        &self,
201        record: &BoxRecord,
202        request: ExecutionEventsRequest,
203    ) -> ExecutionManagerResult<ExecutionEventBatch> {
204        self.backend_for_record(record)?
205            .events(record, request)
206            .await
207    }
208
209    async fn execute(
210        &self,
211        record: &BoxRecord,
212        request: ExecRequest,
213    ) -> ExecutionManagerResult<ExecOutput> {
214        self.backend_for_record(record)?
215            .execute(record, request)
216            .await
217    }
218
219    async fn start_process(
220        &self,
221        record: &BoxRecord,
222        request: ExecRequest,
223    ) -> ExecutionManagerResult<ExecutionProcess> {
224        self.backend_for_record(record)?
225            .start_process(record, request)
226            .await
227    }
228
229    async fn start_pty(
230        &self,
231        record: &BoxRecord,
232        request: PtyRequest,
233    ) -> ExecutionManagerResult<ExecutionProcess> {
234        self.backend_for_record(record)?
235            .start_pty(record, request)
236            .await
237    }
238
239    async fn transfer_file(
240        &self,
241        record: &BoxRecord,
242        request: FileRequest,
243    ) -> ExecutionManagerResult<FileResponse> {
244        self.backend_for_record(record)?
245            .transfer_file(record, request)
246            .await
247    }
248
249    async fn filesystem(
250        &self,
251        record: &BoxRecord,
252        request: FilesystemRequest,
253    ) -> ExecutionManagerResult<FilesystemResponse> {
254        self.backend_for_record(record)?
255            .filesystem(record, request)
256            .await
257    }
258
259    async fn prepare_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
260        self.backend_for_record(record)?
261            .prepare_quiescent_rootfs(record)
262            .await
263    }
264
265    async fn cleanup_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
266        self.backend_for_record(record)?
267            .cleanup_quiescent_rootfs(record)
268            .await
269    }
270
271    async fn stop_for_restart(
272        &self,
273        record: &BoxRecord,
274        timeout_secs: Option<u64>,
275    ) -> ExecutionManagerResult<KillOutcome> {
276        self.backend_for_record(record)?
277            .stop_for_restart(record, timeout_secs)
278            .await
279    }
280
281    async fn kill(&self, record: &BoxRecord) -> ExecutionManagerResult<KillOutcome> {
282        self.backend_for_record(record)?.kill(record).await
283    }
284
285    async fn kill_with_status(
286        &self,
287        record: &BoxRecord,
288    ) -> ExecutionManagerResult<LocalExecutionTermination> {
289        self.backend_for_record(record)?
290            .kill_with_status(record)
291            .await
292    }
293}