blackhole 0.20.2

...to throw your threads into.
Documentation
/*
==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--

Black Hole

Copyright (C) 2019-2020, 2022-2023  Anonymous

There are several releases over multiple years,
they are listed as ranges, such as: "2019-2020".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
*/

use std::{
    io::{Error, ErrorKind},
    sync::{
        Arc,
        atomic::{self, AtomicUsize},
    },
    thread,
    time::{Duration, Instant},
};

use blackhole::{ActiveLimit, BlackHole, Job, OneTime, Result};

const ATOMIC_ORDERING: atomic::Ordering = atomic::Ordering::Relaxed;
const DELAY_AS_MILLIS: u64 = 500;

struct Task {
    index: usize,
    data: Arc<AtomicUsize>,
}

impl Job for Task {

    fn run(&mut self) -> Option<Box<dyn Job>> {
        if self.index == 0 {
            panic!("Test panicking...");
        }
        match self.index % 2 {
            0 => {
                println!("#{} -> {}", self.index, self.data.load(ATOMIC_ORDERING));
                None
            },
            _ => {
                let last = self.data.fetch_add(self.index * 2 - 1, ATOMIC_ORDERING);
                println!("{} + {} = {}", last, self.index, last + self.index);
                thread::sleep(Duration::from_millis(DELAY_AS_MILLIS));
                Some(Box::new(Task { index: self.index + 1, data: self.data.clone() }))
            }
        }
    }

}

#[test]
fn tests() -> Result<()> {
    const ACTIVE_LIMIT: usize = 4;
    const QUEUE_LIMIT: usize = 100;
    const COUNT: usize = ACTIVE_LIMIT * 2 + ACTIVE_LIMIT - 1;

    let black_hole = BlackHole::make_with_active_limit(ActiveLimit::try_from(ACTIVE_LIMIT).unwrap(), QUEUE_LIMIT)?;
    let data = Arc::new(AtomicUsize::new(0));
    let start_time = Instant::now();
    for i in 0..=COUNT {
        let data = data.clone();
        black_hole.throw(Task { index: i, data })?;
    }
    black_hole.escape_on_idle()?;

    let expected_max_runtime = Duration::from_millis({
        let active_limit = u64::try_from(ACTIVE_LIMIT).unwrap();
        let left = u64::try_from(COUNT - ACTIVE_LIMIT).unwrap();
        DELAY_AS_MILLIS + left / active_limit * DELAY_AS_MILLIS + match left % active_limit {
            0 => DELAY_AS_MILLIS,
            _ => DELAY_AS_MILLIS * 2
        }
    });
    println!("Expected max runtime: {:?}", &expected_max_runtime);

    let runtime = Instant::now() - start_time;
    println!("Runtime: {:?}", &runtime);

    assert_eq!(data.load(ATOMIC_ORDERING), COUNT * (COUNT + 1) / 2);

    if runtime < expected_max_runtime {
        Ok(())
    } else {
        Err(Error::new(ErrorKind::Other, "::escape_on_idle() took too much time"))
    }
}

#[test]
#[ignore]
fn stressing_black_hole() -> Result<()> {
    const QUEUE_LIMIT: usize = 100_000;
    static mut BYTES: [u8; QUEUE_LIMIT] = [0; QUEUE_LIMIT];

    let black_hole = BlackHole::make_with_active_limit(blackhole::available_parallelism()?, QUEUE_LIMIT)?;
    for i in 0..QUEUE_LIMIT {
        assert!(black_hole.throw(OneTime::new(move || unsafe { BYTES[i] = 1; }))?.is_none());
    }
    black_hole.escape_on_idle()?;

    unsafe {
        for i in 0..BYTES.len() {
            assert_eq!(&BYTES[i], &1);
        }
    }

    Ok(())
}