Skip to main content

steda/
types.rs

1//! Public queue, task, retry, and worker types.
2
3use std::{fmt, str::FromStr, time::Duration};
4
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value as JsonValue};
7use uuid::Uuid;
8
9/// JSON value type for task parameters, headers, checkpoints, and results.
10pub type Json = JsonValue;
11
12/// JSON object type for headers and option payloads.
13pub type JsonObject = Map<String, Json>;
14
15/// Steda logical-task identifier.
16///
17/// Generated by `PostgreSQL` as `UUIDv7`. Task IDs and run IDs are intentionally distinct Rust
18/// types so they cannot be mixed at compile time.
19#[derive(
20    Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq, Hash, PartialOrd, Ord,
21)]
22#[serde(transparent)]
23#[sqlx(transparent)]
24pub struct TaskId(Uuid);
25
26impl TaskId {
27    /// Wrap a UUID as a logical-task identifier.
28    pub const fn from_uuid(value: Uuid) -> Self {
29        Self(value)
30    }
31
32    /// Return the underlying UUID.
33    pub const fn into_uuid(self) -> Uuid {
34        self.0
35    }
36}
37
38impl From<Uuid> for TaskId {
39    fn from(value: Uuid) -> Self {
40        Self::from_uuid(value)
41    }
42}
43
44impl From<TaskId> for Uuid {
45    fn from(value: TaskId) -> Self {
46        value.into_uuid()
47    }
48}
49
50impl fmt::Display for TaskId {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        self.0.fmt(f)
53    }
54}
55
56impl FromStr for TaskId {
57    type Err = uuid::Error;
58
59    fn from_str(value: &str) -> Result<Self, Self::Err> {
60        value.parse().map(Self::from_uuid)
61    }
62}
63
64/// Steda execution-run identifier.
65///
66/// Generated by `PostgreSQL` as `UUIDv7`.
67#[derive(
68    Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq, Hash, PartialOrd, Ord,
69)]
70#[serde(transparent)]
71#[sqlx(transparent)]
72pub struct RunId(Uuid);
73
74impl RunId {
75    /// Wrap a UUID as an execution-run identifier.
76    pub const fn from_uuid(value: Uuid) -> Self {
77        Self(value)
78    }
79
80    /// Return the underlying UUID.
81    pub const fn into_uuid(self) -> Uuid {
82        self.0
83    }
84}
85
86impl From<Uuid> for RunId {
87    fn from(value: Uuid) -> Self {
88        Self::from_uuid(value)
89    }
90}
91
92impl From<RunId> for Uuid {
93    fn from(value: RunId) -> Self {
94        value.into_uuid()
95    }
96}
97
98impl fmt::Display for RunId {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        self.0.fmt(f)
101    }
102}
103
104impl FromStr for RunId {
105    type Err = uuid::Error;
106
107    fn from_str(value: &str) -> Result<Self, Self::Err> {
108        value.parse().map(Self::from_uuid)
109    }
110}
111
112/// Retry strategy for failed tasks.
113///
114/// Retry delays use [`Duration`] at the Rust boundary. Steda converts them to the canonical
115/// numeric-second representation only when crossing into `PostgreSQL`.
116///
117/// ```
118/// use std::time::Duration;
119///
120/// use steda::RetryStrategy;
121///
122/// let strategy =
123///     RetryStrategy::exponential(Duration::from_secs(5), 2.0, Some(Duration::from_secs(300)));
124/// assert!(matches!(strategy, RetryStrategy::Exponential { .. }));
125/// ```
126#[derive(Debug, Clone, Copy, PartialEq)]
127pub enum RetryStrategy {
128    /// Retry after a fixed delay.
129    Fixed {
130        /// Delay before retrying.
131        delay: Duration,
132    },
133
134    /// Retry with exponential backoff.
135    Exponential {
136        /// Initial delay before retrying.
137        initial_delay: Duration,
138        /// Exponential multiplier.
139        factor: f64,
140        /// Optional upper bound for computed delay.
141        max_delay: Option<Duration>,
142    },
143
144    /// Do not retry failed runs.
145    None,
146}
147
148impl RetryStrategy {
149    /// Create a fixed retry strategy.
150    pub const fn fixed(delay: Duration) -> Self {
151        Self::Fixed { delay }
152    }
153
154    /// Create an exponential retry strategy.
155    pub const fn exponential(
156        initial_delay: Duration,
157        factor: f64,
158        max_delay: Option<Duration>,
159    ) -> Self {
160        Self::Exponential { initial_delay, factor, max_delay }
161    }
162
163    /// Disable retries.
164    pub const fn none() -> Self {
165        Self::None
166    }
167}
168
169/// Durable cancellation deadlines for one logical task.
170///
171/// `max_delay` is measured from enqueue until the first start and no longer applies once the task
172/// has begun. `max_duration` is measured from the first start across the remainder of the logical
173/// task, including retries and durable sleeps. `PostgreSQL` evaluates the resulting whole-second
174/// deadlines against its authoritative clock.
175#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
176pub struct CancellationPolicy {
177    /// Maximum duration from first start.
178    pub(crate) max_duration: Option<Duration>,
179
180    /// Maximum delay before first start.
181    pub(crate) max_delay: Option<Duration>,
182}
183
184impl CancellationPolicy {
185    /// Create an empty cancellation policy.
186    pub const fn new() -> Self {
187        Self { max_duration: None, max_delay: None }
188    }
189
190    /// Set the maximum duration from the first start across the logical task.
191    #[must_use]
192    pub const fn max_duration(mut self, duration: Duration) -> Self {
193        self.max_duration = Some(duration);
194        self
195    }
196
197    /// Set the maximum delay between enqueue and the first start.
198    #[must_use]
199    pub const fn max_delay(mut self, delay: Duration) -> Self {
200        self.max_delay = Some(delay);
201        self
202    }
203}
204
205/// Options for spawning a task.
206#[derive(Debug, Clone, Default)]
207pub(crate) struct SpawnConfig {
208    /// Maximum number of attempts, including the first execution.
209    pub max_attempts: Option<u32>,
210
211    /// Retry strategy.
212    pub retry_strategy: Option<RetryStrategy>,
213
214    /// Custom headers for the task.
215    pub headers: Option<JsonObject>,
216
217    /// Cancellation policy.
218    pub cancellation: Option<CancellationPolicy>,
219
220    /// Idempotency key for deduplicating task creation.
221    pub idempotency_key: Option<String>,
222}
223
224/// Internal result of spawning a logical task.
225#[derive(Debug, Clone, Copy)]
226pub(crate) struct SpawnResult {
227    /// Unique logical task identifier.
228    pub task_id: TaskId,
229
230    /// Whether this was a new task, false if deduplicated.
231    pub created: bool,
232}
233
234/// Optional persisted queue-maintenance policy overrides.
235///
236/// Unset values leave the corresponding field at its current/default value. New queues default to
237/// a 30-day terminal task TTL and a maximum of 1,000 logical-task deletions per cleanup pass.
238#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
239pub struct QueuePolicyOptions {
240    /// Terminal task cleanup TTL.
241    pub(crate) cleanup_ttl: Option<Duration>,
242
243    /// Maximum rows cleaned per cleanup pass.
244    pub(crate) cleanup_limit: Option<u32>,
245}
246
247impl QueuePolicyOptions {
248    /// Create an empty set of queue-policy overrides.
249    pub const fn new() -> Self {
250        Self { cleanup_ttl: None, cleanup_limit: None }
251    }
252
253    /// Set the terminal-task cleanup TTL.
254    #[must_use]
255    pub const fn cleanup_ttl(mut self, cleanup_ttl: Duration) -> Self {
256        self.cleanup_ttl = Some(cleanup_ttl);
257        self
258    }
259
260    /// Set the maximum number of logical tasks deleted per cleanup pass.
261    #[must_use]
262    pub const fn cleanup_limit(mut self, cleanup_limit: u32) -> Self {
263        self.cleanup_limit = Some(cleanup_limit);
264        self
265    }
266}
267
268/// Queue maintenance policy snapshot.
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct QueuePolicy {
271    /// Queue name.
272    pub queue_name: String,
273
274    /// Terminal task cleanup TTL.
275    pub cleanup_ttl: Duration,
276
277    /// Maximum cleanup rows per pass.
278    pub cleanup_limit: u32,
279}
280
281/// Result of one retention cleanup pass for a queue.
282#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct QueueCleanup {
284    /// Queue name.
285    pub queue_name: String,
286
287    /// Number of terminal tasks deleted.
288    pub tasks_deleted: u32,
289}
290
291/// Durable task state.
292#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
293#[serde(rename_all = "lowercase")]
294pub enum TaskState {
295    /// Task is waiting to run.
296    Pending,
297
298    /// Task is currently running.
299    Running,
300
301    /// Task is sleeping until a later time.
302    Sleeping,
303
304    /// Task completed successfully.
305    Completed,
306
307    /// Task failed terminally.
308    Failed,
309
310    /// Task was cancelled.
311    Cancelled,
312}
313
314/// Internal JSON-erased task result snapshot returned by `PostgreSQL`.
315#[derive(Debug, Clone, PartialEq, Eq)]
316pub(crate) enum TaskResultSnapshot {
317    /// Task is waiting to run.
318    Pending,
319
320    /// Task is currently running.
321    Running,
322
323    /// Task is sleeping until a later time.
324    Sleeping,
325
326    /// Task completed successfully.
327    Completed {
328        /// Completion payload.
329        result: Json,
330    },
331
332    /// Task failed terminally.
333    Failed {
334        /// Failure payload.
335        failure: Json,
336    },
337
338    /// Task was cancelled.
339    Cancelled,
340}
341
342impl TaskResultSnapshot {
343    /// Whether this raw database snapshot is terminal.
344    pub(crate) const fn is_terminal(&self) -> bool {
345        matches!(self, Self::Completed { .. } | Self::Failed { .. } | Self::Cancelled)
346    }
347}
348
349/// A claimed task ready for execution.
350#[derive(Debug, Clone)]
351pub(crate) struct ClaimedTask {
352    /// Claimed run identifier.
353    pub(crate) run_id: RunId,
354
355    /// Logical task identifier.
356    pub(crate) task_id: TaskId,
357
358    /// Registered task name.
359    pub(crate) task_name: String,
360
361    /// Attempt number for this run.
362    pub(crate) attempt: u32,
363
364    /// Task parameters.
365    pub(crate) params: Json,
366
367    /// Task headers.
368    pub(crate) headers: Option<JsonObject>,
369}