1use std::fs::{File, OpenOptions};
2use std::io::{self, Read, Seek, SeekFrom, Write};
3use std::path::{Path, PathBuf};
4
5use bloomfilter::Bloom;
6use parking_lot::Mutex;
7use serde::{Deserialize, Serialize};
8
9use crate::command::Command;
10use crate::error::{Error, Result};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub struct LogPosition {
15 pub offset: u64,
17 pub length: u32,
19}
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct SparseIndexEntry {
24 pub key: Vec<u8>,
26 pub offset: u64,
28 pub version: u64,
30}
31
32#[derive(Clone, Debug)]
34pub struct SegmentConfig {
35 pub id: u64,
37 pub path: PathBuf,
39 pub sparse_every: usize,
41 pub bloom_expected_items: usize,
43 pub bloom_fp_rate: f64,
45}
46
47impl SegmentConfig {
48 pub fn new(id: u64, path: impl Into<PathBuf>) -> Self {
50 Self {
51 id,
52 path: path.into(),
53 sparse_every: 32,
54 bloom_expected_items: 1024,
55 bloom_fp_rate: 0.01,
56 }
57 }
58
59 fn normalized_sparse_every(&self) -> usize {
60 self.sparse_every.max(1)
61 }
62
63 fn normalized_bloom_items(&self) -> usize {
64 self.bloom_expected_items.max(1)
65 }
66
67 fn normalized_fp_rate(&self) -> f64 {
68 let rate = self.bloom_fp_rate;
69 if !(0.0..1.0).contains(&rate) {
70 0.01
71 } else {
72 rate
73 }
74 }
75}
76
77#[derive(Debug)]
78struct SegmentMetadata {
79 bloom: Bloom<Vec<u8>>,
80 sparse_index: Vec<SparseIndexEntry>,
81 entries_written: u64,
82 bytes_written: u64,
83}
84
85impl SegmentMetadata {
86 fn new(config: &SegmentConfig) -> Self {
87 Self {
88 bloom: Bloom::new_for_fp_rate(
89 config.normalized_bloom_items() as usize,
90 config.normalized_fp_rate(),
91 ),
92 sparse_index: Vec::new(),
93 entries_written: 0,
94 bytes_written: 0,
95 }
96 }
97
98 fn record(&mut self, offset: u64, encoded_len: u32, command: &Command, sparse_every: usize) {
99 self.entries_written += 1;
100 self.bytes_written = offset + 4 + encoded_len as u64;
101 let key_vec = command.key().to_vec();
102 self.bloom.set(&key_vec);
103 if (self.entries_written - 1) % sparse_every as u64 == 0 {
104 self.sparse_index.push(SparseIndexEntry {
105 key: key_vec,
106 offset,
107 version: command.version(),
108 });
109 }
110 }
111}
112
113#[derive(Debug)]
115pub struct LogSegment {
116 id: u64,
117 path: PathBuf,
118 file: Mutex<File>,
119 metadata: Mutex<SegmentMetadata>,
120 sparse_every: usize,
121}
122
123impl LogSegment {
124 pub fn open(config: SegmentConfig) -> Result<Self> {
126 let mut options = OpenOptions::new();
127 options.read(true).write(true).create(true);
128 let path = config.path.clone();
129 let mut file = options.open(&path)?;
130 let mut metadata = SegmentMetadata::new(&config);
131 rebuild_metadata(&mut file, &mut metadata, config.normalized_sparse_every())?;
132 Ok(Self {
133 id: config.id,
134 path,
135 file: Mutex::new(file),
136 metadata: Mutex::new(metadata),
137 sparse_every: config.normalized_sparse_every(),
138 })
139 }
140
141 pub fn id(&self) -> u64 {
143 self.id
144 }
145
146 pub fn path(&self) -> &Path {
148 &self.path
149 }
150
151 pub fn bytes_written(&self) -> u64 {
153 self.metadata.lock().bytes_written
154 }
155
156 pub fn entries(&self) -> u64 {
158 self.metadata.lock().entries_written
159 }
160
161 pub fn sparse_index(&self) -> Vec<SparseIndexEntry> {
163 self.metadata.lock().sparse_index.clone()
164 }
165
166 pub fn might_contain(&self, key: &[u8]) -> bool {
168 self.metadata.lock().bloom.check(&key.to_vec())
169 }
170
171 pub fn locate(&self, key: &[u8]) -> Result<Option<(LogPosition, Command)>> {
173 if !self.might_contain(key) {
174 return Ok(None);
175 }
176
177 let start_offset = {
178 let metadata = self.metadata.lock();
179 let sparse = &metadata.sparse_index;
180 if sparse.is_empty() {
181 0u64
182 } else {
183 let idx = sparse.binary_search_by(|entry| entry.key.as_slice().cmp(key));
184 match idx {
185 Ok(i) => sparse[i].offset,
186 Err(0) => 0,
187 Err(i) => sparse[i - 1].offset,
188 }
189 }
190 };
191
192 let mut file = self.file.lock();
193 file.seek(SeekFrom::Start(start_offset))?;
194
195 let mut offset = start_offset;
196 let mut latest: Option<(LogPosition, Command)> = None;
197
198 loop {
199 let mut len_buf = [0u8; 4];
200 match file.read_exact(&mut len_buf) {
201 Ok(()) => {}
202 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
203 Err(err) => return Err(Error::Io(err)),
204 }
205 let len = u32::from_le_bytes(len_buf);
206 let mut buf = vec![0u8; len as usize];
207 file.read_exact(&mut buf)?;
208 let command: Command = serde_cbor::from_slice(&buf)
209 .map_err(|err| Error::Serialization(err.to_string()))?;
210 if command.key() == key {
211 latest = Some((
212 LogPosition {
213 offset,
214 length: len,
215 },
216 command,
217 ));
218 }
219 offset = offset
220 .checked_add(4 + len as u64)
221 .ok_or_else(|| Error::Serialization("log offset overflow during locate".into()))?;
222 }
223
224 Ok(latest)
225 }
226
227 pub fn append(&self, command: &Command) -> Result<LogPosition> {
229 let encoded =
230 serde_cbor::to_vec(command).map_err(|err| Error::Serialization(err.to_string()))?;
231 if encoded.len() > u32::MAX as usize {
232 return Err(Error::Serialization("command too large".into()));
233 }
234 let len = encoded.len() as u32;
235 let mut file = self.file.lock();
236 let offset = file.seek(SeekFrom::End(0))?;
237 file.write_all(&len.to_le_bytes())?;
238 file.write_all(&encoded)?;
239 file.flush()?;
240
241 let mut metadata = self.metadata.lock();
242 metadata.record(offset, len, command, self.sparse_every);
243 Ok(LogPosition {
244 offset,
245 length: len,
246 })
247 }
248
249 pub fn read(&self, offset: u64) -> Result<Option<Command>> {
251 let upper_bound = self.metadata.lock().bytes_written;
252 if offset >= upper_bound {
253 return Ok(None);
254 }
255 let mut file = self.file.lock();
256 file.seek(SeekFrom::Start(offset))?;
257 let mut len_buf = [0u8; 4];
258 if let Err(err) = file.read_exact(&mut len_buf) {
259 return if err.kind() == io::ErrorKind::UnexpectedEof {
260 Ok(None)
261 } else {
262 Err(Error::Io(err))
263 };
264 }
265 let len = u32::from_le_bytes(len_buf) as usize;
266 let mut buf = vec![0u8; len];
267 file.read_exact(&mut buf)?;
268 let command =
269 serde_cbor::from_slice(&buf).map_err(|err| Error::Serialization(err.to_string()))?;
270 Ok(Some(command))
271 }
272
273 pub fn scan<F>(&self, mut visitor: F) -> Result<()>
275 where
276 F: FnMut(LogPosition, Command) -> Result<()>,
277 {
278 let mut file = File::open(&self.path)?;
279 scan_file(&mut file, |offset, len, command| {
280 visitor(
281 LogPosition {
282 offset,
283 length: len,
284 },
285 command,
286 )
287 })
288 }
289
290 pub fn sync(&self) -> Result<()> {
292 let file = self.file.lock();
293 file.sync_data().map_err(Error::from)
294 }
295}
296
297fn rebuild_metadata(
298 file: &mut File,
299 metadata: &mut SegmentMetadata,
300 sparse_every: usize,
301) -> Result<()> {
302 scan_file(file, |offset, len, command| {
303 metadata.record(offset, len, &command, sparse_every);
304 Ok(())
305 })
306}
307
308fn scan_file<F>(file: &mut File, mut visitor: F) -> Result<()>
309where
310 F: FnMut(u64, u32, Command) -> Result<()>,
311{
312 file.seek(SeekFrom::Start(0))?;
313 let mut offset = 0u64;
314 loop {
315 let mut len_buf = [0u8; 4];
316 match file.read_exact(&mut len_buf) {
317 Ok(()) => {}
318 Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => break,
319 Err(err) => return Err(Error::Io(err)),
320 }
321 let len = u32::from_le_bytes(len_buf);
322 let mut buf = vec![0u8; len as usize];
323 file.read_exact(&mut buf)?;
324 let command: Command =
325 serde_cbor::from_slice(&buf).map_err(|err| Error::Serialization(err.to_string()))?;
326 visitor(offset, len, command)?;
327 offset += 4 + len as u64;
328 }
329 file.seek(SeekFrom::Start(offset))?;
330 Ok(())
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::command::Command;
337 use tempfile::tempdir;
338
339 #[test]
340 fn append_and_read_roundtrip() {
341 let dir = tempdir().unwrap();
342 let path = dir.path().join("segment.log");
343 let segment = LogSegment::open(SegmentConfig::new(1, &path)).unwrap();
344
345 let command = Command::Set {
346 key: b"key".to_vec(),
347 value: b"value".to_vec(),
348 version: 42,
349 timestamp: 1,
350 };
351 let position = segment.append(&command).unwrap();
352 let read_back = segment.read(position.offset).unwrap().unwrap();
353 assert_eq!(read_back, command);
354 assert!(segment.might_contain(b"key"));
355 assert_eq!(segment.entries(), 1);
356 }
357
358 #[test]
359 fn rebuild_metadata_from_existing_file() {
360 let dir = tempdir().unwrap();
361 let path = dir.path().join("segment.log");
362 let cfg = SegmentConfig {
363 sparse_every: 1,
364 ..SegmentConfig::new(5, &path)
365 };
366 let segment = LogSegment::open(cfg.clone()).unwrap();
367 let command = Command::Delete {
368 key: b"alpha".to_vec(),
369 version: 7,
370 timestamp: 2,
371 };
372 let position = segment.append(&command).unwrap();
373 drop(segment);
374
375 let reopened = LogSegment::open(cfg).unwrap();
376 assert_eq!(reopened.entries(), 1);
377 let index = reopened.sparse_index();
378 assert_eq!(index.len(), 1);
379 assert_eq!(index[0].offset, position.offset);
380 assert_eq!(index[0].version, 7);
381 assert!(reopened.read(position.offset).unwrap().is_some());
382 }
383
384 #[test]
385 fn read_out_of_bounds_returns_none() {
386 let dir = tempdir().unwrap();
387 let path = dir.path().join("segment.log");
388 let segment = LogSegment::open(SegmentConfig::new(2, &path)).unwrap();
389 assert!(segment.read(1024).unwrap().is_none());
390 }
391
392 #[test]
393 fn locate_finds_latest_entry_for_key() {
394 let dir = tempdir().unwrap();
395 let path = dir.path().join("segment.log");
396 let mut cfg = SegmentConfig::new(7, &path);
397 cfg.sparse_every = 2;
398 let segment = LogSegment::open(cfg).unwrap();
399
400 for version in 0..5u64 {
401 let command = Command::Set {
402 key: b"target".to_vec(),
403 value: vec![version as u8],
404 version,
405 timestamp: version as i64,
406 };
407 segment.append(&command).unwrap();
408 }
409
410 let command = Command::Set {
411 key: b"other".to_vec(),
412 value: b"value".to_vec(),
413 version: 9,
414 timestamp: 9,
415 };
416 segment.append(&command).unwrap();
417
418 let located = segment.locate(b"target").unwrap().unwrap();
419 assert_eq!(located.1.version(), 4);
420 assert_eq!(located.1.key(), b"target");
421
422 assert!(segment.locate(b"missing").unwrap().is_none());
423 }
424}