1#[cfg(all(test, not(target_os = "wasi")))]
9use std::cell::Cell;
10#[cfg(not(target_os = "wasi"))]
11use std::fs::File;
12use {
13 super::{
14 ReserveRequest, Segment,
15 io::{SourceIo as _, seek_to_log_size},
16 },
17 crate::{error::LogError, io::LogIo},
18 krabka_ids::Offset,
19 krabka_protocol::records::RecordBatch,
20 krabka_units::prelude::{ByteSize, ByteSizeExt as _},
21 std::{
22 io::{self, ErrorKind},
23 mem,
24 sync::{Arc, atomic::Ordering},
25 },
26 tracing::{field, instrument},
27};
28
29const RESERVE_STEP_MIN: u64 = 1 << 20;
38
39pub(super) fn log_refused(operation: &'static str, base_offset: Offset, error: &io::Error) {
40 if error.kind() == ErrorKind::Unsupported {
41 tracing::debug!(operation, base_offset = base_offset.0, %error, "segment preallocation unsupported");
42 } else {
43 tracing::warn!(operation, base_offset = base_offset.0, %error, "segment preallocation refused");
44 }
45}
46
47#[derive(Debug, Clone, Copy)]
49pub(super) struct WriteSnapshot {
50 last_offset: Offset,
51 max_timestamp: i64,
52 max_timestamp_offset: Offset,
53}
54
55#[cfg(all(test, not(target_os = "wasi")))]
56std::thread_local! {
57 static FAIL_FLUSH_HANDLES: Cell<bool> = const { Cell::new(false) };
58}
59
60impl Segment {
61 #[cfg(all(test, not(target_os = "wasi")))]
62 pub(crate) fn test_fail_flush_handles(fail: bool) { FAIL_FLUSH_HANDLES.set(fail); }
63
64 #[cfg(not(target_os = "wasi"))]
65 pub(crate) fn flush_handles(&self) -> io::Result<[Arc<File>; 3]> {
66 #[cfg(test)]
67 if FAIL_FLUSH_HANDLES.get() {
68 return Err(io::Error::other("injected flush handle clone failure"));
69 }
70 Ok([
71 Arc::clone(&self.log_file),
72 Arc::new(self.offset_index.flush_handle()?),
73 Arc::new(self.time_index.flush_handle()?),
74 ])
75 }
76
77 #[cfg(not(target_os = "wasi"))]
79 pub(crate) fn log_file_handle(&self) -> Arc<File> { Arc::clone(&self.log_file) }
80
81 #[cfg(not(target_os = "wasi"))]
84 pub(crate) fn log_file(&self) -> &Arc<File> { &self.log_file }
85
86 pub fn seal(&mut self) -> Result<(), LogError> {
101 self.append_running_max_time_entry()?;
102 self.write_buffered()?;
103 self.sealed = true;
104 self.release_reservation();
105 Ok(())
106 }
107
108 fn reservation_target(&self, size: u64) -> u64 {
115 let step = (size / 4).max(RESERVE_STEP_MIN);
116 let needed = self.log_size.saturating_add(step / 4);
117 if needed >= size {
118 return size;
119 }
120 needed.div_ceil(step).saturating_mul(step).min(size)
121 }
122
123 pub(crate) fn needs_reservation(&self, size: ByteSize) -> bool {
126 self.reserve_requested != ReserveRequest::Refused
127 && self.reservation_target(size.bytes_u64()) > self.log_size.max(self.reserved_end)
128 }
129
130 pub(crate) fn reserve(&mut self, size: ByteSize) {
141 if !self.needs_reservation(size) {
142 return;
143 }
144 self.reserve_requested = ReserveRequest::Size(size.bytes_u64());
145 let target = self.reservation_target(size.bytes_u64());
146 let start = self.log_size.max(self.reserved_end);
147 match self.io.reserve(&self.log_file, start, target - start) {
148 Ok(()) => self.reserved_end = target,
149 Err(error) => {
150 self.reserve_requested = ReserveRequest::Refused;
151 log_refused("reserve", self.base_offset, &error);
152 }
153 }
154 }
155
156 pub(super) fn renew_reservation(&mut self) {
163 self.reserved_end = 0;
164 if let ReserveRequest::Size(size) = self.reserve_requested {
165 self.reserve(ByteSize::from_bytes(size));
166 }
167 }
168
169 fn release_reservation(&mut self) {
176 let reserved_end = mem::take(&mut self.reserved_end);
177 if reserved_end <= self.log_size {
178 return;
179 }
180 if let Err(error) = self.io.release(&self.log_file, self.log_size) {
181 log_refused("release", self.base_offset, &error);
182 }
183 }
184
185 pub(super) fn append_running_max_time_entry(&mut self) -> Result<(), LogError> {
192 if self.max_timestamp < 0 {
193 return Ok(());
194 }
195 let relative = krabka_verified::truncation_relative_offset(
196 self.base_offset.0,
197 self.max_timestamp_offset.0,
198 )
199 .ok_or_else(|| LogError::BadSegmentName("offset overflow in segment".into()))?;
200 self.time_index.maybe_append(self.max_timestamp, relative)
201 }
202
203 pub fn seal_at(&mut self, last: Offset) {
217 self.sealed = true;
218 self.last_offset = last;
219 }
220
221 #[instrument(
223 level = "debug",
224 skip_all,
225 fields(base_offset = self.base_offset.0, log_size = self.log_size),
226 err,
227 )]
228 pub fn flush(&mut self) -> Result<(), LogError> {
231 self.drain_direct()?;
232 self.io.sync_data(&self.log_file)?;
233 self.offset_index.flush()?;
234 self.time_index.flush()?;
235 Ok(())
236 }
237
238 pub(super) fn write_snapshot(&self) -> WriteSnapshot {
241 WriteSnapshot {
242 last_offset: self.last_offset,
243 max_timestamp: self.max_timestamp,
244 max_timestamp_offset: self.max_timestamp_offset,
245 }
246 }
247
248 pub(super) fn rollback_failed_write(
249 &mut self,
250 position: u64,
251 snapshot: WriteSnapshot,
252 ) -> Result<(), LogError> {
253 let WriteSnapshot {
254 last_offset,
255 max_timestamp,
256 max_timestamp_offset,
257 } = snapshot;
258 self.settle_direct();
259 self.log_file.set_len(position)?;
260 self.first_timestamp = None;
261 seek_to_log_size(&self.log_file, position)?;
262 self.log_size = position;
263 self.renew_reservation();
264 self.resync_direct()?;
265 self.last_offset = last_offset;
266 self.max_timestamp = max_timestamp;
267 self.max_timestamp_offset = max_timestamp_offset;
268 let position = u32::try_from(position)
269 .map_err(|_| LogError::BadSegmentName("position overflow".into()))?;
270 self.offset_index.truncate_by_position(position)?;
271 let next = last_offset
272 .0
273 .checked_add(1)
274 .and_then(|next| next.checked_sub(self.base_offset.0))
275 .ok_or_else(|| LogError::BadSegmentName("offset overflow".into()))?;
276 let next_relative =
277 u32::try_from(next).map_err(|_| LogError::BadSegmentName("offset overflow".into()))?;
278 self.time_index.truncate_by_relative_offset(next_relative)?;
279 Ok(())
280 }
281
282 pub(crate) fn set_io(&mut self, io: Arc<dyn LogIo>) {
283 self.offset_index.set_io(Arc::clone(&io));
284 self.time_index.set_io(Arc::clone(&io));
285 if let Some(mode) = self.direct.as_ref() {
286 mode.writer.set_io(Arc::clone(&io));
287 }
288 self.io = io;
289 self.cache_probe_unsupported.store(false, Ordering::Relaxed);
290 }
291
292 #[instrument(
296 level = "info",
297 skip(self),
298 fields(base_offset = self.base_offset.0, new_last_offset = field::Empty),
299 err,
300 )]
301 pub fn truncate_to_relative(&mut self, rel: u32) -> Result<(), LogError> {
304 let read_limit = self
309 .offset_index
310 .position_at_or_after(rel)
311 .map_or(self.log_size, u64::from);
312 let mut buf = Vec::new();
313 let to_read = usize::try_from(read_limit).unwrap_or(usize::MAX);
314 self.read_log_range(0, &mut buf, to_read)?;
315
316 let target_abs = self
317 .base_offset
318 .0
319 .checked_add(i64::from(rel))
320 .map(Offset)
321 .ok_or_else(|| LogError::BadSegmentName("offset overflow".into()))?;
322 let mut cur: &[u8] = &buf;
323 let mut pos: u64 = 0;
324 let mut last_kept_offset = self.base_offset - 1;
325 let mut last_kept_ts = i64::MIN;
326 let mut last_kept_ts_offset = last_kept_offset;
327 while !cur.is_empty() {
328 let before = cur.len();
329 let Ok(batch) = RecordBatch::decode(&mut cur) else {
330 break;
331 };
332 let batch_last_offset = batch
333 .base_offset
334 .checked_add(i64::from(batch.last_offset_delta))
335 .map(Offset)
336 .ok_or_else(|| LogError::Corrupt("batch last offset overflow".into()))?;
337 if !krabka_verified::truncation_batch_retained(batch_last_offset.0, target_abs.0) {
338 break;
339 }
340 pos += (before - cur.len()) as u64;
341 last_kept_offset = batch_last_offset;
342 if batch.max_timestamp > last_kept_ts {
343 last_kept_ts = batch.max_timestamp;
344 last_kept_ts_offset = batch_last_offset;
345 }
346 }
347
348 self.settle_direct();
349 self.log_file.set_len(pos)?;
350 self.first_timestamp = None;
351 seek_to_log_size(&self.log_file, pos)?;
352 self.log_size = pos;
353 self.renew_reservation();
354 self.resync_direct()?;
355 self.last_offset = last_kept_offset;
356 self.max_timestamp = last_kept_ts;
357 self.max_timestamp_offset = last_kept_ts_offset;
358
359 let pos_u32 =
360 u32::try_from(pos).map_err(|_| LogError::BadSegmentName("position overflow".into()))?;
361 self.offset_index.truncate_by_position(pos_u32)?;
362 self.time_index.truncate_by_relative_offset(rel)?;
363 self.sealed = false;
364 tracing::Span::current().record("new_last_offset", self.last_offset.0);
365 Ok(())
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use {
372 super::*,
373 crate::segment::test_support::{
374 DENSE_INDEX, NO_LIMIT, SECOND_THREE_RECORD_BATCH, THREE_RECORD_BATCH, TempSegment,
375 sample_batch, seeded_fixture, segment_at, test_segment, two_record_fixture,
376 },
377 assert2::check,
378 krabka_units::prelude::{ByteSize, ByteSizeExt, kibibytes},
379 tempfile::tempdir,
380 };
381
382 #[test]
392 fn truncating_a_segment_leaves_it_describing_what_it_kept() {
393 let dir = tempdir().unwrap();
394 let mut seg = Segment::create(dir.path(), Offset(100)).unwrap();
395 for i in 0..3i64 {
398 seg.append(
399 &sample_batch()
400 .offset(100 + i * 2)
401 .records(2)
402 .timestamp(500 + i * 100)
403 .call(),
404 DENSE_INDEX,
405 )
406 .unwrap();
407 }
408 let full_size = seg.size();
409 check!(seg.last_offset() == Offset(105));
410 check!(
411 seg.max_timestamp() == 701,
412 "batch 3 carries the newest record"
413 );
414
415 seg.truncate_to_relative(2).unwrap();
418 check!(
419 seg.last_offset() == Offset(101),
420 "last kept batch ends at 101"
421 );
422 check!(seg.max_timestamp() == 501, "the newest surviving record");
423 let kept = seg.size();
424 check!(
425 kept > ByteSize::ZERO && kept < full_size,
426 "shorter, not empty"
427 );
428
429 seg.truncate_to_relative(0).unwrap();
432 check!(seg.size() == ByteSize::ZERO, "no bytes survive");
433 check!(
434 seg.last_offset() == Offset(99),
435 "one before the base, got {:?}",
436 seg.last_offset()
437 );
438 check!(seg.max_timestamp() == i64::MIN, "no records, no timestamp");
439 }
440
441 #[test]
443 fn is_sealed_follows_seal() {
444 let TempSegment {
445 dir: _dir,
446 segment: mut seg,
447 } = two_record_fixture();
448 check!(!seg.is_sealed(), "a fresh segment is open");
449 seg.seal().unwrap();
450 check!(seg.is_sealed(), "a sealed segment reports it");
451 }
452
453 #[test]
460 fn seal_appends_the_final_time_index_entry() {
461 let sparse = kibibytes(4);
462 let cases = [
463 (
464 "no index point yet, and the newest timestamp came first",
465 vec![(0, 100, sparse), (1, 300, sparse), (2, 200, sparse)],
466 (1, Some((300, 1))),
467 ),
468 (
469 "the index already holds the newest timestamp",
470 vec![(0, 100, DENSE_INDEX), (1, 200, DENSE_INDEX)],
471 (1, Some((200, 1))),
472 ),
473 (
474 "an index point lags the newest batch",
475 vec![
476 (0, 100, DENSE_INDEX),
477 (1, 200, DENSE_INDEX),
478 (2, 300, sparse),
479 ],
480 (2, Some((300, 2))),
481 ),
482 ];
483 for (label, batches, expected) in cases {
484 let TempSegment {
485 dir: _dir,
486 segment: mut seg,
487 } = test_segment();
488 for (base, timestamp, interval) in batches {
489 seg.append(
490 &sample_batch().offset(base).timestamp(timestamp).call(),
491 interval,
492 )
493 .unwrap();
494 }
495 seg.seal().unwrap();
496 check!(
497 (
498 seg.time_index.entry_count(),
499 seg.time_index
500 .last_entry()
501 .map(|entry| (entry.timestamp, entry.relative_offset))
502 ) == expected,
503 "{label}"
504 );
505 }
506 }
507
508 #[test]
517 fn truncate_to_relative_uses_batch_last_offset() {
518 let TempSegment {
519 dir: _dir,
520 segment: mut seg,
521 } = seeded_fixture()
522 .batches(&[
523 THREE_RECORD_BATCH, SECOND_THREE_RECORD_BATCH, ])
526 .call();
527 assert2::assert!(seg.last_offset() == 5);
528
529 seg.truncate_to_relative(3).unwrap();
531 let read = seg.read(Offset(0), NO_LIMIT).unwrap();
532 assert2::assert!(seg.last_offset() == Offset(2));
533 assert2::assert!(
534 read == vec![
535 sample_batch()
536 .offset(THREE_RECORD_BATCH.offset.0)
537 .records(THREE_RECORD_BATCH.records.0)
538 .timestamp(THREE_RECORD_BATCH.timestamp.0)
539 .call()
540 ]
541 );
542 let expected_size: usize = read.iter().map(RecordBatch::encoded_len).sum();
543 assert2::assert!(seg.size().bytes_usize() == expected_size);
544 }
545
546 #[test]
547 fn truncate_to_relative_rejects_an_absolute_offset_overflow() {
548 let TempSegment {
549 dir: _dir,
550 segment: mut seg,
551 } = segment_at(Offset(i64::MAX));
552
553 let error = seg
554 .truncate_to_relative(1)
555 .expect_err("base plus relative cut must be checked");
556 assert2::assert!(error.to_string().contains("offset overflow"));
557 }
558
559 #[test]
560 fn flush_succeeds() {
561 let TempSegment {
562 dir: _dir,
563 segment: mut seg,
564 } = test_segment();
565 seg.append(&sample_batch().timestamp(42).call(), kibibytes(4))
566 .unwrap();
567 seg.flush().unwrap();
568 }
569}