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#[derive(Clone, Debug)]
22pub struct CompactionOptions {
23 pub tombstone_grace: Duration,
25 pub min_bytes: u64,
27 pub max_segments: usize,
29 pub emit_snapshot: bool,
31}
32
33#[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 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 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 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 pub fn get_prefix(&self, prefix: &[u8]) -> Vec<(Vec<u8>, IndexEntry)> {
109 self.index.read().get_prefix(prefix)
110 }
111
112 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 pub fn segment_snapshot(&self) -> Vec<Arc<LogSegment>> {
120 self.segments.read().iter().cloned().collect()
121 }
122
123 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 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 pub fn sync(&self) -> Result<()> {
142 let active = { self.active_segment.read().clone() };
143 active.sync()
144 }
145
146 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 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 pub fn load_snapshot(&self) -> Result<Option<(KvState, u64)>> {
178 let snapshot = Snapshot::new(&self.snapshot_path);
179 snapshot.load()
180 }
181
182 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
246fn 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
277fn 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
283fn 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
292fn segment_path(dir: &Path, id: u64) -> PathBuf {
294 dir.join(format!("{SEGMENT_PREFIX}{id:020}{SEGMENT_SUFFIX}"))
295}
296
297fn 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
307fn 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
332fn 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
339struct 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 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}