Skip to main content

hightower_kv/
log_segment.rs

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/// Position of a command entry within a log segment
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub struct LogPosition {
15    /// Byte offset from the start of the segment file
16    pub offset: u64,
17    /// Length of the serialized command in bytes
18    pub length: u32,
19}
20
21/// Entry in the sparse index that aids in binary search during lookups
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct SparseIndexEntry {
24    /// Key for this index entry
25    pub key: Vec<u8>,
26    /// Byte offset where this entry starts in the segment
27    pub offset: u64,
28    /// Version number of the command
29    pub version: u64,
30}
31
32/// Configuration for creating or opening a log segment
33#[derive(Clone, Debug)]
34pub struct SegmentConfig {
35    /// Unique identifier for this segment
36    pub id: u64,
37    /// File system path where the segment is stored
38    pub path: PathBuf,
39    /// How often to record a sparse index entry (every N entries)
40    pub sparse_every: usize,
41    /// Expected number of items for bloom filter sizing
42    pub bloom_expected_items: usize,
43    /// False positive rate for bloom filter
44    pub bloom_fp_rate: f64,
45}
46
47impl SegmentConfig {
48    /// Creates a new segment configuration with default settings
49    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/// Append-only log segment that stores serialized commands with bloom filter and sparse index
114#[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    /// Opens or creates a log segment from the given configuration
125    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    /// Returns the unique identifier for this segment
142    pub fn id(&self) -> u64 {
143        self.id
144    }
145
146    /// Returns the file system path of this segment
147    pub fn path(&self) -> &Path {
148        &self.path
149    }
150
151    /// Returns the total number of bytes written to this segment
152    pub fn bytes_written(&self) -> u64 {
153        self.metadata.lock().bytes_written
154    }
155
156    /// Returns the number of entries written to this segment
157    pub fn entries(&self) -> u64 {
158        self.metadata.lock().entries_written
159    }
160
161    /// Returns a clone of the sparse index for this segment
162    pub fn sparse_index(&self) -> Vec<SparseIndexEntry> {
163        self.metadata.lock().sparse_index.clone()
164    }
165
166    /// Checks the bloom filter to see if this segment might contain the given key
167    pub fn might_contain(&self, key: &[u8]) -> bool {
168        self.metadata.lock().bloom.check(&key.to_vec())
169    }
170
171    /// Locates the latest command for the given key, returning its position and value
172    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    /// Appends a command to the end of the segment and returns its position
228    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    /// Reads a command from the segment at the specified offset
250    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    /// Scans all entries in the segment, calling the visitor function for each
274    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    /// Syncs the segment file to disk
291    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}