use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use bullmq::{Queue, QueueOptions};
use tokio::sync::RwLock;
use crate::types::bullmq::BullMqProvider;
use crate::types::queues::{
BulkJob, BullMqError, EnqueueOptions, JobHandle, JobSnapshot, JobStateFilter, QueueChannel,
QueueCounts,
};
pub const PREFIX_ENV: &str = "FLIPPICO_CACHE_BULLMQ_PREFIX";
pub const DEFAULT_PREFIX: &str = "bull";
pub(crate) fn resolve_prefix(raw: Option<String>) -> String {
match raw {
Some(value) if !value.trim().is_empty() => value.trim().to_string(),
_ => DEFAULT_PREFIX.to_string(),
}
}
pub struct BullMq {
url: String,
prefix: String,
queues: RwLock<HashMap<String, Arc<Queue>>>,
}
impl BullMq {
pub async fn connect(url: String) -> Result<Self, BullMqError> {
Ok(Self {
url,
prefix: resolve_prefix(std::env::var(PREFIX_ENV).ok()),
queues: RwLock::new(HashMap::new()),
})
}
fn queue_options(&self) -> QueueOptions {
let mut opts = QueueOptions {
prefix: self.prefix.clone(),
..Default::default()
};
opts.connection.url = self.url.clone();
opts
}
pub(crate) async fn queue_for(
&self,
channel: &QueueChannel,
) -> Result<Arc<Queue>, BullMqError> {
channel.validate()?;
let name = channel.get_queue();
{
let cache = self.queues.read().await;
if let Some(queue) = cache.get(name) {
return Ok(Arc::clone(queue));
}
}
let queue = Arc::new(Queue::with_options(name, self.queue_options()).await?);
let mut cache = self.queues.write().await;
Ok(Arc::clone(cache.entry(name.to_string()).or_insert(queue)))
}
}
fn snapshot(job: &bullmq::Job) -> JobSnapshot {
JobSnapshot {
id: job.id().to_string(),
name: job.name().to_string(),
data: job.data().clone(),
attempts_made: job.attempts_made(),
timestamp: job.timestamp(),
}
}
#[async_trait]
impl BullMqProvider for BullMq {
async fn enqueue(
&self,
queue: &QueueChannel,
job_name: &str,
payload: serde_json::Value,
options: Option<EnqueueOptions>,
) -> Result<JobHandle, BullMqError> {
let q = self.queue_for(queue).await?;
let opts = options.unwrap_or_default().into_bullmq();
let job = q.add(job_name, payload).options(opts).await?;
Ok(JobHandle {
id: job.id().to_string(),
name: job.name().to_string(),
})
}
async fn enqueue_bulk(
&self,
queue: &QueueChannel,
jobs: Vec<BulkJob>,
) -> Result<Vec<JobHandle>, BullMqError> {
let q = self.queue_for(queue).await?;
let bulk: Vec<bullmq::BulkJob> = jobs
.into_iter()
.map(|job| {
bullmq::BulkJob::with_options(
job.name,
job.payload,
job.options.unwrap_or_default().into_bullmq(),
)
})
.collect();
let added = q.add_bulk(bulk).await?;
Ok(added
.iter()
.map(|job| JobHandle {
id: job.id().to_string(),
name: job.name().to_string(),
})
.collect())
}
async fn job_counts(&self, queue: &QueueChannel) -> Result<QueueCounts, BullMqError> {
let q = self.queue_for(queue).await?;
let c = q.get_job_counts().await?;
Ok(QueueCounts {
waiting: c.waiting,
active: c.active,
delayed: c.delayed,
prioritized: c.prioritized,
completed: c.completed,
failed: c.failed,
waiting_children: c.waiting_children,
paused: c.paused,
})
}
async fn get_job(
&self,
queue: &QueueChannel,
job_id: &str,
) -> Result<Option<JobSnapshot>, BullMqError> {
let q = self.queue_for(queue).await?;
Ok(q.get_job(job_id).await?.as_ref().map(snapshot))
}
async fn list_jobs(
&self,
queue: &QueueChannel,
state: JobStateFilter,
start: i64,
end: i64,
) -> Result<Vec<JobSnapshot>, BullMqError> {
let q = self.queue_for(queue).await?;
let jobs = match state {
JobStateFilter::Waiting => q.get_waiting(start, end).await?,
JobStateFilter::Active => q.get_active(start, end).await?,
JobStateFilter::Delayed => q.get_delayed(start, end).await?,
JobStateFilter::Prioritized => q.get_prioritized(start, end).await?,
JobStateFilter::Completed => q.get_completed(start, end).await?,
JobStateFilter::Failed => q.get_failed(start, end).await?,
};
Ok(jobs.iter().map(snapshot).collect())
}
async fn pause(&self, queue: &QueueChannel) -> Result<(), BullMqError> {
Ok(self.queue_for(queue).await?.pause().await?)
}
async fn resume(&self, queue: &QueueChannel) -> Result<(), BullMqError> {
Ok(self.queue_for(queue).await?.resume().await?)
}
async fn is_paused(&self, queue: &QueueChannel) -> Result<bool, BullMqError> {
Ok(self.queue_for(queue).await?.is_paused().await?)
}
async fn obliterate(&self, queue: &QueueChannel) -> Result<(), BullMqError> {
Ok(self.queue_for(queue).await?.obliterate(true, 1000).await?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prefix_defaults_to_bull_when_unset() {
assert_eq!(resolve_prefix(None), "bull");
}
#[test]
fn prefix_defaults_to_bull_when_blank() {
assert_eq!(resolve_prefix(Some(String::new())), "bull");
assert_eq!(resolve_prefix(Some(" ".to_string())), "bull");
}
#[test]
fn prefix_uses_override_when_set() {
assert_eq!(resolve_prefix(Some("flippico".to_string())), "flippico");
}
#[test]
fn prefix_trims_surrounding_whitespace() {
assert_eq!(resolve_prefix(Some(" flippico ".to_string())), "flippico");
}
}