1use crate::ChangesetOffset;
7use std::{
8 fs::{File, OpenOptions},
9 io::{self, Write},
10 os::unix::fs::FileExt,
11 path::Path,
12};
13
14#[derive(Debug)]
16pub struct ChangesetOffsetWriter {
17 file: File,
18 records_written: u64,
20 pending_truncation: Option<u64>,
22}
23
24impl ChangesetOffsetWriter {
25 const RECORD_SIZE: usize = 16;
27
28 pub fn new(path: impl AsRef<Path>, committed_len: u64) -> io::Result<Self> {
38 let file = OpenOptions::new()
39 .create(true)
40 .truncate(false)
41 .read(true)
42 .write(true)
43 .open(path.as_ref())?;
44
45 let file_len = file.metadata()?.len();
46 let remainder = file_len % Self::RECORD_SIZE as u64;
47
48 let aligned_len = if remainder != 0 {
50 let truncated_len = file_len - remainder;
51 tracing::warn!(
52 target: "reth::static_file",
53 path = %path.as_ref().display(),
54 original_len = file_len,
55 truncated_len,
56 "Truncating partial changeset offset record"
57 );
58 file.set_len(truncated_len)?;
59 file.sync_all()?; truncated_len
61 } else {
62 file_len
63 };
64
65 let records_in_file = aligned_len / Self::RECORD_SIZE as u64;
66
67 match records_in_file.cmp(&committed_len) {
69 std::cmp::Ordering::Greater => {
70 let target_len = committed_len * Self::RECORD_SIZE as u64;
72 tracing::warn!(
73 target: "reth::static_file",
74 path = %path.as_ref().display(),
75 sidecar_records = records_in_file,
76 committed_len,
77 "Truncating uncommitted changeset offset records after crash recovery"
78 );
79 file.set_len(target_len)?;
80 file.sync_all()?; }
82 std::cmp::Ordering::Less => {
83 return Err(io::Error::new(
95 io::ErrorKind::InvalidData,
96 format!(
97 "INVARIANT VIOLATION: Changeset offset sidecar has {} records but header expects {} \
98 (healing should have prevented this - possible bug in healing logic): {}",
99 records_in_file,
100 committed_len,
101 path.as_ref().display()
102 ),
103 ));
104 }
105 std::cmp::Ordering::Equal => {}
106 }
107
108 let records_written = committed_len;
109 let file = OpenOptions::new().create(true).append(true).open(path)?;
110
111 Ok(Self { file, records_written, pending_truncation: None })
112 }
113
114 pub fn append(&mut self, offset: &ChangesetOffset) -> io::Result<()> {
116 self.apply_pending_truncation()?;
117 let mut buf = [0u8; Self::RECORD_SIZE];
118 buf[..8].copy_from_slice(&offset.offset().to_le_bytes());
119 buf[8..].copy_from_slice(&offset.num_changes().to_le_bytes());
120 self.file.write_all(&buf)?;
121 self.records_written += 1;
122 Ok(())
123 }
124
125 pub fn append_many(&mut self, offsets: &[ChangesetOffset]) -> io::Result<()> {
127 for offset in offsets {
128 self.append(offset)?;
129 }
130 Ok(())
131 }
132
133 pub fn sync(&mut self) -> io::Result<()> {
135 self.file.sync_all()
136 }
137
138 pub fn truncate(&mut self, len: u64) -> io::Result<()> {
144 self.file.set_len(len * Self::RECORD_SIZE as u64)?;
145 self.file.sync_all()?;
146 self.pending_truncation = None;
147 self.records_written = len;
148 Ok(())
149 }
150
151 pub const fn truncate_deferred(&mut self, len: u64) {
154 self.records_written = len;
155 self.pending_truncation = Some(len);
156 }
157
158 pub fn apply_pending_truncation(&mut self) -> io::Result<bool> {
162 let Some(len) = self.pending_truncation else { return Ok(false) };
163 self.truncate(len)?;
164 Ok(true)
165 }
166
167 pub const fn len(&self) -> u64 {
169 self.records_written
170 }
171
172 pub const fn is_empty(&self) -> bool {
174 self.records_written == 0
175 }
176}
177
178#[derive(Debug)]
180pub struct ChangesetOffsetReader {
181 file: File,
182 len: u64,
184}
185
186impl ChangesetOffsetReader {
187 const RECORD_SIZE: usize = 16;
189
190 pub fn new(path: impl AsRef<Path>, len: u64) -> io::Result<Self> {
195 let file = File::open(path)?;
196 Ok(Self { file, len })
197 }
198
199 pub fn get(&self, block_index: u64) -> io::Result<Option<ChangesetOffset>> {
202 if block_index >= self.len {
203 return Ok(None);
204 }
205
206 let byte_pos = block_index * Self::RECORD_SIZE as u64;
207 let mut buf = [0u8; Self::RECORD_SIZE];
208 self.file.read_exact_at(&mut buf, byte_pos)?;
209
210 let offset = u64::from_le_bytes(buf[..8].try_into().unwrap());
211 let num_changes = u64::from_le_bytes(buf[8..].try_into().unwrap());
212
213 Ok(Some(ChangesetOffset::new(offset, num_changes)))
214 }
215
216 pub fn get_range(&self, start: u64, end: u64) -> io::Result<Vec<ChangesetOffset>> {
218 let end = end.min(self.len);
219 if start >= end {
220 return Ok(Vec::new());
221 }
222
223 let count = (end - start) as usize;
224 let byte_pos = start * Self::RECORD_SIZE as u64;
225
226 let mut result = Vec::with_capacity(count);
227 let mut buf = [0u8; Self::RECORD_SIZE];
228
229 for i in 0..count {
230 let pos = byte_pos + (i as u64) * Self::RECORD_SIZE as u64;
231 self.file.read_exact_at(&mut buf, pos)?;
232 let offset = u64::from_le_bytes(buf[..8].try_into().unwrap());
233 let num_changes = u64::from_le_bytes(buf[8..].try_into().unwrap());
234 result.push(ChangesetOffset::new(offset, num_changes));
235 }
236
237 Ok(result)
238 }
239
240 pub const fn len(&self) -> u64 {
242 self.len
243 }
244
245 pub const fn is_empty(&self) -> bool {
247 self.len == 0
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use tempfile::tempdir;
255
256 #[test]
257 fn test_write_and_read() {
258 let dir = tempdir().unwrap();
259 let path = dir.path().join("test.csoff");
260
261 {
263 let mut writer = ChangesetOffsetWriter::new(&path, 0).unwrap();
264 writer.append(&ChangesetOffset::new(0, 5)).unwrap();
265 writer.append(&ChangesetOffset::new(5, 3)).unwrap();
266 writer.append(&ChangesetOffset::new(8, 10)).unwrap();
267 writer.sync().unwrap();
268 assert_eq!(writer.len(), 3);
269 }
270
271 {
273 let reader = ChangesetOffsetReader::new(&path, 3).unwrap();
274 assert_eq!(reader.len(), 3);
275
276 let entry = reader.get(0).unwrap().unwrap();
277 assert_eq!(entry.offset(), 0);
278 assert_eq!(entry.num_changes(), 5);
279
280 let entry = reader.get(1).unwrap().unwrap();
281 assert_eq!(entry.offset(), 5);
282 assert_eq!(entry.num_changes(), 3);
283
284 let entry = reader.get(2).unwrap().unwrap();
285 assert_eq!(entry.offset(), 8);
286 assert_eq!(entry.num_changes(), 10);
287
288 assert!(reader.get(3).unwrap().is_none());
289 }
290 }
291
292 #[test]
293 fn test_truncate() {
294 let dir = tempdir().unwrap();
295 let path = dir.path().join("test.csoff");
296
297 let mut writer = ChangesetOffsetWriter::new(&path, 0).unwrap();
298 writer.append(&ChangesetOffset::new(0, 1)).unwrap();
299 writer.append(&ChangesetOffset::new(1, 2)).unwrap();
300 writer.append(&ChangesetOffset::new(3, 3)).unwrap();
301 writer.sync().unwrap();
302
303 writer.truncate(2).unwrap();
304 assert_eq!(writer.len(), 2);
305
306 let reader = ChangesetOffsetReader::new(&path, 2).unwrap();
307 assert_eq!(reader.len(), 2);
308 assert!(reader.get(2).unwrap().is_none());
309 }
310
311 #[test]
312 fn test_partial_record_recovery() {
313 let dir = tempdir().unwrap();
314 let path = dir.path().join("test.csoff");
315
316 {
318 let mut file = std::fs::File::create(&path).unwrap();
319 file.write_all(&100u64.to_le_bytes()).unwrap();
321 file.write_all(&5u64.to_le_bytes()).unwrap();
322 file.write_all(&200u64.to_le_bytes()).unwrap();
324 file.sync_all().unwrap();
325 }
326
327 assert_eq!(std::fs::metadata(&path).unwrap().len(), 24);
329
330 let writer = ChangesetOffsetWriter::new(&path, 1).unwrap();
333 assert_eq!(writer.len(), 1);
334
335 assert_eq!(std::fs::metadata(&path).unwrap().len(), 16);
337
338 let reader = ChangesetOffsetReader::new(&path, 1).unwrap();
340 assert_eq!(reader.len(), 1);
341 let entry = reader.get(0).unwrap().unwrap();
342 assert_eq!(entry.offset(), 100);
343 assert_eq!(entry.num_changes(), 5);
344 }
345
346 #[test]
347 fn test_len_bounds_reads() {
348 let dir = tempdir().unwrap();
349 let path = dir.path().join("test.csoff");
350
351 {
353 let mut writer = ChangesetOffsetWriter::new(&path, 0).unwrap();
354 writer.append(&ChangesetOffset::new(0, 10)).unwrap();
355 writer.append(&ChangesetOffset::new(10, 20)).unwrap();
356 writer.append(&ChangesetOffset::new(30, 30)).unwrap();
357 writer.sync().unwrap();
358 assert_eq!(writer.len(), 3);
359 }
360
361 let reader = ChangesetOffsetReader::new(&path, 2).unwrap();
363 assert_eq!(reader.len(), 2);
364
365 let entry0 = reader.get(0).unwrap().unwrap();
367 assert_eq!(entry0.offset(), 0);
368 assert_eq!(entry0.num_changes(), 10);
369
370 let entry1 = reader.get(1).unwrap().unwrap();
371 assert_eq!(entry1.offset(), 10);
372 assert_eq!(entry1.num_changes(), 20);
373
374 assert!(reader.get(2).unwrap().is_none());
376
377 let range = reader.get_range(0, 5).unwrap();
379 assert_eq!(range.len(), 2);
380 }
381
382 #[test]
383 fn test_truncate_uncommitted_records_on_open() {
384 let dir = tempdir().unwrap();
387 let path = dir.path().join("test.csoff");
388
389 {
391 let mut writer = ChangesetOffsetWriter::new(&path, 0).unwrap();
392 writer.append(&ChangesetOffset::new(0, 5)).unwrap();
393 writer.append(&ChangesetOffset::new(5, 10)).unwrap();
394 writer.append(&ChangesetOffset::new(15, 7)).unwrap(); writer.sync().unwrap();
396 assert_eq!(writer.len(), 3);
397 }
398
399 let committed_len = 2u64;
401 {
402 let writer = ChangesetOffsetWriter::new(&path, committed_len).unwrap();
403 assert_eq!(writer.len(), 2); }
405
406 {
408 let mut writer = ChangesetOffsetWriter::new(&path, 2).unwrap();
409 assert_eq!(writer.len(), 2);
410
411 writer.append(&ChangesetOffset::new(15, 20)).unwrap();
413 writer.sync().unwrap();
414 assert_eq!(writer.len(), 3);
415 }
416
417 {
419 let reader = ChangesetOffsetReader::new(&path, 3).unwrap();
420 assert_eq!(reader.len(), 3);
421
422 let entry0 = reader.get(0).unwrap().unwrap();
423 assert_eq!(entry0.offset(), 0);
424 assert_eq!(entry0.num_changes(), 5);
425
426 let entry1 = reader.get(1).unwrap().unwrap();
427 assert_eq!(entry1.offset(), 5);
428 assert_eq!(entry1.num_changes(), 10);
429
430 let entry2 = reader.get(2).unwrap().unwrap();
432 assert_eq!(entry2.offset(), 15);
433 assert_eq!(entry2.num_changes(), 20); }
435 }
436
437 #[test]
438 fn test_sidecar_shorter_than_committed_errors() {
439 let dir = tempdir().unwrap();
441 let path = dir.path().join("test.csoff");
442
443 {
445 let mut writer = ChangesetOffsetWriter::new(&path, 0).unwrap();
446 writer.append(&ChangesetOffset::new(0, 5)).unwrap();
447 writer.sync().unwrap();
448 }
449
450 let result = ChangesetOffsetWriter::new(&path, 3);
452 assert!(result.is_err());
453 let err = result.unwrap_err();
454 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
455 }
456}