1use {
11 super::prepare::{BatchTally, PrepareBatchSetup, PreparedBatch, prepare_batch},
12 crate::{
13 args::{PartitionRef, RestoreArgs},
14 bound::Predicates,
15 error::RestoreError,
16 verify::VerifiedSegment,
17 },
18 krabka_ids::Offset,
19 krabka_log::Log,
20 krabka_protocol::records::RecordBatchBorrowed,
21 krabka_remote_storage::TopicIdPartition,
22 serde::Serialize,
23 uuid::Uuid,
24};
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
28pub struct SegmentOutcome {
29 pub segment_id: Uuid,
31 pub base_offset: Offset,
33 pub end_offset: Offset,
35 pub batches_kept: u64,
37 pub batches_rewritten: u64,
39 pub batches_emptied: u64,
43 pub records_kept: u64,
45 pub records_dropped: u64,
47 pub bytes_written: u64,
49}
50
51pub async fn write_segment(
59 args: &RestoreArgs,
60 partition: &TopicIdPartition,
61 segment: &VerifiedSegment,
62 predicates: &Predicates,
63) -> Result<SegmentOutcome, RestoreError> {
64 write_segment_after_skip(args, partition, segment, predicates, false).await
65}
66
67pub async fn write_segment_after_skip(
69 args: &RestoreArgs,
70 partition: &TopicIdPartition,
71 segment: &VerifiedSegment,
72 predicates: &Predicates,
73 skipped_predecessor: bool,
74) -> Result<SegmentOutcome, RestoreError> {
75 let partition_ref = PartitionRef {
76 topic: partition.topic.clone(),
77 partition: partition.partition,
78 };
79 let mut outcome = SegmentOutcome {
80 segment_id: segment.facts.segment_id,
81 base_offset: segment.facts.base_offset,
82 end_offset: segment.facts.end_offset,
83 batches_kept: 0,
84 batches_rewritten: 0,
85 batches_emptied: 0,
86 records_kept: 0,
87 records_dropped: 0,
88 bytes_written: 0,
89 };
90
91 if predicates.batch_past_offset_bound(&partition_ref, segment.facts.base_offset) {
95 return Ok(outcome);
96 }
97
98 let mut log = super::open_partition_log(args, &partition.topic, partition.partition)?;
99 if let Some(log) = log.as_mut() {
100 align_log_to_segment(log, segment, partition, skipped_predecessor)?;
101 }
102
103 let raw: &[u8] = &segment.log;
104 let mut pos = 0usize;
105 while let Some(mut cursor) = raw.get(pos..).filter(|rest| !rest.is_empty()) {
106 let remaining_before = cursor.len();
107 let batch = RecordBatchBorrowed::decode_borrow_with_policy(&mut cursor, <_>::default())?;
114 let consumed = remaining_before - cursor.len();
115 let header = batch.header();
116 let batch_base_offset = Offset(header.base_offset.get());
117
118 if predicates.batch_past_offset_bound(&partition_ref, batch_base_offset) {
119 break;
120 }
121
122 let batch_bytes = segment.log.slice(pos..pos + consumed);
123 let records_in_batch = nonneg_u64(header.records_count.get());
124
125 let (mut prepared, tally) = prepare_batch(
126 PrepareBatchSetup::builder()
127 .partition_ref(&partition_ref)
128 .predicates(predicates)
129 .batch(&batch)
130 .batch_bytes(batch_bytes)
131 .records_in_batch(records_in_batch)
132 .build(),
133 )?;
134
135 let last_offset_delta = prepared.last_offset_delta();
136 outcome.bytes_written += bytes_len(prepared.encoded_len());
137 outcome.end_offset = batch_base_offset + i64::from(last_offset_delta);
138
139 if let Some(log) = log.as_mut() {
140 match &mut prepared {
141 PreparedBatch::Verbatim(verbatim) => {
142 log.append_verbatim_at(verbatim, batch_base_offset)?;
143 }
144 PreparedBatch::Owned { batch, .. } => {
145 log.append_at(batch, batch_base_offset)?;
146 }
147 }
148 }
149
150 match tally {
151 BatchTally::Kept => {
152 outcome.batches_kept += 1;
153 outcome.records_kept += records_in_batch;
154 }
155 BatchTally::Rewritten { kept, dropped } => {
156 outcome.batches_rewritten += 1;
157 outcome.records_kept += kept;
158 outcome.records_dropped += dropped;
159 }
160 BatchTally::Emptied => {
161 outcome.batches_emptied += 1;
162 outcome.records_dropped += records_in_batch;
163 }
164 }
165
166 pos += consumed;
167 }
168
169 if let Some(log) = log.as_mut() {
170 log.sync()?;
171 }
172
173 Ok(outcome)
174}
175
176fn align_log_to_segment(
184 log: &mut Log,
185 segment: &VerifiedSegment,
186 partition: &TopicIdPartition,
187 skipped_predecessor: bool,
188) -> Result<(), RestoreError> {
189 let leo = log.log_end_offset();
190 if leo == segment.facts.base_offset {
191 return Ok(());
192 }
193 if leo == Offset(0) {
194 log.reset_to(segment.facts.base_offset)?;
195 return Ok(());
196 }
197 if skipped_predecessor && leo < segment.facts.base_offset {
198 return Ok(());
199 }
200 Err(RestoreError::InvalidArgument(format!(
201 "segment {} of {}-{} starts at offset {}, but the target log already ends at {leo}; \
202 segments must be written in contiguous base-offset order",
203 segment.facts.segment_id, partition.topic, partition.partition, segment.facts.base_offset,
204 )))
205}
206
207fn nonneg_u64(count: i32) -> u64 { u64::try_from(count.max(0)).unwrap_or(0) }
209
210fn bytes_len(len: usize) -> u64 { u64::try_from(len).unwrap_or(u64::MAX) }
212
213#[cfg(test)]
214mod tests {
215 mod retry;
216
217 use {
218 super::*,
219 crate::materialize::test_support::{
220 args_from, batch, materialization_context, record, record_with_key, reopen_orders,
221 topic_id_partition, verified_segment,
222 },
223 assert2::check,
224 krabka_log::{LogConfig, name},
225 krabka_protocol::records::{Attributes, RecordBatch},
226 std::{fs, slice},
227 };
228
229 fn read_restored_pair(log: &Log) -> krabka_log::ReadOutput {
231 let read = read_from_start(log);
232 check!(read.batches.len() == 2);
233 check!(read.batches[0].base_offset == 0);
234 check!(read.batches[0].last_offset_delta == 2);
235 read
236 }
237
238 fn read_from_start(log: &Log) -> krabka_log::ReadOutput {
239 log.read(Offset(0), LogConfig::default().segment_size)
240 .expect("read back")
241 }
242
243 #[tokio::test]
244 async fn keep_only_batches_round_trip_verbatim_at_their_original_offsets() {
245 let (_target, args, partition, predicates) = materialization_context(&[]);
246
247 let batches = vec![
248 batch(0, vec![record(0, "a"), record(1, "b"), record(2, "c")]),
249 batch(3, vec![record(0, "d"), record(1, "e")]),
250 ];
251 let segment = verified_segment(0, &batches);
252
253 let outcome = write_segment(&args, &partition, &segment, &predicates)
254 .await
255 .expect("write_segment");
256
257 check!(outcome.batches_kept == 2);
258 check!(outcome.batches_rewritten == 0);
259 check!(outcome.batches_emptied == 0);
260 check!(outcome.records_kept == 5);
261 check!(outcome.records_dropped == 0);
262 check!(outcome.end_offset == Offset(4));
263
264 let log = reopen_orders(&args);
265 let read = read_from_start(&log);
266 check!(read.batches == batches);
267 }
268
269 #[tokio::test]
270 async fn an_emptied_batch_becomes_a_bare_header_and_the_next_batch_still_appends() {
271 let (_target, args, partition, predicates) =
272 materialization_context(&["--exclude-key", "^drop$"]);
273
274 let excluded = vec![
275 record_with_key(0, "drop"),
276 record_with_key(1, "drop"),
277 record_with_key(2, "drop"),
278 ];
279 let batch_a = batch(0, excluded);
280 let batch_b = batch(3, vec![record(0, "kept")]);
281 let segment = verified_segment(0, &[batch_a, batch_b]);
282
283 let outcome = write_segment(&args, &partition, &segment, &predicates)
284 .await
285 .expect("write_segment");
286
287 check!(outcome.batches_emptied == 1);
288 check!(outcome.batches_kept == 1);
289 check!(outcome.records_dropped == 3);
290 check!(outcome.records_kept == 1);
291 check!(outcome.end_offset == Offset(3));
292
293 let log = reopen_orders(&args);
294 check!(log.log_end_offset() == Offset(4));
297
298 let read = read_restored_pair(&log);
299 check!(read.batches[0].records.is_empty());
300 check!(read.batches[1].base_offset == 3);
301 }
302
303 #[tokio::test]
304 async fn a_filtered_batch_keeps_the_survivors_original_absolute_offsets() {
305 let (_target, args, partition, predicates) =
306 materialization_context(&["--exclude-key", "^drop$"]);
307
308 let batch_a = batch(0, vec![
309 record(0, "keep0"),
310 record_with_key(1, "drop"),
311 record(2, "keep2"),
312 ]);
313 let segment = verified_segment(0, slice::from_ref(&batch_a));
314
315 let outcome = write_segment(&args, &partition, &segment, &predicates)
316 .await
317 .expect("write_segment");
318
319 check!(outcome.batches_rewritten == 1);
320 check!(outcome.batches_kept == 0);
321 check!(outcome.batches_emptied == 0);
322 check!(outcome.records_kept == 2);
323 check!(outcome.records_dropped == 1);
324 check!(outcome.end_offset == Offset(2));
325
326 let log = reopen_orders(&args);
327 let read = read_from_start(&log);
328 let expected = RecordBatch {
329 last_offset_delta: 2,
330 records: vec![record(0, "keep0"), record(2, "keep2")],
331 ..batch_a
332 };
333 check!(read.batches == vec![expected]);
334 }
335
336 #[tokio::test]
337 async fn a_filtered_batch_that_drops_its_trailing_record_does_not_strand_the_next_batch() {
338 let (_target, args, partition, predicates) =
349 materialization_context(&["--exclude-key", "^drop$"]);
350
351 let batch_a = batch(0, vec![record(0, "keep0"), record_with_key(2, "drop")]);
352 let batch_b = batch(3, vec![record(0, "keep3")]);
353 let segment = verified_segment(0, &[batch_a.clone(), batch_b.clone()]);
354
355 let outcome = write_segment(&args, &partition, &segment, &predicates)
356 .await
357 .expect("write_segment must not fail with an offset mismatch");
358
359 check!(outcome.batches_rewritten == 1);
360 check!(outcome.batches_kept == 1);
361 check!(outcome.records_kept == 2);
362 check!(outcome.records_dropped == 1);
363 check!(outcome.end_offset == Offset(3));
364
365 let log = reopen_orders(&args);
366 let read = read_restored_pair(&log);
367 check!(read.batches[0].records == vec![record(0, "keep0")]);
368 check!(read.batches[1].base_offset == 3);
369 check!(read.batches[1].records == vec![record(0, "keep3")]);
370 }
371
372 #[tokio::test]
373 async fn a_skipped_predecessor_preserves_filtered_prefix_and_compacted_suffix_offsets() {
374 let (_target, args, partition, predicates) =
375 materialization_context(&["--exclude-key", "^drop$"]);
376 let prefix = batch(0, vec![record(0, "head"), record_with_key(2, "drop")]);
377 write_segment(
378 &args,
379 &partition,
380 &verified_segment(0, slice::from_ref(&prefix)),
381 &predicates,
382 )
383 .await
384 .unwrap();
385 let suffix = batch(20, vec![record(2, "tail")]);
386 let segment = verified_segment(20, slice::from_ref(&suffix));
387 check!(
388 write_segment(&args, &partition, &segment, &predicates)
389 .await
390 .is_err()
391 );
392 write_segment_after_skip(&args, &partition, &segment, &predicates, true)
393 .await
394 .unwrap();
395 let restored = reopen_orders(&args);
396 check!(restored.log_end_offset() == Offset(23));
397 check!(
398 restored
399 .read(Offset(0), LogConfig::default().segment_size)
400 .unwrap()
401 .batches
402 == vec![
403 RecordBatch {
404 records: vec![record(0, "head")],
405 ..prefix
406 },
407 suffix,
408 ]
409 );
410 }
411
412 #[tokio::test]
413 async fn a_second_segment_continues_without_resetting_the_first() {
414 let (_target, args, partition, predicates) = materialization_context(&[]);
415
416 let batch_1 = batch(0, vec![record(0, "a"), record(1, "b")]);
417 let segment_1 = verified_segment(0, slice::from_ref(&batch_1));
418 let outcome_1 = write_segment(&args, &partition, &segment_1, &predicates)
419 .await
420 .expect("segment 1");
421 check!(outcome_1.end_offset == Offset(1));
422
423 let batch_2 = batch(2, vec![record(0, "c")]);
424 let segment_2 = verified_segment(2, slice::from_ref(&batch_2));
425 let outcome_2 = write_segment(&args, &partition, &segment_2, &predicates)
426 .await
427 .expect("segment 2");
428 check!(outcome_2.end_offset == Offset(2));
429
430 let log = reopen_orders(&args);
431 let read = log
432 .read(Offset(0), LogConfig::default().segment_size)
433 .expect("read back");
434 check!(read.batches == vec![batch_1, batch_2]);
437 }
438
439 #[tokio::test]
440 async fn the_first_segment_resets_an_empty_log_to_a_nonzero_base() {
441 let (_target, args, partition, predicates) = materialization_context(&[]);
442
443 let batches = vec![batch(1000, vec![record(0, "x"), record(1, "y")])];
444 let segment = verified_segment(1000, &batches);
445
446 let outcome = write_segment(&args, &partition, &segment, &predicates)
447 .await
448 .expect("write_segment");
449 check!(outcome.base_offset == Offset(1000));
450 check!(outcome.end_offset == Offset(1001));
451
452 let log = reopen_orders(&args);
453 check!(log.log_start_offset() == Offset(1000));
454 check!(log.log_end_offset() == Offset(1002));
455 let read = log
456 .read(Offset(1000), LogConfig::default().segment_size)
457 .expect("read back");
458 check!(read.batches == batches);
459 }
460
461 #[tokio::test]
462 async fn dry_run_matches_a_real_run_but_writes_nothing() {
463 let mut compressed = batch(0, vec![record(0, "a"), record_with_key(1, "drop")]);
464 compressed.attributes = Attributes(1);
468 let batches = vec![compressed, batch(2, vec![record(0, "c")])];
469 let partition = topic_id_partition("orders", 0);
470 let segment = verified_segment(0, &batches);
475
476 let real_target = tempfile::tempdir().expect("tempdir");
477 let real_args = args_from(&["--exclude-key", "^drop$"], real_target.path());
478 let real_predicates = Predicates::from_args(&real_args).expect("predicates");
479 let real_outcome = write_segment(&real_args, &partition, &segment, &real_predicates)
480 .await
481 .expect("real write");
482 let real_dir = name::partition_dir(&real_args.target.log_dir, "orders", 0);
483 let log_bytes: u64 = fs::read_dir(real_dir)
484 .expect("target partition")
485 .map(|entry| entry.expect("directory entry").path())
486 .filter(|path| path.extension().is_some_and(|extension| extension == "log"))
487 .map(|path| fs::metadata(path).expect("log metadata").len())
488 .sum();
489 check!(real_outcome.bytes_written == log_bytes);
490
491 let dry_target = tempfile::tempdir().expect("tempdir");
492 let mut dry_args = args_from(&["--exclude-key", "^drop$"], dry_target.path());
493 dry_args.dry_run = true;
494 let dry_predicates = Predicates::from_args(&dry_args).expect("predicates");
495 let dry_outcome = write_segment(&dry_args, &partition, &segment, &dry_predicates)
496 .await
497 .expect("dry write");
498
499 check!(dry_outcome == real_outcome);
500 check!(!dry_target.path().join("orders-0").exists());
501 check!(real_target.path().join("orders-0").exists());
502 }
503}