Skip to main content

runledger_postgres/jobs/workflows/
handles.rs

1use std::cmp::min;
2use std::future::Future;
3use std::time::Duration;
4
5use chrono::{DateTime, Utc};
6use runledger_core::jobs::{WorkflowRunEnqueue, WorkflowRunStatus};
7use serde_json::Value;
8use sqlx::postgres::PgListener;
9use sqlx::types::Uuid;
10use tokio::time::{Instant, sleep_until};
11
12use crate::{DbPool, Error, QueryError, QueryErrorCategory, Result};
13
14use super::super::row_decode::{
15    parse_step_key_name, parse_workflow_run_status, parse_workflow_type_name,
16};
17use super::super::workflow_types::{
18    WorkflowRunDbRecord, WorkflowRunHandle, WorkflowRunHandleError, WorkflowRunHandleScope,
19    WorkflowRunResultRecord, WorkflowRunWaitOptions,
20};
21use super::enqueue::enqueue_workflow_run;
22use super::read::get_workflow_run_by_id;
23use super::runtime::WORKFLOW_RUN_TERMINAL_CHANNEL;
24
25struct WorkflowRunHandleLookupRow {
26    id: Uuid,
27    workflow_type: String,
28    organization_id: Option<Uuid>,
29    status: String,
30    idempotency_key: Option<String>,
31    result_step_key: Option<String>,
32    metadata: Value,
33    started_at: DateTime<Utc>,
34    finished_at: Option<DateTime<Utc>>,
35    created_at: DateTime<Utc>,
36    updated_at: DateTime<Utc>,
37}
38
39struct WorkflowRunResultLookupRow {
40    id: Uuid,
41    workflow_type: String,
42    organization_id: Option<Uuid>,
43    status: String,
44    result_step_key: Option<String>,
45    result: Option<Value>,
46    finished_at: Option<DateTime<Utc>>,
47}
48
49enum WorkflowRunResultLookup {
50    Pending,
51    Ready(WorkflowRunResultRecord),
52}
53
54impl WorkflowRunHandle {
55    pub async fn get_status(
56        &self,
57    ) -> std::result::Result<Option<WorkflowRunStatus>, WorkflowRunHandleError> {
58        load_workflow_run_status(&self.pool, self.scope, self.workflow_run_id)
59            .await
60            .map_err(WorkflowRunHandleError::from)
61    }
62
63    pub async fn get_run(
64        &self,
65    ) -> std::result::Result<Option<WorkflowRunDbRecord>, WorkflowRunHandleError> {
66        load_workflow_run_for_scope(&self.pool, self.scope, self.workflow_run_id)
67            .await
68            .map_err(WorkflowRunHandleError::from)
69    }
70
71    pub async fn get_result(
72        &self,
73        options: WorkflowRunWaitOptions,
74    ) -> std::result::Result<WorkflowRunResultRecord, WorkflowRunHandleError> {
75        let poll_interval = normalize_poll_interval(options.poll_interval);
76        if options.timeout == Some(Duration::ZERO) {
77            return match load_workflow_run_result_deadline_probe(
78                &self.pool,
79                self.scope,
80                self.workflow_run_id,
81            )
82            .await?
83            {
84                WorkflowRunResultLookup::Ready(record) => Ok(record),
85                WorkflowRunResultLookup::Pending => Err(WorkflowRunHandleError::Timeout),
86            };
87        }
88
89        let deadline = options.timeout.map(instant_after);
90
91        if let WorkflowRunResultLookup::Ready(record) =
92            load_workflow_run_result_before_deadline_or_final_probe(
93                &self.pool,
94                self.scope,
95                self.workflow_run_id,
96                deadline,
97            )
98            .await?
99        {
100            return Ok(record);
101        }
102        if deadline_has_elapsed(deadline) {
103            return Err(WorkflowRunHandleError::Timeout);
104        }
105
106        let mut listener = match connect_terminal_listener(&self.pool, deadline).await {
107            Ok(listener) => listener,
108            Err(WorkflowRunHandleError::Timeout) if deadline_has_elapsed(deadline) => {
109                return lookup_ready_or_timeout(
110                    load_workflow_run_result_deadline_probe(
111                        &self.pool,
112                        self.scope,
113                        self.workflow_run_id,
114                    )
115                    .await?,
116                );
117            }
118            Err(error) => return Err(error),
119        };
120        if deadline_has_elapsed(deadline) {
121            return lookup_ready_or_timeout(
122                load_workflow_run_result_deadline_probe(
123                    &self.pool,
124                    self.scope,
125                    self.workflow_run_id,
126                )
127                .await?,
128            );
129        }
130
131        // Close the initial-read/LISTEN race. A terminal transition committed
132        // just before LISTEN may not produce a notification for this connection.
133        if let WorkflowRunResultLookup::Ready(record) =
134            load_workflow_run_result_before_deadline_or_final_probe(
135                &self.pool,
136                self.scope,
137                self.workflow_run_id,
138                deadline,
139            )
140            .await?
141        {
142            return Ok(record);
143        }
144        if deadline_has_elapsed(deadline) {
145            return Err(WorkflowRunHandleError::Timeout);
146        }
147
148        // The poll wake-up is an absolute instant so notification traffic for
149        // other workflow runs cannot keep resetting it and starve the polling
150        // backstop.
151        let mut next_poll = instant_after(poll_interval);
152
153        loop {
154            let wake = next_poll_wake(deadline, next_poll);
155
156            if let Some(active_listener) = listener.as_mut() {
157                tokio::select! {
158                    notification = active_listener.recv() => {
159                        match notification {
160                            Ok(notification) => {
161                                if listener_event_needs_result_lookup(
162                                    notification.payload(),
163                                    self.workflow_run_id,
164                                    deadline,
165                                ) {
166                                    if let WorkflowRunResultLookup::Ready(record) =
167                                        load_workflow_run_result_after_notification(
168                                            &self.pool,
169                                            self.scope,
170                                            self.workflow_run_id,
171                                            deadline,
172                                        )
173                                        .await?
174                                    {
175                                        return Ok(record);
176                                    }
177                                    if deadline_has_elapsed(deadline) {
178                                        return Err(WorkflowRunHandleError::Timeout);
179                                    }
180                                }
181                            }
182                            Err(error) => {
183                                tracing::warn!(
184                                    error = %error,
185                                    workflow_run_id = %self.workflow_run_id,
186                                    "workflow result listener failed; falling back to polling"
187                                );
188                                listener = None;
189                                if deadline_has_elapsed(deadline) {
190                                    if let WorkflowRunResultLookup::Ready(record) =
191                                        load_workflow_run_result_deadline_probe(
192                                            &self.pool,
193                                            self.scope,
194                                            self.workflow_run_id,
195                                        )
196                                        .await?
197                                    {
198                                        return Ok(record);
199                                    }
200                                    return Err(WorkflowRunHandleError::Timeout);
201                                }
202                            }
203                        }
204                    }
205                    () = sleep_until(wake.instant) => {
206                        next_poll = instant_after(poll_interval);
207                        let lookup = load_workflow_run_result_after_poll_wake(
208                            &self.pool,
209                            self.scope,
210                            self.workflow_run_id,
211                            deadline,
212                            wake.reached_deadline,
213                        )
214                        .await?;
215                        if let WorkflowRunResultLookup::Ready(record) = lookup {
216                            return Ok(record);
217                        }
218                        if wake.reached_deadline {
219                            return Err(WorkflowRunHandleError::Timeout);
220                        }
221                    }
222                }
223                continue;
224            }
225
226            sleep_until(wake.instant).await;
227            next_poll = instant_after(poll_interval);
228            let lookup = load_workflow_run_result_after_poll_wake(
229                &self.pool,
230                self.scope,
231                self.workflow_run_id,
232                deadline,
233                wake.reached_deadline,
234            )
235            .await?;
236            if let WorkflowRunResultLookup::Ready(record) = lookup {
237                return Ok(record);
238            }
239            if wake.reached_deadline {
240                return Err(WorkflowRunHandleError::Timeout);
241            }
242        }
243    }
244}
245
246pub fn workflow_run_handle(
247    pool: &DbPool,
248    scope: WorkflowRunHandleScope,
249    workflow_run_id: Uuid,
250) -> WorkflowRunHandle {
251    WorkflowRunHandle {
252        workflow_run_id,
253        scope,
254        pool: pool.clone(),
255    }
256}
257
258pub async fn retrieve_workflow_run_handle(
259    pool: &DbPool,
260    scope: WorkflowRunHandleScope,
261    workflow_run_id: Uuid,
262) -> std::result::Result<WorkflowRunHandle, WorkflowRunHandleError> {
263    let Some(_) = load_workflow_run_status(pool, scope, workflow_run_id).await? else {
264        return Err(WorkflowRunHandleError::NotFound);
265    };
266
267    Ok(workflow_run_handle(pool, scope, workflow_run_id))
268}
269
270pub async fn enqueue_workflow_run_handle(
271    pool: &DbPool,
272    payload: &WorkflowRunEnqueue<'_>,
273) -> Result<WorkflowRunHandle> {
274    let workflow_run = enqueue_workflow_run(pool, payload).await?;
275    let scope = workflow_run
276        .organization_id
277        .map(WorkflowRunHandleScope::Organization)
278        .unwrap_or(WorkflowRunHandleScope::Global);
279
280    Ok(workflow_run_handle(pool, scope, workflow_run.id))
281}
282
283const DEADLINE_RESULT_LOOKUP_TIMEOUT: Duration = Duration::from_millis(250);
284
285async fn connect_terminal_listener(
286    pool: &DbPool,
287    deadline: Option<Instant>,
288) -> std::result::Result<Option<PgListener>, WorkflowRunHandleError> {
289    let mut listener = match await_before_deadline(deadline, PgListener::connect_with(pool)).await?
290    {
291        Ok(listener) => listener,
292        Err(error) => {
293            tracing::warn!(
294                error = %error,
295                "workflow result listener connect failed; using polling"
296            );
297            return Ok(None);
298        }
299    };
300
301    if !pool_can_spare_query_connection(pool) {
302        let pool_size = pool.size();
303        let pool_idle = pool.num_idle();
304        let max_connections = pool.options().get_max_connections();
305        tracing::debug!(
306            pool_size,
307            pool_idle,
308            max_connections,
309            "workflow result listener skipped; pool has no spare connection for race-closing reads"
310        );
311        return Ok(None);
312    }
313
314    if let Err(error) =
315        await_before_deadline(deadline, listener.listen(WORKFLOW_RUN_TERMINAL_CHANNEL)).await?
316    {
317        tracing::warn!(
318            error = %error,
319            "workflow result listener subscribe failed; using polling"
320        );
321        return Ok(None);
322    }
323
324    Ok(Some(listener))
325}
326
327fn pool_can_spare_query_connection(pool: &DbPool) -> bool {
328    pool.num_idle() > 0 || pool.size() < pool.options().get_max_connections()
329}
330
331fn normalize_poll_interval(poll_interval: Duration) -> Duration {
332    poll_interval.max(Duration::from_millis(1))
333}
334
335#[derive(Clone, Copy, Debug, Eq, PartialEq)]
336struct PollWake {
337    instant: Instant,
338    reached_deadline: bool,
339}
340
341fn next_poll_wake(deadline: Option<Instant>, next_poll: Instant) -> PollWake {
342    let Some(deadline) = deadline else {
343        return PollWake {
344            instant: next_poll,
345            reached_deadline: false,
346        };
347    };
348
349    PollWake {
350        instant: min(next_poll, deadline),
351        reached_deadline: deadline <= next_poll || deadline <= Instant::now(),
352    }
353}
354
355fn instant_after(duration: Duration) -> Instant {
356    instant_saturating_add(Instant::now(), duration)
357}
358
359fn instant_saturating_add(base: Instant, duration: Duration) -> Instant {
360    base.checked_add(duration)
361        .unwrap_or_else(|| far_future_instant(base))
362}
363
364fn far_future_instant(base: Instant) -> Instant {
365    let mut seconds = 60 * 60 * 24 * 365 * 100;
366    loop {
367        if let Some(instant) = base.checked_add(Duration::from_secs(seconds)) {
368            return instant;
369        }
370        seconds /= 2;
371    }
372}
373
374async fn await_before_deadline<F, T>(
375    deadline: Option<Instant>,
376    future: F,
377) -> std::result::Result<T, WorkflowRunHandleError>
378where
379    F: Future<Output = T>,
380{
381    let Some(deadline) = deadline else {
382        return Ok(future.await);
383    };
384
385    let now = Instant::now();
386    if deadline <= now {
387        return Err(WorkflowRunHandleError::Timeout);
388    }
389
390    tokio::time::timeout(deadline - now, future)
391        .await
392        .map_err(|_| WorkflowRunHandleError::Timeout)
393}
394
395fn notification_matches_workflow_run(payload: &str, workflow_run_id: Uuid) -> bool {
396    serde_json::from_str::<Value>(payload)
397        .ok()
398        .and_then(|value| {
399            value
400                .get("workflow_run_id")
401                .and_then(Value::as_str)
402                .and_then(|raw_id| Uuid::parse_str(raw_id).ok())
403        })
404        == Some(workflow_run_id)
405}
406
407fn listener_event_needs_result_lookup(
408    payload: &str,
409    workflow_run_id: Uuid,
410    deadline: Option<Instant>,
411) -> bool {
412    notification_matches_workflow_run(payload, workflow_run_id) || deadline_has_elapsed(deadline)
413}
414
415fn deadline_has_elapsed(deadline: Option<Instant>) -> bool {
416    deadline.is_some_and(|deadline| deadline <= Instant::now())
417}
418
419async fn load_workflow_run_status(
420    pool: &DbPool,
421    scope: WorkflowRunHandleScope,
422    workflow_run_id: Uuid,
423) -> Result<Option<WorkflowRunStatus>> {
424    let status = match scope {
425        WorkflowRunHandleScope::Organization(organization_id) => {
426            sqlx::query_scalar!(
427                "SELECT status::text AS \"status!\"
428                 FROM workflow_runs
429                 WHERE id = $1
430                   AND organization_id = $2
431                 LIMIT 1",
432                workflow_run_id,
433                organization_id,
434            )
435            .fetch_optional(pool)
436            .await
437        }
438        WorkflowRunHandleScope::Global => {
439            sqlx::query_scalar!(
440                "SELECT status::text AS \"status!\"
441             FROM workflow_runs
442             WHERE id = $1
443               AND organization_id IS NULL
444             LIMIT 1",
445                workflow_run_id,
446            )
447            .fetch_optional(pool)
448            .await
449        }
450        WorkflowRunHandleScope::Admin => {
451            sqlx::query_scalar!(
452                "SELECT status::text AS \"status!\"
453             FROM workflow_runs
454             WHERE id = $1
455             LIMIT 1",
456                workflow_run_id,
457            )
458            .fetch_optional(pool)
459            .await
460        }
461    }
462    .map_err(|error| Error::from_query_sqlx_with_context("load workflow handle status", error))?;
463
464    status.map(parse_workflow_run_status).transpose()
465}
466
467async fn load_workflow_run_for_scope(
468    pool: &DbPool,
469    scope: WorkflowRunHandleScope,
470    workflow_run_id: Uuid,
471) -> Result<Option<WorkflowRunDbRecord>> {
472    match scope {
473        WorkflowRunHandleScope::Organization(organization_id) => {
474            get_workflow_run_by_id(pool, Some(organization_id), workflow_run_id).await
475        }
476        WorkflowRunHandleScope::Global => {
477            load_global_workflow_run_by_id(pool, workflow_run_id).await
478        }
479        WorkflowRunHandleScope::Admin => get_workflow_run_by_id(pool, None, workflow_run_id).await,
480    }
481}
482
483async fn load_global_workflow_run_by_id(
484    pool: &DbPool,
485    workflow_run_id: Uuid,
486) -> Result<Option<WorkflowRunDbRecord>> {
487    let row = sqlx::query_as!(
488        WorkflowRunHandleLookupRow,
489        "SELECT
490            id,
491            workflow_type,
492            organization_id,
493            status::text AS \"status!\",
494            idempotency_key,
495            result_step_key,
496            metadata,
497            started_at,
498            finished_at,
499            created_at,
500            updated_at
501         FROM workflow_runs
502         WHERE id = $1
503           AND organization_id IS NULL
504         LIMIT 1",
505        workflow_run_id,
506    )
507    .fetch_optional(pool)
508    .await
509    .map_err(|error| Error::from_query_sqlx_with_context("load global workflow run", error))?;
510
511    row.map(workflow_run_db_record_from_handle_row).transpose()
512}
513
514fn workflow_run_db_record_from_handle_row(
515    row: WorkflowRunHandleLookupRow,
516) -> Result<WorkflowRunDbRecord> {
517    Ok(WorkflowRunDbRecord {
518        id: row.id,
519        workflow_type: parse_workflow_type_name(row.workflow_type)?,
520        organization_id: row.organization_id,
521        status: parse_workflow_run_status(row.status)?,
522        idempotency_key: row.idempotency_key,
523        result_step_key: row.result_step_key.map(parse_step_key_name).transpose()?,
524        metadata: row.metadata,
525        started_at: row.started_at,
526        finished_at: row.finished_at,
527        created_at: row.created_at,
528        updated_at: row.updated_at,
529    })
530}
531
532async fn load_workflow_run_result(
533    pool: &DbPool,
534    scope: WorkflowRunHandleScope,
535    workflow_run_id: Uuid,
536) -> std::result::Result<WorkflowRunResultLookup, WorkflowRunHandleError> {
537    let Some(row) = load_workflow_run_result_row(pool, scope, workflow_run_id).await? else {
538        return Err(WorkflowRunHandleError::NotFound);
539    };
540
541    let status = parse_workflow_run_status(row.status).map_err(WorkflowRunHandleError::from)?;
542    let Some(raw_step_key) = row.result_step_key else {
543        return Err(WorkflowRunHandleError::ResultNotDeclared);
544    };
545
546    match status {
547        WorkflowRunStatus::Running | WorkflowRunStatus::WaitingForExternal => {
548            Ok(WorkflowRunResultLookup::Pending)
549        }
550        WorkflowRunStatus::CompletedWithErrors | WorkflowRunStatus::Canceled => {
551            Err(WorkflowRunHandleError::UnsuccessfulTerminal { status })
552        }
553        WorkflowRunStatus::Succeeded => {
554            let Some(result) = row.result else {
555                return Err(WorkflowRunHandleError::ResultMissing);
556            };
557            let Some(finished_at) = row.finished_at else {
558                return Err(WorkflowRunHandleError::Storage(handle_internal_error(
559                    "workflow result row is succeeded without finished_at",
560                )));
561            };
562
563            Ok(WorkflowRunResultLookup::Ready(WorkflowRunResultRecord {
564                workflow_run_id: row.id,
565                workflow_type: parse_workflow_type_name(row.workflow_type)
566                    .map_err(WorkflowRunHandleError::from)?,
567                organization_id: row.organization_id,
568                result_step_key: parse_step_key_name(raw_step_key)
569                    .map_err(WorkflowRunHandleError::from)?,
570                result,
571                finished_at,
572            }))
573        }
574    }
575}
576
577async fn load_workflow_run_result_row(
578    pool: &DbPool,
579    scope: WorkflowRunHandleScope,
580    workflow_run_id: Uuid,
581) -> std::result::Result<Option<WorkflowRunResultLookupRow>, WorkflowRunHandleError> {
582    let row = match scope {
583        WorkflowRunHandleScope::Organization(organization_id) => {
584            sqlx::query_as!(
585                WorkflowRunResultLookupRow,
586                "SELECT
587                    id,
588                    workflow_type,
589                    organization_id,
590                    status::text AS \"status!\",
591                    result_step_key,
592                    result,
593                    finished_at
594                 FROM workflow_runs
595                 WHERE id = $1
596                   AND organization_id = $2
597                 LIMIT 1",
598                workflow_run_id,
599                organization_id,
600            )
601            .fetch_optional(pool)
602            .await
603        }
604        WorkflowRunHandleScope::Global => {
605            sqlx::query_as!(
606                WorkflowRunResultLookupRow,
607                "SELECT
608                id,
609                workflow_type,
610                organization_id,
611                status::text AS \"status!\",
612                result_step_key,
613                result,
614                finished_at
615             FROM workflow_runs
616             WHERE id = $1
617               AND organization_id IS NULL
618             LIMIT 1",
619                workflow_run_id,
620            )
621            .fetch_optional(pool)
622            .await
623        }
624        WorkflowRunHandleScope::Admin => {
625            sqlx::query_as!(
626                WorkflowRunResultLookupRow,
627                "SELECT
628                id,
629                workflow_type,
630                organization_id,
631                status::text AS \"status!\",
632                result_step_key,
633                result,
634                finished_at
635             FROM workflow_runs
636             WHERE id = $1
637             LIMIT 1",
638                workflow_run_id,
639            )
640            .fetch_optional(pool)
641            .await
642        }
643    }
644    .map_err(|error| {
645        WorkflowRunHandleError::Storage(Error::from_query_sqlx_with_context(
646            "load workflow result row",
647            error,
648        ))
649    })?;
650
651    Ok(row)
652}
653
654async fn load_workflow_run_result_before_deadline(
655    pool: &DbPool,
656    scope: WorkflowRunHandleScope,
657    workflow_run_id: Uuid,
658    deadline: Option<Instant>,
659) -> std::result::Result<WorkflowRunResultLookup, WorkflowRunHandleError> {
660    await_before_deadline(
661        deadline,
662        load_workflow_run_result(pool, scope, workflow_run_id),
663    )
664    .await?
665}
666
667async fn load_workflow_run_result_after_poll_wake(
668    pool: &DbPool,
669    scope: WorkflowRunHandleScope,
670    workflow_run_id: Uuid,
671    deadline: Option<Instant>,
672    reached_deadline: bool,
673) -> std::result::Result<WorkflowRunResultLookup, WorkflowRunHandleError> {
674    if reached_deadline {
675        return load_workflow_run_result_deadline_probe(pool, scope, workflow_run_id).await;
676    }
677
678    load_workflow_run_result_before_deadline_or_final_probe(pool, scope, workflow_run_id, deadline)
679        .await
680}
681
682async fn load_workflow_run_result_after_notification(
683    pool: &DbPool,
684    scope: WorkflowRunHandleScope,
685    workflow_run_id: Uuid,
686    deadline: Option<Instant>,
687) -> std::result::Result<WorkflowRunResultLookup, WorkflowRunHandleError> {
688    if deadline_has_elapsed(deadline) {
689        return load_workflow_run_result_deadline_probe(pool, scope, workflow_run_id).await;
690    }
691
692    load_workflow_run_result_before_deadline_or_final_probe(pool, scope, workflow_run_id, deadline)
693        .await
694}
695
696async fn load_workflow_run_result_before_deadline_or_final_probe(
697    pool: &DbPool,
698    scope: WorkflowRunHandleScope,
699    workflow_run_id: Uuid,
700    deadline: Option<Instant>,
701) -> std::result::Result<WorkflowRunResultLookup, WorkflowRunHandleError> {
702    match load_workflow_run_result_before_deadline(pool, scope, workflow_run_id, deadline).await {
703        Ok(WorkflowRunResultLookup::Pending) if deadline_has_elapsed(deadline) => {
704            load_workflow_run_result_deadline_probe(pool, scope, workflow_run_id).await
705        }
706        Ok(lookup) => Ok(lookup),
707        Err(WorkflowRunHandleError::Timeout) if deadline_has_elapsed(deadline) => {
708            load_workflow_run_result_deadline_probe(pool, scope, workflow_run_id).await
709        }
710        Err(error) => Err(error),
711    }
712}
713
714async fn load_workflow_run_result_deadline_probe(
715    pool: &DbPool,
716    scope: WorkflowRunHandleScope,
717    workflow_run_id: Uuid,
718) -> std::result::Result<WorkflowRunResultLookup, WorkflowRunHandleError> {
719    tokio::time::timeout(
720        DEADLINE_RESULT_LOOKUP_TIMEOUT,
721        load_workflow_run_result(pool, scope, workflow_run_id),
722    )
723    .await
724    .map_err(|_| WorkflowRunHandleError::Timeout)?
725}
726
727fn lookup_ready_or_timeout(
728    lookup: WorkflowRunResultLookup,
729) -> std::result::Result<WorkflowRunResultRecord, WorkflowRunHandleError> {
730    match lookup {
731        WorkflowRunResultLookup::Ready(record) => Ok(record),
732        WorkflowRunResultLookup::Pending => Err(WorkflowRunHandleError::Timeout),
733    }
734}
735
736fn handle_internal_error(message: &'static str) -> Error {
737    Error::QueryError(QueryError::from_classified(
738        QueryErrorCategory::Internal,
739        "workflow.handle_internal_state",
740        "Workflow handle state is invalid.",
741        message,
742    ))
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    #[test]
750    fn listener_event_needs_lookup_for_match_or_elapsed_deadline() {
751        let workflow_run_id = Uuid::now_v7();
752        let matching_payload = serde_json::json!({
753            "workflow_run_id": workflow_run_id,
754        })
755        .to_string();
756        let unrelated_payload = serde_json::json!({
757            "workflow_run_id": Uuid::now_v7(),
758        })
759        .to_string();
760        let future_deadline = Some(instant_after(Duration::from_secs(60)));
761        let elapsed_deadline = Some(Instant::now() - Duration::from_millis(1));
762
763        assert!(listener_event_needs_result_lookup(
764            &matching_payload,
765            workflow_run_id,
766            future_deadline,
767        ));
768        assert!(!listener_event_needs_result_lookup(
769            &unrelated_payload,
770            workflow_run_id,
771            future_deadline,
772        ));
773        assert!(listener_event_needs_result_lookup(
774            &unrelated_payload,
775            workflow_run_id,
776            elapsed_deadline,
777        ));
778    }
779
780    #[test]
781    fn next_poll_wake_marks_elapsed_deadline_for_final_probe() {
782        let wake = next_poll_wake(
783            Some(Instant::now() - Duration::from_millis(1)),
784            instant_after(Duration::from_secs(30)),
785        );
786
787        assert!(wake.reached_deadline);
788        assert!(wake.instant <= Instant::now());
789    }
790
791    #[test]
792    fn instant_after_saturates_oversized_duration() {
793        let start = Instant::now();
794        let instant = instant_saturating_add(start, Duration::MAX);
795
796        assert!(instant >= start);
797    }
798}