use super::*;
use core::cell::Cell;
use embassy_sync::blocking_mutex::Mutex;
use embassy_time::{Duration, Instant};
#[derive(Clone, Copy)]
pub struct PoolStats {
pub running: u8,
pub busy: u8,
pub min: u8,
pub max: u8,
}
impl PoolStats {
pub fn idle(&self) -> u8 {
self.running.saturating_sub(self.busy)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ScaleAction {
None,
Grow,
Shrink,
}
pub trait ScalingPolicy {
fn decide(&self, stats: PoolStats, now: Instant) -> ScaleAction;
fn deferred_until(&self) -> Option<Instant> {
None
}
}
pub struct DeferredShrink {
cooldown: Duration,
pending: Mutex<CriticalSectionRawMutex, Cell<Option<Instant>>>,
}
impl DeferredShrink {
pub const fn new(cooldown: Duration) -> Self {
Self {
cooldown,
pending: Mutex::new(Cell::new(None)),
}
}
}
impl ScalingPolicy for DeferredShrink {
fn decide(&self, s: PoolStats, now: Instant) -> ScaleAction {
if s.idle() == 0 && s.running < s.max {
self.pending.lock(|p| p.set(None));
return ScaleAction::Grow;
}
if s.idle() >= 2 && s.running > s.min {
match self.pending.lock(|p| p.get()) {
None => {
self.pending.lock(|p| p.set(Some(now + self.cooldown)));
ScaleAction::None
}
Some(deadline) if now >= deadline => {
let next = (s.idle() >= 3).then(|| now + self.cooldown);
self.pending.lock(|p| p.set(next));
ScaleAction::Shrink
}
Some(_) => ScaleAction::None, }
} else {
self.pending.lock(|p| p.set(None));
ScaleAction::None
}
}
fn deferred_until(&self) -> Option<Instant> {
self.pending.lock(|p| p.get())
}
}
pub enum PoolAction {
None,
Start(&'static TaskNode),
Stop(&'static TaskNode),
}
pub struct ElasticPool<P: ScalingPolicy> {
pub nodes: &'static [&'static TaskNode],
pub min: u8,
pub max: u8,
pub policy: P,
}
impl<P: ScalingPolicy> ElasticPool<P> {
fn stats(&self) -> PoolStats {
let (running, busy) = self.nodes.iter().fold((0u8, 0u8), |(r, b), n| {
if n.is_running() {
(r + 1, b + n.is_busy() as u8)
} else {
(r, b)
}
});
PoolStats {
running,
busy,
min: self.min,
max: self.max,
}
}
}
pub trait Pool: Sync {
fn evaluate(&self, now: Instant) -> PoolAction;
fn deferred_until(&self) -> Option<Instant>;
fn members(&self) -> &'static [&'static TaskNode];
}
impl<P: ScalingPolicy + Sync> Pool for ElasticPool<P> {
fn evaluate(&self, now: Instant) -> PoolAction {
match self.policy.decide(self.stats(), now) {
ScaleAction::Grow => self
.nodes
.iter()
.find(|n| matches!(n.mode, Mode::OnDemand) && !n.is_running() && !n.is_disabled())
.map_or(PoolAction::None, |n| PoolAction::Start(n)),
ScaleAction::Shrink => self
.nodes
.iter()
.find(|n| matches!(n.mode, Mode::OnDemand) && n.is_running() && !n.is_busy())
.map_or(PoolAction::None, |n| PoolAction::Stop(n)),
ScaleAction::None => PoolAction::None,
}
}
fn deferred_until(&self) -> Option<Instant> {
self.policy.deferred_until()
}
fn members(&self) -> &'static [&'static TaskNode] {
self.nodes
}
}
async fn drive_pools<const N: usize>(
pools: &[&dyn Pool],
sup: &Supervisor<N>,
spawner: Spawner,
) -> Option<Instant> {
let now = Instant::now();
let mut next: Option<Instant> = None;
for pool in pools {
match pool.evaluate(now) {
PoolAction::Start(n) => {
if sup.deps_running(n) {
let _ = sup.start_node(n, spawner).await;
}
}
PoolAction::Stop(n) => sup.stop_node(n).await,
PoolAction::None => {}
}
if let Some(d) = pool.deferred_until() {
next = Some(next.map_or(d, |c| c.min(d)));
}
}
next
}
async fn deadline_timer(deadline: Option<Instant>) {
match deadline {
Some(t) => Timer::at(t).await,
None => core::future::pending::<()>().await,
}
}
impl<const N: usize> Supervisor<N> {
pub async fn run_pools(&self, spawner: Spawner) {
loop {
let next = drive_pools(self.pools, self, spawner).await;
select(wait_scale(), deadline_timer(next)).await;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Mode, TaskNode};
use embassy_executor::{SpawnError, Spawner};
use embassy_time::{Duration, Instant};
fn noop(_: Spawner) -> Result<(), SpawnError> {
Ok(())
}
fn t0() -> Instant {
Instant::from_ticks(0)
}
fn stats(running: u8, busy: u8, min: u8, max: u8) -> PoolStats {
PoolStats {
running,
busy,
min,
max,
}
}
#[test]
fn grows_when_saturated_below_max() {
let p = DeferredShrink::new(Duration::from_secs(4));
assert!(p.decide(stats(2, 2, 1, 4), t0()) == ScaleAction::Grow);
}
#[test]
fn does_not_grow_at_ceiling() {
let p = DeferredShrink::new(Duration::from_secs(4));
assert!(p.decide(stats(4, 4, 1, 4), t0()) == ScaleAction::None);
}
#[test]
fn defers_then_shrinks_after_cooldown() {
let cooldown = Duration::from_secs(4);
let p = DeferredShrink::new(cooldown);
let now = t0();
assert!(p.decide(stats(3, 1, 1, 4), now) == ScaleAction::None);
assert_eq!(p.deferred_until(), Some(now + cooldown));
assert!(p.decide(stats(3, 1, 1, 4), now + Duration::from_secs(2)) == ScaleAction::None);
assert!(p.decide(stats(3, 1, 1, 4), now + cooldown) == ScaleAction::Shrink);
}
#[test]
fn cancels_pending_shrink_when_surplus_disappears() {
let cooldown = Duration::from_secs(4);
let p = DeferredShrink::new(cooldown);
let now = t0();
assert!(p.decide(stats(3, 1, 1, 4), now) == ScaleAction::None); assert!(p.deferred_until().is_some());
assert!(p.decide(stats(2, 1, 1, 4), now + Duration::from_secs(1)) == ScaleAction::None);
assert_eq!(p.deferred_until(), None);
}
#[test]
fn grow_clears_pending_shrink() {
let cooldown = Duration::from_secs(4);
let p = DeferredShrink::new(cooldown);
let now = t0();
assert!(p.decide(stats(3, 1, 1, 4), now) == ScaleAction::None); assert!(p.deferred_until().is_some());
assert!(p.decide(stats(3, 3, 1, 4), now + Duration::from_secs(1)) == ScaleAction::Grow);
assert_eq!(p.deferred_until(), None);
}
#[test]
fn pool_grows_a_down_member_when_saturated() {
static N0: TaskNode = TaskNode::new("p0", Mode::Terminate, Some(noop), false);
static N1: TaskNode = TaskNode::new("p1", Mode::OnDemand, Some(noop), false);
static N2: TaskNode = TaskNode::new("p2", Mode::OnDemand, Some(noop), false);
static POOL: ElasticPool<DeferredShrink> = ElasticPool {
nodes: &[&N0, &N1, &N2],
min: 1,
max: 3,
policy: DeferredShrink::new(Duration::from_secs(4)),
};
N0.set_running(true);
N0.mark_busy();
match POOL.evaluate(t0()) {
PoolAction::Start(n) => assert!(core::ptr::eq(n, &N1), "first down OnDemand member"),
_ => panic!("expected Start"),
}
}
#[test]
fn pool_shrinks_an_idle_member_after_cooldown() {
static N0: TaskNode = TaskNode::new("p0", Mode::Terminate, Some(noop), false);
static N1: TaskNode = TaskNode::new("p1", Mode::OnDemand, Some(noop), false);
static N2: TaskNode = TaskNode::new("p2", Mode::OnDemand, Some(noop), false);
static POOL: ElasticPool<DeferredShrink> = ElasticPool {
nodes: &[&N0, &N1, &N2],
min: 1,
max: 3,
policy: DeferredShrink::new(Duration::from_secs(4)),
};
N0.set_running(true);
N1.set_running(true);
N2.set_running(true);
let now = t0();
assert!(
matches!(POOL.evaluate(now), PoolAction::None),
"first tick arms cooldown"
);
match POOL.evaluate(now + Duration::from_secs(4)) {
PoolAction::Stop(n) => {
assert!(
core::ptr::eq(n, &N1) || core::ptr::eq(n, &N2),
"an idle OnDemand member"
);
}
_ => panic!("expected Stop"),
}
}
#[test]
fn pool_does_not_grow_a_disabled_member() {
static N0: TaskNode = TaskNode::new("p0", Mode::Terminate, Some(noop), false);
static N1: TaskNode = TaskNode::new("p1", Mode::OnDemand, Some(noop), false);
static POOL: ElasticPool<DeferredShrink> = ElasticPool {
nodes: &[&N0, &N1],
min: 1,
max: 2,
policy: DeferredShrink::new(Duration::from_secs(4)),
};
N0.set_running(true);
N0.mark_busy(); N1.set_disabled(true); assert!(matches!(POOL.evaluate(t0()), PoolAction::None));
}
}