Skip to main content

krabka_log/log/
delivery.rs

1//! The deliver-at-time watermark: the first offset whose activation time
2//! has not passed yet.
3//!
4//! A fetch caps its limit offset at the watermark, so a scheduled batch is
5//! never served before its activation time while the declared clock bound
6//! holds. Nothing here is persisted: restart recomputes using the current
7//! clock and uncertainty bound, which may differ from a previously released
8//! in-memory prefix.
9
10use {
11    super::Log,
12    crate::{
13        config::DeliveryPolicy,
14        delivery::{self, DeliveryAdvance},
15    },
16    krabka_ids::Offset,
17    krabka_units::prelude::TimeExt as _,
18    std::sync::PoisonError,
19};
20
21/// Whether a topic schedules delivery, and its clock bound in milliseconds.
22struct DeliverySettings {
23    scheduled: bool,
24    uncertainty_ms: i64,
25}
26
27/// Where an activation walk stopped: the new watermark, and the activation
28/// time of the first batch that is not yet active.
29struct ActivationWalk {
30    watermark: Offset,
31    pending_ms: Option<i64>,
32}
33
34impl Log {
35    /// Recompute the deliver-at-time watermark for the clock reading
36    /// `now_ms`, and report when the next scheduled batch comes due.
37    ///
38    /// The watermark is the first offset that is not visible yet. A fetch
39    /// caps its limit offset at it, so a batch whose activation time has not
40    /// passed is never served. The value stays inside
41    /// `[log_start_offset(), log_end_offset()]`, and it only moves forward
42    /// while the records under it stay: a truncation carries it back down with
43    /// the records it cut away, because the offsets they held may be filled
44    /// again by a batch that is not due.
45    ///
46    /// A topic that does not schedule delivery answers `log_end_offset()`
47    /// before any I/O: this sits on the fetch hot path and an ordinary topic
48    /// must pay nothing for it. A scheduled topic walks batch headers forward
49    /// from the cached watermark, and skips whole segments whose own maximum
50    /// timestamp proves everything in them is active. A repeat call with no
51    /// change of time and no new records answers from the cache.
52    /// Increasing clock uncertainty or moving the clock backward does not
53    /// retract the published prefix; current settings govern records at and
54    /// above the cached watermark.
55    ///
56    /// Nothing here is persisted. The schedule is in the record timestamps,
57    /// so after a restart or a leader change the walk starts again from the
58    /// log start using the current clock and bound. If uncertainty increased
59    /// or the clock moved backward, a rebuilt watermark can be below the
60    /// prefix this instance had already released.
61    pub fn advance_delivery_watermark(&mut self, now_ms: i64) -> DeliveryAdvance {
62        let DeliverySettings {
63            scheduled,
64            uncertainty_ms,
65        } = self.delivery_settings();
66        if !scheduled {
67            return DeliveryAdvance {
68                watermark: self.log_end_offset(),
69                next_deadline_ms: None,
70            };
71        }
72
73        // The walk reads records, so it starts at the local floor: on a
74        // tiered topic the offsets below it are in the remote tier and the
75        // segments that held them are gone.
76        let start = self.local_log_start_offset();
77        let end = self.log_end_offset();
78        let cursor = self.bounded_watermark();
79
80        if cursor == self.delivery_watermark {
81            match self.delivery_pending_ms {
82                // The batch that stopped the last walk is still waiting, and
83                // no walk can get past it, so the watermark cannot have moved.
84                Some(activation_ms)
85                    if !delivery::batch_is_deliverable(
86                        DeliveryPolicy::Scheduled,
87                        uncertainty_ms,
88                        activation_ms,
89                        now_ms,
90                    ) =>
91                {
92                    return DeliveryAdvance {
93                        watermark: cursor,
94                        next_deadline_ms: Some(delivery::visible_at_ms(
95                            activation_ms,
96                            uncertainty_ms,
97                        )),
98                    };
99                }
100                // The last walk reached the log end and nothing was appended.
101                None if cursor == end => {
102                    return DeliveryAdvance {
103                        watermark: cursor,
104                        next_deadline_ms: None,
105                    };
106                }
107                _ => {}
108            }
109        }
110
111        let ActivationWalk {
112            watermark: candidate,
113            pending_ms,
114        } = self.walk_activation(cursor, end, uncertainty_ms, now_ms);
115        let watermark = Offset(krabka_verified::delivery_watermark_advance(
116            start.0,
117            cursor.0,
118            candidate.0,
119            end.0,
120        ));
121        let pending_ms = if watermark == candidate {
122            pending_ms
123        } else {
124            None
125        };
126        self.delivery_watermark = watermark;
127        self.delivery_pending_ms = pending_ms;
128        DeliveryAdvance {
129            watermark,
130            next_deadline_ms: pending_ms
131                .map(|activation_ms| delivery::visible_at_ms(activation_ms, uncertainty_ms)),
132        }
133    }
134
135    /// Deliver-at-time watermark as the last advance left it.
136    ///
137    /// This method reads the cached value and walks nothing, so a reader that
138    /// must not do I/O can use it. It answers `log_end_offset()` on a topic
139    /// that does not schedule delivery, where every durable record is visible.
140    #[must_use]
141    pub fn delivery_watermark(&self) -> Offset {
142        if self
143            .config
144            .read()
145            .unwrap_or_else(PoisonError::into_inner)
146            .delivery_policy
147            != DeliveryPolicy::Scheduled
148        {
149            return self.log_end_offset();
150        }
151        self.bounded_watermark()
152    }
153
154    /// Inclusive, batch-aligned offset ranges inside `[start, end]` that are
155    /// not visible yet.
156    ///
157    /// A share consumer may skip a waiting record and come back to it later,
158    /// so it needs every gap in the window and not only the leading prefix
159    /// that [`Log::advance_delivery_watermark`] reports. Adjacent ranges are
160    /// merged. Each range covers whole batches even where the window cuts
161    /// through one, because the share reader fetches with [`Log::read_raw`]
162    /// and that is batch-granular.
163    ///
164    /// The result is empty on a topic that does not schedule delivery, and
165    /// empty when the window holds no records.
166    /// Records below the published delivery watermark stay visible even if
167    /// clock uncertainty subsequently increases or the clock moves backward.
168    #[must_use]
169    pub fn pending_activation_ranges(
170        &self,
171        start: Offset,
172        end: Offset,
173        now_ms: i64,
174    ) -> Vec<(Offset, Offset)> {
175        let DeliverySettings {
176            scheduled,
177            uncertainty_ms,
178        } = self.delivery_settings();
179        if !scheduled {
180            return Vec::new();
181        }
182        let low = start.max(self.bounded_watermark());
183        let high = end.min(self.log_end_offset() - 1);
184        if low > high {
185            return Vec::new();
186        }
187        let mut ranges: Vec<(Offset, Offset)> = Vec::new();
188        let mut cursor = low;
189        for segment in self.segments.iter().chain(self.active.as_ref()) {
190            if cursor > high {
191                break;
192            }
193            if segment.last_offset() < cursor {
194                continue;
195            }
196            if let Err(error) = segment.pending_activation_ranges_into(
197                cursor,
198                high,
199                uncertainty_ms,
200                now_ms,
201                &mut ranges,
202            ) {
203                tracing::warn!(
204                    %error,
205                    base_offset = segment.base_offset().0,
206                    "activation scan failed; treating the rest of the window as pending",
207                );
208                // What cannot be read must not be served.
209                ranges.push((cursor, high));
210                break;
211            }
212            cursor = (segment.last_offset() + 1).max(cursor);
213        }
214        delivery::coalesce_ranges(ranges)
215    }
216
217    /// Whether this topic schedules delivery, and its clock bound in
218    /// milliseconds.
219    fn delivery_settings(&self) -> DeliverySettings {
220        let config = self.config.read().unwrap_or_else(PoisonError::into_inner);
221        DeliverySettings {
222            scheduled: config.delivery_policy == DeliveryPolicy::Scheduled,
223            uncertainty_ms: config.delivery_clock_uncertainty.millis_i64_trunc(),
224        }
225    }
226
227    /// The cached watermark held inside the offsets the log still has.
228    ///
229    /// A truncation lowers the log end below the watermark, and a raised log
230    /// start moves past it, so the cached value is bounded on each read rather
231    /// than reset at every mutation site.
232    fn bounded_watermark(&self) -> Offset {
233        self.delivery_watermark
234            .max(self.local_log_start_offset())
235            .min(self.log_end_offset())
236    }
237
238    /// Walk segments from `from` and stop at the first batch that is not
239    /// active. Returns the new watermark and that batch's activation time.
240    ///
241    /// A failed scan stops the walk and keeps the watermark where it reached,
242    /// because a watermark that is too low delays delivery while one that is
243    /// too high breaks the promise never to deliver early.
244    fn walk_activation(
245        &self,
246        from: Offset,
247        end: Offset,
248        uncertainty_ms: i64,
249        now_ms: i64,
250    ) -> ActivationWalk {
251        let mut cursor = from;
252        for segment in self.segments.iter().chain(self.active.as_ref()) {
253            if cursor >= end {
254                break;
255            }
256            if segment.last_offset() < cursor {
257                continue;
258            }
259            match segment.scan_activation(cursor, uncertainty_ms, now_ms) {
260                Ok(scan) => {
261                    cursor = scan.active_end.max(cursor).min(end);
262                    if let Some(activation_ms) = scan.pending_at {
263                        return ActivationWalk {
264                            watermark: cursor,
265                            pending_ms: Some(activation_ms),
266                        };
267                    }
268                }
269                Err(error) => {
270                    tracing::warn!(
271                        %error,
272                        base_offset = segment.base_offset().0,
273                        "activation scan failed; delivery watermark holds",
274                    );
275                    return ActivationWalk {
276                        watermark: cursor,
277                        pending_ms: None,
278                    };
279                }
280            }
281        }
282        ActivationWalk {
283            watermark: cursor,
284            pending_ms: None,
285        }
286    }
287
288    /// Forget what the last activation walk learned at and above
289    /// `changed_from`.
290    ///
291    /// Every path that removes or rewrites records calls this, so the next
292    /// advance walks instead of trusting a deadline for a batch that is no
293    /// longer there.
294    ///
295    /// The watermark comes down with the deadline, and that half is what keeps
296    /// the promise never to deliver early. A truncation cuts a suffix away, and
297    /// [`Log::bounded_watermark`] then masks the stale value against the lower
298    /// log end. But the field still holds it, so an append that pushes the log
299    /// end back above it unmasks it: the next walk resumes at the stale offset
300    /// and steps straight over the records that took the truncated ones' place,
301    /// declaring them visible without ever reading their activation times.
302    pub(super) fn invalidate_delivery_schedule(&mut self, changed_from: Offset) {
303        self.delivery_pending_ms = None;
304        self.delivery_watermark = self.delivery_watermark.min(changed_from);
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use {
311        super::*,
312        crate::{
313            LogConfig,
314            log::test_support::{TempLog, configured_test_log},
315        },
316        assert2::assert,
317        krabka_protocol::records::{Record, RecordBatch},
318        krabka_units::millis,
319    };
320
321    #[test]
322    fn larger_uncertainty_is_prospective_for_the_released_prefix() {
323        let TempLog { dir, mut log } = configured_test_log(LogConfig {
324            delivery_policy: DeliveryPolicy::Scheduled,
325            delivery_clock_uncertainty: millis(0),
326            ..LogConfig::default()
327        });
328        for time in [100, 200] {
329            log.append(&mut RecordBatch {
330                base_timestamp: time,
331                max_timestamp: time,
332                records: vec![Record::default()],
333                ..RecordBatch::default()
334            })
335            .unwrap();
336        }
337        assert!(log.advance_delivery_watermark(150).watermark == Offset(1));
338        let config = LogConfig {
339            delivery_clock_uncertainty: millis(100),
340            ..log.config_snapshot()
341        };
342        log.set_config(config.clone());
343        assert!(log.advance_delivery_watermark(150).watermark == Offset(1));
344        assert!(log.advance_delivery_watermark(50).watermark == Offset(1));
345        assert!(
346            log.pending_activation_ranges(Offset(0), Offset(1), 150)
347                == vec![(Offset(1), Offset(1))]
348        );
349        log.sync().unwrap();
350        drop(log);
351        let mut reopened = Log::open(dir.path(), config).unwrap();
352        assert!(reopened.advance_delivery_watermark(150).watermark == Offset(0));
353        assert!(reopened.advance_delivery_watermark(300).watermark == Offset(2));
354    }
355}