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 LocalExecutionResourcePlan, 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 backend_for_isolation(
79 &self,
80 isolation: ExecutionIsolation,
81 ) -> &Arc<dyn LocalExecutionBackend> {
82 match self.policy.route(isolation) {
83 ManagedRuntimeRoute::BoxVm => &self.legacy,
84 ManagedRuntimeRoute::OciSdk => &self.oci,
85 ManagedRuntimeRoute::Unspecified => {
86 unreachable!("creation policy always selects a route")
87 }
88 }
89 }
90
91 fn route_for_record(&self, record: &BoxRecord) -> ExecutionManagerResult<ManagedRuntimeRoute> {
92 resolved_runtime_route(record)
93 }
94
95 fn backend_for_record(
96 &self,
97 record: &BoxRecord,
98 ) -> ExecutionManagerResult<&Arc<dyn LocalExecutionBackend>> {
99 match self.route_for_record(record)? {
100 ManagedRuntimeRoute::BoxVm => Ok(&self.legacy),
101 ManagedRuntimeRoute::OciSdk => Ok(&self.oci),
102 ManagedRuntimeRoute::Unspecified => unreachable!("route inference is exhaustive"),
103 }
104 }
105}
106
107pub(super) fn resolved_runtime_route(
110 record: &BoxRecord,
111) -> ExecutionManagerResult<ManagedRuntimeRoute> {
112 let metadata = record.managed_execution.as_ref().ok_or_else(|| {
113 ExecutionManagerError::Internal(format!(
114 "execution {} has no managed lifecycle metadata for backend routing",
115 record.id
116 ))
117 })?;
118 metadata
119 .validate()
120 .map_err(|error| ExecutionManagerError::Internal(error.to_string()))?;
121 match metadata.runtime_route {
122 ManagedRuntimeRoute::BoxVm => Ok(ManagedRuntimeRoute::BoxVm),
123 ManagedRuntimeRoute::OciSdk => Ok(ManagedRuntimeRoute::OciSdk),
124 ManagedRuntimeRoute::Unspecified if metadata.oci_runtime.is_some() => {
125 Ok(ManagedRuntimeRoute::OciSdk)
126 }
127 ManagedRuntimeRoute::Unspecified if record.exec_socket_path.as_os_str().is_empty() => {
131 Ok(ManagedRuntimeRoute::OciSdk)
132 }
133 ManagedRuntimeRoute::Unspecified => Ok(ManagedRuntimeRoute::BoxVm),
134 }
135}
136
137#[async_trait]
138impl LocalExecutionBackend for LocalExecutionBackendRouter {
139 async fn preflight_isolation(
140 &self,
141 isolation: ExecutionIsolation,
142 ) -> ExecutionManagerResult<()> {
143 self.backend_for_isolation(isolation)
144 .preflight_isolation(isolation)
145 .await
146 }
147
148 fn route_for_create(&self, record: &BoxRecord) -> ExecutionManagerResult<ManagedRuntimeRoute> {
149 if record.managed_execution.is_none() {
150 return Err(ExecutionManagerError::Internal(format!(
151 "new execution {} has no managed lifecycle metadata for backend routing",
152 record.id
153 )));
154 }
155 Ok(self.policy.route(record.isolation))
156 }
157
158 async fn preflight(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
159 self.backend_for_record(record)?.preflight(record).await
160 }
161
162 async fn plan_create_resources(
163 &self,
164 record: &BoxRecord,
165 ) -> ExecutionManagerResult<LocalExecutionResourcePlan> {
166 self.backend_for_record(record)?
167 .plan_create_resources(record)
168 .await
169 }
170
171 async fn start(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
172 self.backend_for_record(record)?.start(record).await
173 }
174
175 async fn inspect(
176 &self,
177 record: &BoxRecord,
178 ) -> ExecutionManagerResult<LocalExecutionObservation> {
179 self.backend_for_record(record)?.inspect(record).await
180 }
181
182 async fn pause(
183 &self,
184 record: &BoxRecord,
185 keep_memory: bool,
186 ) -> ExecutionManagerResult<LocalExecutionHandle> {
187 self.backend_for_record(record)?
188 .pause(record, keep_memory)
189 .await
190 }
191
192 async fn resume(&self, record: &BoxRecord) -> ExecutionManagerResult<LocalExecutionHandle> {
193 self.backend_for_record(record)?.resume(record).await
194 }
195
196 async fn preflight_resource_update(
197 &self,
198 record: &BoxRecord,
199 update: &ExecutionResourceUpdate,
200 ) -> ExecutionManagerResult<()> {
201 self.backend_for_record(record)?
202 .preflight_resource_update(record, update)
203 .await
204 }
205
206 async fn update_resources(
207 &self,
208 record: &BoxRecord,
209 operation_id: &OperationId,
210 update: &ExecutionResourceUpdate,
211 ) -> ExecutionManagerResult<()> {
212 self.backend_for_record(record)?
213 .update_resources(record, operation_id, update)
214 .await
215 }
216
217 async fn list_processes(
218 &self,
219 record: &BoxRecord,
220 ) -> ExecutionManagerResult<ExecutionProcessInventory> {
221 self.backend_for_record(record)?
222 .list_processes(record)
223 .await
224 }
225
226 async fn stats(&self, record: &BoxRecord) -> ExecutionManagerResult<ExecutionStats> {
227 self.backend_for_record(record)?.stats(record).await
228 }
229
230 async fn events(
231 &self,
232 record: &BoxRecord,
233 request: ExecutionEventsRequest,
234 ) -> ExecutionManagerResult<ExecutionEventBatch> {
235 self.backend_for_record(record)?
236 .events(record, request)
237 .await
238 }
239
240 async fn execute(
241 &self,
242 record: &BoxRecord,
243 request: ExecRequest,
244 ) -> ExecutionManagerResult<ExecOutput> {
245 self.backend_for_record(record)?
246 .execute(record, request)
247 .await
248 }
249
250 async fn start_process(
251 &self,
252 record: &BoxRecord,
253 request: ExecRequest,
254 ) -> ExecutionManagerResult<ExecutionProcess> {
255 self.backend_for_record(record)?
256 .start_process(record, request)
257 .await
258 }
259
260 async fn start_pty(
261 &self,
262 record: &BoxRecord,
263 request: PtyRequest,
264 ) -> ExecutionManagerResult<ExecutionProcess> {
265 self.backend_for_record(record)?
266 .start_pty(record, request)
267 .await
268 }
269
270 async fn transfer_file(
271 &self,
272 record: &BoxRecord,
273 request: FileRequest,
274 ) -> ExecutionManagerResult<FileResponse> {
275 self.backend_for_record(record)?
276 .transfer_file(record, request)
277 .await
278 }
279
280 async fn filesystem(
281 &self,
282 record: &BoxRecord,
283 request: FilesystemRequest,
284 ) -> ExecutionManagerResult<FilesystemResponse> {
285 self.backend_for_record(record)?
286 .filesystem(record, request)
287 .await
288 }
289
290 async fn prepare_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
291 self.backend_for_record(record)?
292 .prepare_quiescent_rootfs(record)
293 .await
294 }
295
296 async fn cleanup_quiescent_rootfs(&self, record: &BoxRecord) -> ExecutionManagerResult<()> {
297 self.backend_for_record(record)?
298 .cleanup_quiescent_rootfs(record)
299 .await
300 }
301
302 async fn stop_for_restart(
303 &self,
304 record: &BoxRecord,
305 timeout_secs: Option<u64>,
306 ) -> ExecutionManagerResult<KillOutcome> {
307 self.backend_for_record(record)?
308 .stop_for_restart(record, timeout_secs)
309 .await
310 }
311
312 async fn kill(&self, record: &BoxRecord) -> ExecutionManagerResult<KillOutcome> {
313 self.backend_for_record(record)?.kill(record).await
314 }
315
316 async fn kill_with_status(
317 &self,
318 record: &BoxRecord,
319 ) -> ExecutionManagerResult<LocalExecutionTermination> {
320 self.backend_for_record(record)?
321 .kill_with_status(record)
322 .await
323 }
324}