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}