1use std::fs::File;
9use std::io::{BufRead, BufReader, Seek, SeekFrom};
10use std::path::{Path, PathBuf};
11
12use kernel::Seq;
13
14use super::ledger::LedgerIndex;
15use crate::error::{StorageError, io_err};
16
17pub struct LineReader<'index> {
36 pub(crate) index: &'index LedgerIndex,
37 pub(crate) dir: PathBuf,
38 pub(crate) open: Option<OpenSegment>,
39}
40
41impl LineReader<'_> {
42 pub fn line_at(&mut self, seq: Seq) -> Result<Vec<u8>, StorageError> {
45 let Some((name, offset)) = self.index.folded.loc_of(seq) else {
46 return Err(StorageError::SeqMissing { seq: seq.value() });
47 };
48 let segment = match self.open.take() {
49 Some(open) if open.name == *name => open,
50 _ => OpenSegment::open(&self.dir, name)?,
51 };
52 self.open.insert(segment).line_at(offset)
53 }
54}
55
56pub(crate) struct OpenSegment {
59 name: String,
60 path: PathBuf,
61 file: BufReader<File>,
62 resume: Option<u64>,
67}
68
69impl OpenSegment {
70 fn open(dir: &Path, name: &str) -> Result<OpenSegment, StorageError> {
71 let path = dir.join(name);
72 let file = File::open(&path).map_err(io_err("open segment", &path))?;
73 Ok(OpenSegment {
74 name: name.to_owned(),
75 path,
76 file: BufReader::new(file),
77 resume: Some(0),
78 })
79 }
80
81 fn line_at(&mut self, offset: u64) -> Result<Vec<u8>, StorageError> {
82 if self.resume != Some(offset) {
83 let distance = self.resume.and_then(|at| {
87 i64::try_from(offset)
88 .ok()?
89 .checked_sub(i64::try_from(at).ok()?)
90 });
91 self.resume = None;
92 let moved = match distance {
93 Some(distance) => self.file.seek_relative(distance),
94 None => self.file.seek(SeekFrom::Start(offset)).map(|_start| ()),
95 };
96 moved.map_err(io_err("seek segment", &self.path))?;
97 }
98 self.resume = None;
99 let mut line = Vec::new();
100 let read = self
101 .file
102 .read_until(b'\n', &mut line)
103 .map_err(io_err("read segment", &self.path))?;
104 self.resume = u64::try_from(read)
105 .ok()
106 .and_then(|len| offset.checked_add(len));
107 if line.last().copied() == Some(b'\n') {
108 line.pop();
109 }
110 Ok(line)
111 }
112}
113
114#[cfg(test)]
115#[allow(
116 clippy::unwrap_used,
117 clippy::expect_used,
118 clippy::panic,
119 clippy::indexing_slicing,
120 reason = "test code"
121)]
122mod tests {
123 use super::*;
124 use kernel::{B3Hash, EventDraft, EventKind, EventRecord, Payload, RunId, TimeMs};
125 fn write_ledger_rolling(dir: &Path, count: u64, per_segment: u64) -> Vec<Vec<u8>> {
128 let run = RunId::from_bytes([7u8; 16]);
129 let mut lines = Vec::new();
130 let mut blob = Vec::new();
131 let mut first_seq = 0u64;
132 let mut prev = B3Hash::digest(b"");
133 for i in 0..count {
134 let draft = EventDraft {
135 run,
136 t: TimeMs::new(i),
137 who: "tester".to_owned(),
138 addr: None,
139 kind: EventKind::RunStarted,
140 data: Payload::new(serde_json::Map::new()).unwrap(),
141 ig: false,
142 };
143 let record = EventRecord::from_draft(draft, Seq::new(i), prev);
144 let line = record.canonical_line().unwrap();
145 prev = B3Hash::digest(&line);
146 blob.extend_from_slice(&line);
147 blob.push(b'\n');
148 lines.push(line);
149 let next = i.saturating_add(1);
150 if next.checked_rem(per_segment) == Some(0) && next < count {
151 std::fs::write(dir.join(format!("ledger-{first_seq:020}.jsonl")), &blob).unwrap();
152 blob.clear();
153 first_seq = next;
154 }
155 }
156 std::fs::write(dir.join(format!("ledger-{first_seq:020}.jsonl")), &blob).unwrap();
157 lines
158 }
159
160 fn write_interleaved(dir: &Path, runs: &[RunId], count: u64) -> Vec<(RunId, Vec<Seq>)> {
161 let mut blob = Vec::new();
162 let mut prev = B3Hash::digest(b"");
163 let mut owned: Vec<(RunId, Vec<Seq>)> = runs.iter().map(|run| (*run, Vec::new())).collect();
164 for i in 0..count {
165 let slot = usize::try_from(i)
166 .unwrap()
167 .checked_rem(runs.len())
168 .expect("at least one run to cycle through");
169 let run = runs[slot];
170 let draft = EventDraft {
171 run,
172 t: TimeMs::new(i),
173 who: "tester".to_owned(),
174 addr: None,
175 kind: EventKind::RunStarted,
176 data: Payload::new(serde_json::Map::new()).unwrap(),
177 ig: false,
178 };
179 let record = EventRecord::from_draft(draft, Seq::new(i), prev);
180 let line = record.canonical_line().unwrap();
181 prev = B3Hash::digest(&line);
182 blob.extend_from_slice(&line);
183 blob.push(b'\n');
184 owned[slot].1.push(Seq::new(i));
185 }
186 std::fs::write(dir.join("ledger-00000000000000000000.jsonl"), &blob).unwrap();
187 owned
188 }
189
190 #[test]
196 fn one_reader_answers_out_of_order_seeks_across_segments() {
197 let tmp = tempfile::tempdir().unwrap();
198 let lines = write_ledger_rolling(tmp.path(), 9, 4);
199 let index = LedgerIndex::rebuild(tmp.path()).unwrap();
200 assert_eq!(index.len(), 9, "three segments, nine lines");
201 let mut reader = index.reader(tmp.path());
202 for value in [8u64, 0, 8, 3, 4, 3, 7, 1, 1, 5] {
207 let got = reader.line_at(Seq::new(value)).unwrap();
208 let expected = &lines[usize::try_from(value).unwrap()];
209 assert_eq!(&got, expected, "seq {value} reads back verbatim");
210 }
211 }
212
213 #[test]
214 fn a_run_s_own_lines_are_found_without_walking_anybody_else_s() {
215 let tmp = tempfile::tempdir().unwrap();
216 let mine = RunId::from_bytes([1u8; 16]);
217 let yours = RunId::from_bytes([2u8; 16]);
218 let city = RunId::CITY;
219 let owned = write_interleaved(tmp.path(), &[mine, yours, city], 30);
220 let index = LedgerIndex::rebuild(tmp.path()).unwrap();
221
222 let all: Vec<Seq> = index.run_seqs_before(mine, None).collect();
223 let mut expected = owned[0].1.clone();
224 expected.reverse();
225 assert_eq!(all, expected, "newest first, and only this run's lines");
226
227 let newest_two: Vec<Seq> = index.run_seqs_before(mine, None).take(2).collect();
230 assert_eq!(newest_two, [Seq::new(27), Seq::new(24)]);
231
232 let older: Vec<Seq> = index
235 .run_seqs_before(mine, Some(Seq::new(24)))
236 .take(2)
237 .collect();
238 assert_eq!(older, [Seq::new(21), Seq::new(18)]);
239
240 let stranger: Vec<Seq> = index
243 .run_seqs_before(RunId::from_bytes([9u8; 16]), None)
244 .collect();
245 assert!(stranger.is_empty());
246
247 assert_eq!(index.run_seqs_before(city, None).count(), 10);
249 }
250
251 #[test]
252 fn the_run_map_survives_a_refresh() {
253 let tmp = tempfile::tempdir().unwrap();
254 let mine = RunId::from_bytes([1u8; 16]);
255 let yours = RunId::from_bytes([2u8; 16]);
256 write_interleaved(tmp.path(), &[mine, yours], 4);
257 let mut index = LedgerIndex::rebuild(tmp.path()).unwrap();
258 assert_eq!(index.run_seqs_before(mine, None).count(), 2);
259
260 write_interleaved(tmp.path(), &[mine, yours], 10);
263 index.refresh(tmp.path()).unwrap();
264 assert_eq!(index.run_seqs_before(mine, None).count(), 5);
265
266 let reopened = LedgerIndex::rebuild(tmp.path()).unwrap();
267 assert_eq!(reopened.len(), 10);
268 assert_eq!(reopened.run_seqs_before(mine, None).count(), 5);
269 }
270}