Skip to main content

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}