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 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 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}