use std::collections::HashMap;
use std::sync::Mutex;
use crate::sandbox::pool::PreparedSlot;
const MAX_POOLED_SNAPSHOTS: usize = 2;
pub struct FillPlan<S> {
pub spawn: usize,
pub evicted: Vec<S>,
}
pub struct SlotPool<S = PreparedSlot> {
inner: Mutex<PoolInner<S>>,
}
struct PoolInner<S> {
ready: HashMap<String, Vec<S>>,
filling: HashMap<String, usize>,
lru: Vec<String>,
}
impl<S> Default for SlotPool<S> {
fn default() -> Self {
Self {
inner: Mutex::new(PoolInner {
ready: HashMap::new(),
filling: HashMap::new(),
lru: Vec::new(),
}),
}
}
}
impl<S> SlotPool<S> {
pub fn claim(&self, snapshot_id: &str) -> Option<S> {
let mut inner = self.inner.lock().unwrap();
let slot = inner.ready.get_mut(snapshot_id)?.pop()?;
inner.touch(snapshot_id);
inner.prune(snapshot_id);
Some(slot)
}
pub fn begin_fill(&self, snapshot_id: &str, target: usize) -> FillPlan<S> {
if target == 0 {
return FillPlan {
spawn: 0,
evicted: Vec::new(),
};
}
let mut inner = self.inner.lock().unwrap();
inner.touch(snapshot_id);
let ready = inner.ready.get(snapshot_id).map_or(0, Vec::len);
let filling = inner.filling.get(snapshot_id).copied().unwrap_or(0);
let spawn = target.saturating_sub(ready + filling);
if spawn > 0 {
*inner.filling.entry(snapshot_id.to_owned()).or_default() += spawn;
}
let mut evicted = Vec::new();
while inner.lru.len() > MAX_POOLED_SNAPSHOTS {
let stale = inner.lru.remove(0);
evicted.extend(inner.ready.remove(&stale).unwrap_or_default());
}
FillPlan { spawn, evicted }
}
pub fn offer(&self, snapshot_id: &str, slot: S) -> Option<S> {
let mut inner = self.inner.lock().unwrap();
inner.finish_fill(snapshot_id);
if inner.lru.iter().any(|id| id == snapshot_id) {
inner
.ready
.entry(snapshot_id.to_owned())
.or_default()
.push(slot);
None
} else {
Some(slot)
}
}
pub fn abandon_fill(&self, snapshot_id: &str) {
let mut inner = self.inner.lock().unwrap();
inner.finish_fill(snapshot_id);
inner.prune(snapshot_id);
}
pub fn drain(&self, snapshot_id: Option<&str>) -> Vec<S> {
let mut inner = self.inner.lock().unwrap();
match snapshot_id {
Some(id) => {
inner.lru.retain(|entry| entry != id);
inner.ready.remove(id).unwrap_or_default()
}
None => {
inner.lru.clear();
inner.ready.drain().flat_map(|(_, slots)| slots).collect()
}
}
}
}
impl<S> PoolInner<S> {
fn touch(&mut self, snapshot_id: &str) {
self.lru.retain(|id| id != snapshot_id);
self.lru.push(snapshot_id.to_owned());
}
fn prune(&mut self, snapshot_id: &str) {
let ready = self.ready.get(snapshot_id).is_some_and(|s| !s.is_empty());
let filling = self.filling.get(snapshot_id).copied().unwrap_or(0) > 0;
if !ready && !filling {
self.ready.remove(snapshot_id);
self.lru.retain(|id| id != snapshot_id);
}
}
fn finish_fill(&mut self, snapshot_id: &str) {
if let Some(count) = self.filling.get_mut(snapshot_id) {
*count = count.saturating_sub(1);
if *count == 0 {
self.filling.remove(snapshot_id);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fill_and_offer(pool: &SlotPool<u32>, snapshot: &str, target: usize, base: u32) {
let plan = pool.begin_fill(snapshot, target);
assert!(plan.evicted.is_empty(), "unexpected eviction while filling");
for offset in 0..plan.spawn {
assert!(
pool.offer(snapshot, base + u32::try_from(offset).unwrap())
.is_none()
);
}
}
#[test]
fn claims_are_keyed_by_snapshot_id() {
let pool = SlotPool::<u32>::default();
fill_and_offer(&pool, "a", 1, 10);
assert_eq!(pool.claim("other"), None);
assert_eq!(pool.claim("a"), Some(10));
assert_eq!(pool.claim("a"), None);
}
#[test]
fn refill_accounts_for_ready_and_in_flight_slots() {
let pool = SlotPool::<u32>::default();
let plan = pool.begin_fill("a", 2);
assert_eq!(plan.spawn, 2);
assert_eq!(pool.begin_fill("a", 2).spawn, 0);
assert!(pool.offer("a", 10).is_none());
assert!(pool.offer("a", 11).is_none());
assert_eq!(pool.begin_fill("a", 2).spawn, 0);
assert_eq!(pool.claim("a"), Some(11));
assert_eq!(pool.begin_fill("a", 2).spawn, 1);
pool.abandon_fill("a");
assert_eq!(pool.begin_fill("a", 2).spawn, 1);
}
#[test]
fn pool_size_zero_disables_pooling() {
let pool = SlotPool::<u32>::default();
let plan = pool.begin_fill("a", 0);
assert_eq!(plan.spawn, 0);
assert_eq!(plan.evicted, Vec::<u32>::new());
assert_eq!(pool.claim("a"), None);
}
#[test]
fn a_third_snapshot_evicts_the_least_recently_restored() {
let pool = SlotPool::<u32>::default();
fill_and_offer(&pool, "a", 1, 10);
fill_and_offer(&pool, "b", 1, 20);
let plan = pool.begin_fill("c", 1);
assert_eq!(plan.spawn, 1);
assert_eq!(plan.evicted, vec![10], "a's slot must be handed back");
assert_eq!(pool.claim("a"), None);
assert_eq!(pool.claim("b"), Some(20));
}
#[test]
fn claiming_refreshes_recency() {
let pool = SlotPool::<u32>::default();
fill_and_offer(&pool, "a", 2, 10);
fill_and_offer(&pool, "b", 1, 20);
assert_eq!(pool.claim("a"), Some(11));
let plan = pool.begin_fill("c", 1);
assert_eq!(plan.evicted, vec![20]);
assert_eq!(pool.claim("b"), None);
assert_eq!(pool.claim("a"), Some(10));
}
#[test]
fn late_offers_for_an_evicted_snapshot_are_rejected() {
let pool = SlotPool::<u32>::default();
let plan = pool.begin_fill("a", 1);
assert_eq!(plan.spawn, 1);
fill_and_offer(&pool, "b", 1, 20);
fill_and_offer(&pool, "c", 1, 30);
assert_eq!(pool.offer("a", 10), Some(10), "must come back for teardown");
assert_eq!(pool.claim("a"), None);
}
#[test]
fn drain_scopes_to_one_snapshot_or_all() {
let pool = SlotPool::<u32>::default();
fill_and_offer(&pool, "a", 2, 10);
fill_and_offer(&pool, "b", 1, 20);
let mut drained = pool.drain(Some("a"));
drained.sort_unstable();
assert_eq!(drained, vec![10, 11]);
assert_eq!(pool.claim("a"), None);
let plan = pool.begin_fill("a", 1);
assert_eq!(plan.spawn, 1);
assert_eq!(pool.drain(Some("a")), Vec::<u32>::new());
assert_eq!(pool.offer("a", 12), Some(12));
assert_eq!(pool.drain(None), vec![20]);
assert_eq!(pool.claim("b"), None);
}
}