use std::time::Duration;
use serde::Deserialize;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
use crate::Kind;
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
use crate::Operation;
pub trait Job: Serialize + DeserializeOwned + Clone + Send + Sync + 'static {
const NAME: &'static str;
const OPTIONS: JobOptions = JobOptions::DEFAULT;
type Output: Serialize + DeserializeOwned + Send + Sync + 'static;
#[cfg(any(feature = "postgres", feature = "sqlite"))]
fn run(
self,
cx: &crate::JobCx,
) -> impl std::future::Future<Output = crate::Result<Self::Output>> + Send;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct JobOptions {
pub queue: &'static str,
pub retries: u32,
pub backoff: Backoff,
pub deadline: Option<Duration>,
pub timeout: Option<Duration>,
pub serialize_by: Option<&'static str>,
pub visible_to: Visibility,
pub on_failure: bool,
}
impl JobOptions {
pub const DEFAULT: Self = Self {
queue: "default",
retries: 4,
backoff: Backoff::DEFAULT,
deadline: None,
timeout: None,
serialize_by: None,
visible_to: Visibility::Owner,
on_failure: false,
};
}
impl Default for JobOptions {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Backoff {
pub min: Duration,
pub max: Duration,
}
impl Backoff {
pub const DEFAULT: Self = Self::exponential(Duration::from_secs(1), Duration::from_secs(3_600));
#[must_use]
pub const fn exponential(min: Duration, max: Duration) -> Self {
Self { min, max }
}
#[must_use]
pub fn ceiling(&self, attempt: u32) -> Duration {
let doublings = attempt.saturating_sub(1).min(32);
self.min
.saturating_mul(1_u32.checked_shl(doublings).unwrap_or(u32::MAX))
.min(self.max)
.max(self.min.min(self.max))
}
#[must_use]
pub fn delay(&self, attempt: u32, random: u64) -> Duration {
let ceiling = u64::try_from(self.ceiling(attempt).as_millis()).unwrap_or(u64::MAX);
let half = ceiling / 2;
Duration::from_millis(half + random % (ceiling - half + 1))
}
}
impl Default for Backoff {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Visibility {
Owner,
SignedIn,
Everyone,
Argument(&'static str),
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(bound = "J: Serialize + DeserializeOwned")]
pub struct Failed<J> {
pub id: String,
pub job: J,
pub error: String,
pub attempts: u32,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, schemars::JsonSchema)]
pub struct JobStatus {
pub state: String,
pub queue: String,
pub attempts: u32,
pub max_attempts: u32,
pub error: Option<String>,
pub result: Option<Value>,
pub progress: Option<String>,
pub fraction: Option<f64>,
pub cancel_requested: bool,
pub run_at: String,
pub finished_at: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, schemars::JsonSchema)]
pub struct JobStatusArgs {
pub id: String,
}
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
impl Operation for JobStatusArgs {
#[cfg(any(feature = "postgres", feature = "sqlite"))]
type Context = crate::QueryCx;
type Output = JobStatus;
const KIND: Kind = Kind::Query;
const NAME: &'static str = "rpc.job_status";
#[cfg(any(feature = "postgres", feature = "sqlite"))]
fn run(self, cx: &Self::Context) -> impl Future<Output = crate::Result<Self::Output>> + Send {
crate::server::jobs::status(cx, self.id)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, schemars::JsonSchema)]
pub struct CancelJob {
pub id: String,
}
#[cfg(any(not(feature = "server"), feature = "postgres", feature = "sqlite"))]
impl Operation for CancelJob {
#[cfg(any(feature = "postgres", feature = "sqlite"))]
type Context = crate::MutationCx;
type Output = ();
const KIND: Kind = Kind::Mutation;
const NAME: &'static str = "rpc.cancel_job";
#[cfg(any(feature = "postgres", feature = "sqlite"))]
async fn run(self, cx: &Self::Context) -> crate::Result<Self::Output> {
let id = crate::server::jobs::parse_id(&self.id)?;
crate::server::jobs::cancel_on(cx.tx(), id).await
}
}
#[cfg(any(feature = "postgres", feature = "sqlite"))]
use std::future::Future;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backoff_doubles_up_to_its_ceiling_with_jitter() {
let backoff = Backoff::exponential(Duration::from_secs(2), Duration::from_secs(300));
assert_eq!(backoff.ceiling(1), Duration::from_secs(2));
assert_eq!(backoff.ceiling(2), Duration::from_secs(4));
assert_eq!(backoff.ceiling(8), Duration::from_secs(256));
assert_eq!(backoff.ceiling(9), Duration::from_secs(300));
assert_eq!(backoff.ceiling(200), Duration::from_secs(300));
for random in [0, 1, 999, u64::MAX] {
let wait = backoff.delay(3, random);
assert!(wait >= Duration::from_secs(4), "{wait:?}");
assert!(wait <= Duration::from_secs(8), "{wait:?}");
}
assert_eq!(Backoff::default(), Backoff::DEFAULT);
assert_eq!(JobOptions::default(), JobOptions::DEFAULT);
}
}