Skip to main content

hightower_kv/
storage.rs

1use std::collections::{HashMap, HashSet};
2use std::fs::{create_dir_all, read_dir, remove_file};
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use parking_lot::{Mutex, RwLock};
8
9use crate::command::Command;
10use crate::config::StoreConfig;
11use crate::error::{Error, Result};
12use crate::index::{Index, IndexEntry};
13use crate::log_segment::{LogSegment, SegmentConfig};
14use crate::snapshot::Snapshot;
15use crate::state::KvState;
16
17const SEGMENT_PREFIX: &str = "segment-";
18const SEGMENT_SUFFIX: &str = ".log";
19
20/// Options for controlling log compaction behavior.
21#[derive(Clone, Debug)]
22pub struct CompactionOptions {
23    /// Minimum age before tombstones are permanently removed
24    pub tombstone_grace: Duration,
25    /// Minimum bytes to accumulate before compaction runs
26    pub min_bytes: u64,
27    /// Maximum number of segments to compact in one pass
28    pub max_segments: usize,
29    /// Whether to emit a snapshot after compaction
30    pub emit_snapshot: bool,
31}
32
33/// Log-structured storage engine with indexing and compaction.
34#[derive(Debug)]
35pub struct Storage {
36    config: StoreConfig,
37    index: RwLock<Index>,
38    segments: RwLock<Vec<Arc<LogSegment>>>,
39    active_segment: RwLock<Arc<LogSegment>>,
40    next_segment_id: Mutex<u64>,
41    snapshot_path: PathBuf,
42}
43
44impl Storage {
45    /// Creates a new storage instance, loading existing segments from disk.
46    pub fn new(config: &StoreConfig) -> Result<Self> {
47        let data_dir = PathBuf::from(&config.data_dir);
48        create_dir_all(&data_dir)?;
49
50        let mut segments = load_segments(&data_dir, config)?;
51        let next_segment_id = match segments.last() {
52            Some(segment) => segment.id() + 1,
53            None => {
54                let segment = Arc::new(open_segment(&data_dir, 1, config)?);
55                segments.push(segment.clone());
56                2
57            }
58        };
59
60        let active_segment = segments
61            .last()
62            .cloned()
63            .ok_or_else(|| Error::Unimplemented("storage::active_segment_missing"))?;
64        let index = rebuild_index(&segments)?;
65
66        let snapshot_path = data_dir.join("snapshot.bin");
67
68        Ok(Self {
69            config: config.clone(),
70            index: RwLock::new(index),
71            segments: RwLock::new(segments),
72            active_segment: RwLock::new(active_segment),
73            next_segment_id: Mutex::new(next_segment_id),
74            snapshot_path,
75        })
76    }
77
78    /// Appends a command to the active segment and updates the index.
79    pub fn apply(&self, command: &Command) -> Result<()> {
80        let active = { self.active_segment.read().clone() };
81        let position = active.append(command)?;
82        let entry = IndexEntry {
83            segment_id: active.id(),
84            offset: position.offset,
85            length: position.length,
86            version: command.version(),
87            is_tombstone: matches!(command, Command::Delete { .. }),
88        };
89        self.index.write().upsert(command.key().to_vec(), entry);
90        if active.bytes_written() >= self.config.max_segment_size {
91            self.roll_segment()?;
92        }
93        Ok(())
94    }
95
96    /// Looks up the index entry for a key, recovering from disk if needed.
97    pub fn lookup(&self, key: &[u8]) -> Option<IndexEntry> {
98        if let Some(entry) = self.index.read().get(key).cloned() {
99            return Some(entry);
100        }
101        match self.recover_index_entry(key) {
102            Ok(entry) => entry,
103            Err(_) => None,
104        }
105    }
106
107    /// Gets all key-entry pairs with the given prefix
108    pub fn get_prefix(&self, prefix: &[u8]) -> Vec<(Vec<u8>, IndexEntry)> {
109        self.index.read().get_prefix(prefix)
110    }
111
112    /// Reads the command corresponding to an index entry from its segment.
113    pub fn fetch_command(&self, entry: &IndexEntry) -> Result<Option<Command>> {
114        let segment = self.segment_for(entry.segment_id)?;
115        segment.read(entry.offset)
116    }
117
118    /// Returns a snapshot of all segments.
119    pub fn segment_snapshot(&self) -> Vec<Arc<LogSegment>> {
120        self.segments.read().iter().cloned().collect()
121    }
122
123    /// Returns a snapshot of sealed (non-active) segments.
124    pub(crate) fn sealed_segments_snapshot(&self) -> Vec<Arc<LogSegment>> {
125        let guard = self.segments.read();
126        if guard.len() <= 1 {
127            return Vec::new();
128        }
129        guard[..guard.len() - 1].to_vec()
130    }
131
132    /// Compacts sealed segments according to the provided options.
133    pub fn compact_all(&self, options: CompactionOptions) -> Result<bool> {
134        match SuspendedCompaction::prepare(self, options.clone())? {
135            Some(compaction) => compaction.execute(options.emit_snapshot),
136            None => Ok(false),
137        }
138    }
139
140    /// Syncs the active segment to disk.
141    pub fn sync(&self) -> Result<()> {
142        let active = { self.active_segment.read().clone() };
143        active.sync()
144    }
145
146    /// Rebuilds the current key-value state from the index.
147    pub fn state_snapshot(&self) -> KvState {
148        let index = self.index.read();
149        let mut state = KvState::new();
150        for (key, entry) in index.iter() {
151            if entry.is_tombstone {
152                state.apply(&Command::Delete {
153                    key: key.clone(),
154                    version: entry.version,
155                    timestamp: 0,
156                });
157                continue;
158            }
159            if let Ok(Some(command)) = self.fetch_command(entry) {
160                state.apply(&command);
161            }
162        }
163        state
164    }
165
166    /// Returns the highest version number in the index.
167    pub fn latest_version(&self) -> u64 {
168        self.index
169            .read()
170            .iter()
171            .map(|(_, entry)| entry.version)
172            .max()
173            .unwrap_or(0)
174    }
175
176    /// Loads a snapshot from disk if one exists.
177    pub fn load_snapshot(&self) -> Result<Option<(KvState, u64)>> {
178        let snapshot = Snapshot::new(&self.snapshot_path);
179        snapshot.load()
180    }
181
182    /// Replays all commands from all segments in order.
183    pub fn replay<F>(&self, mut visitor: F) -> Result<()>
184    where
185        F: FnMut(Command) -> Result<()>,
186    {
187        let segments = self.segments.read();
188        let mut ordered = segments.clone();
189        ordered.sort_by_key(|segment| segment.id());
190        for segment in ordered {
191            segment.scan(|_, command| visitor(command))?;
192        }
193        Ok(())
194    }
195
196    fn segment_for(&self, id: u64) -> Result<Arc<LogSegment>> {
197        let segments = self.segments.read();
198        segments
199            .iter()
200            .find(|segment| segment.id() == id)
201            .cloned()
202            .ok_or_else(|| Error::Unimplemented("storage::segment_not_found"))
203    }
204
205    fn roll_segment(&self) -> Result<()> {
206        let mut id_guard = self.next_segment_id.lock();
207        let id = *id_guard;
208        let dir = PathBuf::from(&self.config.data_dir);
209        let segment = Arc::new(open_segment(&dir, id, &self.config)?);
210        *id_guard = id
211            .checked_add(1)
212            .ok_or(Error::Unimplemented("storage::segment_id_overflow"))?;
213
214        let mut segments = self.segments.write();
215        segments.push(segment.clone());
216        *self.active_segment.write() = segment;
217        Ok(())
218    }
219}
220
221impl Storage {
222    fn recover_index_entry(&self, key: &[u8]) -> Result<Option<IndexEntry>> {
223        let segments = self.segments.read().clone();
224        for segment in segments.iter().rev() {
225            if !segment.might_contain(key) {
226                continue;
227            }
228            if let Some((position, command)) = segment.locate(key)? {
229                let entry = IndexEntry {
230                    segment_id: segment.id(),
231                    offset: position.offset,
232                    length: position.length,
233                    version: command.version(),
234                    is_tombstone: matches!(command, Command::Delete { .. }),
235                };
236                self.index
237                    .write()
238                    .upsert(command.key().to_vec(), entry.clone());
239                return Ok(Some(entry));
240            }
241        }
242        Ok(None)
243    }
244}
245
246/// Loads all log segments from the data directory.
247fn load_segments(dir: &Path, config: &StoreConfig) -> Result<Vec<Arc<LogSegment>>> {
248    if !dir.exists() {
249        return Ok(Vec::new());
250    }
251
252    let mut entries: Vec<(u64, PathBuf)> = Vec::new();
253    for entry in read_dir(dir)? {
254        let entry = entry?;
255        if !entry.file_type()?.is_file() {
256            continue;
257        }
258        let name = entry.file_name();
259        let name = match name.to_str() {
260            Some(name) => name,
261            None => continue,
262        };
263        if let Some(id) = parse_segment_id(name) {
264            entries.push((id, entry.path()));
265        }
266    }
267    entries.sort_by(|a, b| a.0.cmp(&b.0));
268
269    let mut segments = Vec::new();
270    for (id, path) in entries {
271        let segment = Arc::new(open_segment_at_path(id, path, config)?);
272        segments.push(segment);
273    }
274    Ok(segments)
275}
276
277/// Opens a segment with the given ID in the specified directory.
278fn open_segment(dir: &Path, id: u64, config: &StoreConfig) -> Result<LogSegment> {
279    let path = segment_path(dir, id);
280    open_segment_at_path(id, path, config)
281}
282
283/// Opens a segment at the specified path with the given ID.
284fn open_segment_at_path(id: u64, path: PathBuf, config: &StoreConfig) -> Result<LogSegment> {
285    let mut segment_config = SegmentConfig::new(id, path);
286    segment_config.sparse_every = 32;
287    segment_config.bloom_expected_items = normalized_item_estimate(config.max_segment_size);
288    segment_config.bloom_fp_rate = 0.01;
289    LogSegment::open(segment_config)
290}
291
292/// Constructs the file path for a segment with the given ID.
293fn segment_path(dir: &Path, id: u64) -> PathBuf {
294    dir.join(format!("{SEGMENT_PREFIX}{id:020}{SEGMENT_SUFFIX}"))
295}
296
297/// Extracts the segment ID from a filename if it matches the expected pattern.
298fn parse_segment_id(name: &str) -> Option<u64> {
299    if !name.starts_with(SEGMENT_PREFIX) || !name.ends_with(SEGMENT_SUFFIX) {
300        return None;
301    }
302    let start = SEGMENT_PREFIX.len();
303    let end = name.len() - SEGMENT_SUFFIX.len();
304    name[start..end].parse::<u64>().ok()
305}
306
307/// Rebuilds the index by scanning all segments.
308fn rebuild_index(segments: &[Arc<LogSegment>]) -> Result<Index> {
309    let mut builder = crate::index::IndexBuilder::new();
310    let mut ordered_segments = segments.to_vec();
311    ordered_segments.sort_by_key(|segment| segment.id());
312
313    for segment in ordered_segments {
314        let segment_id = segment.id();
315        segment.scan(|position, command| {
316            let key = command.key().to_vec();
317            let entry = IndexEntry {
318                segment_id,
319                offset: position.offset,
320                length: position.length,
321                version: command.version(),
322                is_tombstone: matches!(command, Command::Delete { .. }),
323            };
324            builder.insert(key, entry);
325            Ok(())
326        })?;
327    }
328
329    Ok(Index::rebuild(builder))
330}
331
332/// Estimates the number of items in a segment based on its max size.
333fn normalized_item_estimate(max_segment_size: u64) -> usize {
334    let bytes_per_entry = 256u64;
335    let estimate = (max_segment_size / bytes_per_entry).max(1);
336    estimate.min(usize::MAX as u64) as usize
337}
338
339/// A prepared compaction operation ready to execute.
340struct SuspendedCompaction<'a> {
341    storage: &'a Storage,
342    options: CompactionOptions,
343    sealed: Vec<Arc<LogSegment>>,
344    sealed_ids: HashSet<u64>,
345}
346
347impl<'a> SuspendedCompaction<'a> {
348    fn prepare(storage: &'a Storage, options: CompactionOptions) -> Result<Option<Self>> {
349        let all_sealed = storage.sealed_segments_snapshot();
350        if all_sealed.is_empty() {
351            return Ok(None);
352        }
353
354        let max_segments = options.max_segments;
355        let mut selected = Vec::new();
356        let mut accumulated_bytes = 0u64;
357
358        for segment in all_sealed.iter() {
359            if max_segments != 0 && selected.len() >= max_segments {
360                break;
361            }
362            accumulated_bytes += segment.bytes_written();
363            selected.push(segment.clone());
364            if options.min_bytes == 0 || accumulated_bytes >= options.min_bytes {
365                break;
366            }
367        }
368
369        if selected.is_empty() {
370            return Ok(None);
371        }
372
373        if options.min_bytes > 0 && accumulated_bytes < options.min_bytes {
374            return Ok(None);
375        }
376
377        let sealed_ids = selected.iter().map(|segment| segment.id()).collect();
378        Ok(Some(Self {
379            storage,
380            options,
381            sealed: selected,
382            sealed_ids,
383        }))
384    }
385
386    fn execute(self, emit_snapshot: bool) -> Result<bool> {
387        let SuspendedCompaction {
388            storage,
389            options,
390            sealed,
391            sealed_ids,
392        } = self;
393
394        let snapshot = storage.index.read().snapshot();
395        let mut latest_versions: HashMap<Vec<u8>, IndexEntry> = HashMap::new();
396        for (key, entry) in snapshot.iter() {
397            if sealed_ids.contains(&entry.segment_id) {
398                latest_versions.insert(key.clone(), entry.clone());
399            }
400        }
401        drop(snapshot);
402
403        if latest_versions.is_empty() {
404            return Ok(false);
405        }
406
407        let mut id_guard = storage.next_segment_id.lock();
408        let new_id = *id_guard;
409        *id_guard = new_id
410            .checked_add(1)
411            .ok_or(Error::Unimplemented("storage::segment_id_overflow"))?;
412        drop(id_guard);
413
414        let dir = PathBuf::from(&storage.config.data_dir);
415        let new_segment = Arc::new(open_segment(&dir, new_id, &storage.config)?);
416        let new_segment_path = new_segment.path().to_path_buf();
417        let tombstone_grace = options.tombstone_grace;
418        let now_secs = SystemTime::now()
419            .duration_since(UNIX_EPOCH)
420            .map(|dur| dur.as_secs())
421            .unwrap_or(0);
422        let grace_secs = tombstone_grace.as_secs();
423
424        let mut new_entries: Vec<(Vec<u8>, IndexEntry)> = Vec::new();
425        let mut evicted_keys: Vec<Vec<u8>> = Vec::new();
426        let mut wrote_any = false;
427
428        let old_paths: Vec<PathBuf> = sealed
429            .iter()
430            .map(|segment| segment.path().to_path_buf())
431            .collect();
432
433        for segment in &sealed {
434            let segment_id = segment.id();
435            segment.scan(|_, command| {
436                let entry = match latest_versions.get(command.key()) {
437                    Some(entry)
438                        if entry.segment_id == segment_id && entry.version == command.version() =>
439                    {
440                        entry.clone()
441                    }
442                    _ => return Ok(()),
443                };
444
445                latest_versions.remove(command.key());
446
447                if entry.is_tombstone && grace_secs > 0 {
448                    let timestamp = command.timestamp().max(0) as u64;
449                    if now_secs.saturating_sub(timestamp) >= grace_secs {
450                        evicted_keys.push(command.key().to_vec());
451                        return Ok(());
452                    }
453                }
454
455                let position = new_segment.append(&command)?;
456                new_entries.push((
457                    command.key().to_vec(),
458                    IndexEntry {
459                        segment_id: new_id,
460                        offset: position.offset,
461                        length: position.length,
462                        version: command.version(),
463                        is_tombstone: entry.is_tombstone,
464                    },
465                ));
466                wrote_any = true;
467                Ok(())
468            })?;
469        }
470
471        if !latest_versions.is_empty() {
472            let _ = remove_file(&new_segment_path);
473            return Ok(false);
474        }
475
476        if !wrote_any && evicted_keys.is_empty() {
477            let _ = remove_file(&new_segment_path);
478            return Ok(false);
479        }
480
481        new_segment.sync()?;
482
483        {
484            let mut segments_guard = storage.segments.write();
485            let sealed_len = sealed.len();
486            if segments_guard.len() < sealed_len + 1 {
487                drop(segments_guard);
488                let _ = remove_file(&new_segment_path);
489                return Ok(false);
490            }
491            let matches_snapshot = segments_guard
492                .iter()
493                .take(sealed_len)
494                .zip(sealed.iter())
495                .all(|(current, snapshot)| current.id() == snapshot.id());
496            if !matches_snapshot {
497                drop(segments_guard);
498                let _ = remove_file(&new_segment_path);
499                return Ok(false);
500            }
501            segments_guard.drain(..sealed_len);
502            let insert_pos = segments_guard.len().saturating_sub(1);
503            segments_guard.insert(insert_pos, new_segment.clone());
504        }
505
506        {
507            let mut index_guard = storage.index.write();
508            for key in evicted_keys {
509                index_guard.remove(key.as_slice());
510            }
511            for (key, entry) in new_entries {
512                index_guard.upsert(key, entry.clone());
513            }
514        }
515
516        for path in old_paths {
517            let _ = remove_file(path);
518        }
519
520        if emit_snapshot {
521            let state = storage.state_snapshot();
522            let last_version = storage.latest_version();
523            let snapshot = Snapshot::new(&storage.snapshot_path);
524            if let Err(err) = snapshot.write(&state, last_version) {
525                if !matches!(err, Error::Unimplemented(_)) {
526                    return Err(err);
527                }
528            }
529        }
530
531        Ok(true)
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538    use tempfile::tempdir;
539
540    fn temp_config(dir: &Path) -> StoreConfig {
541        let mut cfg = StoreConfig::default();
542        cfg.data_dir = dir.join("data").to_string_lossy().into_owned();
543        cfg
544    }
545
546    #[test]
547    fn apply_persists_and_indexes_command() {
548        let temp = tempdir().unwrap();
549        let cfg = temp_config(temp.path());
550        let storage = Storage::new(&cfg).unwrap();
551
552        let command = Command::Set {
553            key: b"alpha".to_vec(),
554            value: b"value".to_vec(),
555            version: 3,
556            timestamp: 1,
557        };
558        storage.apply(&command).unwrap();
559
560        let entry = storage.lookup(b"alpha").unwrap();
561        assert!(!entry.is_tombstone);
562        assert_eq!(entry.version, 3);
563        let stored = storage.fetch_command(&entry).unwrap().unwrap();
564        assert_eq!(stored, command);
565    }
566
567    #[test]
568    fn reopen_rebuilds_index_and_tombstones() {
569        let temp = tempdir().unwrap();
570        let cfg = temp_config(temp.path());
571        let storage = Storage::new(&cfg).unwrap();
572
573        storage
574            .apply(&Command::Set {
575                key: b"beta".to_vec(),
576                value: b"v1".to_vec(),
577                version: 10,
578                timestamp: 1,
579            })
580            .unwrap();
581        storage
582            .apply(&Command::Delete {
583                key: b"beta".to_vec(),
584                version: 11,
585                timestamp: 2,
586            })
587            .unwrap();
588        drop(storage);
589
590        let reopened = Storage::new(&cfg).unwrap();
591        let entry = reopened.lookup(b"beta").unwrap();
592        assert!(entry.is_tombstone);
593        assert_eq!(entry.version, 11);
594        let command = reopened.fetch_command(&entry).unwrap().unwrap();
595        assert!(matches!(command, Command::Delete { .. }));
596    }
597
598    #[test]
599    fn parse_segment_id_recognizes_pattern() {
600        assert_eq!(
601            parse_segment_id("segment-00000000000000012345.log"),
602            Some(12345)
603        );
604        assert_eq!(parse_segment_id("bad-name"), None);
605    }
606
607    #[test]
608    fn rolls_segment_when_capacity_exceeded() {
609        let temp = tempdir().unwrap();
610        let mut cfg = temp_config(temp.path());
611        cfg.max_segment_size = 120;
612        let storage = Storage::new(&cfg).unwrap();
613
614        let initial_id = storage.active_segment.read().id();
615        for i in 0..8 {
616            storage
617                .apply(&Command::Set {
618                    key: format!("key{i}").into_bytes(),
619                    value: vec![b'x'; 32],
620                    version: i + 1,
621                    timestamp: i as i64,
622                })
623                .unwrap();
624        }
625        let rolled_id = storage.active_segment.read().id();
626        assert!(rolled_id > initial_id, "expected segment rollover");
627    }
628
629    #[test]
630    fn replay_yields_commands_in_write_order() {
631        let temp = tempdir().unwrap();
632        let cfg = temp_config(temp.path());
633        let storage = Storage::new(&cfg).unwrap();
634
635        let commands = vec![
636            Command::Set {
637                key: b"a".to_vec(),
638                value: b"1".to_vec(),
639                version: 1,
640                timestamp: 1,
641            },
642            Command::Set {
643                key: b"b".to_vec(),
644                value: b"2".to_vec(),
645                version: 2,
646                timestamp: 2,
647            },
648            Command::Delete {
649                key: b"a".to_vec(),
650                version: 3,
651                timestamp: 3,
652            },
653        ];
654
655        for command in &commands {
656            storage.apply(command).unwrap();
657        }
658
659        let mut replayed = Vec::new();
660        storage
661            .replay(|command| {
662                replayed.push((command.key().to_vec(), command.version()));
663                Ok(())
664            })
665            .unwrap();
666
667        let expected: Vec<(Vec<u8>, u64)> = commands
668            .iter()
669            .map(|command| (command.key().to_vec(), command.version()))
670            .collect();
671        assert_eq!(replayed, expected);
672    }
673
674    #[test]
675    fn state_snapshot_reflects_latest_index() {
676        let temp = tempdir().unwrap();
677        let cfg = temp_config(temp.path());
678        let storage = Storage::new(&cfg).unwrap();
679
680        storage
681            .apply(&Command::Set {
682                key: b"a".to_vec(),
683                value: b"1".to_vec(),
684                version: 1,
685                timestamp: 1,
686            })
687            .unwrap();
688        storage
689            .apply(&Command::Delete {
690                key: b"a".to_vec(),
691                version: 2,
692                timestamp: 2,
693            })
694            .unwrap();
695        storage
696            .apply(&Command::Set {
697                key: b"b".to_vec(),
698                value: b"2".to_vec(),
699                version: 3,
700                timestamp: 3,
701            })
702            .unwrap();
703
704        let snapshot = storage.state_snapshot();
705        assert!(snapshot.get(b"a").is_none());
706        assert_eq!(snapshot.get(b"b"), Some(&b"2"[..]));
707    }
708
709    #[test]
710    fn lookup_recovers_entry_from_segments() {
711        let temp = tempdir().unwrap();
712        let cfg = temp_config(temp.path());
713        let storage = Storage::new(&cfg).unwrap();
714
715        let command = Command::Set {
716            key: b"recover".to_vec(),
717            value: b"value".to_vec(),
718            version: 42,
719            timestamp: 1,
720        };
721        storage.apply(&command).unwrap();
722
723        // Remove from hot index to simulate eviction.
724        storage.index.write().remove(b"recover");
725
726        let entry = storage.lookup(b"recover").unwrap();
727        assert_eq!(entry.version, 42);
728        let fetched = storage.fetch_command(&entry).unwrap().unwrap();
729        assert_eq!(fetched, command);
730    }
731}