a3s_box_runtime/local_execution/
api.rs1use a3s_box_core::{
2 CreateExecutionRequest, ExecutionGeneration, ExecutionId, ExecutionLease, ExecutionManager,
3 ExecutionManagerError, ExecutionManagerResult, ExecutionReservation, ExecutionSnapshot,
4 ExecutionSnapshotId, ExecutionState, ExecutionStatus, KillExecutionOptions, KillOutcome,
5 OperationId, ReconcileOutcome, RestartExecutionOptions,
6};
7use async_trait::async_trait;
8
9use super::support::{managed_state, outcome_from_record, require_generation, state_conflict};
10use super::{
11 build_managed_record, status_from_record, LocalExecutionManager, ManagedExecutionState,
12 RuntimeUpdate,
13};
14
15#[async_trait]
16impl ExecutionManager for LocalExecutionManager {
17 async fn create(
18 &self,
19 request: CreateExecutionRequest,
20 operation_id: &OperationId,
21 ) -> ExecutionManagerResult<ExecutionReservation> {
22 let execution_id = ExecutionId::new(uuid::Uuid::new_v4().to_string())?;
23 let record = build_managed_record(
24 &self.home_dir,
25 &execution_id,
26 operation_id.clone(),
27 request,
28 chrono::Utc::now(),
29 )?;
30 let reservation = self.reserve(record).await?;
31 super::record::reservation_from_record(reservation.record())
32 }
33
34 async fn start(
35 &self,
36 execution_id: &ExecutionId,
37 expected_generation: ExecutionGeneration,
38 ) -> ExecutionManagerResult<ExecutionLease> {
39 let _lifecycle_lock =
40 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
41 let record = self
42 .get(execution_id)
43 .await?
44 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
45 super::record::validate_record_health(&record)?;
46 require_generation(&record, execution_id, expected_generation)?;
47 self.ensure_started(record).await
48 }
49
50 async fn inspect(&self, execution_id: &ExecutionId) -> ExecutionManagerResult<ExecutionStatus> {
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 let record = self.stabilize_snapshot(record).await?;
58 let (record, state) = self.observe_record(record).await?;
59 status_from_record(&record, state)
60 }
61
62 async fn read_logs(
63 &self,
64 execution_id: &ExecutionId,
65 expected_generation: ExecutionGeneration,
66 ) -> ExecutionManagerResult<Vec<a3s_box_core::log::LogEntry>> {
67 self.read_structured_logs(execution_id, expected_generation)
68 .await
69 }
70
71 async fn create_filesystem_snapshot(
72 &self,
73 execution_id: &ExecutionId,
74 expected_generation: ExecutionGeneration,
75 snapshot_id: &ExecutionSnapshotId,
76 ) -> ExecutionManagerResult<ExecutionSnapshot> {
77 let _lifecycle_lock =
78 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
79 self.create_snapshot(execution_id, expected_generation, snapshot_id)
80 .await
81 }
82
83 async fn filesystem_snapshot_size(
84 &self,
85 snapshot_id: &ExecutionSnapshotId,
86 ) -> ExecutionManagerResult<Option<u64>> {
87 self.snapshot_size(snapshot_id).await
88 }
89
90 async fn delete_filesystem_snapshot(
91 &self,
92 snapshot_id: &ExecutionSnapshotId,
93 ) -> ExecutionManagerResult<bool> {
94 self.delete_snapshot(snapshot_id).await
95 }
96
97 async fn pause(
98 &self,
99 execution_id: &ExecutionId,
100 expected_generation: ExecutionGeneration,
101 keep_memory: bool,
102 ) -> ExecutionManagerResult<ExecutionLease> {
103 let _lifecycle_lock =
104 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
105 let record = self
106 .get(execution_id)
107 .await?
108 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
109 let record = self.stabilize_snapshot(record).await?;
110 require_generation(&record, execution_id, expected_generation)?;
111 if managed_state(&record)? != ManagedExecutionState::Running {
112 return Err(state_conflict(&record, execution_id, "pause"));
113 }
114 let claimed = self
115 .transition(
116 &record,
117 ManagedExecutionState::Running,
118 ManagedExecutionState::Pausing,
119 RuntimeUpdate::PauseClaim(keep_memory),
120 )
121 .await?;
122 self.finish_pause(claimed).await
123 }
124
125 async fn resume(
126 &self,
127 execution_id: &ExecutionId,
128 expected_generation: ExecutionGeneration,
129 ) -> ExecutionManagerResult<ExecutionLease> {
130 let _lifecycle_lock =
131 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
132 let record = self
133 .get(execution_id)
134 .await?
135 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
136 let record = self.stabilize_snapshot(record).await?;
137 require_generation(&record, execution_id, expected_generation)?;
138 if managed_state(&record)? != ManagedExecutionState::Paused {
139 return Err(state_conflict(&record, execution_id, "resume"));
140 }
141 let claimed = self
142 .transition(
143 &record,
144 ManagedExecutionState::Paused,
145 ManagedExecutionState::Resuming,
146 RuntimeUpdate::None,
147 )
148 .await?;
149 self.finish_resume(claimed).await
150 }
151
152 async fn restart_with_options(
153 &self,
154 execution_id: &ExecutionId,
155 expected_generation: ExecutionGeneration,
156 operation_id: &OperationId,
157 options: RestartExecutionOptions,
158 ) -> ExecutionManagerResult<ExecutionLease> {
159 let _lifecycle_lock =
160 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
161 let record = self
162 .get(execution_id)
163 .await?
164 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
165 super::record::validate_record_health(&record)?;
166 self.restart_record(record, expected_generation, operation_id, options)
167 .await
168 }
169
170 async fn kill(
171 &self,
172 execution_id: &ExecutionId,
173 expected_generation: ExecutionGeneration,
174 ) -> ExecutionManagerResult<KillOutcome> {
175 self.kill_with_options(
176 execution_id,
177 expected_generation,
178 KillExecutionOptions::default(),
179 )
180 .await
181 }
182
183 async fn kill_with_options(
184 &self,
185 execution_id: &ExecutionId,
186 expected_generation: ExecutionGeneration,
187 options: KillExecutionOptions,
188 ) -> ExecutionManagerResult<KillOutcome> {
189 validate_kill_options(options)?;
190 let _lifecycle_lock =
191 super::lifecycle_lock::acquire(&self.home_dir, execution_id.as_str()).await?;
192 let record = self
193 .get(execution_id)
194 .await?
195 .ok_or_else(|| ExecutionManagerError::NotFound(execution_id.clone()))?;
196 let record = self.stabilize_snapshot(record).await?;
197 require_generation(&record, execution_id, expected_generation)?;
198 let state = managed_state(&record)?;
199 if state.is_terminal() {
200 return Ok(KillOutcome::AlreadyStopped);
201 }
202 if matches!(
203 state,
204 ManagedExecutionState::RestartStopping | ManagedExecutionState::RestartStarting
205 ) {
206 return Err(state_conflict(&record, execution_id, "kill"));
207 }
208 let claimed = if state == ManagedExecutionState::Killing {
209 record
210 } else {
211 self.transition(
212 &record,
213 state,
214 ManagedExecutionState::Killing,
215 RuntimeUpdate::KillClaim(options),
216 )
217 .await?
218 };
219 self.finish_kill(claimed).await
220 }
221
222 async fn remove(
223 &self,
224 execution_id: &ExecutionId,
225 expected_generation: ExecutionGeneration,
226 ) -> ExecutionManagerResult<bool> {
227 self.remove_execution(execution_id, expected_generation)
228 .await
229 }
230
231 async fn reconcile(
232 &self,
233 operation_id: &OperationId,
234 ) -> ExecutionManagerResult<ReconcileOutcome> {
235 let Some(initial_record) = self.get_by_operation(operation_id).await? else {
236 return Ok(ReconcileOutcome::Absent);
237 };
238 let _lifecycle_lock =
239 super::lifecycle_lock::acquire(&self.home_dir, &initial_record.id).await?;
240 let Some(record) = self.get_by_operation(operation_id).await? else {
241 return Ok(ReconcileOutcome::Absent);
242 };
243 if record.id != initial_record.id {
244 return Err(ExecutionManagerError::Unavailable(format!(
245 "operation {operation_id} changed execution identity while waiting for its lifecycle lock"
246 )));
247 }
248 super::record::validate_record_health(&record)?;
249 match managed_state(&record)? {
250 ManagedExecutionState::Creating | ManagedExecutionState::Created => Ok(
251 ReconcileOutcome::Created(super::record::reservation_from_record(&record)?),
252 ),
253 ManagedExecutionState::Starting => self.recover_start(record).await,
254 ManagedExecutionState::Pausing => {
255 let (record, state) = self.observe_record(record).await?;
256 if managed_state(&record)? == ManagedExecutionState::Pausing
257 && state == ExecutionState::Running
258 {
259 return self.finish_pause(record).await.map(ReconcileOutcome::Ready);
260 }
261 outcome_from_record(record, state)
262 }
263 ManagedExecutionState::Resuming => {
264 let (record, state) = self.observe_record(record).await?;
265 if managed_state(&record)? == ManagedExecutionState::Resuming
266 && state == ExecutionState::Paused
267 {
268 return self
269 .finish_resume(record)
270 .await
271 .map(ReconcileOutcome::Ready);
272 }
273 outcome_from_record(record, state)
274 }
275 ManagedExecutionState::Snapshotting => self
276 .recover_snapshot(record)
277 .await
278 .map(ReconcileOutcome::Ready),
279 ManagedExecutionState::Killing => {
280 self.finish_kill(record).await?;
281 Ok(ReconcileOutcome::Failed)
282 }
283 ManagedExecutionState::Removing => {
284 self.finish_remove(record).await?;
285 Ok(ReconcileOutcome::Absent)
286 }
287 ManagedExecutionState::RestartStopping | ManagedExecutionState::RestartStarting => self
288 .resume_restart(record)
289 .await
290 .map(ReconcileOutcome::Ready),
291 _ => {
292 let (record, state) = self.observe_record(record).await?;
293 outcome_from_record(record, state)
294 }
295 }
296 }
297}
298
299fn validate_kill_options(options: KillExecutionOptions) -> ExecutionManagerResult<()> {
300 if options.signal.is_some_and(|signal| signal <= 0) {
301 return Err(ExecutionManagerError::InvalidRequest(
302 "kill signal must be positive".to_string(),
303 ));
304 }
305 if options
306 .timeout_secs
307 .is_some_and(|timeout| timeout.checked_mul(1_000).is_none())
308 {
309 return Err(ExecutionManagerError::InvalidRequest(
310 "kill timeout is too large".to_string(),
311 ));
312 }
313 Ok(())
314}