mod ack;
use apalis_core::{
error::{JobError, JobStreamError},
job::{Job, JobStreamResult},
mq::MessageQueue,
request::JobRequest,
worker::WorkerId,
};
use deadpool_lapin::{Manager, Pool};
use futures::StreamExt;
use lapin::{
options::{BasicConsumeOptions, BasicPublishOptions, QueueDeclareOptions},
types::FieldTable,
BasicProperties, Channel, ConnectionProperties, Queue,
};
use serde::{de::DeserializeOwned, Serialize};
use std::{fmt::Debug, marker::PhantomData};
#[derive(Debug)]
pub struct AmqpBackend<J> {
channel: Channel,
queue: Queue,
job_type: PhantomData<J>,
}
impl<J> Clone for AmqpBackend<J> {
fn clone(&self) -> Self {
Self {
channel: self.channel.clone(),
queue: self.queue.clone(),
job_type: PhantomData,
}
}
}
#[async_trait::async_trait]
impl<J: Job + Serialize + DeserializeOwned + Send + Sync + 'static> MessageQueue<J>
for AmqpBackend<J>
{
async fn push(&self, data: J) -> Result<(), JobError> {
let channel = self.channel.clone();
let _confirmation = channel
.basic_publish(
"",
J::NAME,
BasicPublishOptions::default(),
&serde_json::to_vec(&JobRequest::new(data))
.map_err(|e| JobError::Failed(e.into()))?,
BasicProperties::default(),
)
.await
.map_err(|e| JobError::Failed(e.into()))?
.await
.map_err(|e| JobError::Failed(e.into()))?;
Ok(())
}
fn consume(&self, worker: &WorkerId) -> JobStreamResult<J> {
let channel = self.channel.clone();
let worker = worker.clone();
let stream = async_stream::stream! {
let mut consumer = channel
.basic_consume(
J::NAME,
&worker.to_string(),
BasicConsumeOptions::default(),
FieldTable::default(),
)
.await
.map_err(|e| JobStreamError::BrokenPipe(e.into()))?;
while let Some(Ok(item)) = consumer.next().await {
let bytes = item.data;
let mut job: JobRequest<J> = serde_json::from_slice(&bytes)
.map_err(|e| JobStreamError::BrokenPipe(e.into()))?;
job.insert(DeliveryTag(item.delivery_tag)); yield Ok(Some(job));
}
};
stream.boxed()
}
}
#[derive(Debug, Clone)]
pub struct DeliveryTag(u64);
impl<J: Job + Serialize + DeserializeOwned + Send + 'static> AmqpBackend<J> {
pub fn new(channel: Channel, queue: Queue) -> Self {
Self {
channel,
job_type: PhantomData,
queue,
}
}
pub fn channel(&self) -> &Channel {
&self.channel
}
pub fn queue(&self) -> &Queue {
&self.queue
}
pub async fn new_from_addr<S: AsRef<str>>(addr: S) -> Result<Self, lapin::Error> {
let manager = Manager::new(addr.as_ref(), ConnectionProperties::default());
let pool: Pool = deadpool::managed::Pool::builder(manager)
.max_size(10)
.build()
.expect("can create pool");
let amqp_conn = pool
.get()
.await
.map_err(|_e| lapin::Error::ChannelsLimitReached)?;
let channel = amqp_conn.create_channel().await?;
let queue = channel
.queue_declare(
J::NAME,
QueueDeclareOptions::default(),
FieldTable::default(),
)
.await?;
Ok(Self::new(channel, queue))
}
}
#[cfg(test)]
mod tests {
use super::*;
use apalis_core::{
builder::WorkerBuilder, builder::WorkerFactoryFn, context::JobContext, mq::WithMq,
};
use serde::Deserialize;
#[derive(Debug, Serialize, Deserialize)]
struct TestJob;
impl Job for TestJob {
const NAME: &'static str = "TestJob";
}
async fn test_job(_job: TestJob, _ctx: JobContext) {}
#[tokio::test]
async fn it_works() {
let env = std::env::var("AMQP_ADDR").unwrap();
let amqp_backend = AmqpBackend::<TestJob>::new_from_addr(&env).await.unwrap();
let _worker = WorkerBuilder::new("rango-amigo")
.with_mq(amqp_backend)
.build_fn(test_job);
}
}