Skip to content
Open
4 changes: 4 additions & 0 deletions config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ enabled = true
startup_prewarm = true
# Maximum concurrent process creations in one refill batch.
fill_concurrency = 4
# Seconds without warm-process acquisitions before the pool decays its refill
# target back to the low watermark and drains the excess warm processes
# (0 = never decay).
idle_ttl_secs = 600

[memory_snapshot]
# Generated OverlayBD config dedicated to memory snapshot devices.
Expand Down
726 changes: 660 additions & 66 deletions crates/warm-pool/src/lib.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/src/configuration/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ Component sections:
| `[pool.firecracker]` | `maintenance_enabled` | boolean | `true` | Enable the background Firecracker process maintenance worker |
| `[pool.firecracker]` | `startup_prewarm` | boolean | `true` | Spawn warm Firecracker entries up to the low watermark during server startup |
| `[pool.firecracker]` | `fill_concurrency` | integer | `4` | Maximum number of warm Firecracker processes created concurrently by one maintenance refill batch |
| `[pool.firecracker]` | `idle_ttl_secs` | integer | `600` | Seconds without warm-process acquisitions before the refill target decays back to the low watermark and excess warm processes are drained (`0` disables decay) |

Validation rules:

Expand Down
12 changes: 12 additions & 0 deletions src/cfg.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::time::Duration;

pub(crate) mod image;
pub(crate) mod network;
Expand Down Expand Up @@ -215,6 +216,12 @@ pub struct FirecrackerProcessPoolConfig {
pub startup_prewarm: bool,
#[config(default = 4usize)]
pub fill_concurrency: usize,
/// Seconds without pool acquisitions before the geometric fill target
/// decays back to the low watermark and excess warm processes are drained.
/// 0 disables decay and keeps the fill target ratcheted for the process
/// lifetime.
#[config(default = 600u64)]
pub idle_ttl_secs: u64,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -764,6 +771,9 @@ impl AppConfig {
high_watermark: self.pool.high_watermark,
maintenance_enabled: pool.enabled && pool.maintenance_enabled,
startup_prewarm: pool.startup_prewarm,
// The network slot pool holds no processes; keep its historical
// ratchet behavior without idle decay.
idle_ttl: None,
}
}

Expand All @@ -781,6 +791,7 @@ impl AppConfig {
// reusable device shape is image/size dependent.
maintenance_enabled: false,
startup_prewarm: pool.startup_prewarm,
idle_ttl: None,
})
}

Expand All @@ -797,6 +808,7 @@ impl AppConfig {
high_watermark: self.pool.high_watermark,
maintenance_enabled: pool.maintenance_enabled,
startup_prewarm: pool.startup_prewarm,
idle_ttl: (pool.idle_ttl_secs > 0).then(|| Duration::from_secs(pool.idle_ttl_secs)),
},
fill_concurrency: pool.fill_concurrency,
})
Expand Down
7 changes: 5 additions & 2 deletions src/sandbox/firecracker/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,16 @@ impl FirecrackerPool {
}

