mako_engine/deadline.rs
1//! Deadline tracking for regulatory process timers.
2//!
3//! Every MaKo process is subject to hard regulatory deadlines defined in the
4//! BDEW Application Handbooks. Deadline semantics vary by process family:
5//!
6//! | Process family | Deadline unit | Helper |
7//! |---|---|---|
8//! | GPKE Lieferantenwechsel (BK6-22-024) | 24 wall-clock hours | [`fristen::add_hours`] |
9//! | WiM / GeLi Gas / MABIS | Werktage | [`fristen::add_werktage`] |
10//!
11//! Use the helpers in [`crate::fristen`] to compute the correct `due_at`
12//! timestamp before constructing a [`Deadline`].
13//!
14//! The `DeadlineStore` persists these timers per process stream. A background
15//! scheduler polls [`DeadlineStore::due_now`] and dispatches a
16//! `TimeoutDeadline` command to the owning process when a deadline lapses.
17//! The process workflow then handles the command — e.g. by escalating the
18//! case or switching to a failure path.
19//!
20//! # Usage
21//!
22//! ```rust,ignore
23//! use mako_engine::fristen;
24//!
25//! // GPKE 24h Lieferantenwechsel (BK6-22-024):
26//! let due = fristen::add_hours(OffsetDateTime::now_utc(), 24);
27//! // WiM 5-Werktage confirmation window:
28//! let due = fristen::add_werktage(OffsetDateTime::now_utc().date(), 5,
29//! fristen::HolidayCalendar::BdewMaKo).midnight().assume_utc();
30//!
31//! let deadline = Deadline::new(
32//! process.stream_id().clone(),
33//! process.process_id(),
34//! process.tenant_id(),
35//! process.workflow_id().clone(),
36//! "aperak-response-window",
37//! due,
38//! );
39//! deadline_store.register(&deadline).await?;
40//!
41//! // When the counterparty responds in time, cancel the deadline:
42//! deadline_store.cancel(deadline.deadline_id()).await?;
43//!
44//! // Background scheduler (runs every N minutes):
45//! let result = deadline_store.due_now(100).await?;
46//! for d in result.deadlines {
47//! process_handle.execute(TimeoutDeadline { deadline_id: d.deadline_id() }).await?;
48//! deadline_store.cancel(d.deadline_id()).await?;
49//! }
50//! ```
51//!
52//! [`fristen::add_hours`]: crate::fristen::add_hours
53//! [`fristen::add_werktage`]: crate::fristen::add_werktage
54
55use std::sync::Arc;
56
57#[cfg(any(test, feature = "testing"))]
58use std::collections::HashMap;
59#[cfg(any(test, feature = "testing"))]
60use tokio::sync::RwLock;
61
62use time::OffsetDateTime;
63
64use crate::{
65 error::EngineError,
66 ids::{DeadlineId, ProcessId, StreamId, TenantId},
67 version::WorkflowId,
68};
69
70// ── Deadline ──────────────────────────────────────────────────────────────────
71
72/// A registered regulatory deadline for a single process stream.
73///
74/// Create with [`Deadline::new`], persist via [`DeadlineStore::register`], and
75/// cancel via [`DeadlineStore::cancel`] when the process advances past the
76/// deadline before it fires.
77///
78/// The `label` field identifies the deadline type (e.g.
79/// `"aperak-response-window"`) and is used by the scheduler to dispatch the
80/// correct timeout command.
81#[expect(clippy::struct_field_names)]
82#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
83pub struct Deadline {
84 /// Unique identifier for this deadline entry.
85 deadline_id: DeadlineId,
86
87 /// The process stream this deadline belongs to.
88 stream_id: StreamId,
89
90 /// The process instance this deadline belongs to.
91 process_id: ProcessId,
92
93 /// The tenant that owns this process.
94 tenant_id: TenantId,
95
96 /// The workflow that owns this process (name + format version).
97 ///
98 /// Stored so the deadline scheduler can reconstruct a [`ProcessIdentity`]
99 /// and route the `TimeoutExpired` command to the correct workflow type
100 /// without a separate registry lookup.
101 ///
102 /// [`ProcessIdentity`]: crate::ids::ProcessIdentity
103 workflow_id: WorkflowId,
104
105 /// Human-readable label identifying the deadline type.
106 label: Box<str>,
107
108 /// When this deadline expires.
109 due_at: OffsetDateTime,
110
111 /// When this deadline was registered.
112 created_at: OffsetDateTime,
113}
114
115impl Deadline {
116 /// Construct a new deadline.
117 ///
118 /// `deadline_id` and `created_at` are generated automatically.
119 ///
120 /// `workflow_id` must match the [`WorkflowId`] under which the owning
121 /// process was started (i.e. `process.workflow_id().clone()`). The
122 /// deadline scheduler uses it to reconstruct a [`ProcessIdentity`] and
123 /// route the `TimeoutExpired` command to the correct workflow type.
124 ///
125 /// [`ProcessIdentity`]: crate::ids::ProcessIdentity
126 #[must_use]
127 pub fn new(
128 stream_id: StreamId,
129 process_id: ProcessId,
130 tenant_id: TenantId,
131 workflow_id: WorkflowId,
132 label: impl Into<Box<str>>,
133 due_at: OffsetDateTime,
134 ) -> Self {
135 Self {
136 deadline_id: DeadlineId::new(),
137 stream_id,
138 process_id,
139 tenant_id,
140 workflow_id,
141 label: label.into(),
142 due_at,
143 created_at: OffsetDateTime::now_utc(),
144 }
145 }
146
147 /// Return `true` when this deadline has passed relative to `now`.
148 ///
149 /// ```rust
150 /// use mako_engine::deadline::Deadline;
151 /// use mako_engine::ids::{ProcessId, StreamId, TenantId};
152 /// use mako_engine::version::WorkflowId;
153 /// use time::{Duration, OffsetDateTime};
154 ///
155 /// let past = Deadline::new(
156 /// StreamId::new("process/x"),
157 /// ProcessId::new(),
158 /// TenantId::new(),
159 /// WorkflowId::new("gpke-supplier-change", "FV2025-10-01"),
160 /// "aperak-response-window",
161 /// OffsetDateTime::now_utc() - Duration::seconds(1),
162 /// );
163 /// assert!(past.is_due(OffsetDateTime::now_utc()));
164 /// ```
165 #[must_use]
166 pub fn is_due(&self, now: OffsetDateTime) -> bool {
167 self.due_at <= now
168 }
169
170 /// The unique identifier of this deadline.
171 #[must_use]
172 pub fn deadline_id(&self) -> DeadlineId {
173 self.deadline_id
174 }
175
176 /// The stream this deadline belongs to.
177 #[must_use]
178 pub fn stream_id(&self) -> &StreamId {
179 &self.stream_id
180 }
181
182 /// The process instance this deadline belongs to.
183 #[must_use]
184 pub fn process_id(&self) -> ProcessId {
185 self.process_id
186 }
187
188 /// The tenant that owns this process.
189 #[must_use]
190 pub fn tenant_id(&self) -> TenantId {
191 self.tenant_id
192 }
193
194 /// The workflow that owns this process.
195 ///
196 /// Used by the deadline scheduler to reconstruct a [`ProcessIdentity`]
197 /// and route the `TimeoutExpired` command to the correct workflow type.
198 ///
199 /// [`ProcessIdentity`]: crate::ids::ProcessIdentity
200 #[must_use]
201 pub fn workflow_id(&self) -> &WorkflowId {
202 &self.workflow_id
203 }
204
205 /// The human-readable label identifying the deadline type (e.g.
206 /// `"aperak-response-window"`).
207 #[must_use]
208 pub fn label(&self) -> &str {
209 &self.label
210 }
211
212 /// When this deadline expires.
213 #[must_use]
214 pub fn due_at(&self) -> OffsetDateTime {
215 self.due_at
216 }
217
218 /// When this deadline was registered.
219 #[must_use]
220 pub fn created_at(&self) -> OffsetDateTime {
221 self.created_at
222 }
223}
224
225// ── DueNowResult ──────────────────────────────────────────────────────────────
226
227/// Result of a [`DeadlineStore::due_now`] poll.
228///
229/// When `has_more` is `true`, the store has additional expired deadlines beyond
230/// the returned `deadlines`. The scheduler should drain in a loop until
231/// `has_more` is `false` to avoid leaving unfired deadlines in the store.
232///
233/// ```rust
234/// # tokio_test::block_on(async {
235/// # use mako_engine::deadline::{InMemoryDeadlineStore, DeadlineStore, Deadline};
236/// # use mako_engine::ids::{ProcessId, StreamId, TenantId};
237/// # use time::OffsetDateTime;
238/// let store = InMemoryDeadlineStore::new();
239/// loop {
240/// let result = store.due_now(50).await.unwrap();
241/// for deadline in result.deadlines {
242/// // dispatch TimeoutDeadline command …
243/// store.cancel(deadline.deadline_id()).await.unwrap();
244/// }
245/// if !result.has_more { break; }
246/// }
247/// # });
248/// ```
249#[derive(Debug, Clone)]
250pub struct DueNowResult {
251 /// Expired deadlines, ordered soonest-first.
252 pub deadlines: Vec<Deadline>,
253 /// `true` when the store contains more expired deadlines beyond `deadlines`.
254 pub has_more: bool,
255}
256
257// ── DeadlineStore ─────────────────────────────────────────────────────────────
258
259/// Storage contract for process deadlines.
260///
261/// ## Scheduler contract
262///
263/// A background timer task should poll this store periodically:
264///
265/// 1. Call [`DeadlineStore::due_now`] to retrieve expired deadlines.
266/// 2. Dispatch a `TimeoutDeadline` command to each owning process.
267/// 3. Call [`DeadlineStore::cancel`] to remove the fired deadline.
268///
269/// Cancelling a deadline before the scheduler fires it prevents a spurious
270/// `TimeoutDeadline` command from being dispatched to the process. Always
271/// cancel deadlines when the process advances past them naturally (e.g. when
272/// the expected counterparty response arrives in time).
273///
274/// ## Blanket `Arc` implementation
275///
276/// `Arc<S>` implements `DeadlineStore` whenever `S: DeadlineStore`, enabling
277/// shared access from both the scheduler and command handlers.
278#[allow(async_fn_in_trait)]
279pub trait DeadlineStore: Send + Sync {
280 /// Register a new deadline.
281 ///
282 /// Upserts by `deadline_id`: if a deadline with the same ID already
283 /// exists it is replaced.
284 ///
285 /// # Errors
286 ///
287 /// Returns [`EngineError::Deadline`] on storage failure.
288 #[must_use = "dropping a register Result silently loses a regulatory APERAK deadline"]
289 async fn register(&self, deadline: &Deadline) -> Result<(), EngineError>;
290
291 /// Cancel a registered deadline by ID.
292 ///
293 /// No-op when the deadline does not exist.
294 ///
295 /// # Errors
296 ///
297 /// Returns [`EngineError::Deadline`] on storage failure.
298 #[must_use = "dropping a cancel Result silently hides a storage failure"]
299 async fn cancel(&self, id: DeadlineId) -> Result<(), EngineError>;
300
301 /// Return up to `limit` deadlines whose `due_at <= now_utc()`, ordered
302 /// soonest-first.
303 ///
304 /// When the store contains more expired deadlines than `limit`, the
305 /// returned [`DueNowResult::has_more`] is `true`. Callers should drain
306 /// in a loop until `has_more` is `false`.
307 ///
308 /// # Errors
309 ///
310 /// Returns [`EngineError::Deadline`] on storage failure.
311 #[must_use = "dropping a due_now Result silently discards fired deadlines"]
312 async fn due_now(&self, limit: usize) -> Result<DueNowResult, EngineError>;
313
314 /// Return all active deadlines for `stream_id`, in registration order.
315 ///
316 /// # Errors
317 ///
318 /// Returns [`EngineError::Deadline`] on storage failure.
319 #[must_use = "dropping a for_stream Result silently discards deadline data"]
320 async fn for_stream(&self, stream_id: &StreamId) -> Result<Vec<Deadline>, EngineError>;
321
322 /// Total number of registered deadlines.
323 ///
324 /// # Errors
325 ///
326 /// Returns [`EngineError::Deadline`] on storage failure.
327 #[must_use = "dropping a len Result silently discards a store error"]
328 async fn len(&self) -> Result<usize, EngineError>;
329
330 /// Return `true` when no deadlines are registered.
331 ///
332 /// # Errors
333 ///
334 /// Returns [`EngineError::Deadline`] on storage failure.
335 async fn is_empty(&self) -> Result<bool, EngineError> {
336 Ok(self.len().await? == 0)
337 }
338
339 /// Count deadlines whose `due_at ≤ now` that have not yet been cancelled.
340 ///
341 /// Indicates scheduler lag: a non-zero value means `TimeoutExpired` commands
342 /// are not being dispatched in time, which is a compliance violation.
343 ///
344 /// The default implementation delegates to [`due_now`] with a limit of
345 /// 10 000; if there are more overdue deadlines, returns 10 000 (capped).
346 /// Implementations can override for a more efficient point-count query.
347 ///
348 /// # Errors
349 ///
350 /// Returns [`EngineError::Deadline`] on storage failure.
351 ///
352 /// [`due_now`]: DeadlineStore::due_now
353 async fn overdue_count(&self) -> Result<usize, EngineError> {
354 const LIMIT: usize = 10_000;
355 let result = self.due_now(LIMIT).await?;
356 Ok(if result.has_more {
357 LIMIT
358 } else {
359 result.deadlines.len()
360 })
361 }
362}
363
364// ── Arc<S> blanket impl ───────────────────────────────────────────────────────
365
366impl<S: DeadlineStore> DeadlineStore for Arc<S> {
367 async fn register(&self, deadline: &Deadline) -> Result<(), EngineError> {
368 self.as_ref().register(deadline).await
369 }
370
371 async fn cancel(&self, id: DeadlineId) -> Result<(), EngineError> {
372 self.as_ref().cancel(id).await
373 }
374
375 async fn due_now(&self, limit: usize) -> Result<DueNowResult, EngineError> {
376 self.as_ref().due_now(limit).await
377 }
378
379 async fn for_stream(&self, stream_id: &StreamId) -> Result<Vec<Deadline>, EngineError> {
380 self.as_ref().for_stream(stream_id).await
381 }
382
383 async fn len(&self) -> Result<usize, EngineError> {
384 self.as_ref().len().await
385 }
386
387 async fn overdue_count(&self) -> Result<usize, EngineError> {
388 self.as_ref().overdue_count().await
389 }
390}
391
392// ── NoopDeadlineStore ─────────────────────────────────────────────────────────
393
394/// A [`DeadlineStore`] that never persists anything.
395///
396/// `register` succeeds silently; `due_now` always returns an empty list.
397/// Use this as the default when deadline tracking is not needed.
398///
399/// # ⚠️ Silent deadline loss
400///
401/// `NoopDeadlineStore` **discards every deadline registration silently**. No
402/// scheduler timeout will ever fire. Missed deadlines are a compliance
403/// violation under BNetzA monitoring. Do not use in production.
404///
405/// This type is available in all build configurations so it can serve as a
406/// default type parameter in [`EngineBuilder`]. However, `EngineBuilder::new`
407/// (which wires this as the default) is only available with the `testing`
408/// feature or in `cfg(test)`. Production binaries must call
409/// [`EngineBuilder::with_stores`] instead.
410///
411/// [`EngineBuilder`]: crate::builder::EngineBuilder
412/// [`EngineBuilder::with_stores`]: crate::builder::EngineBuilder::with_stores
413#[derive(Debug, Clone, Copy, Default)]
414#[must_use = "NoopDeadlineStore discards all deadlines silently — use a persistent DeadlineStore in production"]
415#[cfg_attr(
416 not(any(test, feature = "testing")),
417 deprecated = "NoopDeadlineStore must not be instantiated in production builds; use a durable DeadlineStore instead"
418)]
419pub struct NoopDeadlineStore;
420
421#[cfg(any(test, feature = "testing"))]
422impl DeadlineStore for NoopDeadlineStore {
423 async fn register(&self, _deadline: &Deadline) -> Result<(), EngineError> {
424 Ok(())
425 }
426
427 async fn cancel(&self, _id: DeadlineId) -> Result<(), EngineError> {
428 Ok(())
429 }
430
431 async fn due_now(&self, _limit: usize) -> Result<DueNowResult, EngineError> {
432 Ok(DueNowResult {
433 deadlines: Vec::new(),
434 has_more: false,
435 })
436 }
437
438 async fn for_stream(&self, _stream_id: &StreamId) -> Result<Vec<Deadline>, EngineError> {
439 Ok(Vec::new())
440 }
441
442 async fn len(&self) -> Result<usize, EngineError> {
443 Ok(0)
444 }
445}
446
447// ── InMemoryDeadlineStore ─────────────────────────────────────────────────────
448
449/// An in-memory [`DeadlineStore`] for tests and development.
450///
451/// Backed by a `HashMap` protected by a `Mutex`. Cloning shares the
452/// underlying data via `Arc` — all clones see the same deadlines.
453///
454/// **Not production-safe.** Use this for:
455/// - Unit and integration tests
456/// - Examples and local development
457/// - Verifying the scheduler loop without an external timer service
458///
459/// Only available in `#[cfg(test)]` or with the `testing` feature enabled.
460#[cfg(any(test, feature = "testing"))]
461#[derive(Debug, Default, Clone)]
462pub struct InMemoryDeadlineStore {
463 inner: Arc<RwLock<HashMap<DeadlineId, Deadline>>>,
464}
465
466#[cfg(any(test, feature = "testing"))]
467impl InMemoryDeadlineStore {
468 /// Create an empty deadline store.
469 #[must_use]
470 pub fn new() -> Self {
471 Self::default()
472 }
473
474 /// Return `true` when no deadlines are registered.
475 pub async fn is_empty(&self) -> bool {
476 self.inner.read().await.is_empty()
477 }
478}
479
480#[cfg(any(test, feature = "testing"))]
481impl DeadlineStore for InMemoryDeadlineStore {
482 async fn register(&self, deadline: &Deadline) -> Result<(), EngineError> {
483 self.inner
484 .write()
485 .await
486 .insert(deadline.deadline_id, deadline.clone());
487 Ok(())
488 }
489
490 async fn cancel(&self, id: DeadlineId) -> Result<(), EngineError> {
491 self.inner.write().await.remove(&id);
492 Ok(())
493 }
494
495 async fn due_now(&self, limit: usize) -> Result<DueNowResult, EngineError> {
496 let now = OffsetDateTime::now_utc();
497 let map = self.inner.read().await;
498 let mut due: Vec<_> = map.values().filter(|d| d.is_due(now)).cloned().collect();
499 // Soonest-first: the scheduler processes the most urgent deadlines first.
500 due.sort_by_key(|d| d.due_at);
501 // Probe one extra to detect whether more remain.
502 let has_more = due.len() > limit;
503 due.truncate(limit);
504 Ok(DueNowResult {
505 deadlines: due,
506 has_more,
507 })
508 }
509
510 async fn for_stream(&self, stream_id: &StreamId) -> Result<Vec<Deadline>, EngineError> {
511 let map = self.inner.read().await;
512 Ok(map
513 .values()
514 .filter(|d| &d.stream_id == stream_id)
515 .cloned()
516 .collect())
517 }
518
519 async fn len(&self) -> Result<usize, EngineError> {
520 Ok(self.inner.read().await.len())
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527 use time::Duration;
528
529 fn make_deadline(due_at: OffsetDateTime) -> Deadline {
530 Deadline::new(
531 StreamId::new("process/test"),
532 ProcessId::new(),
533 TenantId::new(),
534 WorkflowId::new("test-workflow", "FV2025-10-01"),
535 "aperak-response-window",
536 due_at,
537 )
538 }
539
540 #[tokio::test]
541 async fn register_and_cancel() {
542 let store = InMemoryDeadlineStore::new();
543 let d = make_deadline(OffsetDateTime::now_utc() + Duration::days(5));
544 let id = d.deadline_id;
545
546 store.register(&d).await.unwrap();
547 assert_eq!(store.len().await.unwrap(), 1);
548
549 store.cancel(id).await.unwrap();
550 assert!(store.is_empty().await);
551 }
552
553 #[tokio::test]
554 async fn due_now_only_returns_overdue() {
555 let store = InMemoryDeadlineStore::new();
556 let past = make_deadline(OffsetDateTime::now_utc() - Duration::seconds(1));
557 let future = make_deadline(OffsetDateTime::now_utc() + Duration::days(5));
558
559 store.register(&past).await.unwrap();
560 store.register(&future).await.unwrap();
561
562 let due = store.due_now(100).await.unwrap();
563 assert_eq!(due.deadlines.len(), 1);
564 assert_eq!(due.deadlines[0].label.as_ref(), "aperak-response-window");
565 assert!(!due.has_more);
566 }
567
568 #[tokio::test]
569 async fn due_now_ordered_soonest_first() {
570 let store = InMemoryDeadlineStore::new();
571 let t1 = OffsetDateTime::now_utc() - Duration::seconds(60);
572 let t2 = OffsetDateTime::now_utc() - Duration::seconds(10);
573 let t3 = OffsetDateTime::now_utc() - Duration::seconds(1);
574
575 // Register out of order to verify sorting.
576 store.register(&make_deadline(t3)).await.unwrap();
577 store.register(&make_deadline(t1)).await.unwrap();
578 store.register(&make_deadline(t2)).await.unwrap();
579
580 let due = store.due_now(10).await.unwrap();
581 assert_eq!(due.deadlines.len(), 3);
582 assert!(due.deadlines[0].due_at <= due.deadlines[1].due_at);
583 assert!(due.deadlines[1].due_at <= due.deadlines[2].due_at);
584 assert!(!due.has_more);
585 }
586
587 #[tokio::test]
588 async fn for_stream_filters_by_stream() {
589 let store = InMemoryDeadlineStore::new();
590 let stream1 = StreamId::new("process/aaa");
591 let stream2 = StreamId::new("process/bbb");
592 let d1 = Deadline::new(
593 stream1.clone(),
594 ProcessId::new(),
595 TenantId::new(),
596 WorkflowId::new("test-workflow", "FV2025-10-01"),
597 "label",
598 OffsetDateTime::now_utc() + Duration::days(1),
599 );
600 let d2 = Deadline::new(
601 stream2.clone(),
602 ProcessId::new(),
603 TenantId::new(),
604 WorkflowId::new("test-workflow", "FV2025-10-01"),
605 "label",
606 OffsetDateTime::now_utc() + Duration::days(1),
607 );
608
609 store.register(&d1).await.unwrap();
610 store.register(&d2).await.unwrap();
611
612 let for1 = store.for_stream(&stream1).await.unwrap();
613 assert_eq!(for1.len(), 1);
614 assert_eq!(for1[0].stream_id, stream1);
615 }
616
617 #[tokio::test]
618 async fn register_upserts_on_same_id() {
619 let store = InMemoryDeadlineStore::new();
620 let mut d = make_deadline(OffsetDateTime::now_utc() + Duration::days(5));
621 store.register(&d).await.unwrap();
622
623 let new_due = OffsetDateTime::now_utc() + Duration::days(10);
624 d.due_at = new_due;
625 store.register(&d).await.unwrap();
626
627 assert_eq!(
628 store.len().await.unwrap(),
629 1,
630 "upsert must not create a duplicate"
631 );
632 let found = store.for_stream(&d.stream_id).await.unwrap();
633 assert_eq!(found[0].due_at, new_due);
634 }
635
636 #[tokio::test]
637 async fn noop_store_succeeds_silently() {
638 let store = NoopDeadlineStore;
639 let d = make_deadline(OffsetDateTime::now_utc() - Duration::seconds(1));
640 store.register(&d).await.unwrap();
641 assert!(store.due_now(10).await.unwrap().deadlines.is_empty());
642 assert!(store.is_empty().await.unwrap());
643 }
644
645 #[tokio::test]
646 async fn clone_shares_state() {
647 let store1 = InMemoryDeadlineStore::new();
648 let store2 = store1.clone();
649 let d = make_deadline(OffsetDateTime::now_utc() + Duration::days(1));
650 store1.register(&d).await.unwrap();
651 assert_eq!(store2.len().await.unwrap(), 1);
652 }
653
654 #[tokio::test]
655 async fn due_now_has_more_signals_truncation() {
656 let store = InMemoryDeadlineStore::new();
657 let past = OffsetDateTime::now_utc() - Duration::seconds(1);
658 for _ in 0..5 {
659 store.register(&make_deadline(past)).await.unwrap();
660 }
661
662 // Request fewer than available — has_more must be true.
663 let r = store.due_now(3).await.unwrap();
664 assert_eq!(r.deadlines.len(), 3);
665 assert!(r.has_more);
666
667 // Request all — has_more must be false.
668 let r2 = store.due_now(10).await.unwrap();
669 assert_eq!(r2.deadlines.len(), 5);
670 assert!(!r2.has_more);
671 }
672}