1use std::fs::{self, File, OpenOptions};
2use std::io::{self, Read, Write};
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use powdb_storage::catalog::CATALOG_VERSION;
8use powdb_storage::create_data_dir_secure;
9use powdb_storage::wal::{WalRecord, WAL_FORMAT_VERSION};
10
11const SEGMENT_MAGIC: &[u8; 4] = b"PRUL";
12const FOOTER_MAGIC: &[u8; 4] = b"RULF";
13pub const RETAINED_SEGMENT_FORMAT_VERSION: u16 = 1;
14
15const RESERVED: u16 = 0;
16const HEADER_LEN: usize = 4 + 2 + 2 + 2 + 2 + 4 + 8 + 8 + 8 + 16;
17const UNIT_HEADER_LEN: usize = 4 + 4 + 8 + 1 + 8;
18const FOOTER_LEN: usize = 4 + 4;
19const MAX_RETAINED_UNIT_DATA_SIZE: usize = 256 * 1024 * 1024;
20const MAX_RETAINED_SEGMENT_FILE_SIZE: u64 = 1024 * 1024 * 1024;
21static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct SegmentIdentity {
26 pub database_id: [u8; 16],
27 pub primary_generation: u64,
28 pub wal_format_version: u16,
29 pub catalog_version: u16,
30}
31
32impl SegmentIdentity {
33 pub fn current(database_id: [u8; 16], primary_generation: u64) -> Self {
34 Self {
35 database_id,
36 primary_generation,
37 wal_format_version: WAL_FORMAT_VERSION,
38 catalog_version: CATALOG_VERSION,
39 }
40 }
41
42 pub fn validate(self) -> io::Result<()> {
43 validate_identity(self)
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct RetainedUnit {
54 pub tx_id: u64,
55 pub record_type: u8,
56 pub lsn: u64,
57 pub data: Vec<u8>,
58}
59
60impl From<&WalRecord> for RetainedUnit {
61 fn from(record: &WalRecord) -> Self {
62 Self {
63 tx_id: record.tx_id,
64 record_type: record.record_type as u8,
65 lsn: record.lsn,
66 data: record.data.clone(),
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct RetainedSegment {
74 pub identity: SegmentIdentity,
75 pub start_lsn: u64,
76 pub end_lsn: u64,
77 pub units: Vec<RetainedUnit>,
78}
79
80impl RetainedSegment {
81 pub fn new(identity: SegmentIdentity, units: Vec<RetainedUnit>) -> io::Result<Self> {
82 validate_identity(identity)?;
83 validate_units(&units)?;
84 let start_lsn = units[0].lsn;
85 let end_lsn = units[units.len() - 1].lsn;
86 Ok(Self {
87 identity,
88 start_lsn,
89 end_lsn,
90 units,
91 })
92 }
93
94 pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
95 self.validate()?;
96
97 let mut out = Vec::with_capacity(HEADER_LEN + FOOTER_LEN);
98 out.extend_from_slice(SEGMENT_MAGIC);
99 out.extend_from_slice(&RETAINED_SEGMENT_FORMAT_VERSION.to_le_bytes());
100 out.extend_from_slice(&RESERVED.to_le_bytes());
101 out.extend_from_slice(&self.identity.wal_format_version.to_le_bytes());
102 out.extend_from_slice(&self.identity.catalog_version.to_le_bytes());
103 out.extend_from_slice(&checked_unit_count(self.units.len())?.to_le_bytes());
104 out.extend_from_slice(&self.start_lsn.to_le_bytes());
105 out.extend_from_slice(&self.end_lsn.to_le_bytes());
106 out.extend_from_slice(&self.identity.primary_generation.to_le_bytes());
107 out.extend_from_slice(&self.identity.database_id);
108
109 for unit in &self.units {
110 let data_len_u32 = checked_data_len(unit.data.len())?;
111 let total_len = (UNIT_HEADER_LEN as u32)
112 .checked_add(data_len_u32)
113 .ok_or_else(|| invalid_input("retained unit is too large"))?;
114 let crc = unit_crc(unit);
115 out.extend_from_slice(&total_len.to_le_bytes());
116 out.extend_from_slice(&crc.to_le_bytes());
117 out.extend_from_slice(&unit.tx_id.to_le_bytes());
118 out.push(unit.record_type);
119 out.extend_from_slice(&unit.lsn.to_le_bytes());
120 out.extend_from_slice(&unit.data);
121 }
122
123 let file_crc = crc32fast::hash(&out);
124 out.extend_from_slice(FOOTER_MAGIC);
125 out.extend_from_slice(&file_crc.to_le_bytes());
126 Ok(out)
127 }
128
129 pub fn validate(&self) -> io::Result<()> {
130 validate_identity(self.identity)?;
131 validate_units(&self.units)?;
132 if self.start_lsn != self.units[0].lsn {
133 return Err(invalid_input(
134 "retained segment start LSN does not match first unit",
135 ));
136 }
137 if self.end_lsn != self.units[self.units.len() - 1].lsn {
138 return Err(invalid_input(
139 "retained segment end LSN does not match last unit",
140 ));
141 }
142 Ok(())
143 }
144
145 pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
146 if bytes.len() < HEADER_LEN + FOOTER_LEN {
147 return Err(invalid_data("retained segment is truncated"));
148 }
149
150 let footer_start = bytes.len() - FOOTER_LEN;
151 if &bytes[footer_start..footer_start + 4] != FOOTER_MAGIC {
152 return Err(invalid_data("retained segment footer magic mismatch"));
153 }
154 let expected_file_crc = read_u32(bytes, footer_start + 4, "segment footer crc")?;
155 let actual_file_crc = crc32fast::hash(&bytes[..footer_start]);
156 if expected_file_crc != actual_file_crc {
157 return Err(invalid_data("retained segment footer CRC mismatch"));
158 }
159
160 let mut pos = 0usize;
161 if read_exact(bytes, &mut pos, 4, "segment magic")? != SEGMENT_MAGIC {
162 return Err(invalid_data("retained segment magic mismatch"));
163 }
164 let segment_version = read_u16_at_cursor(bytes, &mut pos, "segment version")?;
165 if segment_version != RETAINED_SEGMENT_FORMAT_VERSION {
166 return Err(invalid_data("unsupported retained segment format version"));
167 }
168 let reserved = read_u16_at_cursor(bytes, &mut pos, "reserved")?;
169 if reserved != RESERVED {
170 return Err(invalid_data("retained segment reserved field must be zero"));
171 }
172 let wal_format_version = read_u16_at_cursor(bytes, &mut pos, "WAL format version")?;
173 let catalog_version = read_u16_at_cursor(bytes, &mut pos, "catalog version")?;
174 let record_count = read_u32_at_cursor(bytes, &mut pos, "record count")? as usize;
175 let start_lsn = read_u64_at_cursor(bytes, &mut pos, "start LSN")?;
176 let end_lsn = read_u64_at_cursor(bytes, &mut pos, "end LSN")?;
177 let primary_generation = read_u64_at_cursor(bytes, &mut pos, "primary generation")?;
178 let database_id_slice = read_exact(bytes, &mut pos, 16, "database id")?;
179 let mut database_id = [0u8; 16];
180 database_id.copy_from_slice(database_id_slice);
181
182 let identity = SegmentIdentity {
183 database_id,
184 primary_generation,
185 wal_format_version,
186 catalog_version,
187 };
188 validate_identity(identity)?;
189
190 if record_count == 0 {
191 return Err(invalid_data(
192 "retained segment must contain at least one unit",
193 ));
194 }
195 let max_possible_units = footer_start.saturating_sub(pos) / UNIT_HEADER_LEN;
196 if record_count > max_possible_units {
197 return Err(invalid_data(
198 "retained segment record count exceeds file capacity",
199 ));
200 }
201
202 let mut units = Vec::new();
203 units
204 .try_reserve(record_count)
205 .map_err(|_| invalid_data("retained segment record count is too large"))?;
206 for _ in 0..record_count {
207 if pos >= footer_start {
208 return Err(invalid_data("retained segment record count exceeds file"));
209 }
210 let total_len = read_u32(bytes, pos, "unit total length")? as usize;
211 if total_len < UNIT_HEADER_LEN {
212 return Err(invalid_data("retained unit length is smaller than header"));
213 }
214 if pos
215 .checked_add(total_len)
216 .is_none_or(|end| end > footer_start)
217 {
218 return Err(invalid_data("retained unit extends beyond segment"));
219 }
220 let unit_start = pos;
221 pos += 4;
222 let stored_crc = read_u32_at_cursor(bytes, &mut pos, "unit crc")?;
223 let tx_id = read_u64_at_cursor(bytes, &mut pos, "unit tx id")?;
224 let record_type = read_u8_at_cursor(bytes, &mut pos, "unit record type")?;
225 let lsn = read_u64_at_cursor(bytes, &mut pos, "unit LSN")?;
226 let data_len = total_len - UNIT_HEADER_LEN;
227 if data_len > MAX_RETAINED_UNIT_DATA_SIZE {
228 return Err(invalid_data("retained unit payload exceeds maximum size"));
229 }
230 let data = read_exact(bytes, &mut pos, data_len, "unit payload")?.to_vec();
231 debug_assert_eq!(pos, unit_start + total_len);
232
233 let unit = RetainedUnit {
234 tx_id,
235 record_type,
236 lsn,
237 data,
238 };
239 if !is_known_record_type(unit.record_type) {
240 return Err(invalid_data("unknown retained unit record type"));
241 }
242 if stored_crc != unit_crc(&unit) {
243 return Err(invalid_data("retained unit CRC mismatch"));
244 }
245 units.push(unit);
246 }
247
248 if pos != footer_start {
249 return Err(invalid_data("retained segment has trailing bytes"));
250 }
251
252 let segment = Self::new(identity, units)?;
253 if segment.start_lsn != start_lsn || segment.end_lsn != end_lsn {
254 return Err(invalid_data(
255 "retained segment LSN range does not match records",
256 ));
257 }
258 Ok(segment)
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct SegmentFile {
264 pub path: PathBuf,
265 pub start_lsn: u64,
266 pub end_lsn: u64,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct RetainedTailAvailability {
271 pub since_lsn: u64,
272 pub through_lsn: u64,
273 pub units_available: usize,
274 pub first_lsn: Option<u64>,
275 pub last_lsn: Option<u64>,
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub struct RetainedTailProgress {
280 pub since_lsn: u64,
281 pub requested_through_lsn: u64,
282 pub contiguous_through_lsn: u64,
283 pub units_available: usize,
284 pub first_lsn: Option<u64>,
285 pub last_lsn: Option<u64>,
286}
287
288pub fn segment_file_name(start_lsn: u64, end_lsn: u64) -> String {
289 format!("retained-{start_lsn:020}-{end_lsn:020}.prul")
290}
291
292pub fn write_segment_atomic(dir: &Path, segment: &RetainedSegment) -> io::Result<PathBuf> {
293 create_data_dir_secure(dir)?;
294 let final_path = dir.join(segment_file_name(segment.start_lsn, segment.end_lsn));
295
296 let temp_path = temp_segment_path(dir, segment);
297 let bytes = segment.to_bytes()?;
298 let write_result: io::Result<()> = (|| {
299 let mut file = OpenOptions::new()
300 .write(true)
301 .create_new(true)
302 .open(&temp_path)?;
303 file.write_all(&bytes)?;
304 file.sync_all()?;
305 drop(file);
306
307 fs::hard_link(&temp_path, &final_path)?;
310 fsync_dir(dir)?;
311 let _ = fs::remove_file(&temp_path);
312 let _ = fsync_dir(dir);
313 Ok(())
314 })();
315 if write_result.is_err() {
316 let _ = fs::remove_file(&temp_path);
317 }
318 write_result?;
319 Ok(final_path)
320}
321
322pub fn read_segment_file(path: &Path) -> io::Result<RetainedSegment> {
323 let file = File::open(path)?;
324 let len = file.metadata()?.len();
325 if len > MAX_RETAINED_SEGMENT_FILE_SIZE {
326 return Err(invalid_data("retained segment file exceeds maximum size"));
327 }
328 let capacity = usize::try_from(len)
329 .map_err(|_| invalid_data("retained segment file size exceeds platform capacity"))?;
330 let mut bytes = Vec::new();
331 bytes
332 .try_reserve_exact(capacity)
333 .map_err(|_| invalid_data("retained segment file is too large to allocate"))?;
334 let mut limited = file.take(MAX_RETAINED_SEGMENT_FILE_SIZE + 1);
335 limited.read_to_end(&mut bytes)?;
336 if bytes.len() as u64 > MAX_RETAINED_SEGMENT_FILE_SIZE {
337 return Err(invalid_data("retained segment file exceeds maximum size"));
338 }
339 RetainedSegment::from_bytes(&bytes)
340}
341
342pub fn list_segment_files(dir: &Path) -> io::Result<Vec<SegmentFile>> {
343 let mut files = Vec::new();
344 let entries = match fs::read_dir(dir) {
345 Ok(entries) => entries,
346 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(files),
347 Err(err) => return Err(err),
348 };
349 for entry in entries {
350 let entry = entry?;
351 let path = entry.path();
352 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
353 continue;
354 };
355 if let Some((start_lsn, end_lsn)) = parse_segment_file_name(name) {
356 files.push(SegmentFile {
357 path,
358 start_lsn,
359 end_lsn,
360 });
361 }
362 }
363 files.sort_by_key(|file| (file.start_lsn, file.end_lsn));
364 Ok(files)
365}
366
367pub fn read_units_since(
368 dir: &Path,
369 expected_identity: SegmentIdentity,
370 since_lsn: u64,
371 max_units: usize,
372) -> io::Result<Vec<RetainedUnit>> {
373 read_units_through(dir, expected_identity, since_lsn, u64::MAX, max_units)
374}
375
376pub fn read_units_through(
377 dir: &Path,
378 expected_identity: SegmentIdentity,
379 since_lsn: u64,
380 through_lsn: u64,
381 max_units: usize,
382) -> io::Result<Vec<RetainedUnit>> {
383 validate_identity(expected_identity)?;
384 if max_units == 0 || through_lsn <= since_lsn {
385 return Ok(Vec::new());
386 }
387
388 let mut out = Vec::new();
389 let Some(mut expected_next_lsn) = since_lsn.checked_add(1) else {
390 return Ok(out);
391 };
392 let mut have_started = false;
393
394 for file in list_segment_files(dir)? {
395 if file.start_lsn > through_lsn {
396 if expected_next_lsn <= through_lsn {
397 return Err(invalid_data(format!(
398 "retained segment gap: expected LSN {}, found {}",
399 expected_next_lsn, file.start_lsn
400 )));
401 }
402 break;
403 }
404 let segment = read_segment_file(&file.path)?;
405 if segment.start_lsn != file.start_lsn || segment.end_lsn != file.end_lsn {
406 return Err(invalid_data(format!(
407 "retained segment filename range {}-{} does not match header range {}-{}",
408 file.start_lsn, file.end_lsn, segment.start_lsn, segment.end_lsn
409 )));
410 }
411 if segment.identity != expected_identity {
412 return Err(invalid_data(
413 "retained segment identity does not match expected database history",
414 ));
415 }
416
417 if segment.end_lsn <= since_lsn {
418 continue;
419 }
420
421 if !have_started {
422 if segment.start_lsn > expected_next_lsn {
423 return Err(invalid_data(format!(
424 "retained segment gap: expected LSN {}, found {}",
425 expected_next_lsn, segment.start_lsn
426 )));
427 }
428 } else if segment.start_lsn < expected_next_lsn {
429 return Err(invalid_data(format!(
430 "retained segment overlap: expected LSN {}, found {}",
431 expected_next_lsn, segment.start_lsn
432 )));
433 } else if segment.start_lsn > expected_next_lsn {
434 return Err(invalid_data(format!(
435 "retained segment gap: expected LSN {}, found {}",
436 expected_next_lsn, segment.start_lsn
437 )));
438 }
439
440 for unit in segment.units {
441 if unit.lsn < expected_next_lsn {
442 if have_started {
443 return Err(invalid_data(format!(
444 "retained unit overlap: expected LSN {}, found {}",
445 expected_next_lsn, unit.lsn
446 )));
447 }
448 continue;
449 }
450 if unit.lsn > expected_next_lsn {
451 return Err(invalid_data(format!(
452 "retained unit gap: expected LSN {}, found {}",
453 expected_next_lsn, unit.lsn
454 )));
455 }
456 if unit.lsn > through_lsn {
457 return Ok(out);
458 }
459 out.push(unit);
460 have_started = true;
461 expected_next_lsn = expected_next_lsn
462 .checked_add(1)
463 .ok_or_else(|| invalid_data("retained unit LSN overflow"))?;
464 if out.len() == max_units {
465 return Ok(out);
466 }
467 }
468 }
469 Ok(out)
470}
471
472pub fn validate_retained_tail_available(
473 dir: &Path,
474 expected_identity: SegmentIdentity,
475 since_lsn: u64,
476 through_lsn: u64,
477) -> io::Result<RetainedTailAvailability> {
478 validate_identity(expected_identity)?;
479 if through_lsn <= since_lsn {
480 return Ok(RetainedTailAvailability {
481 since_lsn,
482 through_lsn,
483 units_available: 0,
484 first_lsn: None,
485 last_lsn: None,
486 });
487 }
488
489 let mut expected_next_lsn = since_lsn
490 .checked_add(1)
491 .ok_or_else(|| invalid_data("retained tail LSN overflow"))?;
492 let mut units_available = 0usize;
493 let mut first_lsn = None;
494 let mut have_started = false;
495
496 for file in list_segment_files(dir)? {
497 let segment = read_segment_file(&file.path)?;
498 if segment.start_lsn != file.start_lsn || segment.end_lsn != file.end_lsn {
499 return Err(invalid_data(format!(
500 "retained segment filename range {}-{} does not match header range {}-{}",
501 file.start_lsn, file.end_lsn, segment.start_lsn, segment.end_lsn
502 )));
503 }
504 if segment.identity != expected_identity {
505 return Err(invalid_data(
506 "retained segment identity does not match expected database history",
507 ));
508 }
509
510 if segment.end_lsn < expected_next_lsn {
511 continue;
512 }
513 if !have_started {
514 if segment.start_lsn > expected_next_lsn {
515 return Err(invalid_data(format!(
516 "retained segment gap: expected LSN {}, found {}",
517 expected_next_lsn, segment.start_lsn
518 )));
519 }
520 } else if segment.start_lsn < expected_next_lsn {
521 return Err(invalid_data(format!(
522 "retained segment overlap: expected LSN {}, found {}",
523 expected_next_lsn, segment.start_lsn
524 )));
525 } else if segment.start_lsn > expected_next_lsn {
526 return Err(invalid_data(format!(
527 "retained segment gap: expected LSN {}, found {}",
528 expected_next_lsn, segment.start_lsn
529 )));
530 }
531
532 for unit in segment.units {
533 if unit.lsn < expected_next_lsn {
534 continue;
535 }
536 if unit.lsn > expected_next_lsn {
537 return Err(invalid_data(format!(
538 "retained unit gap: expected LSN {}, found {}",
539 expected_next_lsn, unit.lsn
540 )));
541 }
542 first_lsn = Some(first_lsn.unwrap_or(unit.lsn));
543 have_started = true;
544 units_available = units_available
545 .checked_add(1)
546 .ok_or_else(|| invalid_data("retained tail unit count overflow"))?;
547 if unit.lsn >= through_lsn {
548 return Ok(RetainedTailAvailability {
549 since_lsn,
550 through_lsn,
551 units_available,
552 first_lsn,
553 last_lsn: Some(unit.lsn),
554 });
555 }
556 expected_next_lsn = expected_next_lsn
557 .checked_add(1)
558 .ok_or_else(|| invalid_data("retained tail LSN overflow"))?;
559 }
560 }
561
562 Err(invalid_data(format!(
563 "retained tail missing required LSN {} through {}",
564 expected_next_lsn, through_lsn
565 )))
566}
567
568pub fn retained_tail_progress(
569 dir: &Path,
570 expected_identity: SegmentIdentity,
571 since_lsn: u64,
572 requested_through_lsn: u64,
573) -> io::Result<RetainedTailProgress> {
574 validate_identity(expected_identity)?;
575 if requested_through_lsn <= since_lsn {
576 return Ok(RetainedTailProgress {
577 since_lsn,
578 requested_through_lsn,
579 contiguous_through_lsn: since_lsn,
580 units_available: 0,
581 first_lsn: None,
582 last_lsn: None,
583 });
584 }
585
586 let mut expected_next_lsn = since_lsn
587 .checked_add(1)
588 .ok_or_else(|| invalid_data("retained tail LSN overflow"))?;
589 let mut units_available = 0usize;
590 let mut first_lsn = None;
591 let mut last_lsn = None;
592 let mut have_started = false;
593
594 for file in list_segment_files(dir)? {
595 if file.end_lsn <= since_lsn {
596 continue;
597 }
598 if file.start_lsn > requested_through_lsn {
599 break;
600 }
601
602 let segment = read_segment_file(&file.path)?;
603 if segment.start_lsn != file.start_lsn || segment.end_lsn != file.end_lsn {
604 return Err(invalid_data(format!(
605 "retained segment filename range {}-{} does not match header range {}-{}",
606 file.start_lsn, file.end_lsn, segment.start_lsn, segment.end_lsn
607 )));
608 }
609 if segment.identity != expected_identity {
610 return Err(invalid_data(
611 "retained segment identity does not match expected database history",
612 ));
613 }
614 if segment.end_lsn < expected_next_lsn {
615 continue;
616 }
617
618 if !have_started {
619 if segment.start_lsn > expected_next_lsn {
620 return Err(invalid_data(format!(
621 "retained segment gap: expected LSN {}, found {}",
622 expected_next_lsn, segment.start_lsn
623 )));
624 }
625 } else if segment.start_lsn < expected_next_lsn {
626 return Err(invalid_data(format!(
627 "retained segment overlap: expected LSN {}, found {}",
628 expected_next_lsn, segment.start_lsn
629 )));
630 } else if segment.start_lsn > expected_next_lsn {
631 return Err(invalid_data(format!(
632 "retained segment gap: expected LSN {}, found {}",
633 expected_next_lsn, segment.start_lsn
634 )));
635 }
636
637 for unit in segment.units {
638 if unit.lsn < expected_next_lsn {
639 continue;
640 }
641 if unit.lsn > expected_next_lsn {
642 return Err(invalid_data(format!(
643 "retained unit gap: expected LSN {}, found {}",
644 expected_next_lsn, unit.lsn
645 )));
646 }
647 if unit.lsn > requested_through_lsn {
648 return Ok(RetainedTailProgress {
649 since_lsn,
650 requested_through_lsn,
651 contiguous_through_lsn: last_lsn.unwrap_or(since_lsn),
652 units_available,
653 first_lsn,
654 last_lsn,
655 });
656 }
657
658 first_lsn = Some(first_lsn.unwrap_or(unit.lsn));
659 last_lsn = Some(unit.lsn);
660 have_started = true;
661 units_available = units_available
662 .checked_add(1)
663 .ok_or_else(|| invalid_data("retained tail unit count overflow"))?;
664 expected_next_lsn = expected_next_lsn
665 .checked_add(1)
666 .ok_or_else(|| invalid_data("retained tail LSN overflow"))?;
667 if unit.lsn >= requested_through_lsn {
668 return Ok(RetainedTailProgress {
669 since_lsn,
670 requested_through_lsn,
671 contiguous_through_lsn: unit.lsn,
672 units_available,
673 first_lsn,
674 last_lsn,
675 });
676 }
677 }
678 }
679
680 Ok(RetainedTailProgress {
681 since_lsn,
682 requested_through_lsn,
683 contiguous_through_lsn: last_lsn.unwrap_or(since_lsn),
684 units_available,
685 first_lsn,
686 last_lsn,
687 })
688}
689
690fn parse_segment_file_name(name: &str) -> Option<(u64, u64)> {
691 let rest = name.strip_prefix("retained-")?;
692 let rest = rest.strip_suffix(".prul")?;
693 let (start, end) = rest.split_once('-')?;
694 let start_lsn = start.parse().ok()?;
695 let end_lsn = end.parse().ok()?;
696 Some((start_lsn, end_lsn))
697}
698
699fn temp_segment_path(dir: &Path, segment: &RetainedSegment) -> PathBuf {
700 let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
701 let nanos = SystemTime::now()
702 .duration_since(UNIX_EPOCH)
703 .map(|duration| duration.as_nanos())
704 .unwrap_or(0);
705 dir.join(format!(
706 ".{}.tmp.{}.{}.{}",
707 segment_file_name(segment.start_lsn, segment.end_lsn),
708 std::process::id(),
709 nanos,
710 counter
711 ))
712}
713
714fn validate_identity(identity: SegmentIdentity) -> io::Result<()> {
715 if identity.primary_generation == 0 {
716 return Err(invalid_data(
717 "retained segment primary generation must be non-zero",
718 ));
719 }
720 if identity.database_id == [0; 16] {
721 return Err(invalid_data(
722 "retained segment database id must be non-zero",
723 ));
724 }
725 if identity.wal_format_version != WAL_FORMAT_VERSION {
726 return Err(invalid_data("retained segment WAL format is unsupported"));
727 }
728 if identity.catalog_version != CATALOG_VERSION {
729 return Err(invalid_data(
730 "retained segment catalog format is unsupported",
731 ));
732 }
733 Ok(())
734}
735
736fn validate_units(units: &[RetainedUnit]) -> io::Result<()> {
737 if units.is_empty() {
738 return Err(invalid_input(
739 "retained segment must contain at least one unit",
740 ));
741 }
742 if units.len() > u32::MAX as usize {
743 return Err(invalid_input("retained segment has too many units"));
744 }
745
746 let mut expected_lsn = units[0].lsn;
747 if expected_lsn == 0 {
748 return Err(invalid_input("retained unit LSN must be non-zero"));
749 }
750 for unit in units {
751 if unit.lsn != expected_lsn {
752 return Err(invalid_input("retained unit LSNs must be contiguous"));
753 }
754 if !is_known_record_type(unit.record_type) {
755 return Err(invalid_input("unknown retained unit record type"));
756 }
757 checked_data_len(unit.data.len())?;
758 expected_lsn = expected_lsn
759 .checked_add(1)
760 .ok_or_else(|| invalid_input("retained unit LSN overflow"))?;
761 }
762 Ok(())
763}
764
765fn is_known_record_type(record_type: u8) -> bool {
766 matches!(record_type, 1..=10)
767}
768
769fn unit_crc(unit: &RetainedUnit) -> u32 {
770 let mut crc_input = Vec::with_capacity(17 + unit.data.len());
771 crc_input.extend_from_slice(&unit.tx_id.to_le_bytes());
772 crc_input.push(unit.record_type);
773 crc_input.extend_from_slice(&unit.lsn.to_le_bytes());
774 crc_input.extend_from_slice(&unit.data);
775 crc32fast::hash(&crc_input)
776}
777
778fn checked_unit_count(count: usize) -> io::Result<u32> {
779 u32::try_from(count).map_err(|_| invalid_input("retained segment has too many units"))
780}
781
782fn checked_data_len(len: usize) -> io::Result<u32> {
783 if len > MAX_RETAINED_UNIT_DATA_SIZE {
784 return Err(invalid_input("retained unit payload exceeds maximum size"));
785 }
786 u32::try_from(len).map_err(|_| invalid_input("retained unit payload is too large"))
787}
788
789fn read_exact<'a>(
790 bytes: &'a [u8],
791 pos: &mut usize,
792 len: usize,
793 field: &str,
794) -> io::Result<&'a [u8]> {
795 let end = pos
796 .checked_add(len)
797 .ok_or_else(|| invalid_data(format!("{field} offset overflow")))?;
798 if end > bytes.len() {
799 return Err(invalid_data(format!("truncated {field}")));
800 }
801 let slice = &bytes[*pos..end];
802 *pos = end;
803 Ok(slice)
804}
805
806fn read_u8_at_cursor(bytes: &[u8], pos: &mut usize, field: &str) -> io::Result<u8> {
807 let slice = read_exact(bytes, pos, 1, field)?;
808 Ok(slice[0])
809}
810
811fn read_u16_at_cursor(bytes: &[u8], pos: &mut usize, field: &str) -> io::Result<u16> {
812 let slice = read_exact(bytes, pos, 2, field)?;
813 let arr: [u8; 2] = slice
814 .try_into()
815 .map_err(|_| invalid_data(format!("invalid {field}")))?;
816 Ok(u16::from_le_bytes(arr))
817}
818
819fn read_u32_at_cursor(bytes: &[u8], pos: &mut usize, field: &str) -> io::Result<u32> {
820 let slice = read_exact(bytes, pos, 4, field)?;
821 let arr: [u8; 4] = slice
822 .try_into()
823 .map_err(|_| invalid_data(format!("invalid {field}")))?;
824 Ok(u32::from_le_bytes(arr))
825}
826
827fn read_u64_at_cursor(bytes: &[u8], pos: &mut usize, field: &str) -> io::Result<u64> {
828 let slice = read_exact(bytes, pos, 8, field)?;
829 let arr: [u8; 8] = slice
830 .try_into()
831 .map_err(|_| invalid_data(format!("invalid {field}")))?;
832 Ok(u64::from_le_bytes(arr))
833}
834
835fn read_u32(bytes: &[u8], pos: usize, field: &str) -> io::Result<u32> {
836 let end = pos
837 .checked_add(4)
838 .ok_or_else(|| invalid_data(format!("{field} offset overflow")))?;
839 if end > bytes.len() {
840 return Err(invalid_data(format!("truncated {field}")));
841 }
842 let arr: [u8; 4] = bytes[pos..end]
843 .try_into()
844 .map_err(|_| invalid_data(format!("invalid {field}")))?;
845 Ok(u32::from_le_bytes(arr))
846}
847
848#[cfg(unix)]
849fn fsync_dir(dir: &Path) -> io::Result<()> {
850 File::open(dir)?.sync_all()
851}
852
853#[cfg(not(unix))]
854fn fsync_dir(_dir: &Path) -> io::Result<()> {
855 Ok(())
856}
857
858fn invalid_input(message: impl Into<String>) -> io::Error {
859 io::Error::new(io::ErrorKind::InvalidInput, message.into())
860}
861
862fn invalid_data(message: impl Into<String>) -> io::Error {
863 io::Error::new(io::ErrorKind::InvalidData, message.into())
864}
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869 use std::sync::{Arc, Barrier};
870
871 fn identity() -> SegmentIdentity {
872 SegmentIdentity::current(*b"0123456789abcdef", 7)
873 }
874
875 fn unit(lsn: u64) -> RetainedUnit {
876 RetainedUnit {
877 tx_id: 42,
878 record_type: 1,
879 lsn,
880 data: format!("payload-{lsn}").into_bytes(),
881 }
882 }
883
884 #[test]
885 fn segment_round_trips() {
886 let segment = RetainedSegment::new(identity(), vec![unit(10), unit(11)]).unwrap();
887 let bytes = segment.to_bytes().unwrap();
888 let decoded = RetainedSegment::from_bytes(&bytes).unwrap();
889 assert_eq!(decoded, segment);
890 }
891
892 #[test]
893 fn rejects_empty_and_non_contiguous_segments() {
894 assert!(RetainedSegment::new(identity(), Vec::new()).is_err());
895 let err = RetainedSegment::new(identity(), vec![unit(10), unit(12)]).unwrap_err();
896 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
897 }
898
899 #[test]
900 fn public_segment_literal_is_validated_before_serialization() {
901 let invalid = RetainedSegment {
902 identity: identity(),
903 start_lsn: 99,
904 end_lsn: 100,
905 units: vec![unit(1), unit(2)],
906 };
907
908 let err = invalid.to_bytes().unwrap_err();
909 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
910
911 let dir = tempfile::tempdir().unwrap();
912 let err = write_segment_atomic(dir.path(), &invalid).unwrap_err();
913 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
914 assert!(
915 list_segment_files(dir.path()).unwrap().is_empty(),
916 "invalid public segment literals must not publish unreadable retained logs"
917 );
918 }
919
920 #[test]
921 fn rejects_corrupt_footer_checksum() {
922 let segment = RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap();
923 let mut bytes = segment.to_bytes().unwrap();
924 bytes[HEADER_LEN + 3] ^= 0x55;
925 let err = RetainedSegment::from_bytes(&bytes).unwrap_err();
926 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
927 }
928
929 #[test]
930 fn rejects_truncated_segment() {
931 let segment = RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap();
932 let bytes = segment.to_bytes().unwrap();
933 let truncated = &bytes[..bytes.len() - 3];
934 let err = RetainedSegment::from_bytes(truncated).unwrap_err();
935 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
936 }
937
938 #[test]
939 fn atomic_publish_and_range_read_work() {
940 let dir = tempfile::tempdir().unwrap();
941 let first = RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap();
942 let second = RetainedSegment::new(identity(), vec![unit(3), unit(4)]).unwrap();
943
944 let first_path = write_segment_atomic(dir.path(), &first).unwrap();
945 let second_path = write_segment_atomic(dir.path(), &second).unwrap();
946 assert!(first_path.exists());
947 assert!(second_path.exists());
948
949 let files = list_segment_files(dir.path()).unwrap();
950 assert_eq!(files.len(), 2);
951 assert_eq!(files[0].start_lsn, 1);
952 assert_eq!(files[1].start_lsn, 3);
953
954 let units = read_units_since(dir.path(), identity(), 2, 10).unwrap();
955 let lsns: Vec<u64> = units.into_iter().map(|unit| unit.lsn).collect();
956 assert_eq!(lsns, vec![3, 4]);
957 }
958
959 #[cfg(unix)]
960 #[test]
961 fn atomic_publish_creates_owner_only_segment_dir() {
962 use std::os::unix::fs::PermissionsExt;
963
964 let dir = tempfile::tempdir().unwrap();
965 let segment_dir = dir.path().join("retained");
966 let segment = RetainedSegment::new(identity(), vec![unit(1)]).unwrap();
967
968 write_segment_atomic(&segment_dir, &segment).unwrap();
969
970 let mode = std::fs::metadata(&segment_dir)
971 .unwrap()
972 .permissions()
973 .mode()
974 & 0o777;
975 assert_eq!(mode, 0o700);
976 }
977
978 #[test]
979 fn read_segment_file_rejects_oversized_file_before_reading() {
980 let dir = tempfile::tempdir().unwrap();
981 let path = dir.path().join(segment_file_name(1, 1));
982 let file = File::create(&path).unwrap();
983 file.set_len(MAX_RETAINED_SEGMENT_FILE_SIZE + 1).unwrap();
984 drop(file);
985
986 let err = read_segment_file(&path).unwrap_err();
987 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
988 assert!(err.to_string().contains("maximum size"));
989 }
990
991 #[test]
992 fn range_read_honors_limit() {
993 let dir = tempfile::tempdir().unwrap();
994 let segment = RetainedSegment::new(identity(), vec![unit(1), unit(2), unit(3)]).unwrap();
995 write_segment_atomic(dir.path(), &segment).unwrap();
996
997 let units = read_units_since(dir.path(), identity(), 0, 2).unwrap();
998 let lsns: Vec<u64> = units.into_iter().map(|unit| unit.lsn).collect();
999 assert_eq!(lsns, vec![1, 2]);
1000 }
1001
1002 #[test]
1003 fn range_read_stops_at_through_lsn() {
1004 let dir = tempfile::tempdir().unwrap();
1005 let segment = RetainedSegment::new(
1006 identity(),
1007 vec![unit(1), unit(2), unit(3), unit(4), unit(5)],
1008 )
1009 .unwrap();
1010 write_segment_atomic(dir.path(), &segment).unwrap();
1011
1012 let units = read_units_through(dir.path(), identity(), 1, 3, 10).unwrap();
1013 let lsns: Vec<u64> = units.into_iter().map(|unit| unit.lsn).collect();
1014 assert_eq!(lsns, vec![2, 3]);
1015 }
1016
1017 #[test]
1018 fn retained_tail_progress_reports_contiguous_prefix_without_requiring_target() {
1019 let dir = tempfile::tempdir().unwrap();
1020 write_segment_atomic(
1021 dir.path(),
1022 &RetainedSegment::new(identity(), vec![unit(1), unit(2), unit(3)]).unwrap(),
1023 )
1024 .unwrap();
1025
1026 let progress = retained_tail_progress(dir.path(), identity(), 0, 5).unwrap();
1027 assert_eq!(progress.since_lsn, 0);
1028 assert_eq!(progress.requested_through_lsn, 5);
1029 assert_eq!(progress.contiguous_through_lsn, 3);
1030 assert_eq!(progress.units_available, 3);
1031 assert_eq!(progress.first_lsn, Some(1));
1032 assert_eq!(progress.last_lsn, Some(3));
1033 }
1034
1035 #[test]
1036 fn retained_tail_progress_rejects_gapped_retained_history() {
1037 let dir = tempfile::tempdir().unwrap();
1038 write_segment_atomic(
1039 dir.path(),
1040 &RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap(),
1041 )
1042 .unwrap();
1043 write_segment_atomic(
1044 dir.path(),
1045 &RetainedSegment::new(identity(), vec![unit(4), unit(5)]).unwrap(),
1046 )
1047 .unwrap();
1048
1049 let err = retained_tail_progress(dir.path(), identity(), 0, 5).unwrap_err();
1050 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1051 assert!(err.to_string().contains("gap"));
1052 }
1053
1054 #[test]
1055 fn concurrent_publish_same_range_is_no_clobber() {
1056 let dir = tempfile::tempdir().unwrap();
1057 let segment = Arc::new(RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap());
1058 let barrier = Arc::new(Barrier::new(8));
1059
1060 let mut handles = Vec::new();
1061 for _ in 0..8 {
1062 let dir = dir.path().to_path_buf();
1063 let segment = Arc::clone(&segment);
1064 let barrier = Arc::clone(&barrier);
1065 handles.push(std::thread::spawn(move || {
1066 barrier.wait();
1067 write_segment_atomic(&dir, &segment)
1068 }));
1069 }
1070
1071 let mut successes = 0usize;
1072 let mut already_exists = 0usize;
1073 for handle in handles {
1074 match handle.join().unwrap() {
1075 Ok(_) => successes += 1,
1076 Err(err) if err.kind() == io::ErrorKind::AlreadyExists => already_exists += 1,
1077 Err(err) => panic!("unexpected publish error: {err}"),
1078 }
1079 }
1080 assert_eq!(successes, 1);
1081 assert_eq!(already_exists, 7);
1082
1083 let files = list_segment_files(dir.path()).unwrap();
1084 assert_eq!(files.len(), 1);
1085 let temp_files = std::fs::read_dir(dir.path())
1086 .unwrap()
1087 .filter_map(Result::ok)
1088 .filter(|entry| entry.file_name().to_string_lossy().contains(".tmp."))
1089 .count();
1090 assert_eq!(temp_files, 0);
1091 }
1092
1093 #[test]
1094 fn read_units_since_rejects_missing_segment_gap() {
1095 let dir = tempfile::tempdir().unwrap();
1096 write_segment_atomic(
1097 dir.path(),
1098 &RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap(),
1099 )
1100 .unwrap();
1101 write_segment_atomic(
1102 dir.path(),
1103 &RetainedSegment::new(identity(), vec![unit(4), unit(5)]).unwrap(),
1104 )
1105 .unwrap();
1106
1107 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1108 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1109 assert!(err.to_string().contains("gap"));
1110 }
1111
1112 #[test]
1113 fn validate_retained_tail_available_accepts_contiguous_tail() {
1114 let dir = tempfile::tempdir().unwrap();
1115 write_segment_atomic(
1116 dir.path(),
1117 &RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap(),
1118 )
1119 .unwrap();
1120 write_segment_atomic(
1121 dir.path(),
1122 &RetainedSegment::new(identity(), vec![unit(3), unit(4), unit(5)]).unwrap(),
1123 )
1124 .unwrap();
1125
1126 let availability = validate_retained_tail_available(dir.path(), identity(), 2, 5).unwrap();
1127 assert_eq!(availability.since_lsn, 2);
1128 assert_eq!(availability.through_lsn, 5);
1129 assert_eq!(availability.units_available, 3);
1130 assert_eq!(availability.first_lsn, Some(3));
1131 assert_eq!(availability.last_lsn, Some(5));
1132 }
1133
1134 #[test]
1135 fn validate_retained_tail_available_allows_empty_target_range() {
1136 let dir = tempfile::tempdir().unwrap();
1137 let availability = validate_retained_tail_available(dir.path(), identity(), 7, 7).unwrap();
1138 assert_eq!(availability.units_available, 0);
1139 assert_eq!(availability.first_lsn, None);
1140 assert_eq!(availability.last_lsn, None);
1141 }
1142
1143 #[test]
1144 fn validate_retained_tail_available_rejects_missing_tail() {
1145 let dir = tempfile::tempdir().unwrap();
1146 write_segment_atomic(
1147 dir.path(),
1148 &RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap(),
1149 )
1150 .unwrap();
1151 write_segment_atomic(
1152 dir.path(),
1153 &RetainedSegment::new(identity(), vec![unit(4), unit(5)]).unwrap(),
1154 )
1155 .unwrap();
1156
1157 let err = validate_retained_tail_available(dir.path(), identity(), 2, 5).unwrap_err();
1158 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1159 assert!(err.to_string().contains("gap"));
1160 }
1161
1162 #[test]
1163 fn validate_retained_tail_available_rejects_overlapping_segments() {
1164 let dir = tempfile::tempdir().unwrap();
1165 write_segment_atomic(
1166 dir.path(),
1167 &RetainedSegment::new(identity(), vec![unit(1), unit(2), unit(3)]).unwrap(),
1168 )
1169 .unwrap();
1170 write_segment_atomic(
1171 dir.path(),
1172 &RetainedSegment::new(identity(), vec![unit(3), unit(4)]).unwrap(),
1173 )
1174 .unwrap();
1175
1176 let err = validate_retained_tail_available(dir.path(), identity(), 0, 4).unwrap_err();
1177 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1178 assert!(err.to_string().contains("overlap"));
1179 }
1180
1181 #[test]
1182 fn read_units_since_rejects_overlapping_segments() {
1183 let dir = tempfile::tempdir().unwrap();
1184 write_segment_atomic(
1185 dir.path(),
1186 &RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap(),
1187 )
1188 .unwrap();
1189 write_segment_atomic(
1190 dir.path(),
1191 &RetainedSegment::new(identity(), vec![unit(2), unit(3)]).unwrap(),
1192 )
1193 .unwrap();
1194
1195 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1196 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1197 assert!(err.to_string().contains("overlap"));
1198 }
1199
1200 #[test]
1201 fn read_units_since_rejects_filename_header_range_mismatch() {
1202 let dir = tempfile::tempdir().unwrap();
1203 let segment = RetainedSegment::new(identity(), vec![unit(1), unit(2)]).unwrap();
1204 std::fs::write(
1205 dir.path().join(segment_file_name(10, 11)),
1206 segment.to_bytes().unwrap(),
1207 )
1208 .unwrap();
1209
1210 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1211 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1212 assert!(err.to_string().contains("filename range"));
1213 }
1214
1215 #[test]
1216 fn read_units_since_rejects_mixed_database_identity() {
1217 let dir = tempfile::tempdir().unwrap();
1218 let wrong_identity = SegmentIdentity::current(*b"fedcba9876543210", 7);
1219 write_segment_atomic(
1220 dir.path(),
1221 &RetainedSegment::new(wrong_identity, vec![unit(1), unit(2)]).unwrap(),
1222 )
1223 .unwrap();
1224
1225 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1226 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1227 assert!(err.to_string().contains("identity"));
1228 }
1229
1230 #[test]
1231 fn read_units_since_rejects_mixed_primary_generation() {
1232 let dir = tempfile::tempdir().unwrap();
1233 let wrong_identity = SegmentIdentity::current(*b"0123456789abcdef", 8);
1234 write_segment_atomic(
1235 dir.path(),
1236 &RetainedSegment::new(wrong_identity, vec![unit(1), unit(2)]).unwrap(),
1237 )
1238 .unwrap();
1239
1240 let err = read_units_since(dir.path(), identity(), 0, 10).unwrap_err();
1241 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1242 assert!(err.to_string().contains("identity"));
1243 }
1244
1245 #[test]
1246 fn rejects_zero_database_id_and_primary_generation() {
1247 assert!(RetainedSegment::new(SegmentIdentity::current([0; 16], 1), vec![unit(1)]).is_err());
1248 assert!(RetainedSegment::new(
1249 SegmentIdentity::current(*b"0123456789abcdef", 0),
1250 vec![unit(1)]
1251 )
1252 .is_err());
1253 }
1254
1255 #[test]
1256 fn rejects_impossible_record_count_before_allocating() {
1257 let segment = RetainedSegment::new(identity(), vec![unit(1)]).unwrap();
1258 let mut bytes = segment.to_bytes().unwrap();
1259 bytes[12..16].copy_from_slice(&u32::MAX.to_le_bytes());
1260 rewrite_footer_crc(&mut bytes);
1261
1262 let err = RetainedSegment::from_bytes(&bytes).unwrap_err();
1263 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1264 assert!(err.to_string().contains("record count"));
1265 }
1266
1267 fn rewrite_footer_crc(bytes: &mut [u8]) {
1268 let footer_start = bytes.len() - FOOTER_LEN;
1269 let crc = crc32fast::hash(&bytes[..footer_start]);
1270 bytes[footer_start + 4..footer_start + 8].copy_from_slice(&crc.to_le_bytes());
1271 }
1272}