1use a3s_box_core::{
2 CreateExecutionRequest, ExecutionEventBatch, ExecutionEventsRequest, ExecutionGeneration,
3 ExecutionId, ExecutionLease, ExecutionManager, ExecutionManagerError, ExecutionManagerResult,
4 ExecutionProcessInventory, ExecutionReservation, ExecutionResourceUpdate, ExecutionSnapshot,
5 ExecutionSnapshotId, ExecutionState, ExecutionStats, ExecutionStatus, KillExecutionOptions,
6 KillOutcome, OperationId, ReconcileOutcome, RestartExecutionOptions,
7};
8use async_trait::async_trait;
9
10use super::support::{managed_state, outcome_from_record, require_generation, state_conflict};
11use super::{
12 build_managed_record, status_from_record, LocalExecutionManager, ManagedExecutionState,
13 RuntimeUpdate,
14};
15
16#[async_trait]
17impl ExecutionManager for LocalExecutionManager {
18 async fn create(
19 &self,
20 request: CreateExecutionRequest,
21 operation_id: &OperationId,
22 ) -> ExecutionManagerResult<ExecutionReservation> {
23 let execution_id = ExecutionId::new(uuid::Uuid::new_v4().to_string())?;
24 let mut record = build_managed_record(
25 &self.home_dir,
26 &execution_id,
27 operation_id.clone(),
28 request,
29 chrono::Utc::now(),
30 )?;
31 let route = self.backend.route_for_create(&record)?;
32 record
33 .managed_execution
34 .as_mut()
35 .ok_or_else(|| {
36 ExecutionManagerError::Internal(format!(
37 "new execution {execution_id} has no managed lifecycle metadata"
38 ))
39 })?
40 .runtime_route = route;
41 self.backend.preflight(&record).await?;
42 let reservation = self.reserve(record).await?;
43 super::record::reservation_from_record(reservation.record())
44 }
45
46 async fn start(
47 &self,
48 execution_id: &ExecutionId,
49 expected_generation: ExecutionGeneration,
50 ) -> ExecutionManagerResult<ExecutionLease> {
51 let _lifecycle_lock =
52 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
53 let record = self
54 .get(execution_id)
55 .await?
56 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
57 super::record::validate_record_health(&record)?;
58 require_generation(&record, execution_id, expected_generation)?;
59 self.ensure_started(record).await
60 }
61
62 async fn inspect(&self, execution_id: &ExecutionId) -> ExecutionManagerResult<ExecutionStatus> {
63 let lifecycle_lock =
64 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
65 let manager = self.clone();
66 let execution_id = execution_id.clone();
67 let execution_id_label = execution_id.clone();
68 tokio::spawn(async move {
69 let _lifecycle_lock = lifecycle_lock;
74 let record = manager
75 .get(&execution_id)
76 .await?
77 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
78 let record = manager.stabilize_snapshot(record).await?;
79 let (record, state) = manager.observe_record(record).await?;
80 status_from_record(&record, state)
81 })
82 .await
83 .map_err(|error| {
84 ExecutionManagerError::Internal(format!(
85 "inspection task failed for {execution_id_label}: {error}"
86 ))
87 })?
88 }
89
90 async fn read_logs(
91 &self,
92 execution_id: &ExecutionId,
93 expected_generation: ExecutionGeneration,
94 ) -> ExecutionManagerResult<Vec<a3s_box_core::log::LogEntry>> {
95 self.read_structured_logs(execution_id, expected_generation)
96 .await
97 }
98
99 async fn list_processes(
100 &self,
101 execution_id: &ExecutionId,
102 expected_generation: ExecutionGeneration,
103 ) -> ExecutionManagerResult<ExecutionProcessInventory> {
104 let record = self
105 .require_observable_record(execution_id, expected_generation)
106 .await?;
107 self.require_same_observable_runtime(&record, execution_id, expected_generation)
108 .await?;
109 let inventory = self.backend.list_processes(&record).await?;
110 self.require_same_observable_runtime(&record, execution_id, expected_generation)
111 .await?;
112 if inventory.execution_id != *execution_id || inventory.generation != expected_generation {
113 return Err(ExecutionManagerError::Internal(format!(
114 "backend returned process inventory for a different execution generation than {execution_id} generation {}",
115 expected_generation.get()
116 )));
117 }
118 inventory.validate()?;
119 Ok(inventory)
120 }
121
122 async fn stats(
123 &self,
124 execution_id: &ExecutionId,
125 expected_generation: ExecutionGeneration,
126 ) -> ExecutionManagerResult<ExecutionStats> {
127 let record = self
128 .require_observable_record(execution_id, expected_generation)
129 .await?;
130 self.require_same_observable_runtime(&record, execution_id, expected_generation)
131 .await?;
132 let stats = self.backend.stats(&record).await?;
133 self.require_same_observable_runtime(&record, execution_id, expected_generation)
134 .await?;
135 if stats.execution_id != *execution_id || stats.generation != expected_generation {
136 return Err(ExecutionManagerError::Internal(format!(
137 "backend returned stats for a different execution generation than {execution_id} generation {}",
138 expected_generation.get()
139 )));
140 }
141 stats.validate()?;
142 Ok(stats)
143 }
144
145 async fn events(
146 &self,
147 execution_id: &ExecutionId,
148 expected_generation: ExecutionGeneration,
149 request: ExecutionEventsRequest,
150 ) -> ExecutionManagerResult<ExecutionEventBatch> {
151 request.validate()?;
152 let after_sequence = request.after_sequence;
153 let record = self
154 .require_observable_record(execution_id, expected_generation)
155 .await?;
156 self.require_same_observable_runtime(&record, execution_id, expected_generation)
157 .await?;
158 let batch = self.backend.events(&record, request).await?;
159 self.require_same_observable_runtime(&record, execution_id, expected_generation)
160 .await?;
161 if batch.execution_id != *execution_id || batch.generation != expected_generation {
162 return Err(ExecutionManagerError::Internal(format!(
163 "backend returned events for a different execution generation than {execution_id} generation {}",
164 expected_generation.get()
165 )));
166 }
167 batch.validate_after(after_sequence)?;
168 Ok(batch)
169 }
170
171 async fn update_resources(
172 &self,
173 execution_id: &ExecutionId,
174 expected_generation: ExecutionGeneration,
175 operation_id: &OperationId,
176 update: ExecutionResourceUpdate,
177 ) -> ExecutionManagerResult<ExecutionLease> {
178 update.validate()?;
179 let _lifecycle_lock =
180 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
181 let record = self
182 .get(execution_id)
183 .await?
184 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
185 let record = self.stabilize_snapshot(record).await?;
186 require_generation(&record, execution_id, expected_generation)?;
187 let state = managed_state(&record)?;
188 if let Some(completed) = record
189 .managed_execution
190 .as_ref()
191 .and_then(|metadata| metadata.last_resource_update.as_ref())
192 .filter(|completed| completed.operation_id == *operation_id)
193 {
194 if completed.generation != expected_generation || completed.update != update {
195 return Err(ExecutionManagerError::Conflict {
196 execution_id: execution_id.clone(),
197 message: format!(
198 "resource update operation {operation_id} was already completed with different intent"
199 ),
200 });
201 }
202 if state != ManagedExecutionState::Running {
203 return Err(state_conflict(
204 &record,
205 execution_id,
206 "replay resource update",
207 ));
208 }
209 return super::record::lease_from_record(&record);
210 }
211 if state == ManagedExecutionState::UpdatingResources {
212 let pending = record
213 .managed_execution
214 .as_ref()
215 .and_then(|metadata| metadata.pending_operation.as_ref());
216 if !matches!(
217 pending,
218 Some(crate::ManagedExecutionOperation::UpdateResources {
219 operation_id: pending_id,
220 update: pending_update,
221 }) if pending_id == operation_id && pending_update == &update
222 ) {
223 return Err(ExecutionManagerError::Conflict {
224 execution_id: execution_id.clone(),
225 message: "another resource update is already in progress".to_string(),
226 });
227 }
228 return self.finish_resource_update(record).await;
229 }
230 if state != ManagedExecutionState::Running {
231 return Err(state_conflict(&record, execution_id, "update resources"));
232 }
233 self.backend
234 .preflight_resource_update(&record, &update)
235 .await?;
236 let claimed = self
237 .transition(
238 &record,
239 ManagedExecutionState::Running,
240 ManagedExecutionState::UpdatingResources,
241 RuntimeUpdate::ResourceUpdateClaim {
242 operation_id: operation_id.clone(),
243 update,
244 },
245 )
246 .await?;
247 self.finish_resource_update(claimed).await
248 }
249
250 async fn create_filesystem_snapshot(
251 &self,
252 execution_id: &ExecutionId,
253 expected_generation: ExecutionGeneration,
254 snapshot_id: &ExecutionSnapshotId,
255 ) -> ExecutionManagerResult<ExecutionSnapshot> {
256 let _lifecycle_lock =
257 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
258 self.create_snapshot(execution_id, expected_generation, snapshot_id)
259 .await
260 }
261
262 async fn filesystem_snapshot_size(
263 &self,
264 snapshot_id: &ExecutionSnapshotId,
265 ) -> ExecutionManagerResult<Option<u64>> {
266 self.snapshot_size(snapshot_id).await
267 }
268
269 async fn delete_filesystem_snapshot(
270 &self,
271 snapshot_id: &ExecutionSnapshotId,
272 ) -> ExecutionManagerResult<bool> {
273 self.delete_snapshot(snapshot_id).await
274 }
275
276 async fn pause(
277 &self,
278 execution_id: &ExecutionId,
279 expected_generation: ExecutionGeneration,
280 keep_memory: bool,
281 ) -> ExecutionManagerResult<ExecutionLease> {
282 let _lifecycle_lock =
283 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
284 let record = self
285 .get(execution_id)
286 .await?
287 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
288 let record = self.stabilize_snapshot(record).await?;
289 require_generation(&record, execution_id, expected_generation)?;
290 if managed_state(&record)? != ManagedExecutionState::Running {
291 return Err(state_conflict(&record, execution_id, "pause"));
292 }
293 let backend_operation_id =
294 OperationId::new(format!("managed-pause-{}", uuid::Uuid::new_v4().simple()))?;
295 let claimed = self
296 .transition(
297 &record,
298 ManagedExecutionState::Running,
299 ManagedExecutionState::Pausing,
300 RuntimeUpdate::PauseClaim {
301 keep_memory,
302 operation_id: backend_operation_id,
303 },
304 )
305 .await?;
306 self.finish_pause(claimed).await
307 }
308
309 async fn resume(
310 &self,
311 execution_id: &ExecutionId,
312 expected_generation: ExecutionGeneration,
313 ) -> ExecutionManagerResult<ExecutionLease> {
314 let _lifecycle_lock =
315 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
316 let record = self
317 .get(execution_id)
318 .await?
319 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
320 let record = self.stabilize_snapshot(record).await?;
321 require_generation(&record, execution_id, expected_generation)?;
322 if managed_state(&record)? != ManagedExecutionState::Paused {
323 return Err(state_conflict(&record, execution_id, "resume"));
324 }
325 let backend_operation_id =
326 OperationId::new(format!("managed-resume-{}", uuid::Uuid::new_v4().simple()))?;
327 let claimed = self
328 .transition(
329 &record,
330 ManagedExecutionState::Paused,
331 ManagedExecutionState::Resuming,
332 RuntimeUpdate::ResumeClaim(backend_operation_id),
333 )
334 .await?;
335 self.finish_resume(claimed).await
336 }
337
338 async fn restart_with_options(
339 &self,
340 execution_id: &ExecutionId,
341 expected_generation: ExecutionGeneration,
342 operation_id: &OperationId,
343 options: RestartExecutionOptions,
344 ) -> ExecutionManagerResult<ExecutionLease> {
345 let _lifecycle_lock =
346 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
347 let record = self
348 .get(execution_id)
349 .await?
350 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
351 super::record::validate_record_health(&record)?;
352 self.restart_record(record, expected_generation, operation_id, options)
353 .await
354 }
355
356 async fn kill(
357 &self,
358 execution_id: &ExecutionId,
359 expected_generation: ExecutionGeneration,
360 ) -> ExecutionManagerResult<KillOutcome> {
361 self.kill_with_options(
362 execution_id,
363 expected_generation,
364 KillExecutionOptions::default(),
365 )
366 .await
367 }
368
369 async fn kill_with_options(
370 &self,
371 execution_id: &ExecutionId,
372 expected_generation: ExecutionGeneration,
373 options: KillExecutionOptions,
374 ) -> ExecutionManagerResult<KillOutcome> {
375 validate_kill_options(options)?;
376 let _lifecycle_lock =
377 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
378 let record = self
379 .get(execution_id)
380 .await?
381 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
382 let record = self.stabilize_snapshot(record).await?;
383 require_generation(&record, execution_id, expected_generation)?;
384 let state = managed_state(&record)?;
385 if state.is_terminal() {
386 return Ok(KillOutcome::AlreadyStopped);
387 }
388 if matches!(
389 state,
390 ManagedExecutionState::RestartStopping | ManagedExecutionState::RestartStarting
391 ) {
392 return Err(state_conflict(&record, execution_id, "kill"));
393 }
394 let claimed = if state == ManagedExecutionState::Killing {
395 record
396 } else {
397 self.transition(
398 &record,
399 state,
400 ManagedExecutionState::Killing,
401 RuntimeUpdate::KillClaim(options),
402 )
403 .await?
404 };
405 self.finish_kill(claimed).await
406 }
407
408 async fn remove(
409 &self,
410 execution_id: &ExecutionId,
411 expected_generation: ExecutionGeneration,
412 ) -> ExecutionManagerResult<bool> {
413 self.remove_execution(execution_id, expected_generation)
414 .await
415 }
416
417 async fn reconcile(
418 &self,
419 operation_id: &OperationId,
420 ) -> ExecutionManagerResult<ReconcileOutcome> {
421 let Some(initial_record) = self.get_by_operation(operation_id).await? else {
422 return Ok(ReconcileOutcome::Absent);
423 };
424 let _lifecycle_lock =
425 super::lifecycle_lock::acquire(&self.home_dir, &initial_record.id).await?;
426 let Some(record) = self.get_by_operation(operation_id).await? else {
427 return Ok(ReconcileOutcome::Absent);
428 };
429 if record.id != initial_record.id {
430 return Err(ExecutionManagerError::Unavailable(format!(
431 "operation {operation_id} changed execution identity while waiting for its lifecycle lock"
432 )));
433 }
434 super::record::validate_record_health(&record)?;
435 match managed_state(&record)? {
436 ManagedExecutionState::Creating | ManagedExecutionState::Created => Ok(
437 ReconcileOutcome::Created(super::record::reservation_from_record(&record)?),
438 ),
439 ManagedExecutionState::Starting => self.recover_start(record).await,
440 ManagedExecutionState::Pausing => {
441 let (record, state) = self.observe_record(record).await?;
442 if managed_state(&record)? == ManagedExecutionState::Pausing
443 && state == ExecutionState::Running
444 {
445 return self.finish_pause(record).await.map(ReconcileOutcome::Ready);
446 }
447 outcome_from_record(record, state)
448 }
449 ManagedExecutionState::Resuming => {
450 let (record, state) = self.observe_record(record).await?;
451 if managed_state(&record)? == ManagedExecutionState::Resuming
452 && state == ExecutionState::Paused
453 {
454 return self
455 .finish_resume(record)
456 .await
457 .map(ReconcileOutcome::Ready);
458 }
459 outcome_from_record(record, state)
460 }
461 ManagedExecutionState::UpdatingResources => {
462 let execution_id = super::record::execution_id(&record)?;
463 match self.finish_resource_update(record).await {
464 Ok(lease) => Ok(ReconcileOutcome::Ready(lease)),
465 Err(error) => {
466 let current = self
467 .get(&execution_id)
468 .await?
469 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
470 match managed_state(¤t)? {
471 ManagedExecutionState::Stopped | ManagedExecutionState::Failed => {
472 Ok(ReconcileOutcome::Failed)
473 }
474 _ => Err(error),
475 }
476 }
477 }
478 }
479 ManagedExecutionState::Snapshotting => self
480 .recover_snapshot(record)
481 .await
482 .map(ReconcileOutcome::Ready),
483 ManagedExecutionState::Killing => {
484 self.finish_kill(record).await?;
485 Ok(ReconcileOutcome::Failed)
486 }
487 ManagedExecutionState::Removing => {
488 self.finish_remove(record).await?;
489 Ok(ReconcileOutcome::Absent)
490 }
491 ManagedExecutionState::RestartStopping | ManagedExecutionState::RestartStarting => self
492 .resume_restart(record)
493 .await
494 .map(ReconcileOutcome::Ready),
495 _ => {
496 let (record, state) = self.observe_record(record).await?;
497 outcome_from_record(record, state)
498 }
499 }
500 }
501}
502
503fn validate_kill_options(options: KillExecutionOptions) -> ExecutionManagerResult<()> {
504 if options
505 .signal
506 .is_some_and(|signal| signal <= 0 || 128_i32.checked_add(signal).is_none())
507 {
508 return Err(ExecutionManagerError::InvalidRequest(
509 "kill signal must be positive and representable as a Box exit code".to_string(),
510 ));
511 }
512 if options
513 .timeout_secs
514 .is_some_and(|timeout| timeout.checked_mul(1_000).is_none())
515 {
516 return Err(ExecutionManagerError::InvalidRequest(
517 "kill timeout is too large".to_string(),
518 ));
519 }
520 Ok(())
521}