use anyhow::{Context, Result};
use apalis::layers::ErrorHandlingLayer;
use apalis::layers::catch_panic::CatchPanicLayer;
use apalis::layers::retry::{RetryLayer, RetryPolicy};
use apalis::prelude::{Attempt, Data, Monitor, TaskId, WorkerBuilder, WorkerFactoryFn};
use async_trait::async_trait;
use nest_rs_core::{Container, ReachableProviders, Transport, inventory};
use nest_rs_queue::ProcessMethod;
use tokio_util::sync::CancellationToken;
use tracing::Instrument;
use crate::connection::QueueConnection;
pub struct QueueWorker {
methods: Vec<&'static ProcessMethod>,
container: Option<Container>,
}
impl QueueWorker {
pub fn new() -> Self {
Self {
methods: Vec::new(),
container: None,
}
}
}
impl Default for QueueWorker {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Transport for QueueWorker {
async fn configure(&mut self, container: &Container) -> Result<()> {
let reachable = container.get::<ReachableProviders>();
let mut methods: Vec<&'static ProcessMethod> = Vec::new();
for entry in inventory::iter::<ProcessMethod>() {
let provider_id = (entry.provider_type_id)();
if let Some(r) = reachable.as_ref()
&& !r.0.contains(&provider_id)
{
tracing::warn!(
target: "nest_rs::queue",
processor = entry.name,
queue = entry.queue,
"skipped #[process] method: provider unreachable from app's module tree",
);
continue;
}
methods.push(entry);
}
self.methods = methods;
if !self.methods.is_empty() {
container.get::<QueueConnection>().context(
"QueueWorker found #[processor]s but no QueueConnection in the container — \
seed one with App::builder().provide_factory(|_| QueueConnection::connect(url))",
)?;
for m in &self.methods {
tracing::info!(
target: "nest_rs::queue",
processor = m.name,
queue = m.queue,
concurrency = m.concurrency,
retries = m.retries,
"registered queue processor",
);
}
}
self.container = Some(container.clone());
Ok(())
}
async fn serve(self: Box<Self>, cancel: CancellationToken) -> Result<()> {
if self.methods.is_empty() {
cancel.cancelled().await;
return Ok(());
}
let container = self
.container
.expect("QueueWorker::configure must run before serve");
let connection = container
.get::<QueueConnection>()
.expect("QueueConnection presence is verified in configure");
let mut monitor = Monitor::new();
for method in &self.methods {
monitor = build_worker(monitor, &connection, container.clone(), method);
}
monitor
.run_with_signal(async move {
cancel.cancelled().await;
Ok(())
})
.await?;
Ok(())
}
}
fn build_worker(
monitor: Monitor,
conn: &QueueConnection,
container: Container,
method: &ProcessMethod,
) -> Monitor {
let storage = conn.consumer_storage(method.queue, method.concurrency);
let handler = method.handler;
let queue_name = method.queue;
let processor_name = method.name;
let worker = WorkerBuilder::new(method.queue)
.layer(ErrorHandlingLayer::new())
.layer(RetryLayer::new(RetryPolicy::retries(method.retries)))
.layer(CatchPanicLayer::new())
.data(container)
.backend(storage)
.build_fn(
move |job: serde_json::Value,
container: Data<Container>,
task_id: TaskId,
attempt: Attempt| {
let container = (*container).clone();
let span = tracing::info_span!(
target: "nest_rs::queue",
"process job",
queue = queue_name,
processor = processor_name,
job_id = %task_id,
attempt = attempt.current(),
);
async move {
tracing::debug!(
target: "nest_rs::queue",
attempt = attempt.current(),
"job started",
);
let started = ::std::time::Instant::now();
let result = handler(job, container).await;
let elapsed_ms = started.elapsed().as_millis() as u64;
match &result {
Ok(()) => tracing::info!(
target: "nest_rs::queue",
elapsed_ms,
"job ok",
),
Err(e) => tracing::warn!(
target: "nest_rs::queue",
elapsed_ms,
error = %e,
"job failed",
),
}
result
}
.instrument(span)
},
);
monitor.register(worker)
}