use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{from_row::FromRow, row::Row};
use sqlx_postgres::PgRow;
use std::{error::Error, fmt, str::FromStr};
use uuid::Uuid;
use crate::ids::JobId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseJobKindError(String);
impl fmt::Display for ParseJobKindError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown job kind `{}`", self.0)
}
}
impl Error for ParseJobKindError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseJobStatusError(String);
impl fmt::Display for ParseJobStatusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown job status `{}`", self.0)
}
}
impl Error for ParseJobStatusError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum JobKind {
SendEmail,
ResizeImage,
SummarizeText,
WebhookDelivery,
}
impl JobKind {
pub fn as_str(self) -> &'static str {
match self {
JobKind::SendEmail => "send_email",
JobKind::ResizeImage => "resize_image",
JobKind::SummarizeText => "summarize_text",
JobKind::WebhookDelivery => "webhook_delivery",
}
}
}
impl FromStr for JobKind {
type Err = ParseJobKindError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"send_email" => Ok(JobKind::SendEmail),
"resize_image" => Ok(JobKind::ResizeImage),
"summarize_text" => Ok(JobKind::SummarizeText),
"webhook_delivery" => Ok(JobKind::WebhookDelivery),
other => Err(ParseJobKindError(other.to_string())),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum JobStatus {
Queued,
Running,
Succeeded,
Retrying,
FailedPermanent,
Cancelled,
}
impl JobStatus {
pub fn is_terminal(self) -> bool {
matches!(
self,
JobStatus::Succeeded | JobStatus::FailedPermanent | JobStatus::Cancelled
)
}
pub fn as_str(self) -> &'static str {
match self {
JobStatus::Queued => "queued",
JobStatus::Running => "running",
JobStatus::Succeeded => "succeeded",
JobStatus::Retrying => "retrying",
JobStatus::FailedPermanent => "failed_permanent",
JobStatus::Cancelled => "cancelled",
}
}
}
impl FromStr for JobStatus {
type Err = ParseJobStatusError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"queued" => Ok(JobStatus::Queued),
"running" => Ok(JobStatus::Running),
"succeeded" => Ok(JobStatus::Succeeded),
"retrying" => Ok(JobStatus::Retrying),
"failed_permanent" => Ok(JobStatus::FailedPermanent),
"cancelled" => Ok(JobStatus::Cancelled),
other => Err(ParseJobStatusError(other.to_string())),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct Job {
pub id: JobId,
pub kind: JobKind,
#[schema(value_type = serde_json::Value)]
pub payload: serde_json::Value,
pub status: JobStatus,
pub attempts: i32,
pub max_attempts: i32,
pub last_error: Option<String>,
pub run_at: DateTime<Utc>,
pub locked_at: Option<DateTime<Utc>>,
pub locked_by: Option<String>,
pub cancel_requested: bool,
pub idempotency_key: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl<'r> FromRow<'r, PgRow> for Job {
fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
let kind_raw: String = row.try_get("kind")?;
let status_raw: String = row.try_get("status")?;
let kind = JobKind::from_str(&kind_raw).map_err(|e| sqlx::Error::ColumnDecode {
index: "kind".into(),
source: Box::new(e),
})?;
let status = JobStatus::from_str(&status_raw).map_err(|e| sqlx::Error::ColumnDecode {
index: "status".into(),
source: Box::new(e),
})?;
Ok(Self {
id: JobId(row.try_get::<Uuid, _>("id")?),
kind,
payload: row.try_get("payload")?,
status,
attempts: row.try_get("attempts")?,
max_attempts: row.try_get("max_attempts")?,
last_error: row.try_get("last_error")?,
run_at: row.try_get("run_at")?,
locked_at: row.try_get("locked_at")?,
locked_by: row.try_get("locked_by")?,
cancel_requested: row.try_get("cancel_requested")?,
idempotency_key: row.try_get("idempotency_key")?,
created_at: row.try_get("created_at")?,
updated_at: row.try_get("updated_at")?,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct NewJob {
pub kind: JobKind,
#[schema(value_type = serde_json::Value)]
pub payload: serde_json::Value,
#[serde(default)]
pub max_attempts: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, utoipa::ToSchema)]
pub enum JobOutcome {
Succeeded,
Failed,
FailedPermanent,
Cancelled,
}