use std::marker::PhantomData;
use chrono::{DateTime, Utc};
use serde::Serialize;
use uuid::Uuid;
use super::config::JobModel;
use super::dialect::{JobDb, sql, stored_time};
use super::error::EnqueueError;
use super::validate::validate_kind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JobStatus {
Pending,
Running,
Succeeded,
Dead,
Cancelled,
}
impl std::fmt::Display for JobStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pending => f.write_str("pending"),
Self::Running => f.write_str("running"),
Self::Succeeded => f.write_str("succeeded"),
Self::Dead => f.write_str("dead"),
Self::Cancelled => f.write_str("cancelled"),
}
}
}
impl JobStatus {
pub fn from_db(s: &str) -> Option<Self> {
match s {
"pending" => Some(Self::Pending),
"running" => Some(Self::Running),
"succeeded" => Some(Self::Succeeded),
"dead" => Some(Self::Dead),
"cancelled" => Some(Self::Cancelled),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct EnqueuedJob {
pub id: Uuid,
pub status: JobStatus,
}
pub struct JobRequest<J> {
kind: &'static str,
version: i16,
max_attempts_default: u32,
max_attempts_override: Option<u32>,
payload: serde_json::Value,
run_at: Option<DateTime<Utc>>,
_job: PhantomData<J>,
}
impl<J> JobRequest<J> {
pub fn new(model: &JobModel<J>, payload: &J) -> Result<Self, EnqueueError>
where
J: Serialize + serde::de::DeserializeOwned,
{
model
.validate_identity()
.map_err(EnqueueError::invalid_kind)?;
validate_kind(model.kind()).map_err(EnqueueError::invalid_kind)?;
let payload = serde_json::to_value(payload).map_err(EnqueueError::serialize)?;
let size = serde_json::to_vec(&payload)
.map_err(EnqueueError::serialize)?
.len();
if size > model.max_payload_bytes() {
return Err(EnqueueError::payload_too_large(
size,
model.max_payload_bytes(),
));
}
Ok(Self {
kind: model.kind(),
version: model.version(),
max_attempts_default: model.max_attempts(),
max_attempts_override: None,
payload,
run_at: None,
_job: PhantomData,
})
}
pub fn run_at(mut self, run_at: DateTime<Utc>) -> Self {
self.run_at = Some(run_at);
self
}
pub fn delay(self, delay: std::time::Duration) -> Self {
self.run_at(Utc::now() + chrono::Duration::from_std(delay).unwrap_or_default())
}
pub fn max_attempts(mut self, attempts: u32) -> Self {
self.max_attempts_override = Some(if attempts < 1 { 1 } else { attempts });
self
}
pub fn kind(&self) -> &'static str {
self.kind
}
pub fn version(&self) -> i16 {
self.version
}
pub fn payload(&self) -> &serde_json::Value {
&self.payload
}
pub fn run_at_ts(&self) -> Option<DateTime<Utc>> {
self.run_at
}
pub fn effective_max_attempts(&self) -> u32 {
self.max_attempts_override
.unwrap_or(self.max_attempts_default)
}
}
pub(crate) async fn insert_job<'c, E>(
executor: E,
request: &JobRequest<impl Serialize + serde::de::DeserializeOwned>,
) -> Result<EnqueuedJob, EnqueueError>
where
E: sqlx::Executor<'c, Database = JobDb>,
{
let id = Uuid::new_v4();
let max_attempts: i32 = request.effective_max_attempts().min(i32::MAX as u32) as i32;
let run_at = stored_time(request.run_at_ts().unwrap_or_else(Utc::now));
sqlx::query(sql::INSERT)
.bind(id)
.bind(request.kind())
.bind(request.version())
.bind(sqlx::types::Json(request.payload()))
.bind(max_attempts)
.bind(run_at)
.bind(run_at)
.execute(executor)
.await
.map_err(EnqueueError::database)?;
Ok(EnqueuedJob {
id,
status: JobStatus::Pending,
})
}