A lightweight, bounded-concurrency job queue for Rust. Submit closures, cap how many run at once, get live status updates, and let panics fail gracefully — all backed by a single scheduler thread.
- Bounded concurrency — set a hard cap on simultaneous worker threads
- Priority queueing — jump a job to the front with a single flag
- Panic isolation — panicking closures become
Failedstatuses, not crashes - Live status callbacks — implement
QueueListenerto observe every transition - Cancel pending jobs — remove a queued job before it starts
- Cheap client handle —
QueueStagerisClone + Send, share it freely across threads
use std::sync::Arc;
let queue = Queue::new(4, vec![]); // max 4 concurrent jobs, no listeners
let stager = queue.stager(); // grab a handle before start() consumes the queue
queue.start(); // scheduler runs in a background thread
let id = stager.stage_job(|| {
println!("hello from a worker thread");
}, false);Queue::new(max_jobs, listeners) accepts the maximum number of jobs that may run at the same time. Jobs beyond that cap wait in a FIFO queue until a slot opens.
// Run at most 2 jobs simultaneously
let queue = Queue::new(2, vec![]);Pass 1 for strictly serial execution.
Every stage_job call takes a top_priority: bool. When true, the job is pushed to the front of the pending queue instead of the back.
let normal_id = stager.stage_job(|| do_work(), false);
let priority_id = stager.stage_job(|| do_urgent(), true);
// priority_id will be picked up before normal_idPending(position) ──► Running ──► Successful
└─► Failed(msg)
└──► Cancelled (only reachable while still pending)
| Status | Meaning |
|---|---|
Pending { position } |
Waiting in queue; 0 means next up |
Running |
Executing on a worker thread |
Successful |
Closure returned without panicking |
Failed(String) |
Closure panicked; message included |
Cancelled |
Removed from queue before it started |
Implement QueueListener and pass it to Queue::new:
#[derive(Debug)]
struct MyListener;
impl QueueListener for MyListener {
fn on_update(&self, id: Uuid, status: QueueStatus) {
println!("{id} → {status:?}");
}
}
let queue = Queue::new(4, vec![Arc::new(MyListener)]);on_update is called from the scheduler thread — keep it fast and non-blocking.
let id = stager.stage_job(|| expensive_work(), false);
// Changed your mind before it started?
stager.cancel(id);
// → listeners receive QueueStatus::Cancelled
⚠️ Cancel only affects pending jobs. Once a job isRunning,cancelis a no-op — the worker thread is already executing the closure and there is no built-in preemption. If you need to abort a running job, implement cooperative cancellation inside the closure:let stop = Arc::new(AtomicBool::new(false)); let stop_clone = stop.clone(); stager.stage_job(move || { while !stop_clone.load(Ordering::Relaxed) { do_one_unit_of_work(); } }, false); // From another thread, whenever you want to stop it: stop.store(true, Ordering::Relaxed);
Panics inside closures are caught via std::panic::catch_unwind and reported as QueueStatus::Failed(message). The scheduler thread and all other running jobs are unaffected.
stager.stage_job(|| panic!("something went wrong"), false);
// → QueueStatus::Failed("something went wrong")| Crate | Usage |
|---|---|
crossbeam-channel |
Lock-free channels between stager, scheduler, and workers |
uuid |
Unique job identifiers (requires the v4 feature) |
log |
Trace/warn logging (bring your own backend) |
[dependencies]
crossbeam-channel = "0.5"
uuid = { version = "1", features = ["v4"] }
log = "0.4"Documentation generated by Claude. If you notice any problems, please open a GitHub Issue!