fn run_maintenance_cycle(&self) -> Result<()> {
match self.pool.compute_maintenance_action(self.pool.len()) {
match self.pool.compute_maintenance_action() {
PoolMaintenanceAction::Fill(to_fill) => {
self.runtime.block_on(self.fill_warm_entries(to_fill))?;
}
PoolMaintenanceAction::Drain(to_drain) => {
for _ in 0..to_drain {
let Some(warm) = self.pool.try_drain_one() else {
// Re-validate each removal at execution time: the computed
// drain count can be stale if an acquisition cancelled an
// in-progress decay or the pool length changed.
let Some(warm) = self.pool.try_drain_one_for_maintenance() else {
Comment thread
epicvinny marked this conversation as resolved.
break;
};
self.cleanup_warm_blocking(warm, false)?;
Expand Down
39 changes: 31 additions & 8 deletions src/sandbox/network/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ impl NetworkManager {
high_watermark,
maintenance_enabled,
startup_prewarm: true,
idle_ttl: None,
},
address_plan: NetworkAddressPlan::default(),
netns_dir: std::env::temp_dir().join("aenv-network-tests/netns"),
Expand Down Expand Up @@ -287,12 +288,12 @@ impl NetworkManager {
}

#[cfg(test)]
fn compute_maintenance_action(&self, pool_len: usize) -> PoolMaintenanceAction {
self.pool.compute_maintenance_action(pool_len)
fn compute_maintenance_action(&self) -> PoolMaintenanceAction {
self.pool.compute_maintenance_action()
}

fn run_pool_maintenance_cycle(&self) -> Result<()> {
let action = self.pool.compute_maintenance_action(self.pool.len());
let action = self.pool.compute_maintenance_action();

match action {
PoolMaintenanceAction::Fill(to_fill) => {
Expand Down Expand Up @@ -326,7 +327,10 @@ impl NetworkManager {
}
PoolMaintenanceAction::Drain(to_drain) => {
for _ in 0..to_drain {
let maybe_slot = self.pool.try_drain_one();
// Re-validate each removal at execution time: the computed
// drain count can be stale if an acquisition cancelled an
// in-progress decay or the pool length changed.
let maybe_slot = self.pool.try_drain_one_for_maintenance();
let Some(slot) = maybe_slot else {
break;
};
Expand Down Expand Up @@ -897,25 +901,44 @@ mod tests {
#[test]
fn maintenance_action_fills_to_current_target() {
let manager = NetworkManager::new(true, 4, 10);
// Two warm slots below the low watermark: fill the missing two.
for idx in 1..=2u32 {
manager.allocated.insert(idx as usize);
manager.pool.release(test_slot(idx)).unwrap();
}
assert_eq!(
manager.compute_maintenance_action(2),
manager.compute_maintenance_action(),
PoolMaintenanceAction::Fill(2)
);
// Seven warm slots within the watermarks: no action.
for idx in 3..=7u32 {
manager.allocated.insert(idx as usize);
manager.pool.release(test_slot(idx)).unwrap();
}
assert_eq!(
manager.compute_maintenance_action(7),
manager.compute_maintenance_action(),
PoolMaintenanceAction::Idle
);
}

#[test]
fn maintenance_action_drains_above_high_watermark() {
let manager = NetworkManager::new(true, 2, 4);
// Eight warm slots above the high watermark: drain the excess.
for idx in 1..=8u32 {
manager.allocated.insert(idx as usize);
manager.pool.release(test_slot(idx)).unwrap();
}
assert_eq!(
manager.compute_maintenance_action(8),
manager.compute_maintenance_action(),
PoolMaintenanceAction::Drain(4)
);
// At the high watermark: no action.
for _ in 0..4 {
assert!(manager.pool.try_drain_one().is_some());
}
assert_eq!(
manager.compute_maintenance_action(4),
manager.compute_maintenance_action(),
PoolMaintenanceAction::Idle
);
}
Expand Down
2 changes: 2 additions & 0 deletions storage/ublk-daemon/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ fn load_pool_config(
.startup_prewarm
.or_else(|| pool.and_then(|pool| pool.startup_prewarm))
.unwrap_or(true),
idle_ttl: None,
}))
}

Expand Down Expand Up @@ -232,6 +233,7 @@ fn default_pool_config() -> warm_pool::PoolConfig {
high_watermark: 64,
maintenance_enabled: false,
startup_prewarm: true,
idle_ttl: None,
}
}

Expand Down
7 changes: 3 additions & 4 deletions storage/ublk-daemon/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1442,7 +1442,7 @@ fn schedule_idle_pool_refill(
pool.refill_inflight.store(false, Ordering::Release);

if matches!(
pool.idle.compute_maintenance_action(pool.idle.len()),
pool.idle.compute_maintenance_action(),
PoolMaintenanceAction::Fill(_)
) {
schedule_idle_pool_refill(pool, ctrl_ring, virtual_size);
Expand All @@ -1455,9 +1455,7 @@ async fn refill_idle_pool(
ctrl_ring: IoRingHandle<io_uring::squeue::Entry128>,
virtual_size: u64,
) -> Result<()> {
let PoolMaintenanceAction::Fill(to_create) =
pool.idle.compute_maintenance_action(pool.idle.len())
else {
let PoolMaintenanceAction::Fill(to_create) = pool.idle.compute_maintenance_action() else {
return Ok(());
};

Expand Down Expand Up @@ -1658,6 +1656,7 @@ mod tests {
high_watermark: 1,
maintenance_enabled: false,
startup_prewarm: false,
idle_ttl: None,
}
}

Expand Down
Loading