Skip to main content

powdb_sync/
segment.rs

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/// Segment identity metadata used to reject forked or incompatible histories.
24#[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/// One retained replication unit.
48///
49/// V1 stores current WAL records as units. The segment envelope owns durability,
50/// identity, range, and corruption checks; the future applier can convert the
51/// record type back into storage-layer replay actions.
52#[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/// Immutable retained-unit segment.
72#[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        // `rename` is atomic but not no-clobber on Unix. A hard link in the
308        // same directory creates the final name only if it does not exist.
309        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}