Type-safe job queues for Rust on RabbitMQ.
Queues are an enum with [macro@Queues]; jobs are structs with
[macro@Job]. A job statically knows its queue, so [Producer::enqueue] and
[WorkerBuilder::handler] are checked at compile time. A job belonging to
another application's queue set does not compile.
This crate is a facade: it re-exports [queuey_core], the derive
macros from queuey-macros, and (behind the default rabbitmq
feature) [queuey_rabbitmq]. Depending on it alone is enough.
use queuey::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Queues)]
#[queues(prefix = "myapp")]
enum AppQueues {
#[queue(prefetch = 10)]
Emails,
#[queue(retry(max_attempts = 3, backoff = "exponential", base = "1s", max = "2m"))]
Images,
}
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Emails, retry(max_attempts = 5))]
struct SendEmail { to: String, body: String }
struct EmailHandler;
#[async_trait]
impl JobHandler for EmailHandler {
type Job = SendEmail;
async fn handle(&self, job: SendEmail, ctx: JobContext) -> Result<(), JobError> {
tracing::info!(to = %job.to, attempt = ctx.attempt, "sending");
Ok(())
}
}
#[tokio::main]
async fn main() -> queuey::Result<()> {
let backend = Arc::new(RabbitMqBackend::connect("amqp://guest:guest@localhost:5672/%2f").await?);
let producer = Producer::<AppQueues, _>::new(backend.clone()).await?;
producer.enqueue(&SendEmail { to: "a@b.c".into(), body: "hi".into() }).await?;
let worker = Worker::<AppQueues, _>::builder(backend)
.handler(EmailHandler)
// The worker leaves the shared backend open by default; this process has
// nothing else to do with it.
.close_backend_on_shutdown(true)
.build()
.await?;
worker.run().await?;
Ok(())
}
Deferral (rate limits, Retry-After)
A job that cannot run yet, because the API answered 429 Too Many Requests with
Retry-After: 30, is not a failure. The handler returns
[JobError::deferred] / [JobError::deferred_msg] and the job is parked for
exactly that long, then comes back ahead of the backlog that piled up
meanwhile. It costs no attempt: ctx.attempt is unchanged and the retry policy
is never consulted, so a job may defer itself indefinitely.
[JobContext::deferrals] counts how often it happened, which is how a handler
caps it (return [JobError::Fatal] once it has had enough).
Coming back first is a broker priority: #[queue(max_priority = n)] declares
how many levels the queue has, and deferred jobs return at the top while
ordinary work sits at 0. The default is [DEFAULT_MAX_PRIORITY]; 0 turns
priorities off, and deferred jobs then come back FIFO. [Producer::defer] is
the producer-side twin: same hold, same priority, without a first run.
use Duration;
use *;
;
async
Where to look next
- [
prelude]: the one glob import above. - [
macro@Queues] / [macro@Job]: the full attribute grammar. - [
Worker] / [Producer]: the runtime. - [
RetryPolicy] / [Backoff]: retry and backoff semantics. - [
JobError::Deferred] / [Producer::defer]: deferral semantics. - [
MemoryBackend]: an in-process backend for tests; see thememory_quickstartexample.
How the derive macros find this crate
Generated code needs a path to queuey-core. The macros read the
calling crate's Cargo.toml and prefer a dependency on queuey,
emitting ::queuey::__core, a hidden re-export of the core crate.
Depending only on this facade therefore needs no crate = "..." attribute.
A crate that depends on queuey-core directly gets
::queuey_core instead.