Skip to main content

pgtask_postgres/
store.rs

1use std::{
2    collections::{HashMap, HashSet},
3    num::NonZeroU32,
4    sync::Arc,
5    time::Duration,
6};
7
8use chrono::{DateTime, Utc};
9use pgtask_core::{
10    Checkpoint, EnqueueRequest, EnqueueResult, HandlerVersion, LeaseRenewal, LeaseToken, MisfirePolicy, Queue,
11    QueueConfig, QueueName, RetryPolicy, Schedule, ScheduleConfig, ScheduleDefinition, ScheduleError, ScheduleId,
12    ScheduleName, Signal, SignalName, StepName, StorageProtocolRange, Task, TaskId, TaskName, TaskResult, TaskState,
13    WorkerId, WorkerRecord,
14};
15use serde_json::Value;
16use sqlx::{
17    FromRow, PgConnection, PgPool,
18    postgres::{PgListener, PgPoolOptions},
19};
20use thiserror::Error;
21use tokio::sync::{broadcast, mpsc, oneshot};
22use tracing::{Instrument, info_span};
23use uuid::Uuid;
24
25static MIGRATION_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
26
27const UNDEFINED_SCHEMA: &str = "3F000";
28
29#[derive(Debug, Error)]
30pub enum PostgresError {
31    #[error("database operation failed: {0}")]
32    Database(#[from] sqlx::Error),
33    #[error("database migration failed: {0}")]
34    Migration(#[from] sqlx::migrate::MigrateError),
35    #[error("invalid task data returned by Postgres: {0}")]
36    InvalidTask(String),
37    #[error("max_attempts must be greater than zero")]
38    InvalidMaxAttempts,
39    #[error("claim limit must be greater than zero")]
40    InvalidClaimLimit,
41    #[error("retention limit must be greater than zero")]
42    InvalidRetentionLimit,
43    #[error("lease duration must be greater than zero")]
44    InvalidLeaseDuration,
45    #[error("at least one handler capability is required")]
46    MissingCapabilities,
47    #[error("at least one queue is required")]
48    MissingQueues,
49    #[error("handler version exceeds the Postgres integer range")]
50    InvalidHandlerVersion,
51    #[error("schedule claim limit must be greater than zero")]
52    InvalidScheduleLimit,
53    #[error("sleep duration exceeds the PostgreSQL bigint range")]
54    InvalidSleepDuration,
55    #[error("wait recovery limit must be greater than zero")]
56    InvalidWaitLimit,
57    #[error("result wait timeout must be greater than zero and fit in the PostgreSQL bigint range")]
58    InvalidResultWaitTimeout,
59    #[error("retry policy values exceed the PostgreSQL integer range")]
60    InvalidRetryPolicy,
61    #[error("notification listener failed: {0}")]
62    Notification(String),
63    #[error("invalid storage protocol range {minimum}..={maximum} returned by Postgres")]
64    InvalidStorageProtocolRange { minimum: i32, maximum: i32 },
65    #[error(
66        "database storage protocols {database_minimum}..={database_maximum} are incompatible with client protocols {client_minimum}..={client_maximum}"
67    )]
68    IncompatibleStorageProtocol {
69        database_minimum: u32,
70        database_maximum: u32,
71        client_minimum: u32,
72        client_maximum: u32,
73    },
74    #[error(transparent)]
75    Schedule(#[from] ScheduleError),
76}
77
78#[derive(Clone)]
79pub struct StoreConfig {
80    database_url: String,
81    listener_url: String,
82    query_connections: NonZeroU32,
83    listener_connections: NonZeroU32,
84}
85
86impl StoreConfig {
87    pub fn new(database_url: impl Into<String>) -> Self {
88        let database_url = database_url.into();
89        Self {
90            listener_url: database_url.clone(),
91            database_url,
92            query_connections: NonZeroU32::new(10).expect("10 is nonzero"),
93            listener_connections: NonZeroU32::MIN,
94        }
95    }
96
97    #[must_use]
98    pub fn with_listener_url(mut self, listener_url: impl Into<String>) -> Self {
99        self.listener_url = listener_url.into();
100        self
101    }
102
103    #[must_use]
104    pub const fn with_query_connections(mut self, connections: NonZeroU32) -> Self {
105        self.query_connections = connections;
106        self
107    }
108
109    #[must_use]
110    pub const fn with_listener_connections(mut self, connections: NonZeroU32) -> Self {
111        self.listener_connections = connections;
112        self
113    }
114}
115
116#[derive(Clone, Debug)]
117pub struct Store {
118    pool: PgPool,
119    notifications: Arc<NotificationHub>,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct Notification {
124    channel: String,
125    payload: String,
126}
127
128impl Notification {
129    pub fn channel(&self) -> &str {
130        &self.channel
131    }
132
133    pub fn payload(&self) -> &str {
134        &self.payload
135    }
136}
137
138#[derive(Debug)]
139pub struct ReadyListener {
140    filters: HashMap<String, Option<String>>,
141    receiver: broadcast::Receiver<NotificationEvent>,
142}
143
144impl ReadyListener {
145    pub async fn recv(&mut self) -> Result<Notification, PostgresError> {
146        loop {
147            match self.receiver.recv().await {
148                Ok(NotificationEvent::Ready(notification))
149                    if self.filters.get(notification.channel()).is_some_and(|payload| {
150                        payload.as_ref().is_none_or(|payload| payload == notification.payload())
151                    }) =>
152                {
153                    return Ok(notification);
154                }
155                Ok(NotificationEvent::Ready(_)) | Err(broadcast::error::RecvError::Lagged(_)) => {}
156                Ok(NotificationEvent::Disconnected(error)) => return Err(PostgresError::Notification(error)),
157                Err(broadcast::error::RecvError::Closed) => {
158                    return Err(PostgresError::Notification("notification hub stopped".to_owned()));
159                }
160            }
161        }
162    }
163}
164
165#[derive(Clone, Debug)]
166enum NotificationEvent {
167    Ready(Notification),
168    Disconnected(String),
169}
170
171#[derive(Debug)]
172struct NotificationHub {
173    commands: mpsc::Sender<NotificationCommand>,
174    events: broadcast::Sender<NotificationEvent>,
175}
176
177#[derive(Debug)]
178struct NotificationCommand {
179    channels: Vec<String>,
180    ready: oneshot::Sender<Result<(), String>>,
181}
182
183#[derive(Clone, Debug, PartialEq)]
184pub enum SignalWait {
185    Ready(Value),
186    Waiting,
187}
188
189#[derive(Clone, Debug, PartialEq)]
190pub enum ResultWait {
191    Ready(Value),
192    Waiting,
193}
194
195#[derive(Clone, Debug, PartialEq)]
196pub enum TaskResultWait {
197    Ready(TaskResult),
198    NotFound,
199    TimedOut,
200}
201
202#[derive(Clone, Copy, Debug, PartialEq, Eq)]
203pub struct QueueDemand {
204    pub ready_tasks: u64,
205    pub capable_tasks: u64,
206    pub unroutable_tasks: u64,
207}
208
209#[derive(Clone, Debug)]
210pub struct SignalWaitRequest<'a> {
211    pub task_id: TaskId,
212    pub attempt: u16,
213    pub lease_token: LeaseToken,
214    pub step_name: &'a StepName,
215    pub occurrence: u32,
216    pub signal_name: &'a SignalName,
217    pub signal_occurrence: u32,
218    pub timeout: Option<Duration>,
219}
220
221#[derive(Clone, Debug)]
222pub struct ResultWaitRequest<'a> {
223    pub task_id: TaskId,
224    pub attempt: u16,
225    pub lease_token: LeaseToken,
226    pub step_name: &'a StepName,
227    pub occurrence: u32,
228    pub result_task_id: TaskId,
229    pub timeout: Option<Duration>,
230}
231
232#[derive(Clone, Debug)]
233pub struct SpawnRequest<'a> {
234    pub parent_task_id: TaskId,
235    pub parent_attempt: u16,
236    pub parent_lease_token: LeaseToken,
237    pub step_name: &'a StepName,
238    pub occurrence: u32,
239    pub task: &'a EnqueueRequest,
240}
241
242struct SuspendTaskRequest<'a> {
243    task_id: TaskId,
244    attempt: u16,
245    lease_token: LeaseToken,
246    step_name: &'a StepName,
247    occurrence: u32,
248    wake_at: Option<DateTime<Utc>>,
249    delay_milliseconds: Option<i64>,
250}
251
252impl NotificationHub {
253    fn start(pool: PgPool) -> Arc<Self> {
254        let (commands, receiver) = mpsc::channel(128);
255        let (events, _) = broadcast::channel(1_024);
256        let hub = Arc::new(Self { commands, events });
257        tokio::spawn(run_notification_hub(pool, receiver, hub.events.clone()));
258        hub
259    }
260
261    async fn subscribe(&self, filters: HashMap<String, Option<String>>) -> Result<ReadyListener, PostgresError> {
262        let channels = filters.keys().cloned().collect();
263        let receiver = self.events.subscribe();
264        let (ready, confirmation) = oneshot::channel();
265        self.commands
266            .send(NotificationCommand { channels, ready })
267            .await
268            .map_err(|_| PostgresError::Notification("notification hub stopped".to_owned()))?;
269        confirmation
270            .await
271            .map_err(|_| PostgresError::Notification("notification hub stopped".to_owned()))?
272            .map_err(PostgresError::Notification)?;
273        Ok(ReadyListener { filters, receiver })
274    }
275}
276
277async fn run_notification_hub(
278    pool: PgPool,
279    mut commands: mpsc::Receiver<NotificationCommand>,
280    events: broadcast::Sender<NotificationEvent>,
281) {
282    let mut channels = HashSet::new();
283    while let Some(command) = commands.recv().await {
284        channels.extend(command.channels);
285        let mut listener = match PgListener::connect_with(&pool).await {
286            Ok(listener) => listener,
287            Err(error) => {
288                let _ = command.ready.send(Err(error.to_string()));
289                continue;
290            }
291        };
292        if let Err(error) = listen_to_channels(&mut listener, &channels).await {
293            let _ = command.ready.send(Err(error.to_string()));
294            continue;
295        }
296        let _ = command.ready.send(Ok(()));
297
298        'connected: loop {
299            tokio::select! {
300                command = commands.recv() => {
301                    let Some(command) = command else {
302                        return;
303                    };
304                    let new_channels: Vec<_> = command
305                        .channels
306                        .into_iter()
307                        .filter(|channel| channels.insert(channel.clone()))
308                        .collect();
309                    for channel in new_channels {
310                        if let Err(error) = listener.listen(&channel).await {
311                            let message = error.to_string();
312                            let _ = command.ready.send(Err(message.clone()));
313                            let _ = events.send(NotificationEvent::Disconnected(message));
314                            break 'connected;
315                        }
316                    }
317                    let _ = command.ready.send(Ok(()));
318                }
319                notification = listener.recv() => {
320                    match notification {
321                        Ok(notification) => {
322                            let _ = events.send(NotificationEvent::Ready(Notification {
323                                channel: notification.channel().to_owned(),
324                                payload: notification.payload().to_owned(),
325                            }));
326                        }
327                        Err(error) => {
328                            let _ = events.send(NotificationEvent::Disconnected(error.to_string()));
329                            break 'connected;
330                        }
331                    }
332                }
333            }
334        }
335    }
336}
337
338async fn listen_to_channels(listener: &mut PgListener, channels: &HashSet<String>) -> Result<(), sqlx::Error> {
339    for channel in channels {
340        listener.listen(channel).await?;
341    }
342    Ok(())
343}
344
345impl Store {
346    pub async fn connect(database_url: &str) -> Result<Self, PostgresError> {
347        Self::connect_with_config(&StoreConfig::new(database_url)).await
348    }
349
350    pub async fn connect_with_config(config: &StoreConfig) -> Result<Self, PostgresError> {
351        let pool = PgPoolOptions::new()
352            .max_connections(config.query_connections.get())
353            .connect(&config.database_url)
354            .await?;
355        let listener_pool = PgPoolOptions::new()
356            .max_connections(config.listener_connections.get())
357            .connect(&config.listener_url)
358            .await?;
359        Ok(Self {
360            pool,
361            notifications: NotificationHub::start(listener_pool),
362        })
363    }
364
365    pub fn from_pool(pool: PgPool) -> Self {
366        Self::from_pools(pool.clone(), pool)
367    }
368
369    pub fn from_pools(pool: PgPool, listener_pool: PgPool) -> Self {
370        Self {
371            pool,
372            notifications: NotificationHub::start(listener_pool),
373        }
374    }
375
376    pub const fn pool(&self) -> &PgPool {
377        &self.pool
378    }
379
380    pub async fn health(&self) -> Result<(), PostgresError> {
381        sqlx::query("SELECT 1").execute(&self.pool).await?;
382        Ok(())
383    }
384
385    pub async fn storage_protocol_version(&self) -> Result<u32, PostgresError> {
386        let version: i32 = sqlx::query_scalar("SELECT pgtask.storage_protocol_version()")
387            .fetch_one(&self.pool)
388            .await?;
389        u32::try_from(version).map_err(invalid_number)
390    }
391
392    pub async fn storage_protocol_range(&self) -> Result<StorageProtocolRange, PostgresError> {
393        let (minimum, maximum): (i32, i32) =
394            sqlx::query_as("SELECT minimum, maximum FROM pgtask.storage_protocol_range()")
395                .fetch_one(&self.pool)
396                .await?;
397        let Ok(minimum_value) = u32::try_from(minimum) else {
398            return Err(PostgresError::InvalidStorageProtocolRange { minimum, maximum });
399        };
400        let Ok(maximum_value) = u32::try_from(maximum) else {
401            return Err(PostgresError::InvalidStorageProtocolRange { minimum, maximum });
402        };
403        StorageProtocolRange::new(minimum_value, maximum_value)
404            .ok_or(PostgresError::InvalidStorageProtocolRange { minimum, maximum })
405    }
406
407    /// Returns `None` when the schema is absent, so a caller may still migrate it.
408    pub async fn ensure_storage_protocol(
409        &self,
410        client: StorageProtocolRange,
411    ) -> Result<Option<StorageProtocolRange>, PostgresError> {
412        let database = match self.storage_protocol_range().await {
413            Ok(database) => database,
414            Err(PostgresError::Database(sqlx::Error::Database(error)))
415                if error.code().as_deref() == Some(UNDEFINED_SCHEMA) =>
416            {
417                return Ok(None);
418            }
419            Err(error) => return Err(error),
420        };
421        if database.overlaps(client) {
422            return Ok(Some(database));
423        }
424        Err(PostgresError::IncompatibleStorageProtocol {
425            database_minimum: database.minimum,
426            database_maximum: database.maximum,
427            client_minimum: client.minimum,
428            client_maximum: client.maximum,
429        })
430    }
431
432    pub async fn ready_listener(&self, queue_name: &QueueName) -> Result<ReadyListener, PostgresError> {
433        self.ready_listener_for(std::slice::from_ref(queue_name)).await
434    }
435
436    pub async fn ready_listener_for(&self, queue_names: &[QueueName]) -> Result<ReadyListener, PostgresError> {
437        if queue_names.is_empty() {
438            return Err(PostgresError::MissingQueues);
439        }
440        let mut channels = HashMap::from([("pgtask_schedule".to_owned(), None), ("pgtask_wait".to_owned(), None)]);
441        for queue_name in queue_names {
442            let channel: String = sqlx::query_scalar("SELECT pgtask.ready_channel($1)")
443                .bind(queue_name.as_str())
444                .fetch_one(&self.pool)
445                .await?;
446            channels.insert(channel, Some(queue_name.to_string()));
447        }
448        self.notifications.subscribe(channels).await
449    }
450
451    pub async fn result_listener(&self, task_id: TaskId) -> Result<ReadyListener, PostgresError> {
452        let channel: String = sqlx::query_scalar("SELECT pgtask.result_channel($1)")
453            .bind(task_id.as_uuid())
454            .fetch_one(&self.pool)
455            .await?;
456        self.notifications
457            .subscribe(HashMap::from([(channel, Some(task_id.to_string()))]))
458            .await
459    }
460
461    pub async fn register_worker(
462        &self,
463        worker_id: WorkerId,
464        queue_name: &QueueName,
465        version: &str,
466        capabilities: &[(TaskName, HandlerVersion, RetryPolicy)],
467        ttl: Duration,
468    ) -> Result<(), PostgresError> {
469        if capabilities.is_empty() {
470            return Err(PostgresError::MissingCapabilities);
471        }
472        let ttl_milliseconds = i64::try_from(ttl.as_millis()).map_err(|_| PostgresError::InvalidLeaseDuration)?;
473        if ttl_milliseconds == 0 {
474            return Err(PostgresError::InvalidLeaseDuration);
475        }
476        let task_names: Vec<_> = capabilities.iter().map(|(name, _, _)| name.as_str()).collect();
477        let handler_versions: Vec<_> = capabilities
478            .iter()
479            .map(|(_, handler_version, _)| {
480                i32::try_from(handler_version.get()).map_err(|_| PostgresError::InvalidHandlerVersion)
481            })
482            .collect::<Result<_, _>>()?;
483        let policies = capabilities
484            .iter()
485            .map(|(_, _, policy)| retry_policy_columns(*policy))
486            .collect::<Result<Vec<_>, _>>()?;
487        let retry_kinds: Vec<_> = policies.iter().map(|policy| policy.kind).collect();
488        let base_delays: Vec<_> = policies.iter().map(|policy| policy.base_delay).collect();
489        let factors: Vec<_> = policies.iter().map(|policy| policy.factor).collect();
490        let max_delays: Vec<_> = policies.iter().map(|policy| policy.max_delay).collect();
491        sqlx::query("SELECT pgtask.register_worker($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)")
492            .bind(worker_id.as_uuid())
493            .bind(queue_name.as_str())
494            .bind(version)
495            .bind(&task_names)
496            .bind(&handler_versions)
497            .bind(&retry_kinds)
498            .bind(&base_delays)
499            .bind(&factors)
500            .bind(&max_delays)
501            .bind(ttl_milliseconds)
502            .execute(&self.pool)
503            .await?;
504        Ok(())
505    }
506
507    pub async fn heartbeat_worker(
508        &self,
509        worker_id: WorkerId,
510        ttl: Duration,
511        draining: bool,
512    ) -> Result<bool, PostgresError> {
513        let ttl_milliseconds = i64::try_from(ttl.as_millis()).map_err(|_| PostgresError::InvalidLeaseDuration)?;
514        if ttl_milliseconds == 0 {
515            return Err(PostgresError::InvalidLeaseDuration);
516        }
517        let updated = sqlx::query_scalar("SELECT pgtask.heartbeat_worker($1, $2, $3)")
518            .bind(worker_id.as_uuid())
519            .bind(ttl_milliseconds)
520            .bind(draining)
521            .fetch_one(&self.pool)
522            .await?;
523        Ok(updated)
524    }
525
526    /// Counts workers the database still considers live, which is not the same as the number of
527    /// processes reporting metrics: a worker whose heartbeat fails keeps reporting and stops counting.
528    pub async fn live_worker_count(&self, queue_name: &QueueName) -> Result<u64, PostgresError> {
529        let count: i64 = sqlx::query_scalar("SELECT pgtask.live_worker_count($1)")
530            .bind(queue_name.as_str())
531            .fetch_one(&self.pool)
532            .await?;
533        u64::try_from(count).map_err(invalid_number)
534    }
535
536    pub async fn get_worker(&self, worker_id: WorkerId) -> Result<Option<WorkerRecord>, PostgresError> {
537        let row: Option<WorkerRow> = sqlx::query_as(
538            r"
539            SELECT id, queue_name, version, draining, started_at, heartbeat_at, expires_at
540            FROM pgtask.workers
541            WHERE id = $1
542            ",
543        )
544        .bind(worker_id.as_uuid())
545        .fetch_optional(&self.pool)
546        .await?;
547        let Some(row) = row else {
548            return Ok(None);
549        };
550        let capabilities: Vec<CapabilityRow> = sqlx::query_as(
551            r"
552            SELECT task_name, handler_version
553            FROM pgtask.worker_capabilities
554            WHERE worker_id = $1
555            ORDER BY task_name, handler_version
556            ",
557        )
558        .bind(worker_id.as_uuid())
559        .fetch_all(&self.pool)
560        .await?;
561        Ok(Some(row.try_into_record(capabilities)?))
562    }
563
564    pub async fn migrate(&self) -> Result<(), PostgresError> {
565        let _process_guard = MIGRATION_LOCK.lock().await;
566        let mut migrations = sqlx::migrate!();
567        migrations.dangerous_set_table_name("public._sqlx_migrations");
568        let mut connection = self.pool.acquire().await?;
569        sqlx::query("SELECT pg_advisory_lock($1)")
570            .bind(123_656_071_951_211_i64)
571            .execute(&mut *connection)
572            .await?;
573        let migration = migrations.run(&mut *connection).await;
574        let unlock = sqlx::query("SELECT pg_advisory_unlock($1)")
575            .bind(123_656_071_951_211_i64)
576            .execute(&mut *connection)
577            .await;
578        migration?;
579        unlock?;
580        Ok(())
581    }
582
583    pub async fn configure_grants(
584        &self,
585        owner: &str,
586        producer: &str,
587        worker: &str,
588        observer: &str,
589        administrator: &str,
590    ) -> Result<(), PostgresError> {
591        sqlx::query("SELECT pgtask.configure_grants($1::regrole, $2::regrole, $3::regrole, $4::regrole, $5::regrole)")
592            .bind(owner)
593            .bind(producer)
594            .bind(worker)
595            .bind(observer)
596            .bind(administrator)
597            .execute(&self.pool)
598            .await?;
599        Ok(())
600    }
601
602    pub async fn put_queue(&self, config: &QueueConfig) -> Result<Queue, PostgresError> {
603        let retention_seconds = i64::try_from(config.terminal_retention.as_secs())
604            .map_err(|_| PostgresError::InvalidTask("queue retention exceeds the Postgres bigint range".to_owned()))?;
605        let idempotency_retention_seconds = i64::try_from(config.idempotency_retention.as_secs()).map_err(|_| {
606            PostgresError::InvalidTask("idempotency retention exceeds the Postgres bigint range".to_owned())
607        })?;
608        let max_outstanding_tasks = config
609            .max_outstanding_tasks
610            .map(|maximum| i64::try_from(maximum.get()))
611            .transpose()
612            .map_err(|_| PostgresError::InvalidTask("queue capacity exceeds the Postgres bigint range".to_owned()))?;
613        let starvation_timeout_seconds = i64::try_from(config.starvation_timeout.as_secs()).map_err(|_| {
614            PostgresError::InvalidTask("starvation timeout exceeds the Postgres bigint range".to_owned())
615        })?;
616        let row: QueueRow = sqlx::query_as(
617            "SELECT name, terminal_retention_seconds, idempotency_retention_seconds, max_outstanding_tasks, starvation_timeout_seconds, paused_at, created_at, updated_at FROM pgtask.put_queue($1, $2, $3, $4, $5)",
618        )
619        .bind(config.name.as_str())
620        .bind(retention_seconds)
621        .bind(idempotency_retention_seconds)
622        .bind(max_outstanding_tasks)
623        .bind(starvation_timeout_seconds)
624        .fetch_one(&self.pool)
625        .await?;
626        Queue::try_from(row)
627    }
628
629    pub async fn get_queue(&self, queue_name: &QueueName) -> Result<Option<Queue>, PostgresError> {
630        let row: Option<QueueRow> = sqlx::query_as(
631            r"
632            SELECT name, terminal_retention_seconds, idempotency_retention_seconds, max_outstanding_tasks,
633                starvation_timeout_seconds, paused_at, created_at, updated_at
634            FROM pgtask.queues
635            WHERE name = $1
636            ",
637        )
638        .bind(queue_name.as_str())
639        .fetch_optional(&self.pool)
640        .await?;
641        row.map(Queue::try_from).transpose()
642    }
643
644    pub async fn queue_demand(
645        &self,
646        queue_name: &QueueName,
647        capabilities: &[(TaskName, HandlerVersion)],
648    ) -> Result<QueueDemand, PostgresError> {
649        if capabilities.is_empty() {
650            return Err(PostgresError::MissingCapabilities);
651        }
652        let task_names: Vec<_> = capabilities.iter().map(|(name, _)| name.as_str()).collect();
653        let handler_versions: Vec<_> = capabilities
654            .iter()
655            .map(|(_, version)| i32::try_from(version.get()).map_err(|_| PostgresError::InvalidHandlerVersion))
656            .collect::<Result<_, _>>()?;
657        let row: QueueDemandRow = sqlx::query_as("SELECT * FROM pgtask.queue_demand($1, $2, $3)")
658            .bind(queue_name.as_str())
659            .bind(&task_names)
660            .bind(&handler_versions)
661            .fetch_one(&self.pool)
662            .await?;
663        Ok(QueueDemand {
664            ready_tasks: u64::try_from(row.ready).map_err(invalid_number)?,
665            capable_tasks: u64::try_from(row.capable).map_err(invalid_number)?,
666            unroutable_tasks: u64::try_from(row.unroutable).map_err(invalid_number)?,
667        })
668    }
669
670    pub async fn set_queue_paused(&self, queue_name: &QueueName, paused: bool) -> Result<Option<Queue>, PostgresError> {
671        let row: Option<QueueRow> = sqlx::query_as(
672            "SELECT name, terminal_retention_seconds, idempotency_retention_seconds, max_outstanding_tasks, starvation_timeout_seconds, paused_at, created_at, updated_at FROM pgtask.set_queue_paused($1, $2)",
673        )
674        .bind(queue_name.as_str())
675        .bind(paused)
676        .fetch_optional(&self.pool)
677        .await?;
678        row.map(Queue::try_from).transpose()
679    }
680
681    pub async fn put_schedule(&self, config: &ScheduleConfig) -> Result<Schedule, PostgresError> {
682        Self::validate_request(&config.task)?;
683        let now: DateTime<Utc> = sqlx::query_scalar("SELECT statement_timestamp()")
684            .fetch_one(&self.pool)
685            .await?;
686        let next_run_at = config.start_at.unwrap_or(config.definition.next_after(now)?);
687        let (kind, interval_milliseconds, cron_expression) = match &config.definition {
688            ScheduleDefinition::Interval { every } => (
689                "interval",
690                Some(i64::try_from(every.as_millis()).map_err(|_| ScheduleError::IntervalOutOfRange)?),
691                None,
692            ),
693            ScheduleDefinition::Cron { expression } => ("cron", None, Some(expression.as_str())),
694        };
695        let (misfire_policy, catch_up_limit) = match config.misfire_policy {
696            MisfirePolicy::Skip => ("skip", None),
697            MisfirePolicy::Latest => ("latest", None),
698            MisfirePolicy::CatchUp { limit } => ("catch_up", Some(i32::from(limit.get()))),
699        };
700        let handler_version =
701            i32::try_from(config.task.handler_version.get()).map_err(|_| PostgresError::InvalidHandlerVersion)?;
702        let row: ScheduleRow = sqlx::query_as(
703            r"
704            SELECT id, name, kind, interval_milliseconds, cron_expression, misfire_policy, catch_up_limit,
705                queue_name, task_name, handler_version, payload, headers, priority, max_attempts,
706                next_run_at, paused_at, created_at, updated_at
707            FROM pgtask.put_schedule($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
708            ",
709        )
710        .bind(config.id.as_uuid())
711        .bind(config.name.as_str())
712        .bind(kind)
713        .bind(interval_milliseconds)
714        .bind(cron_expression)
715        .bind(misfire_policy)
716        .bind(catch_up_limit)
717        .bind(config.task.queue_name.as_str())
718        .bind(config.task.task_name.as_str())
719        .bind(handler_version)
720        .bind(&config.task.payload)
721        .bind(Value::Object(config.task.headers.clone()))
722        .bind(config.task.priority)
723        .bind(i32::from(config.task.max_attempts))
724        .bind(next_run_at)
725        .fetch_one(&self.pool)
726        .await?;
727        Schedule::try_from(row)
728    }
729
730    pub async fn get_schedule(&self, schedule_id: ScheduleId) -> Result<Option<Schedule>, PostgresError> {
731        let row: Option<ScheduleRow> = sqlx::query_as(
732            r"
733            SELECT id, name, kind, interval_milliseconds, cron_expression, misfire_policy, catch_up_limit,
734                queue_name, task_name, handler_version, payload, headers, priority, max_attempts,
735                next_run_at, paused_at, created_at, updated_at
736            FROM pgtask.get_schedule($1)
737            ",
738        )
739        .bind(schedule_id.as_uuid())
740        .fetch_optional(&self.pool)
741        .await?;
742        row.map(Schedule::try_from).transpose()
743    }
744
745    pub async fn set_schedule_paused(
746        &self,
747        schedule_id: ScheduleId,
748        paused: bool,
749    ) -> Result<Option<Schedule>, PostgresError> {
750        let row: Option<ScheduleRow> = sqlx::query_as(
751            r"
752            SELECT id, name, kind, interval_milliseconds, cron_expression, misfire_policy, catch_up_limit,
753                queue_name, task_name, handler_version, payload, headers, priority, max_attempts,
754                next_run_at, paused_at, created_at, updated_at
755            FROM pgtask.set_schedule_paused($1, $2)
756            ",
757        )
758        .bind(schedule_id.as_uuid())
759        .bind(paused)
760        .fetch_optional(&self.pool)
761        .await?;
762        row.map(Schedule::try_from).transpose()
763    }
764
765    pub async fn delete_schedule(&self, schedule_id: ScheduleId) -> Result<bool, PostgresError> {
766        let deleted = sqlx::query_scalar("SELECT pgtask.delete_schedule($1)")
767            .bind(schedule_id.as_uuid())
768            .fetch_one(&self.pool)
769            .await?;
770        Ok(deleted)
771    }
772
773    pub async fn materialize_due_schedules(&self, limit: u16) -> Result<u64, PostgresError> {
774        if limit == 0 {
775            return Err(PostgresError::InvalidScheduleLimit);
776        }
777        let started_at = std::time::Instant::now();
778        let mut transaction = self.pool.begin().await?;
779        let now: DateTime<Utc> = sqlx::query_scalar("SELECT statement_timestamp()")
780            .fetch_one(&mut *transaction)
781            .await?;
782        let rows: Vec<ScheduleRow> = sqlx::query_as(
783            r"
784            SELECT id, name, kind, interval_milliseconds, cron_expression, misfire_policy, catch_up_limit,
785                queue_name, task_name, handler_version, payload, headers, priority, max_attempts,
786                next_run_at, paused_at, created_at, updated_at
787            FROM pgtask.claim_due_schedules($1)
788            ",
789        )
790        .bind(i32::from(limit))
791        .fetch_all(&mut *transaction)
792        .await?;
793        let mut total = 0_u64;
794        for row in rows {
795            let schedule = Schedule::try_from(row)?;
796            let materialization =
797                schedule
798                    .config
799                    .definition
800                    .materialize(schedule.next_run_at, now, schedule.config.misfire_policy)?;
801            let span = info_span!(
802                "pgtask.schedule.materialize",
803                pgtask.schedule.name = %schedule.config.name,
804                pgtask.schedule.occurrences = materialization.occurrences.len(),
805            );
806            let materialized: i64 = sqlx::query_scalar("SELECT pgtask.materialize_schedule($1, $2, $3, $4)")
807                .bind(schedule.config.id.as_uuid())
808                .bind(schedule.next_run_at)
809                .bind(&materialization.occurrences)
810                .bind(materialization.next_run_at)
811                .fetch_one(&mut *transaction)
812                .instrument(span)
813                .await?;
814            let materialized = u64::try_from(materialized).map_err(invalid_number)?;
815            let kind = match schedule.config.definition {
816                ScheduleDefinition::Interval { .. } => "interval",
817                ScheduleDefinition::Cron { .. } => "cron",
818            };
819            let lag = materialization
820                .occurrences
821                .first()
822                .map_or(Duration::ZERO, |occurrence| {
823                    now.signed_duration_since(*occurrence).to_std().unwrap_or_default()
824                });
825            pgtask_otel::record_schedule_occurrences(
826                schedule.config.task.queue_name.as_str(),
827                schedule.config.task.task_name.as_str(),
828                kind,
829                materialized,
830                materialization.skipped,
831                lag,
832            );
833            total = total
834                .checked_add(materialized)
835                .ok_or_else(|| PostgresError::InvalidTask("materialized task count overflowed".to_owned()))?;
836        }
837        transaction.commit().await?;
838        pgtask_otel::record_schedule_materialization(started_at.elapsed());
839        Ok(total)
840    }
841
842    pub async fn next_schedule_delay(&self) -> Result<Option<Duration>, PostgresError> {
843        let milliseconds: Option<i64> = sqlx::query_scalar("SELECT pgtask.next_schedule_delay_milliseconds()")
844            .fetch_one(&self.pool)
845            .await?;
846        milliseconds
847            .map(|milliseconds| u64::try_from(milliseconds).map(Duration::from_millis))
848            .transpose()
849            .map_err(invalid_number)
850    }
851
852    pub async fn next_task_delay(
853        &self,
854        queue_name: &QueueName,
855        capabilities: &[(TaskName, HandlerVersion)],
856    ) -> Result<Option<Duration>, PostgresError> {
857        if capabilities.is_empty() {
858            return Err(PostgresError::MissingCapabilities);
859        }
860        let task_names: Vec<_> = capabilities.iter().map(|(name, _)| name.as_str()).collect();
861        let handler_versions: Vec<_> = capabilities
862            .iter()
863            .map(|(_, version)| i32::try_from(version.get()).map_err(|_| PostgresError::InvalidHandlerVersion))
864            .collect::<Result<_, _>>()?;
865        let milliseconds: Option<i64> = sqlx::query_scalar("SELECT pgtask.next_task_delay_milliseconds($1, $2, $3)")
866            .bind(queue_name.as_str())
867            .bind(&task_names)
868            .bind(&handler_versions)
869            .fetch_one(&self.pool)
870            .await?;
871        milliseconds
872            .map(|milliseconds| u64::try_from(milliseconds).map(Duration::from_millis))
873            .transpose()
874            .map_err(invalid_number)
875    }
876
877    pub async fn delete_expired_terminal(&self, queue_name: &QueueName, limit: u16) -> Result<u64, PostgresError> {
878        if limit == 0 {
879            return Err(PostgresError::InvalidRetentionLimit);
880        }
881        let deleted: i64 = sqlx::query_scalar("SELECT pgtask.delete_expired_terminal($1, $2)")
882            .bind(queue_name.as_str())
883            .bind(i32::from(limit))
884            .fetch_one(&self.pool)
885            .await?;
886        u64::try_from(deleted).map_err(invalid_number)
887    }
888
889    pub async fn delete_expired_idempotency_keys(
890        &self,
891        queue_name: &QueueName,
892        limit: u16,
893    ) -> Result<u64, PostgresError> {
894        if limit == 0 {
895            return Err(PostgresError::InvalidRetentionLimit);
896        }
897        let deleted: i64 = sqlx::query_scalar("SELECT pgtask.delete_expired_idempotency_keys($1, $2)")
898            .bind(queue_name.as_str())
899            .bind(i32::from(limit))
900            .fetch_one(&self.pool)
901            .await?;
902        u64::try_from(deleted).map_err(invalid_number)
903    }
904
905    pub async fn enqueue(&self, request: &EnqueueRequest) -> Result<EnqueueResult, PostgresError> {
906        Self::validate_request(request)?;
907        let handler_version =
908            i32::try_from(request.handler_version.get()).map_err(|_| PostgresError::InvalidHandlerVersion)?;
909        let span = info_span!(
910            "pgtask.enqueue",
911            otel.kind = "producer",
912            pgtask.task.name = %request.task_name,
913            pgtask.queue.name = %request.queue_name,
914        );
915        let headers = pgtask_otel::inject_span_context(&request.headers, &span);
916        let row: EnqueueRow =
917            sqlx::query_as("SELECT task_id, created FROM pgtask.enqueue($1, $2, $3, $4, $5, $6, $7, $8, $9)")
918                .bind(request.task_name.as_str())
919                .bind(&request.payload)
920                .bind(request.queue_name.as_str())
921                .bind(handler_version)
922                .bind(request.run_at)
923                .bind(request.priority)
924                .bind(i32::from(request.max_attempts))
925                .bind(&request.idempotency_key)
926                .bind(Value::Object(headers))
927                .fetch_one(&self.pool)
928                .instrument(span)
929                .await?;
930
931        let result: EnqueueResult = row.into();
932        if result.created {
933            pgtask_otel::record_enqueued(request.queue_name.as_str(), request.task_name.as_str(), 1);
934        }
935        Ok(result)
936    }
937
938    pub async fn spawn_task(&self, request: SpawnRequest<'_>) -> Result<Option<EnqueueResult>, PostgresError> {
939        Self::validate_request(request.task)?;
940        let parent_occurrence = i32::try_from(request.occurrence).map_err(invalid_number)?;
941        let handler_version =
942            i32::try_from(request.task.handler_version.get()).map_err(|_| PostgresError::InvalidHandlerVersion)?;
943        let span = info_span!(
944            "pgtask.spawn",
945            otel.kind = "producer",
946            pgtask.task.parent_id = %request.parent_task_id,
947            pgtask.task.name = %request.task.task_name,
948            pgtask.queue.name = %request.task.queue_name,
949            pgtask.step.name = %request.step_name,
950            pgtask.step.occurrence = request.occurrence,
951        );
952        let headers = pgtask_otel::inject_span_context(&request.task.headers, &span);
953        let row: Option<EnqueueRow> = sqlx::query_as(
954            "SELECT task_id, created FROM pgtask.spawn_task($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
955        )
956        .bind(request.parent_task_id.as_uuid())
957        .bind(i32::from(request.parent_attempt))
958        .bind(request.parent_lease_token.as_uuid())
959        .bind(request.step_name.as_str())
960        .bind(parent_occurrence)
961        .bind(request.task.task_name.as_str())
962        .bind(&request.task.payload)
963        .bind(request.task.queue_name.as_str())
964        .bind(handler_version)
965        .bind(request.task.run_at)
966        .bind(request.task.priority)
967        .bind(i32::from(request.task.max_attempts))
968        .bind(Value::Object(headers))
969        .fetch_optional(&self.pool)
970        .instrument(span)
971        .await?;
972        let result = row.map(EnqueueResult::from);
973        if let Some(result) = result
974            && result.created
975        {
976            pgtask_otel::record_enqueued(request.task.queue_name.as_str(), request.task.task_name.as_str(), 1);
977        }
978        Ok(result)
979    }
980
981    pub async fn enqueue_on(
982        connection: &mut PgConnection,
983        request: &EnqueueRequest,
984    ) -> Result<EnqueueResult, PostgresError> {
985        Self::validate_request(request)?;
986        let handler_version =
987            i32::try_from(request.handler_version.get()).map_err(|_| PostgresError::InvalidHandlerVersion)?;
988        let span = info_span!(
989            "pgtask.enqueue",
990            otel.kind = "producer",
991            pgtask.task.name = %request.task_name,
992            pgtask.queue.name = %request.queue_name,
993        );
994        let headers = pgtask_otel::inject_span_context(&request.headers, &span);
995        let row: EnqueueRow =
996            sqlx::query_as("SELECT task_id, created FROM pgtask.enqueue($1, $2, $3, $4, $5, $6, $7, $8, $9)")
997                .bind(request.task_name.as_str())
998                .bind(&request.payload)
999                .bind(request.queue_name.as_str())
1000                .bind(handler_version)
1001                .bind(request.run_at)
1002                .bind(request.priority)
1003                .bind(i32::from(request.max_attempts))
1004                .bind(&request.idempotency_key)
1005                .bind(Value::Object(headers))
1006                .fetch_one(connection)
1007                .instrument(span)
1008                .await?;
1009
1010        let result: EnqueueResult = row.into();
1011        if result.created {
1012            pgtask_otel::record_enqueued(request.queue_name.as_str(), request.task_name.as_str(), 1);
1013        }
1014        Ok(result)
1015    }
1016
1017    pub async fn enqueue_many(&self, requests: &[EnqueueRequest]) -> Result<Vec<EnqueueResult>, PostgresError> {
1018        for request in requests {
1019            Self::validate_request(request)?;
1020            i32::try_from(request.handler_version.get()).map_err(|_| PostgresError::InvalidHandlerVersion)?;
1021        }
1022        let span = info_span!(
1023            "pgtask.enqueue_many",
1024            otel.kind = "producer",
1025            pgtask.task.count = requests.len()
1026        );
1027        let requests_with_context: Vec<_> = requests
1028            .iter()
1029            .cloned()
1030            .map(|mut request| {
1031                request.headers = pgtask_otel::inject_span_context(&request.headers, &span);
1032                request
1033            })
1034            .collect();
1035        let rows: Vec<BatchEnqueueRow> = sqlx::query_as(
1036            "SELECT request_index, task_id, created FROM pgtask.enqueue_many($1) ORDER BY request_index",
1037        )
1038        .bind(
1039            serde_json::to_value(&requests_with_context)
1040                .map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1041        )
1042        .fetch_all(&self.pool)
1043        .instrument(span)
1044        .await?;
1045        Self::batch_results(rows, requests)
1046    }
1047
1048    pub async fn enqueue_many_on(
1049        connection: &mut PgConnection,
1050        requests: &[EnqueueRequest],
1051    ) -> Result<Vec<EnqueueResult>, PostgresError> {
1052        for request in requests {
1053            Self::validate_request(request)?;
1054            i32::try_from(request.handler_version.get()).map_err(|_| PostgresError::InvalidHandlerVersion)?;
1055        }
1056        let span = info_span!(
1057            "pgtask.enqueue_many",
1058            otel.kind = "producer",
1059            pgtask.task.count = requests.len()
1060        );
1061        let requests_with_context: Vec<_> = requests
1062            .iter()
1063            .cloned()
1064            .map(|mut request| {
1065                request.headers = pgtask_otel::inject_span_context(&request.headers, &span);
1066                request
1067            })
1068            .collect();
1069        let rows: Vec<BatchEnqueueRow> = sqlx::query_as(
1070            "SELECT request_index, task_id, created FROM pgtask.enqueue_many($1) ORDER BY request_index",
1071        )
1072        .bind(
1073            serde_json::to_value(&requests_with_context)
1074                .map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1075        )
1076        .fetch_all(connection)
1077        .instrument(span)
1078        .await?;
1079        Self::batch_results(rows, requests)
1080    }
1081
1082    fn batch_results(
1083        rows: Vec<BatchEnqueueRow>,
1084        requests: &[EnqueueRequest],
1085    ) -> Result<Vec<EnqueueResult>, PostgresError> {
1086        if rows.len() != requests.len()
1087            || rows
1088                .iter()
1089                .enumerate()
1090                .any(|(index, row)| usize::try_from(row.request_index) != Ok(index))
1091        {
1092            return Err(PostgresError::InvalidTask(
1093                "batch enqueue returned an invalid result set".to_owned(),
1094            ));
1095        }
1096        Ok(rows
1097            .into_iter()
1098            .zip(requests)
1099            .map(|(row, request)| {
1100                if row.created {
1101                    pgtask_otel::record_enqueued(request.queue_name.as_str(), request.task_name.as_str(), 1);
1102                }
1103                EnqueueResult {
1104                    task_id: TaskId::from_uuid(row.task_id),
1105                    created: row.created,
1106                }
1107            })
1108            .collect())
1109    }
1110
1111    fn validate_request(request: &EnqueueRequest) -> Result<(), PostgresError> {
1112        if request.max_attempts == 0 {
1113            return Err(PostgresError::InvalidMaxAttempts);
1114        }
1115        Ok(())
1116    }
1117
1118    pub async fn claim(
1119        &self,
1120        queue_name: &QueueName,
1121        worker_id: WorkerId,
1122        capabilities: &[(TaskName, HandlerVersion)],
1123        limit: u16,
1124        lease_duration: Duration,
1125    ) -> Result<Vec<Task>, PostgresError> {
1126        if limit == 0 {
1127            return Err(PostgresError::InvalidClaimLimit);
1128        }
1129        if lease_duration.is_zero() {
1130            return Err(PostgresError::InvalidLeaseDuration);
1131        }
1132        if capabilities.is_empty() {
1133            return Err(PostgresError::MissingCapabilities);
1134        }
1135
1136        let task_names: Vec<&str> = capabilities.iter().map(|(name, _)| name.as_str()).collect();
1137        let handler_versions: Vec<i32> = capabilities
1138            .iter()
1139            .map(|(_, version)| i32::try_from(version.get()).map_err(|_| PostgresError::InvalidHandlerVersion))
1140            .collect::<Result<_, _>>()?;
1141        let lease_milliseconds =
1142            i64::try_from(lease_duration.as_millis()).map_err(|_| PostgresError::InvalidLeaseDuration)?;
1143
1144        let span = info_span!(
1145            "pgtask.claim",
1146            pgtask.queue.name = %queue_name,
1147            pgtask.claim.limit = limit,
1148        );
1149        let rows: Vec<TaskRow> = sqlx::query_as(
1150            r"
1151            SELECT id, queue_name, task_name, handler_version, payload, headers, state, priority,
1152                run_at, attempt, max_attempts, lease_token, lease_owner, lease_expires_at,
1153                created_at, updated_at, completed_at, result, error, parent_task_id,
1154                retry_kind, retry_base_delay_milliseconds, retry_factor, retry_max_delay_milliseconds
1155            FROM pgtask.claim($1, $2, $3, $4, $5, $6)
1156            ",
1157        )
1158        .bind(queue_name.as_str())
1159        .bind(worker_id.as_uuid())
1160        .bind(&task_names)
1161        .bind(&handler_versions)
1162        .bind(i32::from(limit))
1163        .bind(lease_milliseconds)
1164        .fetch_all(&self.pool)
1165        .instrument(span)
1166        .await?;
1167
1168        rows.into_iter()
1169            .map(|row| {
1170                let task = Task::try_from(row)?;
1171                pgtask_otel::record_claimed(task.queue_name.as_str(), task.task_name.as_str());
1172                Ok(task)
1173            })
1174            .collect()
1175    }
1176
1177    pub async fn get_task(&self, task_id: TaskId) -> Result<Option<Task>, PostgresError> {
1178        let row: Option<TaskRow> = sqlx::query_as(
1179            r"
1180            SELECT id, queue_name, task_name, handler_version, payload, headers, state, priority,
1181                run_at, attempt, max_attempts, lease_token, lease_owner, lease_expires_at,
1182                created_at, updated_at, completed_at, result, error, parent_task_id,
1183                retry_kind, retry_base_delay_milliseconds, retry_factor, retry_max_delay_milliseconds
1184            FROM pgtask.get_task($1)
1185            ",
1186        )
1187        .bind(task_id.as_uuid())
1188        .fetch_optional(&self.pool)
1189        .await?;
1190        row.map(Task::try_from).transpose()
1191    }
1192
1193    pub async fn task_count_by_state(&self, queue_name: &QueueName, state: TaskState) -> Result<u64, PostgresError> {
1194        let count: i64 =
1195            sqlx::query_scalar("SELECT count(*) FROM pgtask.task_view WHERE queue_name = $1 AND state = $2")
1196                .bind(queue_name.as_str())
1197                .bind(state.as_str())
1198                .fetch_one(&self.pool)
1199                .await?;
1200        Ok(u64::try_from(count).expect("PostgreSQL count is nonnegative"))
1201    }
1202
1203    pub async fn get_checkpoint(
1204        &self,
1205        task_id: TaskId,
1206        handler_version: HandlerVersion,
1207        step_name: &StepName,
1208        occurrence: u32,
1209    ) -> Result<Option<Checkpoint>, PostgresError> {
1210        let handler_version = i32::try_from(handler_version.get()).map_err(|_| PostgresError::InvalidHandlerVersion)?;
1211        let occurrence = i32::try_from(occurrence).map_err(invalid_number)?;
1212        let row: Option<CheckpointRow> = sqlx::query_as(
1213            r"
1214            SELECT task_id, handler_version, step_name, occurrence, value, created_at
1215            FROM pgtask.get_checkpoint($1, $2, $3, $4)
1216            ",
1217        )
1218        .bind(task_id.as_uuid())
1219        .bind(handler_version)
1220        .bind(step_name.as_str())
1221        .bind(occurrence)
1222        .fetch_optional(&self.pool)
1223        .await?;
1224        row.map(Checkpoint::try_from).transpose()
1225    }
1226
1227    pub async fn commit_checkpoint(
1228        &self,
1229        task_id: TaskId,
1230        attempt: u16,
1231        lease_token: LeaseToken,
1232        step_name: &StepName,
1233        occurrence: u32,
1234        value: &Value,
1235    ) -> Result<Option<Checkpoint>, PostgresError> {
1236        let occurrence = i32::try_from(occurrence).map_err(invalid_number)?;
1237        let row: Option<CheckpointRow> = sqlx::query_as(
1238            r"
1239            SELECT task_id, handler_version, step_name, occurrence, value, created_at
1240            FROM pgtask.commit_checkpoint($1, $2, $3, $4, $5, $6)
1241            ",
1242        )
1243        .bind(task_id.as_uuid())
1244        .bind(i32::from(attempt))
1245        .bind(lease_token.as_uuid())
1246        .bind(step_name.as_str())
1247        .bind(occurrence)
1248        .bind(value)
1249        .fetch_optional(&self.pool)
1250        .await?;
1251        row.map(Checkpoint::try_from).transpose()
1252    }
1253
1254    pub async fn sleep_until(
1255        &self,
1256        task_id: TaskId,
1257        attempt: u16,
1258        lease_token: LeaseToken,
1259        step_name: &StepName,
1260        occurrence: u32,
1261        wake_at: DateTime<Utc>,
1262    ) -> Result<Option<DateTime<Utc>>, PostgresError> {
1263        self.suspend_task(SuspendTaskRequest {
1264            task_id,
1265            attempt,
1266            lease_token,
1267            step_name,
1268            occurrence,
1269            wake_at: Some(wake_at),
1270            delay_milliseconds: None,
1271        })
1272        .await
1273    }
1274
1275    pub async fn sleep_for(
1276        &self,
1277        task_id: TaskId,
1278        attempt: u16,
1279        lease_token: LeaseToken,
1280        step_name: &StepName,
1281        occurrence: u32,
1282        duration: Duration,
1283    ) -> Result<Option<DateTime<Utc>>, PostgresError> {
1284        let delay_milliseconds =
1285            i64::try_from(duration.as_millis()).map_err(|_| PostgresError::InvalidSleepDuration)?;
1286        self.suspend_task(SuspendTaskRequest {
1287            task_id,
1288            attempt,
1289            lease_token,
1290            step_name,
1291            occurrence,
1292            wake_at: None,
1293            delay_milliseconds: Some(delay_milliseconds),
1294        })
1295        .await
1296    }
1297
1298    async fn suspend_task(&self, request: SuspendTaskRequest<'_>) -> Result<Option<DateTime<Utc>>, PostgresError> {
1299        let occurrence = i32::try_from(request.occurrence).map_err(invalid_number)?;
1300        let wake_at = sqlx::query_scalar("SELECT pgtask.suspend_task($1, $2, $3, $4, $5, $6, $7)")
1301            .bind(request.task_id.as_uuid())
1302            .bind(i32::from(request.attempt))
1303            .bind(request.lease_token.as_uuid())
1304            .bind(request.step_name.as_str())
1305            .bind(occurrence)
1306            .bind(request.wake_at)
1307            .bind(request.delay_milliseconds)
1308            .fetch_one(&self.pool)
1309            .await?;
1310        Ok(wake_at)
1311    }
1312
1313    pub async fn emit_signal(
1314        &self,
1315        task_id: TaskId,
1316        signal_name: &SignalName,
1317        occurrence: u32,
1318        value: &Value,
1319    ) -> Result<Signal, PostgresError> {
1320        let occurrence = i32::try_from(occurrence).map_err(invalid_number)?;
1321        let row: SignalRow = sqlx::query_as(
1322            "SELECT task_id, signal_name, occurrence, value, created_at FROM pgtask.emit_signal($1, $2, $3, $4)",
1323        )
1324        .bind(task_id.as_uuid())
1325        .bind(signal_name.as_str())
1326        .bind(occurrence)
1327        .bind(value)
1328        .fetch_one(&self.pool)
1329        .await?;
1330        Signal::try_from(row)
1331    }
1332
1333    pub async fn wait_for_signal(&self, request: SignalWaitRequest<'_>) -> Result<Option<SignalWait>, PostgresError> {
1334        let occurrence = i32::try_from(request.occurrence).map_err(invalid_number)?;
1335        let signal_occurrence = i32::try_from(request.signal_occurrence).map_err(invalid_number)?;
1336        let timeout_milliseconds = request
1337            .timeout
1338            .map(|duration| i64::try_from(duration.as_millis()).map_err(|_| PostgresError::InvalidSleepDuration))
1339            .transpose()?;
1340        let row: Option<SignalWaitRow> =
1341            sqlx::query_as("SELECT status, checkpoint FROM pgtask.wait_for_signal($1, $2, $3, $4, $5, $6, $7, $8)")
1342                .bind(request.task_id.as_uuid())
1343                .bind(i32::from(request.attempt))
1344                .bind(request.lease_token.as_uuid())
1345                .bind(request.step_name.as_str())
1346                .bind(occurrence)
1347                .bind(request.signal_name.as_str())
1348                .bind(signal_occurrence)
1349                .bind(timeout_milliseconds)
1350                .fetch_optional(&self.pool)
1351                .await?;
1352        row.map(SignalWait::try_from).transpose()
1353    }
1354
1355    pub async fn wait_for_result(&self, request: ResultWaitRequest<'_>) -> Result<Option<ResultWait>, PostgresError> {
1356        let occurrence = i32::try_from(request.occurrence).map_err(invalid_number)?;
1357        let timeout_milliseconds = request
1358            .timeout
1359            .map(|duration| {
1360                if duration.is_zero() {
1361                    return Err(PostgresError::InvalidResultWaitTimeout);
1362                }
1363                i64::try_from(duration.as_millis()).map_err(|_| PostgresError::InvalidResultWaitTimeout)
1364            })
1365            .transpose()?;
1366        let row: Option<ResultWaitRow> =
1367            sqlx::query_as("SELECT status, checkpoint FROM pgtask.wait_for_result($1, $2, $3, $4, $5, $6, $7)")
1368                .bind(request.task_id.as_uuid())
1369                .bind(i32::from(request.attempt))
1370                .bind(request.lease_token.as_uuid())
1371                .bind(request.step_name.as_str())
1372                .bind(occurrence)
1373                .bind(request.result_task_id.as_uuid())
1374                .bind(timeout_milliseconds)
1375                .fetch_optional(&self.pool)
1376                .await?;
1377        row.map(ResultWait::try_from).transpose()
1378    }
1379
1380    pub async fn task_result(&self, task_id: TaskId) -> Result<Option<TaskResult>, PostgresError> {
1381        let row: Option<TaskResultRow> =
1382            sqlx::query_as("SELECT state, result, error, completed_at FROM pgtask.task_result($1)")
1383                .bind(task_id.as_uuid())
1384                .fetch_optional(&self.pool)
1385                .await?;
1386        row.map(TaskResult::try_from).transpose()
1387    }
1388
1389    pub async fn wait_for_task_result(
1390        &self,
1391        task_id: TaskId,
1392        timeout: Option<Duration>,
1393    ) -> Result<TaskResultWait, PostgresError> {
1394        let mut listener = self.result_listener(task_id).await?;
1395        let Some(result) = self.task_result(task_id).await? else {
1396            return Ok(TaskResultWait::NotFound);
1397        };
1398        if result.state.is_terminal() {
1399            return Ok(TaskResultWait::Ready(result));
1400        }
1401
1402        let wait = async {
1403            loop {
1404                let notification = listener.recv().await?;
1405                if notification.payload() != task_id.to_string() {
1406                    continue;
1407                }
1408                let result = self.task_result(task_id).await?.ok_or_else(|| {
1409                    PostgresError::InvalidTask("task disappeared while waiting for result".to_owned())
1410                })?;
1411                if result.state.is_terminal() {
1412                    return Ok(result);
1413                }
1414            }
1415        };
1416        match timeout {
1417            Some(timeout) => match tokio::time::timeout(timeout, wait).await {
1418                Ok(result) => result.map(TaskResultWait::Ready),
1419                Err(_) => Ok(TaskResultWait::TimedOut),
1420            },
1421            None => wait.await.map(TaskResultWait::Ready),
1422        }
1423    }
1424
1425    pub async fn recover_wait_timeouts(&self, limit: u16) -> Result<u64, PostgresError> {
1426        if limit == 0 {
1427            return Err(PostgresError::InvalidWaitLimit);
1428        }
1429        let recovered: i64 = sqlx::query_scalar("SELECT pgtask.recover_wait_timeouts($1)")
1430            .bind(i32::from(limit))
1431            .fetch_one(&self.pool)
1432            .await?;
1433        u64::try_from(recovered).map_err(invalid_number)
1434    }
1435
1436    pub async fn recover_result_wait_timeouts(&self, limit: u16) -> Result<u64, PostgresError> {
1437        if limit == 0 {
1438            return Err(PostgresError::InvalidWaitLimit);
1439        }
1440        let recovered: i64 = sqlx::query_scalar("SELECT pgtask.recover_result_wait_timeouts($1)")
1441            .bind(i32::from(limit))
1442            .fetch_one(&self.pool)
1443            .await?;
1444        u64::try_from(recovered).map_err(invalid_number)
1445    }
1446
1447    pub async fn next_wait_delay(&self) -> Result<Option<Duration>, PostgresError> {
1448        let milliseconds: Option<i64> = sqlx::query_scalar("SELECT pgtask.next_wait_delay_milliseconds()")
1449            .fetch_one(&self.pool)
1450            .await?;
1451        milliseconds
1452            .map(|milliseconds| u64::try_from(milliseconds).map(Duration::from_millis))
1453            .transpose()
1454            .map_err(invalid_number)
1455    }
1456
1457    pub async fn cancel(&self, task_id: TaskId) -> Result<bool, PostgresError> {
1458        let cancelled: Option<CancelledTaskRow> =
1459            sqlx::query_as("SELECT queue_name, task_name FROM pgtask.cancel_task($1)")
1460                .bind(task_id.as_uuid())
1461                .fetch_optional(&self.pool)
1462                .await?;
1463        if let Some(cancelled) = cancelled {
1464            pgtask_otel::record_cancelled(&cancelled.queue_name, &cancelled.task_name);
1465            Ok(true)
1466        } else {
1467            Ok(false)
1468        }
1469    }
1470
1471    pub async fn renew_lease(
1472        &self,
1473        task_id: TaskId,
1474        attempt: u16,
1475        lease_token: LeaseToken,
1476        lease_duration: Duration,
1477    ) -> Result<bool, PostgresError> {
1478        let renewed = self
1479            .renew_leases(
1480                &[LeaseRenewal {
1481                    task_id,
1482                    attempt,
1483                    lease_token,
1484                }],
1485                lease_duration,
1486            )
1487            .await?;
1488        Ok(renewed.contains(&task_id))
1489    }
1490
1491    pub async fn renew_leases(
1492        &self,
1493        leases: &[LeaseRenewal],
1494        lease_duration: Duration,
1495    ) -> Result<Vec<TaskId>, PostgresError> {
1496        if lease_duration.is_zero() {
1497            return Err(PostgresError::InvalidLeaseDuration);
1498        }
1499        if leases.is_empty() {
1500            return Ok(Vec::new());
1501        }
1502        let lease_milliseconds =
1503            i64::try_from(lease_duration.as_millis()).map_err(|_| PostgresError::InvalidLeaseDuration)?;
1504        let task_ids: Vec<_> = leases.iter().map(|lease| lease.task_id.as_uuid()).collect();
1505        let attempts: Vec<_> = leases.iter().map(|lease| i32::from(lease.attempt)).collect();
1506        let lease_tokens: Vec<_> = leases.iter().map(|lease| lease.lease_token.as_uuid()).collect();
1507        let renewed: Vec<Uuid> = sqlx::query_scalar("SELECT * FROM pgtask.renew_leases($1, $2, $3, $4)")
1508            .bind(&task_ids)
1509            .bind(&attempts)
1510            .bind(&lease_tokens)
1511            .bind(lease_milliseconds)
1512            .fetch_all(&self.pool)
1513            .await?;
1514        Ok(renewed.into_iter().map(TaskId::from_uuid).collect())
1515    }
1516
1517    pub async fn complete(
1518        &self,
1519        task_id: TaskId,
1520        attempt: u16,
1521        lease_token: LeaseToken,
1522        result: Option<&Value>,
1523    ) -> Result<bool, PostgresError> {
1524        let completed = sqlx::query_scalar("SELECT pgtask.complete_task($1, $2, $3, $4)")
1525            .bind(task_id.as_uuid())
1526            .bind(i32::from(attempt))
1527            .bind(lease_token.as_uuid())
1528            .bind(result)
1529            .fetch_one(&self.pool)
1530            .await?;
1531        Ok(completed)
1532    }
1533
1534    pub async fn fail(
1535        &self,
1536        task_id: TaskId,
1537        attempt: u16,
1538        lease_token: LeaseToken,
1539        error: &Value,
1540        retry_after: Option<Duration>,
1541    ) -> Result<Option<TaskState>, PostgresError> {
1542        let retry_milliseconds = retry_after
1543            .map(|duration| i64::try_from(duration.as_millis()).map_err(|_| PostgresError::InvalidLeaseDuration))
1544            .transpose()?;
1545        let state: Option<String> = sqlx::query_scalar("SELECT pgtask.fail_task($1, $2, $3, $4, $5)")
1546            .bind(task_id.as_uuid())
1547            .bind(i32::from(attempt))
1548            .bind(lease_token.as_uuid())
1549            .bind(error)
1550            .bind(retry_milliseconds)
1551            .fetch_one(&self.pool)
1552            .await?;
1553        state.map(|value| parse_state(&value)).transpose()
1554    }
1555
1556    pub async fn recover_expired(&self, queue_name: &QueueName, limit: u16) -> Result<u64, PostgresError> {
1557        if limit == 0 {
1558            return Err(PostgresError::InvalidClaimLimit);
1559        }
1560        let recovered: i64 = sqlx::query_scalar("SELECT pgtask.recover_expired($1, $2)")
1561            .bind(queue_name.as_str())
1562            .bind(i32::from(limit))
1563            .fetch_one(&self.pool)
1564            .await?;
1565        let recovered = u64::try_from(recovered).map_err(invalid_number)?;
1566        if recovered > 0 {
1567            pgtask_otel::record_recovered(queue_name.as_str(), recovered);
1568        }
1569        Ok(recovered)
1570    }
1571}
1572
1573#[derive(FromRow)]
1574struct EnqueueRow {
1575    task_id: Uuid,
1576    created: bool,
1577}
1578
1579#[derive(FromRow)]
1580struct QueueDemandRow {
1581    #[sqlx(rename = "ready_tasks")]
1582    ready: i64,
1583    #[sqlx(rename = "capable_tasks")]
1584    capable: i64,
1585    #[sqlx(rename = "unroutable_tasks")]
1586    unroutable: i64,
1587}
1588
1589#[derive(FromRow)]
1590struct BatchEnqueueRow {
1591    request_index: i64,
1592    task_id: Uuid,
1593    created: bool,
1594}
1595
1596#[derive(FromRow)]
1597struct QueueRow {
1598    name: String,
1599    terminal_retention_seconds: i64,
1600    idempotency_retention_seconds: i64,
1601    max_outstanding_tasks: Option<i64>,
1602    starvation_timeout_seconds: i64,
1603    paused_at: Option<DateTime<Utc>>,
1604    created_at: DateTime<Utc>,
1605    updated_at: DateTime<Utc>,
1606}
1607
1608#[derive(FromRow)]
1609struct ScheduleRow {
1610    id: Uuid,
1611    name: String,
1612    kind: String,
1613    interval_milliseconds: Option<i64>,
1614    cron_expression: Option<String>,
1615    misfire_policy: String,
1616    catch_up_limit: Option<i32>,
1617    queue_name: String,
1618    task_name: String,
1619    handler_version: i32,
1620    payload: Value,
1621    headers: Value,
1622    priority: i16,
1623    max_attempts: i32,
1624    next_run_at: DateTime<Utc>,
1625    paused_at: Option<DateTime<Utc>>,
1626    created_at: DateTime<Utc>,
1627    updated_at: DateTime<Utc>,
1628}
1629
1630#[derive(FromRow)]
1631struct WorkerRow {
1632    id: Uuid,
1633    queue_name: String,
1634    version: String,
1635    draining: bool,
1636    started_at: DateTime<Utc>,
1637    heartbeat_at: DateTime<Utc>,
1638    expires_at: DateTime<Utc>,
1639}
1640
1641#[derive(FromRow)]
1642struct CapabilityRow {
1643    task_name: String,
1644    handler_version: i32,
1645}
1646
1647#[derive(FromRow)]
1648struct CheckpointRow {
1649    task_id: Uuid,
1650    handler_version: i32,
1651    step_name: String,
1652    occurrence: i32,
1653    value: Value,
1654    created_at: DateTime<Utc>,
1655}
1656
1657#[derive(FromRow)]
1658struct SignalRow {
1659    task_id: Uuid,
1660    signal_name: String,
1661    occurrence: i32,
1662    value: Value,
1663    created_at: DateTime<Utc>,
1664}
1665
1666#[derive(FromRow)]
1667struct SignalWaitRow {
1668    status: String,
1669    checkpoint: Option<Value>,
1670}
1671
1672#[derive(FromRow)]
1673struct ResultWaitRow {
1674    status: String,
1675    checkpoint: Option<Value>,
1676}
1677
1678#[derive(FromRow)]
1679struct TaskResultRow {
1680    state: String,
1681    result: Option<Value>,
1682    error: Option<Value>,
1683    completed_at: Option<DateTime<Utc>>,
1684}
1685
1686impl TryFrom<CheckpointRow> for Checkpoint {
1687    type Error = PostgresError;
1688
1689    fn try_from(row: CheckpointRow) -> Result<Self, Self::Error> {
1690        Ok(Self {
1691            task_id: TaskId::from_uuid(row.task_id),
1692            handler_version: HandlerVersion::new(
1693                NonZeroU32::new(u32::try_from(row.handler_version).map_err(invalid_number)?)
1694                    .ok_or_else(|| PostgresError::InvalidTask("handler version is zero".to_owned()))?,
1695            ),
1696            step_name: StepName::new(row.step_name).map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1697            occurrence: u32::try_from(row.occurrence).map_err(invalid_number)?,
1698            value: row.value,
1699            created_at: row.created_at,
1700        })
1701    }
1702}
1703
1704impl TryFrom<SignalRow> for Signal {
1705    type Error = PostgresError;
1706
1707    fn try_from(row: SignalRow) -> Result<Self, Self::Error> {
1708        Ok(Self {
1709            task_id: TaskId::from_uuid(row.task_id),
1710            name: SignalName::new(row.signal_name).map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1711            occurrence: u32::try_from(row.occurrence).map_err(invalid_number)?,
1712            value: row.value,
1713            created_at: row.created_at,
1714        })
1715    }
1716}
1717
1718impl TryFrom<SignalWaitRow> for SignalWait {
1719    type Error = PostgresError;
1720
1721    fn try_from(row: SignalWaitRow) -> Result<Self, Self::Error> {
1722        match (row.status.as_str(), row.checkpoint) {
1723            ("ready", Some(checkpoint)) => Ok(Self::Ready(checkpoint)),
1724            ("waiting", None) => Ok(Self::Waiting),
1725            _ => Err(PostgresError::InvalidTask("invalid signal wait result".to_owned())),
1726        }
1727    }
1728}
1729
1730impl TryFrom<ResultWaitRow> for ResultWait {
1731    type Error = PostgresError;
1732
1733    fn try_from(row: ResultWaitRow) -> Result<Self, Self::Error> {
1734        match (row.status.as_str(), row.checkpoint) {
1735            ("ready", Some(checkpoint)) => Ok(Self::Ready(checkpoint)),
1736            ("waiting", None) => Ok(Self::Waiting),
1737            _ => Err(PostgresError::InvalidTask("invalid result wait response".to_owned())),
1738        }
1739    }
1740}
1741
1742impl TryFrom<TaskResultRow> for TaskResult {
1743    type Error = PostgresError;
1744
1745    fn try_from(row: TaskResultRow) -> Result<Self, Self::Error> {
1746        Ok(Self {
1747            state: parse_state(&row.state)?,
1748            result: row.result,
1749            error: row.error,
1750            completed_at: row.completed_at,
1751        })
1752    }
1753}
1754
1755impl WorkerRow {
1756    fn try_into_record(self, capabilities: Vec<CapabilityRow>) -> Result<WorkerRecord, PostgresError> {
1757        let capabilities = capabilities
1758            .into_iter()
1759            .map(|capability| -> Result<_, PostgresError> {
1760                Ok((
1761                    TaskName::new(capability.task_name)
1762                        .map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1763                    HandlerVersion::new(
1764                        NonZeroU32::new(u32::try_from(capability.handler_version).map_err(invalid_number)?)
1765                            .ok_or_else(|| PostgresError::InvalidTask("handler version is zero".to_owned()))?,
1766                    ),
1767                ))
1768            })
1769            .collect::<Result<_, _>>()?;
1770        Ok(WorkerRecord {
1771            id: WorkerId::from_uuid(self.id),
1772            queue_name: QueueName::new(self.queue_name)
1773                .map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1774            version: self.version,
1775            draining: self.draining,
1776            started_at: self.started_at,
1777            heartbeat_at: self.heartbeat_at,
1778            expires_at: self.expires_at,
1779            capabilities,
1780        })
1781    }
1782}
1783
1784impl TryFrom<QueueRow> for Queue {
1785    type Error = PostgresError;
1786
1787    fn try_from(row: QueueRow) -> Result<Self, Self::Error> {
1788        Ok(Self {
1789            name: QueueName::new(row.name).map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1790            terminal_retention: Duration::from_secs(
1791                u64::try_from(row.terminal_retention_seconds).map_err(invalid_number)?,
1792            ),
1793            idempotency_retention: Duration::from_secs(
1794                u64::try_from(row.idempotency_retention_seconds).map_err(invalid_number)?,
1795            ),
1796            max_outstanding_tasks: row
1797                .max_outstanding_tasks
1798                .map(|maximum| {
1799                    u64::try_from(maximum)
1800                        .map_err(invalid_number)
1801                        .and_then(|maximum| std::num::NonZeroU64::new(maximum).ok_or_else(|| invalid_number(maximum)))
1802                })
1803                .transpose()?,
1804            starvation_timeout: Duration::from_secs(
1805                u64::try_from(row.starvation_timeout_seconds).map_err(invalid_number)?,
1806            ),
1807            paused_at: row.paused_at,
1808            created_at: row.created_at,
1809            updated_at: row.updated_at,
1810        })
1811    }
1812}
1813
1814impl TryFrom<ScheduleRow> for Schedule {
1815    type Error = PostgresError;
1816
1817    fn try_from(row: ScheduleRow) -> Result<Self, Self::Error> {
1818        let definition = match (row.kind.as_str(), row.interval_milliseconds, row.cron_expression) {
1819            ("interval", Some(milliseconds), None) => ScheduleDefinition::interval(Duration::from_millis(
1820                u64::try_from(milliseconds).map_err(invalid_number)?,
1821            ))?,
1822            ("cron", None, Some(expression)) => ScheduleDefinition::cron(expression)?,
1823            _ => return Err(PostgresError::InvalidTask("invalid schedule definition".to_owned())),
1824        };
1825        let misfire_policy = match (row.misfire_policy.as_str(), row.catch_up_limit) {
1826            ("skip", None) => MisfirePolicy::Skip,
1827            ("latest", None) => MisfirePolicy::Latest,
1828            ("catch_up", Some(limit)) => MisfirePolicy::CatchUp {
1829                limit: std::num::NonZeroU16::new(u16::try_from(limit).map_err(invalid_number)?)
1830                    .ok_or_else(|| PostgresError::InvalidTask("catch-up limit is zero".to_owned()))?,
1831            },
1832            _ => return Err(PostgresError::InvalidTask("invalid schedule misfire policy".to_owned())),
1833        };
1834        let headers = row
1835            .headers
1836            .as_object()
1837            .cloned()
1838            .ok_or_else(|| PostgresError::InvalidTask("schedule headers are not an object".to_owned()))?;
1839        let mut task = EnqueueRequest::new(
1840            TaskName::new(row.task_name).map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1841            row.payload,
1842        );
1843        task.handler_version = HandlerVersion::new(
1844            NonZeroU32::new(u32::try_from(row.handler_version).map_err(invalid_number)?)
1845                .ok_or_else(|| PostgresError::InvalidTask("handler version is zero".to_owned()))?,
1846        );
1847        task.queue_name =
1848            QueueName::new(row.queue_name).map_err(|error| PostgresError::InvalidTask(error.to_string()))?;
1849        task.priority = row.priority;
1850        task.max_attempts = u16::try_from(row.max_attempts).map_err(invalid_number)?;
1851        task.headers = headers;
1852        Ok(Self {
1853            config: ScheduleConfig {
1854                id: ScheduleId::from_uuid(row.id),
1855                name: ScheduleName::new(row.name).map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1856                definition,
1857                misfire_policy,
1858                task,
1859                start_at: None,
1860            },
1861            next_run_at: row.next_run_at,
1862            paused_at: row.paused_at,
1863            created_at: row.created_at,
1864            updated_at: row.updated_at,
1865        })
1866    }
1867}
1868
1869impl From<EnqueueRow> for EnqueueResult {
1870    fn from(row: EnqueueRow) -> Self {
1871        Self {
1872            task_id: TaskId::from_uuid(row.task_id),
1873            created: row.created,
1874        }
1875    }
1876}
1877
1878#[derive(FromRow)]
1879struct TaskRow {
1880    id: Uuid,
1881    parent_task_id: Option<Uuid>,
1882    queue_name: String,
1883    task_name: String,
1884    handler_version: i32,
1885    payload: Value,
1886    headers: Value,
1887    state: String,
1888    priority: i16,
1889    run_at: DateTime<Utc>,
1890    attempt: i32,
1891    max_attempts: i32,
1892    retry_kind: Option<String>,
1893    retry_base_delay_milliseconds: Option<i64>,
1894    retry_factor: Option<i32>,
1895    retry_max_delay_milliseconds: Option<i64>,
1896    lease_token: Option<Uuid>,
1897    lease_owner: Option<Uuid>,
1898    lease_expires_at: Option<DateTime<Utc>>,
1899    created_at: DateTime<Utc>,
1900    updated_at: DateTime<Utc>,
1901    completed_at: Option<DateTime<Utc>>,
1902    result: Option<Value>,
1903    error: Option<Value>,
1904}
1905
1906#[derive(FromRow)]
1907struct CancelledTaskRow {
1908    queue_name: String,
1909    task_name: String,
1910}
1911
1912impl TryFrom<TaskRow> for Task {
1913    type Error = PostgresError;
1914
1915    fn try_from(row: TaskRow) -> Result<Self, Self::Error> {
1916        let headers = row
1917            .headers
1918            .as_object()
1919            .cloned()
1920            .ok_or_else(|| PostgresError::InvalidTask("headers are not an object".to_owned()))?;
1921        Ok(Self {
1922            id: TaskId::from_uuid(row.id),
1923            parent_task_id: row.parent_task_id.map(TaskId::from_uuid),
1924            queue_name: QueueName::new(row.queue_name)
1925                .map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1926            task_name: TaskName::new(row.task_name).map_err(|error| PostgresError::InvalidTask(error.to_string()))?,
1927            handler_version: HandlerVersion::new(
1928                NonZeroU32::new(u32::try_from(row.handler_version).map_err(invalid_number)?)
1929                    .ok_or_else(|| PostgresError::InvalidTask("handler version is zero".to_owned()))?,
1930            ),
1931            payload: row.payload,
1932            headers,
1933            state: parse_state(&row.state)?,
1934            priority: row.priority,
1935            run_at: row.run_at,
1936            attempt: u16::try_from(row.attempt).map_err(invalid_number)?,
1937            max_attempts: u16::try_from(row.max_attempts).map_err(invalid_number)?,
1938            retry_policy: parse_retry_policy(
1939                row.retry_kind.as_deref(),
1940                row.retry_base_delay_milliseconds,
1941                row.retry_factor,
1942                row.retry_max_delay_milliseconds,
1943            )?,
1944            lease_token: row.lease_token.map(LeaseToken::from_uuid),
1945            lease_owner: row.lease_owner.map(WorkerId::from_uuid),
1946            lease_expires_at: row.lease_expires_at,
1947            created_at: row.created_at,
1948            updated_at: row.updated_at,
1949            completed_at: row.completed_at,
1950            result: row.result,
1951            error: row.error,
1952        })
1953    }
1954}
1955
1956fn parse_state(value: &str) -> Result<TaskState, PostgresError> {
1957    match value {
1958        "pending" => Ok(TaskState::Pending),
1959        "running" => Ok(TaskState::Running),
1960        "waiting" => Ok(TaskState::Waiting),
1961        "succeeded" => Ok(TaskState::Succeeded),
1962        "failed" => Ok(TaskState::Failed),
1963        "cancelled" => Ok(TaskState::Cancelled),
1964        other => Err(PostgresError::InvalidTask(format!("unknown state {other:?}"))),
1965    }
1966}
1967
1968struct RetryPolicyColumns {
1969    kind: &'static str,
1970    base_delay: Option<i64>,
1971    factor: Option<i32>,
1972    max_delay: Option<i64>,
1973}
1974
1975fn retry_policy_columns(policy: RetryPolicy) -> Result<RetryPolicyColumns, PostgresError> {
1976    let milliseconds =
1977        |duration: Duration| i64::try_from(duration.as_millis()).map_err(|_| PostgresError::InvalidRetryPolicy);
1978    match policy {
1979        RetryPolicy::Never => Ok(RetryPolicyColumns {
1980            kind: "never",
1981            base_delay: None,
1982            factor: None,
1983            max_delay: None,
1984        }),
1985        RetryPolicy::Fixed { delay } => Ok(RetryPolicyColumns {
1986            kind: "fixed",
1987            base_delay: Some(milliseconds(delay)?),
1988            factor: None,
1989            max_delay: None,
1990        }),
1991        RetryPolicy::Exponential {
1992            base_delay,
1993            factor,
1994            max_delay,
1995        } => Ok(RetryPolicyColumns {
1996            kind: "exponential",
1997            base_delay: Some(milliseconds(base_delay)?),
1998            factor: Some(i32::try_from(factor).map_err(|_| PostgresError::InvalidRetryPolicy)?),
1999            max_delay: Some(milliseconds(max_delay)?),
2000        }),
2001    }
2002}
2003
2004fn parse_retry_policy(
2005    kind: Option<&str>,
2006    base_delay_milliseconds: Option<i64>,
2007    factor: Option<i32>,
2008    max_delay_milliseconds: Option<i64>,
2009) -> Result<Option<RetryPolicy>, PostgresError> {
2010    let duration = |milliseconds: i64| {
2011        u64::try_from(milliseconds)
2012            .map(Duration::from_millis)
2013            .map_err(invalid_number)
2014    };
2015    match (kind, base_delay_milliseconds, factor, max_delay_milliseconds) {
2016        (None, None, None, None) => Ok(None),
2017        (Some("never"), None, None, None) => Ok(Some(RetryPolicy::Never)),
2018        (Some("fixed"), Some(delay), None, None) => Ok(Some(RetryPolicy::Fixed {
2019            delay: duration(delay)?,
2020        })),
2021        (Some("exponential"), Some(base_delay), Some(factor), Some(max_delay)) => Ok(Some(RetryPolicy::Exponential {
2022            base_delay: duration(base_delay)?,
2023            factor: u32::try_from(factor).map_err(invalid_number)?,
2024            max_delay: duration(max_delay)?,
2025        })),
2026        _ => Err(PostgresError::InvalidTask("invalid retry policy".to_owned())),
2027    }
2028}
2029
2030fn invalid_number(error: impl std::fmt::Display) -> PostgresError {
2031    PostgresError::InvalidTask(error.to_string())
2032}