use awa_model::{insert_with, AwaError, InsertOpts, JobArgs, JobRow};
use serde::de::DeserializeOwned;
use sqlx::PgConnection;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct EnqueueRequest<F> {
pub(crate) args: F,
pub(crate) opts: InsertOpts,
}
impl<F: JobArgs> EnqueueRequest<F> {
pub fn new(args: F) -> Self {
Self {
args,
opts: InsertOpts::default(),
}
}
pub fn queue(mut self, queue: impl Into<String>) -> Self {
self.opts.queue = queue.into();
self
}
pub fn priority(mut self, priority: i16) -> Self {
self.opts.priority = priority;
self
}
pub fn max_attempts(mut self, max_attempts: i16) -> Self {
self.opts.max_attempts = max_attempts;
self
}
pub fn with_opts(mut self, opts: InsertOpts) -> Self {
self.opts = opts;
self
}
}
impl<F: JobArgs> From<F> for EnqueueRequest<F> {
fn from(args: F) -> Self {
Self::new(args)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Outcome {
Completed,
Retried,
Exhausted,
Cancelled,
WaitingForCallback,
Rescued,
}
#[derive(Debug, Clone)]
pub enum OutcomeContext {
Retried {
error: String,
attempt: i16,
next_run_at: chrono::DateTime<chrono::Utc>,
},
Exhausted {
error: String,
attempt: i16,
},
Cancelled {
reason: String,
},
WaitingForCallback,
Rescued {
reason: crate::events::RescueReason,
},
}
pub(crate) trait EnqueueFollowUp: Send + Sync {
fn run<'a>(
&'a self,
conn: &'a mut PgConnection,
job: &'a JobRow,
outcome_context: Option<&'a OutcomeContext>,
) -> Pin<Box<dyn Future<Output = Result<(), AwaError>> + Send + 'a>>;
}
pub(crate) type BoxedEnqueueSpec = Arc<dyn EnqueueFollowUp + 'static>;
fn decode_trigger_args<T: DeserializeOwned>(job: &JobRow) -> Result<T, AwaError> {
serde_json::from_value(job.args.clone()).map_err(|err| {
AwaError::Validation(format!(
"follow-up enqueue: failed to decode trigger args for kind {}: {err}",
job.kind
))
})
}
fn catch_make_panic<R>(
kind: &str,
f: impl FnOnce() -> R + std::panic::UnwindSafe,
) -> Result<R, AwaError> {
std::panic::catch_unwind(f).map_err(|panic| {
let detail = if let Some(msg) = panic.downcast_ref::<&'static str>() {
(*msg).to_string()
} else if let Some(msg) = panic.downcast_ref::<String>() {
msg.clone()
} else {
"panic payload not a string".to_string()
};
AwaError::Validation(format!(
"follow-up enqueue closure for kind {kind} panicked: {detail}"
))
})
}
pub(crate) struct CompletedFollowUp<T, F, MakeFn> {
pub(crate) make: MakeFn,
pub(crate) _phantom: PhantomData<fn() -> (T, F)>,
}
impl<T, F, MakeFn> EnqueueFollowUp for CompletedFollowUp<T, F, MakeFn>
where
T: JobArgs + DeserializeOwned + Send + Sync + 'static,
F: JobArgs + Send + Sync + 'static,
MakeFn: Fn(T, &JobRow) -> EnqueueRequest<F> + Send + Sync + 'static,
{
fn run<'a>(
&'a self,
conn: &'a mut PgConnection,
job: &'a JobRow,
_outcome_context: Option<&'a OutcomeContext>,
) -> Pin<Box<dyn Future<Output = Result<(), AwaError>> + Send + 'a>> {
Box::pin(async move {
let args: T = decode_trigger_args(job)?;
let request = catch_make_panic(
&job.kind,
std::panic::AssertUnwindSafe(|| (self.make)(args, job)),
)?;
insert_with(&mut *conn, &request.args, request.opts).await?;
Ok(())
})
}
}
pub(crate) struct RetriedFollowUp<T, F, MakeFn> {
pub(crate) make: MakeFn,
pub(crate) _phantom: PhantomData<fn() -> (T, F)>,
}
impl<T, F, MakeFn> EnqueueFollowUp for RetriedFollowUp<T, F, MakeFn>
where
T: JobArgs + DeserializeOwned + Send + Sync + 'static,
F: JobArgs + Send + Sync + 'static,
MakeFn: Fn(T, &JobRow, &str, i16, chrono::DateTime<chrono::Utc>) -> EnqueueRequest<F>
+ Send
+ Sync
+ 'static,
{
fn run<'a>(
&'a self,
conn: &'a mut PgConnection,
job: &'a JobRow,
outcome_context: Option<&'a OutcomeContext>,
) -> Pin<Box<dyn Future<Output = Result<(), AwaError>> + Send + 'a>> {
Box::pin(async move {
let Some(OutcomeContext::Retried {
error,
attempt,
next_run_at,
}) = outcome_context
else {
return Err(AwaError::Validation(
"RetriedFollowUp dispatched without a Retried OutcomeContext".into(),
));
};
let args: T = decode_trigger_args(job)?;
let request = catch_make_panic(
&job.kind,
std::panic::AssertUnwindSafe(|| {
(self.make)(args, job, error, *attempt, *next_run_at)
}),
)?;
insert_with(&mut *conn, &request.args, request.opts).await?;
Ok(())
})
}
}
pub(crate) struct ExhaustedFollowUp<T, F, MakeFn> {
pub(crate) make: MakeFn,
pub(crate) _phantom: PhantomData<fn() -> (T, F)>,
}
impl<T, F, MakeFn> EnqueueFollowUp for ExhaustedFollowUp<T, F, MakeFn>
where
T: JobArgs + DeserializeOwned + Send + Sync + 'static,
F: JobArgs + Send + Sync + 'static,
MakeFn: Fn(T, &JobRow, &str, i16) -> EnqueueRequest<F> + Send + Sync + 'static,
{
fn run<'a>(
&'a self,
conn: &'a mut PgConnection,
job: &'a JobRow,
outcome_context: Option<&'a OutcomeContext>,
) -> Pin<Box<dyn Future<Output = Result<(), AwaError>> + Send + 'a>> {
Box::pin(async move {
let Some(OutcomeContext::Exhausted { error, attempt }) = outcome_context else {
return Err(AwaError::Validation(
"ExhaustedFollowUp dispatched without an Exhausted OutcomeContext".into(),
));
};
let args: T = decode_trigger_args(job)?;
let request = catch_make_panic(
&job.kind,
std::panic::AssertUnwindSafe(|| (self.make)(args, job, error, *attempt)),
)?;
insert_with(&mut *conn, &request.args, request.opts).await?;
Ok(())
})
}
}
pub(crate) struct CancelledFollowUp<T, F, MakeFn> {
pub(crate) make: MakeFn,
pub(crate) _phantom: PhantomData<fn() -> (T, F)>,
}
impl<T, F, MakeFn> EnqueueFollowUp for CancelledFollowUp<T, F, MakeFn>
where
T: JobArgs + DeserializeOwned + Send + Sync + 'static,
F: JobArgs + Send + Sync + 'static,
MakeFn: Fn(T, &JobRow, &str) -> EnqueueRequest<F> + Send + Sync + 'static,
{
fn run<'a>(
&'a self,
conn: &'a mut PgConnection,
job: &'a JobRow,
outcome_context: Option<&'a OutcomeContext>,
) -> Pin<Box<dyn Future<Output = Result<(), AwaError>> + Send + 'a>> {
Box::pin(async move {
let Some(OutcomeContext::Cancelled { reason }) = outcome_context else {
return Err(AwaError::Validation(
"CancelledFollowUp dispatched without a Cancelled OutcomeContext".into(),
));
};
let args: T = decode_trigger_args(job)?;
let request = catch_make_panic(
&job.kind,
std::panic::AssertUnwindSafe(|| (self.make)(args, job, reason)),
)?;
insert_with(&mut *conn, &request.args, request.opts).await?;
Ok(())
})
}
}
pub(crate) struct WaitingForCallbackFollowUp<T, F, MakeFn> {
pub(crate) make: MakeFn,
pub(crate) _phantom: PhantomData<fn() -> (T, F)>,
}
impl<T, F, MakeFn> EnqueueFollowUp for WaitingForCallbackFollowUp<T, F, MakeFn>
where
T: JobArgs + DeserializeOwned + Send + Sync + 'static,
F: JobArgs + Send + Sync + 'static,
MakeFn: Fn(T, &JobRow) -> EnqueueRequest<F> + Send + Sync + 'static,
{
fn run<'a>(
&'a self,
conn: &'a mut PgConnection,
job: &'a JobRow,
_outcome_context: Option<&'a OutcomeContext>,
) -> Pin<Box<dyn Future<Output = Result<(), AwaError>> + Send + 'a>> {
Box::pin(async move {
let args: T = decode_trigger_args(job)?;
let request = catch_make_panic(
&job.kind,
std::panic::AssertUnwindSafe(|| (self.make)(args, job)),
)?;
insert_with(&mut *conn, &request.args, request.opts).await?;
Ok(())
})
}
}
pub(crate) struct RescuedFollowUp<T, F, MakeFn> {
pub(crate) make: MakeFn,
pub(crate) _phantom: PhantomData<fn() -> (T, F)>,
}
impl<T, F, MakeFn> EnqueueFollowUp for RescuedFollowUp<T, F, MakeFn>
where
T: JobArgs + DeserializeOwned + Send + Sync + 'static,
F: JobArgs + Send + Sync + 'static,
MakeFn:
Fn(T, &JobRow, crate::events::RescueReason) -> EnqueueRequest<F> + Send + Sync + 'static,
{
fn run<'a>(
&'a self,
conn: &'a mut PgConnection,
job: &'a JobRow,
outcome_context: Option<&'a OutcomeContext>,
) -> Pin<Box<dyn Future<Output = Result<(), AwaError>> + Send + 'a>> {
Box::pin(async move {
let Some(OutcomeContext::Rescued { reason }) = outcome_context else {
return Err(AwaError::Validation(
"RescuedFollowUp dispatched without a Rescued OutcomeContext".into(),
));
};
let args: T = decode_trigger_args(job)?;
let request = catch_make_panic(
&job.kind,
std::panic::AssertUnwindSafe(|| (self.make)(args, job, *reason)),
)?;
insert_with(&mut *conn, &request.args, request.opts).await?;
Ok(())
})
}
}
pub(crate) async fn dispatch_specs_in_tx(
conn: &mut PgConnection,
job: &JobRow,
specs: &[BoxedEnqueueSpec],
outcome_context: Option<&OutcomeContext>,
) -> Result<(), AwaError> {
for spec in specs {
spec.run(conn, job, outcome_context).await?;
}
Ok(())
}