use std::{
error::Error,
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
use queuey::prelude::*;
const EMAILS: &str = "quickstart.emails";
const IMAGES: &str = "quickstart.images";
const API: &str = "quickstart.api";
const EXPECTED: usize = 6;
const DEADLINE: Duration = Duration::from_secs(10);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Queues)]
#[queues(prefix = "quickstart")]
enum AppQueues {
#[queue(
prefetch = 8,
retry(max_attempts = 3, backoff = "fixed", delay = "50ms")
)]
Emails,
#[queue(prefetch = 4)]
Images,
#[queue(prefetch = 4, max_priority = 10)]
Api,
}
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Emails)]
struct SendEmail {
to: String,
flaky_for: u32,
fatal: bool,
}
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Images, retry(max_attempts = 2, backoff = "exponential", base = "20ms", max = "100ms"))]
struct ResizeImage {
path: String,
}
#[derive(Debug, Serialize, Deserialize, Job)]
#[job(queue = AppQueues::Api)]
struct CallApi {
url: String,
retry_after_ms: u64,
}
struct Settled(AtomicUsize);
impl Settled {
fn bump(&self) -> usize {
self.0.fetch_add(1, Ordering::SeqCst) + 1
}
fn count(&self) -> usize {
self.0.load(Ordering::SeqCst)
}
}
struct EmailHandler {
settled: Arc<Settled>,
}
#[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,
max_attempts = ctx.max_attempts,
last = ctx.is_last_attempt(),
"handling email"
);
if job.fatal {
self.settled.bump();
return Err(JobError::fatal_msg(format!("{} is not a mailbox", job.to)));
}
if ctx.attempt <= job.flaky_for {
return Err(JobError::retryable_msg("smtp connection reset"));
}
self.settled.bump();
Ok(())
}
}
struct ApiHandler {
settled: Arc<Settled>,
}
#[async_trait]
impl JobHandler for ApiHandler {
type Job = CallApi;
async fn handle(&self, job: CallApi, ctx: JobContext) -> Result<(), JobError> {
if ctx.deferrals == 0 {
let retry_after = Duration::from_millis(job.retry_after_ms);
tracing::info!(url = %job.url, ?retry_after, "429 Too Many Requests");
return Err(JobError::deferred_msg(
retry_after,
"rate limited (HTTP 429)",
));
}
tracing::info!(
url = %job.url,
attempt = ctx.attempt,
deferrals = ctx.deferrals,
priority = ctx.priority,
"calling api"
);
self.settled.bump();
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,queuey_core=info".into()),
)
.init();
let backend = Arc::new(MemoryBackend::new());
let settled = Arc::new(Settled(AtomicUsize::new(0)));
let producer = Producer::<AppQueues, _>::new(backend.clone()).await?;
let worker = Worker::<AppQueues, _>::builder(backend.clone())
.handler(EmailHandler {
settled: settled.clone(),
})
.handler(ApiHandler {
settled: settled.clone(),
})
.handler(FnHandler::<ResizeImage, _>::new({
let settled = settled.clone();
move |job: ResizeImage, ctx: JobContext| {
let settled = settled.clone();
async move {
tracing::info!(path = %job.path, attempt = ctx.attempt, "resizing image");
settled.bump();
Ok(())
}
}
}))
.build()
.await?;
let handle = worker.handle();
let running = tokio::spawn(worker.run());
for to in ["ada@example.com", "grace@example.com"] {
producer
.enqueue(&SendEmail {
to: to.to_owned(),
flaky_for: 0,
fatal: false,
})
.await?;
}
producer
.enqueue(&SendEmail {
to: "flaky@example.com".to_owned(),
flaky_for: 2,
fatal: false,
})
.await?;
producer
.enqueue(&SendEmail {
to: "not-an-address".to_owned(),
flaky_for: 0,
fatal: true,
})
.await?;
producer
.enqueue(&ResizeImage {
path: "cat.png".to_owned(),
})
.await?;
producer
.enqueue(&CallApi {
url: "https://api.example.com/v1/things".to_owned(),
retry_after_ms: 150,
})
.await?;
let finished = tokio::time::timeout(DEADLINE, async {
while settled.count() < EXPECTED {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await;
handle.shutdown();
running.await??;
if finished.is_err() {
return Err(format!(
"only {}/{EXPECTED} jobs settled within {DEADLINE:?}",
settled.count()
)
.into());
}
for queue in [EMAILS, IMAGES, API] {
tracing::info!(
queue,
acked = backend.acked(queue).len(),
dead = backend.dead_letters(queue).len(),
pending = backend.pending(queue),
"queue summary"
);
for (envelope, reason) in backend.dead_letters(queue) {
tracing::warn!(job_id = %envelope.job_id, attempt = envelope.attempt, %reason, "dead letter");
}
}
backend.close().await?;
tracing::info!(settled = settled.count(), "done");
Ok(())
}