a3s_box_runtime/local_execution/
router.rs1use 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
24pub enum OciMigrationPolicy {
25 #[default]
27 LegacyOnly,
28 SandboxViaOci,
30 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#[derive(Clone)]
52pub struct LocalExecutionBackendRouter {
53 legacy: Arc<dyn LocalExecutionBackend>,
54 oci: Arc<dyn LocalExecutionBackend>,
55 policy: OciMigrationPolicy,
56}
57
58impl LocalExecutionBackendRouter {
59 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 #[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
94pub(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 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}