1use {
8 super::{
9 Segment,
10 io::{write_all, write_all_vectored},
11 lifecycle::WriteSnapshot,
12 },
13 crate::{VerbatimBatch, error::LogError, index::OffsetIndexEntry},
14 bytes::Bytes,
15 krabka_ids::{LeaderEpoch, Offset},
16 krabka_protocol::records::{HEADER_LEN, RecordBatch, patch_base_offset_and_leader_epoch},
17 krabka_units::prelude::{ByteSize, ByteSizeExt},
18 std::{io, io::IoSlice},
19 tracing::{field, instrument},
20};
21
22#[derive(Clone, Copy)]
24enum BatchBytes<'a> {
25 Whole(&'a Bytes),
27 Split(&'a [u8], &'a [u8]),
29}
30
31struct DirectParts<'a> {
34 parts: &'a [&'a [u8]],
35 cached: Bytes,
36}
37
38impl BatchBytes<'_> {
39 fn len(self) -> usize {
40 match self {
41 Self::Whole(bytes) => bytes.len(),
42 Self::Split(header, body) => header.len() + body.len(),
43 }
44 }
45}
46
47#[derive(Clone, Copy)]
49pub struct PlacedBatch<'a> {
50 pub(crate) batch: &'a VerbatimBatch,
51 pub(crate) base: Offset,
52}
53
54struct AppendCoordinates {
57 last_offset: i64,
58 size: u64,
59}
60
61impl Segment {
62 fn append_coordinates(
63 &self,
64 base: i64,
65 delta: i32,
66 len: usize,
67 ) -> Result<AppendCoordinates, LogError> {
68 let (last, _) = krabka_verified::local_append_coordinates(base, base, delta)
69 .ok_or_else(|| LogError::InvalidArgument("invalid batch offset interval".into()))?;
70 let len = u64::try_from(len)
71 .map_err(|_| LogError::InvalidArgument("encoded batch length overflow".into()))?;
72 let size = self
73 .log_size
74 .checked_add(len)
75 .ok_or_else(|| LogError::InvalidArgument("segment byte length overflow".into()))?;
76 Ok(AppendCoordinates {
77 last_offset: last,
78 size,
79 })
80 }
81
82 #[instrument(
91 level = "debug",
92 skip(self, batch),
93 fields(
94 base_offset = self.base_offset.0,
95 batch_base = batch.base_offset,
96 bytes = batch.encoded_len(),
97 position = field::Empty,
98 ),
99 err,
100 )]
101 pub fn append(
104 &mut self,
105 batch: &RecordBatch,
106 index_interval: ByteSize,
107 ) -> Result<u64, LogError> {
108 self.ensure_writable()?;
109
110 let mut buf = bytes::BytesMut::with_capacity(batch.encoded_len());
111 batch.encode(&mut buf)?;
112 let bytes = buf.freeze();
113
114 self.append_encoded(batch, index_interval, &bytes)
115 }
116
117 pub(crate) fn append_encoded(
119 &mut self,
120 batch: &RecordBatch,
121 index_interval: ByteSize,
122 bytes: &Bytes,
123 ) -> Result<u64, LogError> {
124 self.ensure_writable()?;
125 self.write_batch(
127 batch.base_offset,
128 batch.last_offset_delta,
129 batch.max_timestamp,
130 index_interval,
131 BatchBytes::Whole(bytes),
132 )
133 }
134
135 #[instrument(
154 level = "debug",
155 skip(self, bytes),
156 fields(
157 seg_base_offset = self.base_offset.0,
158 base_offset = base_offset.0,
159 bytes_len = bytes.len(),
160 position = field::Empty,
161 ),
162 err,
163 )]
164 pub fn append_verbatim(
167 &mut self,
168 bytes: &[u8],
169 base_offset: Offset,
170 last_offset_delta: i32,
171 max_timestamp: i64,
172 leader_epoch: LeaderEpoch,
173 index_interval: ByteSize,
174 ) -> Result<u64, LogError> {
175 self.ensure_writable()?;
176 let Some((&header, body)) = bytes.split_first_chunk::<HEADER_LEN>() else {
177 return Err(LogError::Corrupt(
178 "verbatim batch shorter than v2 header".into(),
179 ));
180 };
181
182 let mut header = header;
190 patch_base_offset_and_leader_epoch(&mut header, base_offset.0, leader_epoch.0);
192
193 self.write_batch(
194 base_offset.0,
195 last_offset_delta,
196 max_timestamp,
197 index_interval,
198 BatchBytes::Split(&header, body),
199 )
200 }
201
202 fn write_bytes(&mut self, batch: BatchBytes<'_>, chunk_ends: &[usize]) -> io::Result<()> {
211 let Some(mode) = self.direct.as_mut() else {
212 return match batch {
213 BatchBytes::Whole(bytes) => write_all(&*self.io, &self.log_file, bytes),
214 BatchBytes::Split(header, body) => {
215 write_all_vectored(&*self.io, &self.log_file, &mut [
216 IoSlice::new(header),
217 IoSlice::new(body),
218 ])
219 }
220 };
221 };
222 let DirectParts { parts, cached } = match batch {
223 BatchBytes::Whole(bytes) => DirectParts {
224 parts: &[bytes],
225 cached: bytes.clone(),
226 },
227 BatchBytes::Split(header, body) => DirectParts {
228 parts: &[header, body],
229 cached: Bytes::from([header, body].concat()),
230 },
231 };
232 mode.writer.append(parts)?;
233 mode.cache.set_written(mode.writer.written());
234 if chunk_ends.is_empty() {
235 mode.cache.push(cached);
236 } else {
237 mode.cache.push_group(&cached, chunk_ends);
238 }
239 Ok(())
240 }
241
242 fn ensure_writable(&self) -> Result<(), LogError> {
243 if self.sealed {
244 return Err(LogError::Io(io::Error::other("segment is sealed")));
245 }
246 Ok(())
247 }
248
249 fn write_batch(
250 &mut self,
251 base: i64,
252 delta: i32,
253 max_timestamp: i64,
254 index_interval: ByteSize,
255 batch: BatchBytes<'_>,
256 ) -> Result<u64, LogError> {
257 let AppendCoordinates {
258 last_offset,
259 size: new_log_size,
260 } = self.append_coordinates(base, delta, batch.len())?;
261 let position = self.log_size;
262 let previous = self.write_snapshot();
263 let index_entry = self.index_entry_for(position, last_offset, index_interval)?;
264 if let Err(error) = self.write_bytes(batch, &[]) {
265 self.rollback_failed_write(position, previous)?;
266 return Err(error.into());
267 }
268 self.log_size = new_log_size;
269 self.finish_append(position, previous, index_entry, last_offset, max_timestamp)
270 }
271
272 pub(crate) fn append_verbatim_group(
275 &mut self,
276 batches: &[PlacedBatch<'_>],
277 index_interval: ByteSize,
278 ) -> Result<(), LogError> {
279 self.ensure_writable()?;
280 let size = batches.iter().map(|placed| placed.batch.bytes.len()).sum();
281 let mut bytes = bytes::BytesMut::with_capacity(size);
282 let mut chunk_ends = Vec::with_capacity(batches.len());
283 for &PlacedBatch { batch, base } in batches {
284 let Some((&header, body)) = batch.bytes.split_first_chunk::<HEADER_LEN>() else {
285 return Err(LogError::Corrupt(
286 "verbatim batch shorter than v2 header".into(),
287 ));
288 };
289 let mut header = header;
290 patch_base_offset_and_leader_epoch(&mut header, base.0, batch.leader_epoch.0);
291 bytes.extend_from_slice(&header);
292 bytes.extend_from_slice(body);
293 chunk_ends.push(bytes.len());
294 }
295 let bytes = bytes.freeze();
296 let start = self.log_size;
297 let snapshot = self.write_snapshot();
298 let result = (|| {
299 self.write_bytes(BatchBytes::Whole(&bytes), &chunk_ends)?;
300 for &PlacedBatch { batch, base } in batches {
301 let position = self.log_size;
302 let AppendCoordinates {
303 last_offset: last,
304 size,
305 } = self.append_coordinates(base.0, batch.last_offset_delta, batch.bytes.len())?;
306 let entry = self.index_entry_for(position, last, index_interval)?;
307 self.log_size = size;
308 self.record_batch(last, batch.max_timestamp);
309 if let Some(entry) = entry {
310 self.append_index_entries(entry)?;
311 }
312 }
313 Ok(())
314 })();
315 if result.is_err() {
316 self.rollback_failed_write(start, snapshot)?;
317 }
318 result
319 }
320
321 fn finish_append(
323 &mut self,
324 position: u64,
325 previous: WriteSnapshot,
326 index_entry: Option<OffsetIndexEntry>,
327 last_offset: i64,
328 max_timestamp: i64,
329 ) -> Result<u64, LogError> {
330 self.record_batch(last_offset, max_timestamp);
331 if let Some(entry) = index_entry
332 && let Err(error) = self.append_index_entries(entry)
333 {
334 self.rollback_failed_write(position, previous)?;
335 return Err(error);
336 }
337 tracing::Span::current().record("position", position);
338 Ok(position)
339 }
340
341 fn index_entry_for(
350 &self,
351 position: u64,
352 last_offset: i64,
353 index_interval: ByteSize,
354 ) -> Result<Option<OffsetIndexEntry>, LogError> {
355 let last_indexed = self
361 .offset_index
362 .last_entry()
363 .map_or(0, |entry| u64::from(entry.position));
364 let should_index = position.saturating_sub(last_indexed) > index_interval.bytes_u64();
365 if !should_index {
366 return Ok(None);
367 }
368 let rel = u32::try_from(last_offset - self.base_offset.0)
369 .map_err(|_| LogError::BadSegmentName("offset overflow in segment".into()))?;
370 let pos = u32::try_from(position)
371 .map_err(|_| LogError::BadSegmentName("position overflow in segment".into()))?;
372 Ok(Some(OffsetIndexEntry {
373 relative_offset: rel,
374 position: pos,
375 }))
376 }
377
378 fn record_batch(&mut self, last_offset: i64, max_timestamp: i64) {
382 self.last_offset = Offset(last_offset);
383 if max_timestamp > self.max_timestamp {
384 self.max_timestamp = max_timestamp;
385 self.max_timestamp_offset = Offset(last_offset);
386 }
387 }
388
389 fn append_index_entries(&mut self, entry: OffsetIndexEntry) -> Result<(), LogError> {
394 self.offset_index
395 .append(entry.relative_offset, entry.position)?;
396 self.append_running_max_time_entry()
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use {
403 super::*,
404 crate::{
405 name::log_path,
406 segment::{
407 io::SourceIo as _,
408 test_support::{
409 DENSE_INDEX, FIRST_LARGE_TIMESTAMP_BATCH, NO_LIMIT, ONE_RECORD_BATCH,
410 SECOND_LARGE_TIMESTAMP_BATCH, SECOND_SINGLE_RECORD_BATCH, TempSegment,
411 sample_batch, seeded_fixture, segment_at, test_batch_at, test_segment,
412 },
413 },
414 },
415 krabka_protocol::records::RecordBatch,
416 krabka_units::prelude::kibibytes,
417 std::fs,
418 };
419
420 #[test]
421 fn append_then_read_back() {
422 let TempSegment {
423 dir: _dir,
424 segment: mut seg,
425 } = test_segment();
426 let b1 = sample_batch()
427 .offset(FIRST_LARGE_TIMESTAMP_BATCH.offset.0)
428 .records(FIRST_LARGE_TIMESTAMP_BATCH.records.0)
429 .timestamp(FIRST_LARGE_TIMESTAMP_BATCH.timestamp.0)
430 .call();
431 let b2 = sample_batch()
432 .offset(SECOND_LARGE_TIMESTAMP_BATCH.offset.0)
433 .records(SECOND_LARGE_TIMESTAMP_BATCH.records.0)
434 .timestamp(SECOND_LARGE_TIMESTAMP_BATCH.timestamp.0)
435 .call();
436 seg.append(&b1, kibibytes(4)).unwrap();
437 seg.append(&b2, kibibytes(4)).unwrap();
438 let read = seg.read(Offset(0), NO_LIMIT).unwrap();
439 assert2::assert!(seg.last_offset() == Offset(4));
440 assert2::assert!(read == vec![b1, b2]);
441 }
442
443 #[test]
444 fn append_rejects_offset_successor_overflow_before_writing() {
445 let TempSegment {
446 dir,
447 segment: mut seg,
448 } = segment_at(Offset(i64::MAX));
449 let batch = sample_batch().offset(i64::MAX).timestamp(100).call();
450
451 let error = seg.append(&batch, DENSE_INDEX).unwrap_err();
452
453 assert2::assert!(matches!(error, LogError::InvalidArgument(_)));
454 assert2::assert!(seg.size().bytes_u64() == 0);
455 assert2::assert!(fs::metadata(log_path(dir.path(), i64::MAX)).unwrap().len() == 0);
456 }
457
458 #[test]
459 fn append_after_truncate_writes_at_new_eof() {
460 let TempSegment {
461 dir: _dir,
462 segment: mut seg,
463 } = seeded_fixture().batches(&[ONE_RECORD_BATCH]).call();
464 let expected_position = seg.size().bytes_u64();
465 seg.append(
466 &sample_batch()
467 .offset(SECOND_SINGLE_RECORD_BATCH.offset.0)
468 .records(SECOND_SINGLE_RECORD_BATCH.records.0)
469 .timestamp(SECOND_SINGLE_RECORD_BATCH.timestamp.0)
470 .call(),
471 DENSE_INDEX,
472 )
473 .unwrap();
474
475 seg.truncate_to_relative(1).unwrap();
476 let position = seg
477 .append(&sample_batch().offset(1).timestamp(300).call(), DENSE_INDEX)
478 .unwrap();
479
480 let read = seg.read(Offset(0), NO_LIMIT).unwrap();
481 assert2::assert!(position == expected_position);
482 assert2::assert!(seg.last_offset() == Offset(1));
483 assert2::assert!(
484 read == vec![
485 sample_batch()
486 .offset(ONE_RECORD_BATCH.offset.0)
487 .records(ONE_RECORD_BATCH.records.0)
488 .timestamp(ONE_RECORD_BATCH.timestamp.0)
489 .call(),
490 sample_batch().offset(1).timestamp(300).call()
491 ]
492 );
493 }
494
495 #[test]
496 fn append_to_sealed_segment_errors() {
497 let TempSegment {
498 dir: _dir,
499 segment: mut seg,
500 } = test_segment();
501 seg.seal().unwrap();
502 assert2::assert!(seg.is_sealed());
503 let err = seg
504 .append(&sample_batch().call(), kibibytes(4))
505 .unwrap_err();
506 assert2::assert!(matches!(err, LogError::Io(_)));
507 }
508
509 #[test]
510 fn append_verbatim_is_byte_exact_except_offset_and_epoch() {
511 let TempSegment {
512 dir,
513 segment: mut seg,
514 } = test_segment();
515 let mut producer = test_batch_at(Offset(0));
518 producer.base_offset = 999; producer.partition_leader_epoch = -1; producer.last_offset_delta = 0;
521 producer.max_timestamp = 1_000;
522 let mut wire = bytes::BytesMut::new();
523 producer.encode(&mut wire).unwrap();
524 let wire = wire.freeze();
525
526 let assigned_base = Offset(0);
528 let stamped_epoch = 7i32;
529 seg.append_verbatim(
530 &wire,
531 assigned_base,
532 0,
533 1_000,
534 LeaderEpoch(stamped_epoch),
535 DENSE_INDEX,
536 )
537 .unwrap();
538 assert2::assert!(seg.last_offset() == 0);
539
540 let mut on_disk = Vec::new();
542 seg.read_log_range(0, &mut on_disk, usize::MAX).unwrap();
543 let mut expected_wire = wire.to_vec();
544 expected_wire[0..8].copy_from_slice(&assigned_base.0.to_be_bytes());
545 expected_wire[12..16].copy_from_slice(&stamped_epoch.to_be_bytes());
546 assert2::assert!(on_disk == expected_wire);
547
548 let mut cur: &[u8] = &on_disk;
550 let decoded = RecordBatch::decode(&mut cur).unwrap();
551 assert2::assert!(decoded.base_offset == assigned_base.0);
552 assert2::assert!(decoded.partition_leader_epoch == stamped_epoch);
553 drop(dir);
554 }
555
556 #[test]
557 fn append_verbatim_updates_index_and_last_offset() {
558 let TempSegment {
559 dir,
560 segment: mut seg,
561 } = test_segment();
562 let mut producer = test_batch_at(Offset(0));
563 producer.last_offset_delta = 2; producer.max_timestamp = 5_000;
565 let mut wire = bytes::BytesMut::new();
566 producer.encode(&mut wire).unwrap();
567 let wire = wire.freeze();
568
569 seg.append_verbatim(&wire, Offset(0), 2, 5_000, LeaderEpoch(0), DENSE_INDEX)
570 .unwrap();
571 let read = seg.read(Offset(2), NO_LIMIT).unwrap();
573 assert2::assert!(seg.last_offset() == Offset(2));
574 assert2::assert!(seg.max_timestamp() == 5_000);
575 assert2::assert!(read == vec![producer]);
576 drop(dir);
577 }
578
579 #[test]
580 fn append_verbatim_to_sealed_segment_errors() {
581 let TempSegment {
582 dir,
583 segment: mut seg,
584 } = test_segment();
585 seg.seal().unwrap();
586 let mut wire = bytes::BytesMut::new();
587 test_batch_at(Offset(0)).encode(&mut wire).unwrap();
588 let err = seg
589 .append_verbatim(&wire.freeze(), Offset(0), 0, 0, LeaderEpoch(0), DENSE_INDEX)
590 .unwrap_err();
591 assert2::assert!(matches!(err, LogError::Io(_)));
592 drop(dir);
593 }
594}