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