use std::{any::type_name, pin::Pin, sync::Arc, time::Instant};
use futures::{stream::FuturesUnordered, Future, StreamExt};
use lapin::{
options::{
BasicAckOptions, BasicCancelOptions, BasicConsumeOptions, BasicPublishOptions,
BasicQosOptions,
},
types::{FieldTable, ShortString},
BasicProperties, Channel, Connection, Consumer,
};
use metrics::gauge;
use tokio::sync::broadcast;
use tracing::{debug, error, error_span, info, trace, warn, Instrument};
use crate::{Error, Handler, HandlerConfig, Request, Respond, Result};
type HandlerTask = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type HandlerTaskFactory<S> =
Box<dyn FnOnce(Channel, Consumer, f64, Arc<S>, broadcast::Receiver<()>) -> HandlerTask + Send>;
#[allow(clippy::too_many_arguments)]
fn handler_task<H, S, Args, Res>(
routing_key: String,
handler: H,
channel: Channel,
mut consumer: Consumer,
prefetch: f64,
state: Arc<S>,
mut shutdown: broadcast::Receiver<()>,
should_reply: bool,
) -> HandlerTask
where
H: Handler<Args, Res, S>,
Res: Respond,
S: Send + Sync + 'static,
{
Box::pin(async move {
let mut tasks = FuturesUnordered::new();
let ret = loop {
let delivery = tokio::select! {
biased;
_ = shutdown.recv() => {
info!("Graceful shutdown signal received in handler {}.", type_name::<H>());
break Ok(())
}
Some(result) = tasks.next() => if let Err(e) = result {
error!("Handler {} panicked: {}", type_name::<H>().to_string(), e);
continue
} else {
continue;
},
delivery = consumer.next() => match delivery {
Some(delivery) => delivery,
None => {
error!("Consumer cancelled, attempting to gracefully shut down...");
break Err(Error::ConsumerCancelled(routing_key));
},
},
};
let req = match delivery {
Err(e) => {
error!("Error when receiving delivery on routing key \"{routing_key}\": {e:#}");
continue;
}
Ok(delivery) => Request::new(channel.clone(), delivery, state.clone()),
};
let handler = handler.clone();
let channel = channel.clone();
tasks.push(tokio::spawn(async move {
let span = error_span!("request", req_id = %req.req_id());
handle_request(req, handler, channel, should_reply)
.instrument(span)
.await;
}));
};
let queue = consumer.queue();
let consumer_tag = consumer.tag();
let tag = consumer_tag.as_str();
if let Err(e) = channel
.basic_cancel(tag, BasicCancelOptions::default())
.await
{
error!("Failed to cancel consumer with tag {tag} and queue {queue} during graceful shutdown of handler task {} (graceful shutdown will continue regardless): {e}", type_name::<H>())
}
gauge!("kanin.prefetch_capacity", "queue" => queue.to_string()).decrement(prefetch);
if tasks.is_empty() {
info!("No outstanding messages on handler {}.", type_name::<H>())
} else {
info!(
"Handler {} finishing {} requests...",
type_name::<H>(),
tasks.len()
);
let start = Instant::now();
while let Some(res) = tasks.next().await {
if let Err(e) = res {
error!(
"Handler {} panicked during graceful shutdown (graceful shutdown will continue): {}",
type_name::<H>().to_string(),
e
);
}
if !tasks.is_empty() {
info!(
"Handler {} still working on {} requests ({:?})...",
type_name::<H>(),
tasks.len(),
start.elapsed(),
)
}
}
info!(
"Handler {} finished in {:?}.",
type_name::<H>(),
start.elapsed(),
)
}
ret
})
}
async fn handle_request<H, S, Args, Res>(
mut req: Request<S>,
handler: H,
channel: Channel,
should_reply: bool,
) where
H: Handler<Args, Res, S>,
Res: Respond,
{
let handler_name = std::any::type_name::<H>();
let app_id = req.app_id().unwrap_or("<unknown>");
info!("Received request on handler {handler_name:?} from {app_id}");
if req.delivery().redelivered {
info!("Request was redelivered.");
}
let t = std::time::Instant::now();
let response = handler.call(&mut req).await;
let properties = req.properties();
let reply_to = properties.reply_to();
let correlation_id = properties.correlation_id();
debug!("Handler {handler_name:?} produced response {response:?}");
let bytes_response = response.respond();
let elapsed = t.elapsed();
match (should_reply, reply_to) {
(true, Some(reply_to)) => {
let mut props = BasicProperties::default();
if let Some(correlation_id) = correlation_id {
props = props.with_correlation_id(correlation_id.clone());
} else {
warn!("Request from handler {handler_name:?} did not contain a `correlation_id` property. A reply will be published, but the receiver may not recognize it as the reply for their request. (all properties: {properties:?})");
}
if bytes_response.is_empty() {
warn!("Handler {handler_name:?} produced an empty response to a message with a `reply_to` property. This is probably undesired, as the caller likely expects more of a response (elapsed={elapsed:?})");
} else {
info!(
"Response with {} bytes that will be published to {reply_to} (elapsed={elapsed:?})",
bytes_response.len()
);
}
props = props.with_content_type(ShortString::from("application/octet-stream"));
let publish = channel
.basic_publish(
HandlerConfig::DEFAULT_EXCHANGE,
reply_to.as_str(),
BasicPublishOptions::default(),
&bytes_response,
props,
)
.await;
match publish {
Ok(_confirm) => {
debug!("Successfully published reply to routing key \"{reply_to}\"");
}
Err(e) => {
error!("Error when publishing reply to routing key \"{reply_to}\": {e:#}");
}
}
}
(true, None) if !bytes_response.is_empty() => {
warn!("Received non-empty message from handler {handler_name:?} but the request did not contain a `reply_to` property, so no reply could be published (all properties: {properties:?}, elapsed={elapsed:?}).");
}
(true, None) => {
info!(
"Handler {handler_name} finished (empty, should_reply = true, elapsed={elapsed:?})",
);
}
(false, _) => {
let len = bytes_response.len();
info!(
"Handler {handler_name} finished ({len} bytes, should_reply = false, elapsed={elapsed:?}).",
);
}
};
if !req.acked {
match req.ack(BasicAckOptions::default()).await {
Ok(()) => debug!("Successfully acked request."),
Err(e) => error!("Failed to ack request: {e:#}"),
}
}
}
pub(super) struct TaskFactory<S> {
routing_key: String,
config: HandlerConfig,
factory: HandlerTaskFactory<S>,
}
impl<S> TaskFactory<S> {
pub(super) fn new<H, Args, Res>(routing_key: String, handler: H, config: HandlerConfig) -> Self
where
H: Handler<Args, Res, S>,
Res: Respond,
S: Send + Sync + 'static,
{
let should_reply = config.should_reply;
Self {
routing_key: routing_key.clone(),
config,
factory: Box::new(
move |channel: Channel,
consumer: Consumer,
prefetch: f64,
state: Arc<S>,
shutdown: broadcast::Receiver<()>| {
handler_task(
routing_key,
handler,
channel,
consumer,
prefetch,
state,
shutdown,
should_reply,
)
},
),
}
}
pub(super) fn routing_key(&self) -> &str {
&self.routing_key
}
pub(super) async fn build(
self,
conn: &Connection,
state: Arc<S>,
shutdown: broadcast::Receiver<()>,
) -> lapin::Result<HandlerTask> {
debug!(
"Building task for handler on routing key {:?}",
self.routing_key(),
);
trace!("Creating channel for handler...");
let channel = conn.create_channel().await?;
trace!(
"Reporting basic quality of service with prefetch {}...",
self.config.prefetch
);
channel
.basic_qos(self.config.prefetch, BasicQosOptions::default())
.await?;
let queue_name = self.config.queue.as_deref().unwrap_or(&self.routing_key);
let prefetch_f64: f64 = self.config.prefetch.into();
gauge!("kanin.prefetch_capacity", "queue" => queue_name.to_string())
.increment(prefetch_f64);
trace!("Declaring queue {queue_name:?} prior to binding...");
channel
.queue_declare(queue_name, self.config.options, self.config.arguments)
.await?;
trace!(
"Binding to queue {queue_name:?} on exchange {:?} on routing key {:?}...",
self.config.exchange,
self.routing_key
);
channel
.queue_bind(
queue_name,
&self.config.exchange,
&self.routing_key,
Default::default(),
Default::default(),
)
.await?;
trace!("Creating consumer on routing key {}...", self.routing_key);
let consumer = channel
.basic_consume(
queue_name,
&self.routing_key,
BasicConsumeOptions::default(),
FieldTable::default(),
)
.await?;
Ok((self.factory)(
channel,
consumer,
prefetch_f64,
state,
shutdown,
))
}
}