use std::time::Duration;
use async_trait::async_trait;
use serde::{Serialize, de::DeserializeOwned};
use uuid::Uuid;
use crate::{AppContext, Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkerMode {
BackgroundQueue,
ForegroundBlocking,
BackgroundAsync,
}
#[async_trait]
pub trait BackgroundWorker<A>: Send + Sync
where
A: Serialize + DeserializeOwned + Send + Sync + 'static,
{
fn build(ctx: &AppContext) -> Self;
#[must_use]
fn class_name() -> String
where
Self: Sized,
{
let name = std::any::type_name::<Self>();
name.rsplit("::").next().unwrap_or(name).to_string()
}
async fn perform_later(ctx: &AppContext, args: A) -> Result<String>
where
Self: Sized + 'static,
{
Self::perform_in(ctx, args, Duration::ZERO).await
}
async fn perform_in(ctx: &AppContext, args: A, delay: Duration) -> Result<String>
where
Self: Sized + 'static,
{
match ctx.config.workers {
WorkerMode::BackgroundQueue => {
#[cfg(feature = "queue")]
{
let job = crate::queue::job::<Self, A>(&args)?.run_in(delay);
enqueue(ctx, &job).await
}
#[cfg(not(feature = "queue"))]
{
let _ = (args, delay);
no_queue()
}
}
WorkerMode::ForegroundBlocking => {
Self::build(ctx).perform(args).await?;
Ok(Uuid::new_v4().to_string())
}
WorkerMode::BackgroundAsync => {
let worker = Self::build(ctx);
tokio::spawn(async move {
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
if let Err(error) = worker.perform(args).await {
tracing::error!(%error, "background job failed");
}
});
Ok(Uuid::new_v4().to_string())
}
}
}
#[must_use]
fn max_attempts() -> Option<i32>
where
Self: Sized,
{
None
}
#[must_use]
fn timeout() -> Option<Duration>
where
Self: Sized,
{
None
}
async fn perform(&self, args: A) -> Result<()>;
}
#[cfg(feature = "queue")]
async fn enqueue(ctx: &AppContext, job: &crate::queue::NewJob) -> Result<String> {
let queue = ctx
.queue_provider
.as_ref()
.ok_or_else(|| Error::Worker("no job queue configured".to_string()))?;
queue.enqueue_job(job).await
}
#[cfg(not(feature = "queue"))]
fn no_queue() -> Result<String> {
Err(Error::Worker("no job queue configured".to_string()))
}
#[cfg(all(
test,
not(any(
axa_db = "postgres_seaorm",
axa_db = "mysql_seaorm",
axa_db = "sqlite_seaorm",
axa_db = "postgres_toasty",
axa_db = "mysql_toasty"
)),
not(feature = "queue")
))]
mod tests {
use super::*;
use crate::{
Environment,
config::{Config, Defaults},
};
use serde::Deserialize;
use std::sync::atomic::{AtomicUsize, Ordering};
static DONE: AtomicUsize = AtomicUsize::new(0);
#[derive(Serialize, Deserialize)]
struct Args {
amount: usize,
fail: bool,
}
struct AddWorker;
#[async_trait]
impl BackgroundWorker<Args> for AddWorker {
fn build(_ctx: &AppContext) -> Self {
Self
}
async fn perform(&self, args: Args) -> Result<()> {
if args.fail {
return Err(Error::Worker("told to fail".to_string()));
}
DONE.fetch_add(args.amount, Ordering::SeqCst);
Ok(())
}
}
fn context(mode: &str) -> AppContext {
let defaults = Defaults {
database: "",
..Defaults::default()
};
let config = Config::from_lookup(&Environment::Test, &defaults, |name| {
(name == "WORKER_MODE").then(|| mode.to_string())
})
.unwrap();
AppContext::for_test(Environment::Test, config)
}
#[test]
fn class_name_is_the_bare_type_name() {
assert_eq!(AddWorker::class_name(), "AddWorker");
}
#[tokio::test]
async fn foreground_jobs_run_before_returning_and_report_failure() {
let ctx = context("ForegroundBlocking");
let before = DONE.load(Ordering::SeqCst);
AddWorker::perform_later(
&ctx,
Args {
amount: 3,
fail: false,
},
)
.await
.unwrap();
assert!(DONE.load(Ordering::SeqCst) >= before + 3);
assert!(
AddWorker::perform_later(
&ctx,
Args {
amount: 0,
fail: true
}
)
.await
.is_err()
);
}
#[tokio::test]
async fn queued_jobs_fail_without_a_queue() {
let ctx = context("BackgroundQueue");
let error = AddWorker::perform_later(
&ctx,
Args {
amount: 1,
fail: false,
},
)
.await
.unwrap_err();
assert!(error.to_string().contains("no job queue"));
}
}