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