Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/runtime/src/pool/warm_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,17 @@ impl WarmPool {
pub async fn release(&self, vm: VmManager) -> Result<()> {
let mut idle = self.idle.lock().await;

// Don't return a VM to a pool that is shutting down: drain_idle has (or
// soon will have) cleared `idle` and won't run again, so a push here leaks
// the VM (no Drop reaper). Checked under the idle lock so it is atomic with
// a concurrent drain_idle. Destroy the VM instead.
if *self.shutdown_rx.borrow() {
drop(idle);
let mut vm = vm;
vm.destroy().await?;
return Ok(());
}

if idle.len() >= self.config.max_size {
// Pool is full — destroy the VM
drop(idle); // Release lock before async destroy
Expand Down Expand Up @@ -809,16 +820,22 @@ impl WarmPool {
// booting, drain_idle has already cleared
// `idle` and will not run again, so a VM
// pushed now leaks (no Drop reaper). Destroy
// it instead.
// it instead. Acquire the idle lock FIRST and
// re-check shutdown UNDER it: drain_idle drains
// while holding this same lock (always after
// signal_shutdown), so the check-and-push is
// atomic against it — closing the TOCTOU window
// that an unlocked `borrow()` check left open.
let mut pool = idle.lock().await;
if *shutdown_rx.borrow() {
drop(pool);
tracing::debug!(
box_id = %box_id,
"Pool shutting down mid-replenish; destroying freshly-booted VM"
);
let _ = vm.destroy_with_timeout(2000).await;
continue;
}
let mut pool = idle.lock().await;
pool.push(WarmVm {
vm,
created_at: Instant::now(),
Expand Down
Loading