Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing your executors

Rust: paused time

The runtime uses tokio's clock, so tests with paused time run schedules, timeouts and backoffs instantly and deterministically:

#[tokio::test(start_paused = true)]
async fn retries_then_succeeds() {
    let s = Scheduler::<u64, u64>::builder()
        .pool(Pool::fixed("default", 1).executor_fn(|ctx, x: Arc<u64>| async move {
            if ctx.attempt < 3 { Err(TaskError::retryable("flaky")) } else { Ok(*x) }
        }))
        .build()
        .await
        .unwrap();
    let h = s.submit(Task::new(7)).await.unwrap();
    assert_eq!(*h.await.unwrap(), 7);   // backoffs elapse in virtual time
}

Fixed starts and cron schedules work the same way: tokio::time::sleep(Duration::from_secs(60)) inside a paused test advances the scheduler's clock by a minute without waiting.

Rust: the engine directly

cronoq-core is a pure state machine. Drive it with explicit times and inspect the actions it emits; this is how cronoq's own deterministic simulation tests work:

use cronoq_core::*;

let mut core: Core<&str> = Core::new(CoreConfig::default());
core.submit(Time::ZERO, TaskSpec::new("a").priority(Priority::High)).unwrap();
let Some(Action::Dispatch(d)) = core.poll_action() else { panic!() };
core.complete(Time::from_secs_f64(0.5), d.lease, Outcome::Success);
core.check_invariants();

Python

Run executors through a real scheduler inside an asyncio test (pytest-asyncio):

import cronoq, pytest

@pytest.mark.asyncio
async def test_executor_retries():
    calls = 0
    async def run(ctx, x):
        nonlocal calls
        calls += 1
        if ctx.attempt < 2:
            raise cronoq.Retryable("flaky")
        return x * 2
    retry = cronoq.RetryPolicy(max_attempts=3, initial_backoff=0.01, jitter=False)
    async with cronoq.Scheduler([cronoq.Pool("default", run, retry=retry)]) as s:
        assert await (await s.submit(21)) == 42
    assert calls == 2

Against a server

Start cronoq-server with a temporary config on free ports (grpc_addr = "127.0.0.1:0" works when you embed it with cronoq_server::run), connect a test worker, and submit through the client. The repository's crates/cronoq-server/tests/server.rs and crates/cronoq-py/python/tests/test_client.py are complete examples.

run reports the addresses it bound once the server is the leader. When a test needs them earlier, for example to reach a standby, bind the listeners on port 0 yourself and pass them to cronoq_server::run_with_listeners. Choosing a free port, closing it, and putting its number in the config is not safe when tests run in parallel: another process can take the port before the server binds it. A server started as a separate process has to bind its ports itself, so retry the start on new ports if it exits with "Address already in use".