use std::{fmt, marker::PhantomData, time::Duration};
use futures_util::future::BoxFuture;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
use sqlx::{Postgres, Transaction};
use crate::{
RunId,
db::await_task_result_snapshot,
error::{Error, Result},
queue::{Queue, validate_queue_name},
types::{
CancellationPolicy, Json, JsonObject, RetryStrategy, SpawnConfig, TaskId,
TaskResultSnapshot, TaskState,
},
};
pub(crate) const MAX_TASK_NAME_BYTES: usize = 1024;
pub(crate) fn validate_task_name(name: &str) -> Result<()> {
if name.trim().is_empty() {
return Err(Error::InvalidOptions("task name must be provided".to_owned()));
}
if name.len() > MAX_TASK_NAME_BYTES {
return Err(Error::InvalidOptions(format!(
"task name must be at most {MAX_TASK_NAME_BYTES} bytes"
)));
}
Ok(())
}
pub struct Task<Input, Output> {
name: &'static str,
marker: PhantomData<fn(Input) -> Output>,
}
impl<Input, Output> Copy for Task<Input, Output> {}
impl<Input, Output> Clone for Task<Input, Output> {
fn clone(&self) -> Self {
*self
}
}
impl<Input, Output> Task<Input, Output> {
pub const fn name(self) -> &'static str {
self.name
}
}
impl<Input, Output> Task<Input, Output>
where
Input: Serialize + DeserializeOwned + Send + 'static,
Output: Serialize + DeserializeOwned + Send + 'static,
{
pub const fn new(name: &'static str) -> Self {
Self { name, marker: PhantomData }
}
}
impl<Input, Output> fmt::Debug for Task<Input, Output> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Task").field(&self.name).finish()
}
}
#[must_use = "spawn calls do nothing until awaited"]
pub struct Spawn<'a, Input, Output> {
queue: &'a Queue,
task: Task<Input, Output>,
input: Input,
options: SpawnConfig,
}
impl<Input, Output> fmt::Debug for Spawn<'_, Input, Output> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Spawn")
.field("task_name", &self.task.name())
.field("queue_name", &self.queue.name())
.field("options", &self.options)
.finish_non_exhaustive()
}
}
impl<'a, Input, Output> Spawn<'a, Input, Output> {
pub(crate) fn new(queue: &'a Queue, task: Task<Input, Output>, input: Input) -> Self {
Self { queue, task, input, options: SpawnConfig::default() }
}
pub const fn max_attempts(mut self, max_attempts: u32) -> Self {
self.options.max_attempts = Some(max_attempts);
self
}
pub const fn retry_strategy(mut self, strategy: RetryStrategy) -> Self {
self.options.retry_strategy = Some(strategy);
self
}
pub fn headers(mut self, headers: JsonObject) -> Self {
self.options.headers = Some(headers);
self
}
pub const fn cancellation(mut self, cancellation: CancellationPolicy) -> Self {
self.options.cancellation = Some(cancellation);
self
}
pub fn idempotency_key(mut self, key: impl Into<String>) -> Self {
self.options.idempotency_key = Some(key.into());
self
}
}
impl<Input, Output> Spawn<'_, Input, Output>
where
Input: Serialize + Send + 'static,
Output: DeserializeOwned + Send + 'static,
{
pub async fn submit(
self,
transaction: &mut Transaction<'_, Postgres>,
) -> Result<SpawnedTask<Input, Output>> {
self.queue.spawn_typed_on(self.task, self.input, self.options, transaction).await
}
}
impl<'a, Input, Output> IntoFuture for Spawn<'a, Input, Output>
where
Input: Serialize + Send + 'static,
Output: DeserializeOwned + Send + 'static,
{
type Output = Result<SpawnedTask<Input, Output>>;
type IntoFuture = BoxFuture<'a, Self::Output>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move { self.queue.spawn_typed(self.task, self.input, self.options).await })
}
}
pub struct TaskRef<Input, Output> {
queue_name: String,
task_name: String,
task_id: TaskId,
marker: PhantomData<fn(Input) -> Output>,
}
impl<Input, Output> TaskRef<Input, Output> {
fn from_parts(task: Task<Input, Output>, queue_name: String, task_id: TaskId) -> Self {
Self { queue_name, task_name: task.name().to_owned(), task_id, marker: PhantomData }
}
pub fn queue_name(&self) -> &str {
&self.queue_name
}
pub fn task_name(&self) -> &str {
&self.task_name
}
pub const fn task_id(&self) -> TaskId {
self.task_id
}
}
impl<Input, Output> Clone for TaskRef<Input, Output> {
fn clone(&self) -> Self {
Self {
queue_name: self.queue_name.clone(),
task_name: self.task_name.clone(),
task_id: self.task_id,
marker: PhantomData,
}
}
}
impl<Input, Output> fmt::Debug for TaskRef<Input, Output> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TaskRef")
.field("queue_name", &self.queue_name)
.field("task_name", &self.task_name)
.field("task_id", &self.task_id)
.finish()
}
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TaskRefWire {
queue_name: String,
task_name: String,
task_id: TaskId,
}
impl<Input, Output> Serialize for TaskRef<Input, Output>
where
Input: Serialize + DeserializeOwned + Send + 'static,
Output: Serialize + DeserializeOwned + Send + 'static,
{
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
TaskRefWire {
queue_name: self.queue_name.clone(),
task_name: self.task_name.clone(),
task_id: self.task_id,
}
.serialize(serializer)
}
}
impl<'de, Input, Output> Deserialize<'de> for TaskRef<Input, Output>
where
Input: Serialize + DeserializeOwned + Send + 'static,
Output: Serialize + DeserializeOwned + Send + 'static,
{
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let wire = TaskRefWire::deserialize(deserializer)?;
validate_task_name(&wire.task_name).map_err(serde::de::Error::custom)?;
let queue_name = validate_queue_name(&wire.queue_name).map_err(serde::de::Error::custom)?;
Ok(Self {
queue_name,
task_name: wire.task_name,
task_id: wire.task_id,
marker: PhantomData,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "state", rename_all = "lowercase")]
pub enum TaskSnapshot<T> {
Pending,
Running,
Sleeping,
Completed {
result: T,
},
Failed {
failure: Json,
},
Cancelled,
}
impl<T> TaskSnapshot<T> {
pub const fn state(&self) -> TaskState {
match self {
Self::Pending => TaskState::Pending,
Self::Running => TaskState::Running,
Self::Sleeping => TaskState::Sleeping,
Self::Completed { .. } => TaskState::Completed,
Self::Failed { .. } => TaskState::Failed,
Self::Cancelled => TaskState::Cancelled,
}
}
pub const fn is_terminal(&self) -> bool {
matches!(self, Self::Completed { .. } | Self::Failed { .. } | Self::Cancelled)
}
}
pub struct TaskHandle<Input, Output> {
queue: Queue,
task_ref: TaskRef<Input, Output>,
}
impl<Input, Output> fmt::Debug for TaskHandle<Input, Output> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TaskHandle").field("task_ref", &self.task_ref).finish_non_exhaustive()
}
}
impl<Input, Output> Clone for TaskHandle<Input, Output> {
fn clone(&self) -> Self {
Self { queue: self.queue.clone(), task_ref: self.task_ref.clone() }
}
}
impl<Input, Output> TaskHandle<Input, Output> {
pub(crate) fn new(queue: Queue, task: Task<Input, Output>, task_id: TaskId) -> Self {
let task_ref = TaskRef::from_parts(task, queue.name().to_owned(), task_id);
Self { queue, task_ref }
}
pub(crate) fn from_ref(queue: Queue, task_ref: TaskRef<Input, Output>) -> Self {
debug_assert_eq!(queue.name(), task_ref.queue_name());
Self { queue, task_ref }
}
pub const fn task_id(&self) -> TaskId {
self.task_ref.task_id()
}
pub fn task_ref(&self) -> TaskRef<Input, Output> {
self.task_ref.clone()
}
pub async fn snapshot(&self) -> Result<Option<TaskSnapshot<Output>>>
where
Output: DeserializeOwned,
{
self.queue
.fetch_task_result(self.task_ref.task_name(), self.task_id())
.await?
.map(decode_snapshot::<Output>)
.transpose()
}
pub async fn cancel(&self) -> Result<()> {
self.queue.ensure_task_ref(self.task_ref.task_name(), self.task_id()).await?;
self.queue.cancel_task(self.task_id()).await
}
pub async fn cancel_in(&self, transaction: &mut Transaction<'_, Postgres>) -> Result<()> {
let connection = &mut **transaction;
self.queue
.ensure_task_ref_on(self.task_ref.task_name(), self.task_id(), &mut *connection)
.await?;
self.queue.cancel_task_on(self.task_id(), &mut *connection).await
}
pub async fn retry(&self) -> Result<RunId> {
self.queue.ensure_task_ref(self.task_ref.task_name(), self.task_id()).await?;
self.queue.retry_task(self.task_id()).await
}
pub async fn retry_in(&self, transaction: &mut Transaction<'_, Postgres>) -> Result<RunId> {
let connection = &mut **transaction;
self.queue
.ensure_task_ref_on(self.task_ref.task_name(), self.task_id(), &mut *connection)
.await?;
self.queue.retry_task_on(self.task_id(), &mut *connection).await
}
pub async fn result(&self) -> Result<Output>
where
Output: DeserializeOwned,
{
self.result_inner(None).await
}
pub async fn result_with_timeout(&self, timeout: Duration) -> Result<Output>
where
Output: DeserializeOwned,
{
self.result_inner(Some(timeout)).await
}
async fn result_inner(&self, timeout: Option<Duration>) -> Result<Output>
where
Output: DeserializeOwned,
{
let snapshot = await_task_result_snapshot(
self.queue.pool(),
self.queue.name(),
self.task_ref.task_name(),
self.task_id(),
timeout,
)
.await?;
decode_result(snapshot)
}
}
pub struct SpawnedTask<Input, Output> {
handle: TaskHandle<Input, Output>,
created: bool,
}
impl<Input, Output> fmt::Debug for SpawnedTask<Input, Output> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SpawnedTask")
.field("handle", &self.handle)
.field("created", &self.created)
.finish()
}
}
impl<Input, Output> Clone for SpawnedTask<Input, Output> {
fn clone(&self) -> Self {
Self { handle: self.handle.clone(), created: self.created }
}
}
impl<Input, Output> SpawnedTask<Input, Output> {
pub(crate) const fn new(handle: TaskHandle<Input, Output>, created: bool) -> Self {
Self { handle, created }
}
pub const fn created(&self) -> bool {
self.created
}
pub const fn task_id(&self) -> TaskId {
self.handle.task_id()
}
pub fn task_ref(&self) -> TaskRef<Input, Output> {
self.handle.task_ref()
}
pub fn into_handle(self) -> TaskHandle<Input, Output> {
self.handle
}
pub async fn snapshot(&self) -> Result<Option<TaskSnapshot<Output>>>
where
Output: DeserializeOwned,
{
self.handle.snapshot().await
}
pub async fn cancel(&self) -> Result<()> {
self.handle.cancel().await
}
pub async fn cancel_in(&self, transaction: &mut Transaction<'_, Postgres>) -> Result<()> {
self.handle.cancel_in(transaction).await
}
pub async fn retry(&self) -> Result<RunId> {
self.handle.retry().await
}
pub async fn retry_in(&self, transaction: &mut Transaction<'_, Postgres>) -> Result<RunId> {
self.handle.retry_in(transaction).await
}
pub async fn result(&self) -> Result<Output>
where
Output: DeserializeOwned,
{
self.handle.result().await
}
pub async fn result_with_timeout(&self, timeout: Duration) -> Result<Output>
where
Output: DeserializeOwned,
{
self.handle.result_with_timeout(timeout).await
}
}
fn decode_snapshot<R: DeserializeOwned>(snapshot: TaskResultSnapshot) -> Result<TaskSnapshot<R>> {
Ok(match snapshot {
TaskResultSnapshot::Pending => TaskSnapshot::Pending,
TaskResultSnapshot::Running => TaskSnapshot::Running,
TaskResultSnapshot::Sleeping => TaskSnapshot::Sleeping,
TaskResultSnapshot::Completed { result } => {
TaskSnapshot::Completed { result: serde_json::from_value(result)? }
}
TaskResultSnapshot::Failed { failure } => TaskSnapshot::Failed { failure },
TaskResultSnapshot::Cancelled => TaskSnapshot::Cancelled,
})
}
pub(crate) fn decode_result<R: DeserializeOwned>(snapshot: TaskResultSnapshot) -> Result<R> {
match snapshot {
TaskResultSnapshot::Completed { result } => Ok(serde_json::from_value(result)?),
TaskResultSnapshot::Failed { failure } => Err(Error::TaskFailed { failure }),
TaskResultSnapshot::Cancelled => Err(Error::Cancelled),
TaskResultSnapshot::Pending
| TaskResultSnapshot::Running
| TaskResultSnapshot::Sleeping => {
Err(Error::Other("task result wait returned a non-terminal snapshot".to_owned()))
}
}
}
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use uuid::Uuid;
use super::{Task, TaskId, TaskRef, TaskSnapshot, validate_task_name};
const REFERENCE_TASK: Task<Value, Value> = Task::new("reference-task");
const OTHER_TASK: Task<Value, Value> = Task::new("other-task");
#[test]
fn task_name_validation_uses_utf8_byte_length() {
assert!(validate_task_name("task").is_ok());
assert!(validate_task_name(" ").is_err());
assert!(validate_task_name(&format!("{}é", "x".repeat(1022))).is_ok());
assert!(validate_task_name(&format!("{}é", "x".repeat(1023))).is_err());
}
#[test]
fn task_reference_serialization_preserves_task_identity() {
let task_id = TaskId::from_uuid(Uuid::nil());
let task_ref = TaskRef::from_parts(REFERENCE_TASK, "queue".to_owned(), task_id);
let encoded = serde_json::to_value(&task_ref).unwrap();
assert_eq!(
encoded,
json!({
"queueName": "queue",
"taskName": "reference-task",
"taskId": task_id,
})
);
let decoded: TaskRef<Value, Value> = serde_json::from_value(encoded).unwrap();
assert_eq!(decoded.queue_name(), "queue");
assert_eq!(decoded.task_name(), "reference-task");
assert_eq!(decoded.task_id(), task_id);
}
#[test]
fn task_reference_deserialization_rejects_invalid_identity() {
let task_id = TaskId::from_uuid(Uuid::nil());
for task_name in [" ".to_owned(), format!("{}é", "x".repeat(1023))] {
let encoded = json!({
"queueName": "queue",
"taskName": task_name,
"taskId": task_id,
});
let error = serde_json::from_value::<TaskRef<Value, Value>>(encoded)
.expect_err("invalid task name must be rejected");
assert!(error.to_string().contains("task name"));
}
let encoded = json!({
"queueName": " ",
"taskName": REFERENCE_TASK.name(),
"taskId": task_id,
});
let error = serde_json::from_value::<TaskRef<Value, Value>>(encoded)
.expect_err("invalid queue name must be rejected");
assert!(error.to_string().contains("queue name"));
}
#[test]
fn task_references_keep_same_typed_tasks_distinct_by_name() {
let task_id = TaskId::from_uuid(Uuid::nil());
let reference = TaskRef::from_parts(REFERENCE_TASK, "queue".to_owned(), task_id);
let other = TaskRef::from_parts(OTHER_TASK, "queue".to_owned(), task_id);
assert_eq!(reference.task_name(), "reference-task");
assert_eq!(other.task_name(), "other-task");
}
#[test]
fn typed_task_snapshot_serializes_with_state_tag() {
let snapshot = TaskSnapshot::Completed { result: json!({"ok": true}) };
assert_eq!(
serde_json::to_value(snapshot).unwrap(),
json!({"state": "completed", "result": {"ok": true}})
);
}
}