oxana 2.0.1

A simple & fast job queue system.
Documentation
use std::collections::HashMap;
use std::sync::{Arc, Mutex, MutexGuard};

use tokio::sync::{Mutex as TokioMutex, Notify};

use crate::queue::{QueueRuntimeConfig, QueueState};

pub struct QueueControlsMap {
    inner: TokioMutex<HashMap<String, Arc<QueueControl>>>,
}

impl QueueControlsMap {
    pub fn new() -> Self {
        Self {
            inner: TokioMutex::new(HashMap::new()),
        }
    }

    pub async fn get_or_create(
        &self,
        key: String,
        config: QueueRuntimeConfig,
    ) -> Arc<QueueControl> {
        let mut map = self.inner.lock().await;
        Arc::clone(
            map.entry(key)
                .or_insert_with(|| Arc::new(QueueControl::new(config))),
        )
    }

    pub async fn busy_count(&self) -> usize {
        let map = self.inner.lock().await;
        map.values().map(|control| control.active_count()).sum()
    }
}

pub struct QueueControl {
    inner: Mutex<QueueControlState>,
    notify: Notify,
}

struct QueueControlState {
    concurrency: usize,
    active: usize,
    queue_state: QueueState,
}

pub struct QueuePermit {
    control: Arc<QueueControl>,
}

impl std::fmt::Debug for QueuePermit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("QueuePermit").finish_non_exhaustive()
    }
}

impl QueueControl {
    fn new(config: QueueRuntimeConfig) -> Self {
        Self {
            inner: Mutex::new(QueueControlState {
                concurrency: config.concurrency.unwrap_or(1),
                active: 0,
                queue_state: config.state,
            }),
            notify: Notify::new(),
        }
    }

    pub async fn acquire(self: &Arc<Self>) -> QueuePermit {
        loop {
            let notified = self.notify.notified();
            {
                let mut state = self.lock_state();
                if state.queue_state.is_active() && state.active < state.concurrency {
                    state.active += 1;
                    return QueuePermit {
                        control: Arc::clone(self),
                    };
                }
            }
            notified.await;
        }
    }

    pub fn apply_config(&self, config: QueueRuntimeConfig) {
        {
            let mut state = self.lock_state();
            if let Some(concurrency) = config.concurrency {
                state.concurrency = concurrency;
            }
            state.queue_state = config.state;
        }
        self.notify.notify_waiters();
    }

    fn active_count(&self) -> usize {
        self.lock_state().active
    }

    fn release(&self) {
        {
            let mut state = self.lock_state();
            state.active = state.active.saturating_sub(1);
        }
        self.notify.notify_waiters();
    }

    fn lock_state(&self) -> MutexGuard<'_, QueueControlState> {
        self.inner.lock().unwrap_or_else(|err| err.into_inner())
    }
}

impl Drop for QueuePermit {
    fn drop(&mut self) {
        self.control.release();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tokio::time::{Duration, timeout};

    #[tokio::test]
    async fn paused_control_blocks_until_active() {
        let control = Arc::new(QueueControl::new(QueueRuntimeConfig {
            concurrency: Some(1),
            state: QueueState::Paused,
        }));

        assert!(
            timeout(Duration::from_millis(20), control.acquire())
                .await
                .is_err()
        );

        control.apply_config(QueueRuntimeConfig::new(1));

        assert!(
            timeout(Duration::from_millis(20), control.acquire())
                .await
                .is_ok()
        );
    }

    #[tokio::test]
    async fn reducing_concurrency_waits_for_active_permits_to_finish() {
        let control = Arc::new(QueueControl::new(QueueRuntimeConfig::new(2)));

        let first = control.acquire().await;
        let second = control.acquire().await;

        control.apply_config(QueueRuntimeConfig::new(1));

        assert!(
            timeout(Duration::from_millis(20), control.acquire())
                .await
                .is_err()
        );

        drop(second);
        assert!(
            timeout(Duration::from_millis(20), control.acquire())
                .await
                .is_err()
        );

        drop(first);
        assert!(
            timeout(Duration::from_millis(20), control.acquire())
                .await
                .is_ok()
        );
    }
}