acropolis 0.2.1

UNDER CONSTRUCTION: A safe local-first Rust Cardano node scaffold.
Documentation
use std::fmt;
use std::time::Duration;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SchedulerConfig {
    pub worker_pool_size: usize,
    pub task_queue_size: usize,
}

impl Default for SchedulerConfig {
    fn default() -> Self {
        Self {
            worker_pool_size: 10,
            task_queue_size: 100,
        }
    }
}

pub struct ScheduledTask {
    interval: u64,
    ticks_since_last_run: u64,
    running: bool,
    task: Box<dyn FnMut() + Send>,
    on_busy: Option<Box<dyn FnMut() + Send>>,
}

pub struct ManualScheduler {
    interval: Duration,
    tasks: Vec<ScheduledTask>,
}

impl ManualScheduler {
    pub fn new(interval: Duration) -> Self {
        Self {
            interval,
            tasks: Vec::new(),
        }
    }

    pub fn interval(&self) -> Duration {
        self.interval
    }

    pub fn change_interval(&mut self, interval: Duration) -> Result<(), SchedulerError> {
        if interval.is_zero() {
            return Err(SchedulerError::NonPositiveInterval);
        }
        self.interval = interval;
        Ok(())
    }

    pub fn register(
        &mut self,
        interval_ticks: u64,
        task: impl FnMut() + Send + 'static,
        on_busy: Option<impl FnMut() + Send + 'static>,
    ) -> Result<(), SchedulerError> {
        if interval_ticks == 0 {
            return Err(SchedulerError::NonPositiveTaskInterval);
        }
        self.tasks.push(ScheduledTask {
            interval: interval_ticks,
            ticks_since_last_run: 0,
            running: false,
            task: Box::new(task),
            on_busy: on_busy.map(|f| Box::new(f) as Box<dyn FnMut() + Send>),
        });
        Ok(())
    }

    pub fn tick(&mut self) -> usize {
        let mut ran = 0;
        for task in &mut self.tasks {
            task.ticks_since_last_run += 1;
            if task.ticks_since_last_run < task.interval {
                continue;
            }
            task.ticks_since_last_run = 0;
            if task.running {
                if let Some(on_busy) = &mut task.on_busy {
                    on_busy();
                }
                continue;
            }
            task.running = true;
            (task.task)();
            task.running = false;
            ran += 1;
        }
        ran
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchedulerError {
    NonPositiveInterval,
    NonPositiveTaskInterval,
}

impl fmt::Display for SchedulerError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NonPositiveInterval => f.write_str("scheduler interval must be positive"),
            Self::NonPositiveTaskInterval => {
                f.write_str("scheduled task interval must be positive")
            }
        }
    }
}

impl std::error::Error for SchedulerError {}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{Arc, Mutex};

    #[test]
    fn scheduler_runs_tasks_at_tick_intervals() {
        let count = Arc::new(Mutex::new(0));
        let seen = Arc::clone(&count);
        let mut scheduler = ManualScheduler::new(Duration::from_secs(1));
        scheduler
            .register(2, move || *seen.lock().unwrap() += 1, None::<fn()>)
            .unwrap();

        assert_eq!(scheduler.tick(), 0);
        assert_eq!(scheduler.tick(), 1);
        assert_eq!(*count.lock().unwrap(), 1);
    }

    #[test]
    fn scheduler_rejects_zero_interval() {
        let mut scheduler = ManualScheduler::new(Duration::from_secs(1));
        assert_eq!(
            scheduler.change_interval(Duration::ZERO),
            Err(SchedulerError::NonPositiveInterval)
        );
    }
}