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