Skip to main content

krabka_log/
delivery.rs

1//! Deliver-at-time visibility: when a scheduled batch becomes readable.
2//!
3//! On a topic set to [`DeliveryPolicy::Scheduled`](crate::DeliveryPolicy), a
4//! record batch is not visible the moment it is durable. It becomes visible
5//! once its activation time has passed. The activation time is the batch's
6//! `max_timestamp`, the v2 header field, so a producer schedules delivery
7//! through the timestamp it stamps and no sidecar is needed.
8//!
9//! The schedule lives entirely in the records, which is why nothing here is
10//! persisted. A watermark is derived state: after a restart or a leader
11//! change it is recomputed from the segments, and a recomputation can only
12//! agree with what the records say.
13
14use {crate::DeliveryPolicy, krabka_ids::Offset};
15
16/// Result of
17/// [`Log::advance_delivery_watermark`](crate::Log::advance_delivery_watermark).
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct DeliveryAdvance {
20    /// First offset that is not visible yet. Every offset below it has
21    /// reached its activation time, so a fetch may serve it.
22    ///
23    /// On a topic that does not schedule delivery this is the log end offset:
24    /// everything written is visible.
25    pub watermark: Offset,
26
27    /// Epoch-millisecond instant at which the first waiting batch above
28    /// [`Self::watermark`] becomes visible, or `None` when nothing is waiting.
29    ///
30    /// The value already includes the configured clock-uncertainty bound, so
31    /// a scheduler compares it against its own clock and adds nothing. A
32    /// caller that wakes at this instant and advances the watermark again
33    /// finds that batch visible.
34    pub next_deadline_ms: Option<i64>,
35}
36
37/// The instant a batch that activates at `activation_ms` becomes visible.
38///
39/// The bound is added, never subtracted: if the clock reads `c` while true
40/// time lies in `[c - e, c + e]`, then `c >= activation + e` proves true time
41/// has reached the activation instant, so delivery is never early.
42pub fn visible_at_ms(activation_ms: i64, uncertainty_ms: i64) -> i64 {
43    activation_ms.saturating_add(uncertainty_ms)
44}
45
46/// Apply the shared scheduled-delivery visibility rule to one batch.
47#[must_use]
48pub fn batch_is_deliverable(
49    policy: DeliveryPolicy,
50    uncertainty_ms: i64,
51    activation_ms: i64,
52    now_ms: i64,
53) -> bool {
54    krabka_verified::scheduled_delivery_visible(
55        policy == DeliveryPolicy::Scheduled,
56        uncertainty_ms,
57        activation_ms,
58        now_ms,
59    )
60}
61
62/// Merge inclusive offset ranges that touch or overlap.
63///
64/// The walk produces them in ascending order, one per batch, so a single pass
65/// is enough. Adjacent batches merge because `[0, 4]` and `[5, 9]` describe
66/// one uninterrupted run of waiting records.
67pub fn coalesce_ranges(ranges: Vec<(Offset, Offset)>) -> Vec<(Offset, Offset)> {
68    let mut out: Vec<(Offset, Offset)> = Vec::with_capacity(ranges.len());
69    for (low, high) in ranges {
70        match out.last_mut() {
71            Some(last) => {
72                let (merge, merged_high) =
73                    krabka_verified::coalesce_delivery_range(last.1.0, low.0, high.0);
74                if merge {
75                    last.1 = Offset(merged_high);
76                } else {
77                    out.push((low, high));
78                }
79            }
80            None => out.push((low, high)),
81        }
82    }
83    out
84}
85
86#[cfg(test)]
87mod tests {
88    use {super::*, assert2::check};
89
90    #[test]
91    fn adjacent_and_overlapping_ranges_merge_but_separated_ones_do_not() {
92        let merged = coalesce_ranges(vec![
93            (Offset(0), Offset(4)),
94            // Touches the previous range.
95            (Offset(5), Offset(9)),
96            // Contained in the run so far: the end must not move backwards.
97            (Offset(6), Offset(7)),
98            // One offset of daylight: a separate range.
99            (Offset(11), Offset(12)),
100        ]);
101        check!(merged == vec![(Offset(0), Offset(9)), (Offset(11), Offset(12))]);
102        check!(
103            coalesce_ranges(vec![
104                (Offset(i64::MAX - 1), Offset(i64::MAX)),
105                (Offset(i64::MAX), Offset(i64::MAX)),
106            ]) == vec![(Offset(i64::MAX - 1), Offset(i64::MAX))]
107        );
108    }
109
110    #[test]
111    fn no_ranges_merge_to_nothing() {
112        check!(coalesce_ranges(Vec::new()).is_empty());
113    }
114
115    #[test]
116    fn the_visible_instant_adds_the_bound_and_saturates() {
117        check!(visible_at_ms(1_000, 250) == 1_250);
118        check!(visible_at_ms(i64::MAX, 250) == i64::MAX);
119    }
120
121    #[test]
122    fn batch_visibility_delegates_to_the_proved_deadline_rule() {
123        check!(batch_is_deliverable(
124            crate::DeliveryPolicy::Immediate,
125            250,
126            10_000,
127            0
128        ));
129        check!(!batch_is_deliverable(
130            crate::DeliveryPolicy::Scheduled,
131            250,
132            10_000,
133            10_249
134        ));
135        check!(batch_is_deliverable(
136            crate::DeliveryPolicy::Scheduled,
137            250,
138            10_000,
139            10_250
140        ));
141    }
142}