arcbox_computer_runtime/sandbox/
pool.rs1use super::boot::{StageError, stage_rootfs_cow_or_copy};
17use super::reconcile::{self, ComputerStateRecord, POOL_SLOT_PREFIX};
18use super::*;
19use crate::config::JailerConfig;
20use crate::snapshot::SnapshotMeta;
21
22pub use super::policy::pool::SlotPool;
26
27pub struct PreparedSlot {
31 pub slot_id: String,
34 pub prepared: Arc<dyn PreparedVm>,
36 pub cow_handle: Option<CowHandle>,
38 pub rootfs: PathBuf,
41 pub image: CheckpointImage,
44 pub vm_dir: PathBuf,
46}
47
48pub(super) async fn prepare_slot(
53 driver: &dyn VmDriver,
54 config: &RuntimeConfig,
55 cow_manager: &CowManager,
56 snapshot: &SnapshotMeta,
57) -> Result<PreparedSlot> {
58 let fc_cfg = &config.firecracker;
59 let jc = fc_cfg.jailer.as_ref().ok_or_else(|| {
60 ComputerError::Config("restore slot pooling requires jailer isolation".into())
61 })?;
62 let mut suffix = Uuid::new_v4().simple().to_string();
70 suffix.truncate(16);
71 let slot_id = format!("{POOL_SLOT_PREFIX}{suffix}");
72 let vm_dir = PathBuf::from(&fc_cfg.data_dir)
73 .join("sandboxes")
74 .join(&slot_id);
75
76 reconcile::create_runtime_dir(&vm_dir)?;
79 reconcile::write_state_record(
80 &vm_dir,
81 &ComputerStateRecord::new(&slot_id, None, None, None, config, None)?,
82 )?;
83
84 match stage_slot(driver, config, jc, cow_manager, snapshot, &slot_id, &vm_dir).await {
85 Ok(staged) => Ok(PreparedSlot {
86 slot_id,
87 prepared: staged.prepared,
88 cow_handle: staged.cow_handle,
89 rootfs: staged.rootfs,
90 image: staged.image,
91 vm_dir,
92 }),
93 Err(mut failure) => {
94 let fc_unwound = match failure.prepared.take() {
97 Some(prepared) => match prepared.discard().await {
98 Ok(_) => true,
99 Err(error) => {
100 warn!(slot_id, error = %error, "failed slot prepare left its vmm behind");
101 false
102 }
103 },
104 None => true,
105 };
106 let cow_unwound = match failure.cow_handle.take() {
107 Some(handle) => match cow_manager.teardown_checked(&handle).await {
108 Ok(()) => true,
109 Err(error) => {
110 warn!(slot_id, error = %error, "failed slot prepare left CoW resources behind");
111 false
112 }
113 },
114 None => true,
115 };
116 if fc_unwound && cow_unwound {
119 if let Err(error) = reconcile::clear_state_record(&vm_dir) {
120 warn!(slot_id, error = %error, "failed slot prepare kept its journal");
121 } else {
122 let _ = tokio::fs::remove_dir_all(&vm_dir).await;
123 }
124 }
125 Err(failure.error)
126 }
127 }
128}
129
130struct SlotFailure {
131 error: ComputerError,
132 prepared: Option<Arc<dyn PreparedVm>>,
133 cow_handle: Option<CowHandle>,
134}
135
136impl From<ComputerError> for SlotFailure {
137 fn from(error: ComputerError) -> Self {
138 Self {
139 error,
140 prepared: None,
141 cow_handle: None,
142 }
143 }
144}
145
146struct StagedSlot {
148 prepared: Arc<dyn PreparedVm>,
149 cow_handle: Option<CowHandle>,
150 rootfs: PathBuf,
151 image: CheckpointImage,
152}
153
154#[allow(
155 clippy::result_large_err,
156 reason = "the failure hands back the half-built sandbox's resources; boxing it is a refactor of its own"
157)]
158async fn stage_slot(
159 driver: &dyn VmDriver,
160 config: &RuntimeConfig,
161 jc: &JailerConfig,
162 cow_manager: &CowManager,
163 snapshot: &SnapshotMeta,
164 slot_id: &str,
165 vm_dir: &Path,
166) -> std::result::Result<StagedSlot, SlotFailure> {
167 let prepared: Arc<dyn PreparedVm> = Arc::from(
168 super::prepare_capability(driver)
169 .prepare(
170 &VmId::new(slot_id).map_err(ComputerError::from)?,
171 &IsolationSpec::try_from(jc)?,
172 vm_dir,
173 )
174 .await
175 .map_err(ComputerError::from)?,
176 );
177 let pid = super::journaled_pid(&*prepared);
178 let vmm = super::journaled_vmm(&*prepared);
179 let journal = |cow: Option<&CowHandle>| {
180 reconcile::write_state_record(
181 vm_dir,
182 &ComputerStateRecord::new(slot_id, pid, None, cow, config, None)?.with_vmm(vmm.clone()),
183 )
184 };
185 let carry = |error: ComputerError, prepared, cow_handle| SlotFailure {
186 error,
187 prepared,
188 cow_handle,
189 };
190 if let Err(error) = journal(None) {
191 return Err(carry(error, Some(prepared), None));
192 }
193
194 let staging = super::staging_capability(prepared.staging());
198 if let Some(kernel) = snapshot.kernel_path.as_deref() {
199 if let Err(error) = staging.stage_kernel(Path::new(kernel)).await {
200 return Err(carry(error.into(), Some(prepared), None));
201 }
202 }
203
204 let Some(rootfs) = snapshot.rootfs_path.as_deref() else {
205 return Err(carry(
206 ComputerError::Snapshot(format!(
207 "checkpoint {} records no rootfs template; there is no disk to pre-warm a slot \
208 with",
209 snapshot.id
210 )),
211 Some(prepared),
212 None,
213 ));
214 };
215 let staged =
216 match stage_rootfs_cow_or_copy(cow_manager, staging, slot_id, rootfs, &journal).await {
217 Ok(staged) => staged,
218 Err(StageError {
219 error,
220 cow_handle: leaked,
221 }) => return Err(carry(error, Some(prepared), leaked)),
222 };
223
224 let image = match super::catalogued_checkpoint(snapshot) {
225 Ok(catalogued) => staging
226 .stage_checkpoint(&catalogued)
227 .await
228 .map_err(Into::into),
229 Err(error) => Err(error),
230 };
231 match image {
232 Ok(image) => Ok(StagedSlot {
233 prepared,
234 cow_handle: staged.cow_handle,
235 rootfs: staged.path,
236 image,
237 }),
238 Err(error) => Err(carry(error, Some(prepared), staged.cow_handle)),
239 }
240}
241
242pub(super) async fn drain_pool_slots(
246 pool: &SlotPool,
247 cow_manager: &CowManager,
248 snapshot_id: Option<&str>,
249) {
250 for slot in pool.drain(snapshot_id) {
251 let slot_id = slot.slot_id.clone();
252 if let Err(error) = destroy_slot(cow_manager, slot).await {
253 warn!(
254 slot_id,
255 error = %error,
256 "pool slot teardown incomplete; the startup sweep will retry"
257 );
258 }
259 }
260}
261
262pub(super) async fn destroy_slot(cow_manager: &CowManager, mut slot: PreparedSlot) -> Result<()> {
268 slot.prepared.discard().await?;
271 if let Some(handle) = slot.cow_handle.take() {
272 cow_manager.teardown_checked(&handle).await?;
273 }
274 reconcile::clear_state_record(&slot.vm_dir)?;
275 match tokio::fs::remove_dir_all(&slot.vm_dir).await {
276 Ok(()) => Ok(()),
277 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
278 Err(error) => Err(ComputerError::Io(error)),
279 }
280}
281
282pub fn claim_restore_slot(
290 pool: &SlotPool,
291 cow_manager: &Arc<CowManager>,
292 snapshot_id: &str,
293) -> Option<PreparedSlot> {
294 loop {
295 let slot = pool.claim(snapshot_id)?;
296 if slot.prepared.alive() {
297 return Some(slot);
298 }
299 warn!(
300 snapshot_id,
301 slot_id = %slot.slot_id,
302 "discarding pre-warmed slot whose vmm died"
303 );
304 spawn_slot_teardown(cow_manager, slot);
305 }
306}
307
308fn spawn_slot_teardown(cow_manager: &Arc<CowManager>, slot: PreparedSlot) {
309 {
310 let cow_manager = Arc::clone(cow_manager);
311 tokio::spawn(async move {
312 let slot_id = slot.slot_id.clone();
313 if let Err(error) = destroy_slot(&cow_manager, slot).await {
314 warn!(
315 slot_id,
316 error = %error,
317 "pool slot teardown incomplete; the startup sweep will retry"
318 );
319 }
320 });
321 }
322}
323
324pub fn spawn_pool_refill(
329 pool: &Arc<SlotPool>,
330 driver: &Arc<dyn VmDriver>,
331 config: &Arc<RuntimeConfig>,
332 cow_manager: &Arc<CowManager>,
333 snapshots: &Arc<SnapshotCatalog>,
334 snapshot_id: &str,
335) {
336 {
337 let plan = pool.begin_fill(snapshot_id, config.firecracker.pool_size);
338 for slot in plan.evicted {
339 spawn_slot_teardown(cow_manager, slot);
340 }
341 for _ in 0..plan.spawn {
342 let pool = Arc::clone(pool);
343 let driver = Arc::clone(driver);
344 let config = Arc::clone(config);
345 let cow_manager = Arc::clone(cow_manager);
346 let snapshots = Arc::clone(snapshots);
347 let snapshot_id = snapshot_id.to_owned();
348 tokio::spawn(async move {
349 let staged = match snapshots.find_by_id(&snapshot_id) {
351 Ok(meta) => prepare_slot(driver.as_ref(), &config, &cow_manager, &meta).await,
352 Err(error) => Err(error.into()),
353 };
354 match staged {
355 Ok(slot) => match pool.offer(&snapshot_id, slot) {
356 None => debug!(snapshot_id, "restore slot pre-warmed"),
357 Some(rejected) => {
358 let slot_id = rejected.slot_id.clone();
360 if let Err(error) = destroy_slot(&cow_manager, rejected).await {
361 warn!(
362 slot_id,
363 error = %error,
364 "pool slot teardown incomplete; the startup sweep will retry"
365 );
366 }
367 }
368 },
369 Err(error) => {
370 pool.abandon_fill(&snapshot_id);
371 warn!(snapshot_id, error = %error, "restore slot pre-warm failed");
372 }
373 }
374 });
375 }
376 }
377}
378
379impl ComputerManager {
380 pub(super) async fn drain_pool(&self, snapshot_id: Option<&str>) {
383 drain_pool_slots(&self.pool, &self.cow_manager, snapshot_id).await;
384 }
385
386 pub async fn shutdown(&self) {
391 self.drain_pool(None).await;
392 }
393}