1use crate::error::{Error, Result};
5#[cfg(feature = "test-hooks")]
6use crate::kv::hooks::{CrashOperation, CrashSimulator, CrashTiming, IoHooks};
7use crate::kv::{KVStore, KVTransaction};
8use crate::log::wal::{WalReader, WalRecord, WalWriter};
9use crate::storage::flush::write_empty_vector_segment;
10use crate::storage::sstable::{SstableReader, SstableWriter};
11use crate::txn::TxnManager;
12use crate::types::{Key, TxnId, TxnMode, TxnState, Value};
13use std::collections::{BTreeMap, HashMap};
14use std::ops::Bound::{Excluded, Included};
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
17use std::sync::{Arc, RwLock, RwLockReadGuard};
18use tracing::warn;
19
20#[derive(Debug, Clone, Default)]
22pub struct MemoryStats {
23 pub total_bytes: usize,
25 pub kv_bytes: usize,
27 pub index_bytes: usize,
29}
30
31#[derive(Clone)]
33pub struct MemoryKV {
34 manager: Arc<MemoryTxnManager>,
35}
36
37impl MemoryKV {
38 pub fn new() -> Self {
40 Self {
41 manager: Arc::new(MemoryTxnManager::new(None, None, None)),
42 }
43 }
44
45 pub fn memory_stats(&self) -> MemoryStats {
47 self.manager.memory_stats()
48 }
49
50 pub fn new_with_limit(limit: Option<usize>) -> Self {
52 Self {
53 manager: Arc::new(MemoryTxnManager::new_with_limit(limit)),
54 }
55 }
56
57 pub fn open(path: &Path) -> Result<Self> {
59 let wal_writer = WalWriter::new(path)?;
60 let sstable_path = path.with_extension("sst");
61 let manager = Arc::new(MemoryTxnManager::new(
62 Some(wal_writer),
63 Some(path.to_path_buf()),
64 Some(sstable_path),
65 ));
66 manager.recover()?;
67 Ok(Self { manager })
68 }
69
70 #[cfg(feature = "test-hooks")]
72 pub fn open_with_io_hooks(path: &Path, hooks: Arc<dyn IoHooks>) -> Result<Self> {
73 let wal_writer = WalWriter::new(path)?;
74 let sstable_path = path.with_extension("sst");
75 let manager = Arc::new(MemoryTxnManager::new(
76 Some(wal_writer),
77 Some(path.to_path_buf()),
78 Some(sstable_path),
79 ));
80 manager.set_io_hooks(Some(hooks));
81 manager.recover()?;
82 Ok(Self { manager })
83 }
84
85 #[cfg(feature = "test-hooks")]
87 pub fn open_with_crash_hooks(path: &Path, crash_sim: Arc<CrashSimulator>) -> Result<Self> {
88 let wal_writer = WalWriter::new(path)?;
89 let sstable_path = path.with_extension("sst");
90 let manager = Arc::new(MemoryTxnManager::new(
91 Some(wal_writer),
92 Some(path.to_path_buf()),
93 Some(sstable_path),
94 ));
95 manager.set_crash_sim(Some(crash_sim));
96 manager.recover()?;
97 Ok(Self { manager })
98 }
99
100 pub fn flush(&self) -> Result<()> {
102 self.manager.flush()
103 }
104}
105
106impl Default for MemoryKV {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl KVStore for MemoryKV {
113 type Transaction<'a> = MemoryTransaction<'a>;
114 type Manager<'a> = &'a MemoryTxnManager;
115
116 fn txn_manager(&self) -> Self::Manager<'_> {
117 &self.manager
118 }
119
120 fn begin(&self, mode: TxnMode) -> Result<Self::Transaction<'_>> {
121 self.manager.begin_internal(mode)
122 }
123}
124
125type VersionedValue = (Value, u64);
127
128struct MemorySharedState {
130 data: RwLock<BTreeMap<Key, VersionedValue>>,
132 next_txn_id: AtomicU64,
134 commit_version: AtomicU64,
136 wal_writer: Option<RwLock<WalWriter>>,
138 wal_path: Option<PathBuf>,
140 sstable: RwLock<Option<SstableReader>>,
142 sstable_path: Option<PathBuf>,
144 memory_limit: RwLock<Option<usize>>,
146 current_memory: AtomicUsize,
148 #[cfg(feature = "test-hooks")]
149 io_hooks: RwLock<Option<Arc<dyn IoHooks>>>,
151 #[cfg(feature = "test-hooks")]
152 crash_sim: RwLock<Option<Arc<CrashSimulator>>>,
154}
155
156impl MemorySharedState {
157 fn check_memory_limit(&self, additional: usize) -> Result<()> {
159 if let Some(limit) = *self.memory_limit.read().unwrap() {
160 let current = self.current_memory.load(Ordering::Relaxed);
161 let requested = current.saturating_add(additional);
162 if requested > limit {
163 return Err(Error::MemoryLimitExceeded { limit, requested });
164 }
165 }
166 Ok(())
167 }
168
169 fn memory_stats(&self) -> MemoryStats {
171 let kv_bytes = self.current_memory.load(Ordering::Relaxed);
172 MemoryStats {
173 total_bytes: kv_bytes,
174 kv_bytes,
175 index_bytes: 0,
176 }
177 }
178
179 fn recompute_current_memory(&self) {
181 let data = self.data.read().unwrap();
182 let mut total = 0usize;
183 for (k, (v, _)) in data.iter() {
184 total = total.saturating_add(k.len() + v.len());
185 }
186 self.current_memory.store(total, Ordering::Relaxed);
187 }
188}
189
190pub struct MemoryTxnManager {
192 state: Arc<MemorySharedState>,
193}
194
195impl MemoryTxnManager {
196 fn new_with_params(
197 wal_writer: Option<WalWriter>,
198 wal_path: Option<PathBuf>,
199 sstable_path: Option<PathBuf>,
200 memory_limit: Option<usize>,
201 ) -> Self {
202 Self {
203 state: Arc::new(MemorySharedState {
204 data: RwLock::new(BTreeMap::new()),
205 next_txn_id: AtomicU64::new(1),
206 commit_version: AtomicU64::new(0),
207 wal_writer: wal_writer.map(RwLock::new),
208 wal_path,
209 sstable: RwLock::new(None),
210 sstable_path,
211 memory_limit: RwLock::new(memory_limit),
212 current_memory: AtomicUsize::new(0),
213 #[cfg(feature = "test-hooks")]
214 io_hooks: RwLock::new(None),
215 #[cfg(feature = "test-hooks")]
216 crash_sim: RwLock::new(None),
217 }),
218 }
219 }
220
221 fn new(
222 wal_writer: Option<WalWriter>,
223 wal_path: Option<PathBuf>,
224 sstable_path: Option<PathBuf>,
225 ) -> Self {
226 Self::new_with_params(wal_writer, wal_path, sstable_path, None)
227 }
228
229 pub fn new_with_limit(limit: Option<usize>) -> Self {
231 Self::new_with_params(None, None, None, limit)
232 }
233
234 #[cfg(feature = "test-hooks")]
235 fn set_io_hooks(&self, hooks: Option<Arc<dyn IoHooks>>) {
236 let mut guard = self.state.io_hooks.write().unwrap();
237 *guard = hooks;
238 }
239
240 #[cfg(feature = "test-hooks")]
241 fn set_crash_sim(&self, crash_sim: Option<Arc<CrashSimulator>>) {
242 let mut guard = self.state.crash_sim.write().unwrap();
243 *guard = crash_sim;
244 }
245
246 #[cfg(feature = "test-hooks")]
247 fn io_hooks(&self) -> Option<Arc<dyn IoHooks>> {
248 self.state.io_hooks.read().unwrap().clone()
249 }
250
251 #[cfg(feature = "test-hooks")]
252 fn crash_sim(&self) -> Option<Arc<CrashSimulator>> {
253 self.state.crash_sim.read().unwrap().clone()
254 }
255
256 pub fn memory_stats(&self) -> MemoryStats {
258 self.state.memory_stats()
259 }
260
261 pub fn set_memory_limit(&self, limit: Option<usize>) {
263 let mut guard = self.state.memory_limit.write().unwrap();
264 *guard = limit;
265 }
266
267 pub fn snapshot(&self) -> Vec<(Key, Value)> {
269 let data = self.state.data.read().unwrap();
270 data.iter()
271 .map(|(k, (v, _))| (k.clone(), v.clone()))
272 .collect()
273 }
274
275 pub fn clear_all(&self) {
277 let mut data = self.state.data.write().unwrap();
278 data.clear();
279 drop(data);
280 self.state.current_memory.store(0, Ordering::Relaxed);
281 self.state.commit_version.store(0, Ordering::Relaxed);
282 }
283
284 pub fn compact_with_limit<F>(
287 &self,
288 input_bytes: usize,
289 output_bytes: usize,
290 run: F,
291 ) -> Result<bool>
292 where
293 F: FnOnce() -> Result<()>,
294 {
295 if let Some(limit) = *self.state.memory_limit.read().unwrap() {
296 let current = self.state.current_memory.load(Ordering::Relaxed);
297 let prospective = current
299 .saturating_sub(input_bytes)
300 .saturating_add(output_bytes);
301 if prospective > limit {
302 warn!(
303 limit,
304 requested = prospective,
305 "compaction skipped due to memory limit"
306 );
307 return Ok(false);
308 }
309 }
310
311 run()?;
312
313 let current = self.state.current_memory.load(Ordering::Relaxed);
315 let new_usage = current
316 .saturating_sub(input_bytes)
317 .saturating_add(output_bytes);
318 self.state
319 .current_memory
320 .store(new_usage, Ordering::Relaxed);
321 Ok(true)
322 }
323
324 #[cfg(feature = "test-hooks")]
325 fn trigger_crash(&self, operation: CrashOperation, timing: CrashTiming) {
326 if let Some(sim) = self.crash_sim() {
327 sim.check_crash(operation, timing);
328 }
329 }
330
331 #[cfg(feature = "test-hooks")]
332 fn notify_wal_hooks(&self, data: &[u8], timing: CrashTiming) -> Result<()> {
333 match timing {
334 CrashTiming::Before => {
335 self.trigger_crash(CrashOperation::WalWrite, CrashTiming::Before);
336 if let Some(hooks) = self.io_hooks() {
337 hooks.before_wal_write(data).map_err(Error::Io)?;
338 hooks.before_fsync().map_err(Error::Io)?;
339 }
340 self.trigger_crash(CrashOperation::WalFsync, CrashTiming::Before);
341 }
342 CrashTiming::During => {
343 self.trigger_crash(CrashOperation::WalWrite, CrashTiming::During);
344 self.trigger_crash(CrashOperation::WalFsync, CrashTiming::During);
345 }
346 CrashTiming::After => {
347 self.trigger_crash(CrashOperation::WalWrite, CrashTiming::After);
348 self.trigger_crash(CrashOperation::WalFsync, CrashTiming::After);
349 if let Some(hooks) = self.io_hooks() {
350 hooks.after_wal_write(data).map_err(Error::Io)?;
351 hooks.after_fsync().map_err(Error::Io)?;
352 }
353 }
354 }
355 Ok(())
356 }
357
358 #[cfg(feature = "test-hooks")]
359 fn notify_compaction(&self, timing: CrashTiming) {
360 self.trigger_crash(CrashOperation::Compaction, timing);
361 if let Some(hooks) = self.io_hooks() {
362 match timing {
363 CrashTiming::Before => hooks.on_compaction_start(),
364 CrashTiming::After => hooks.on_compaction_end(),
365 CrashTiming::During => {}
366 }
367 }
368 }
369
370 fn append_wal_record(&self, wal: &mut WalWriter, record: &WalRecord) -> Result<()> {
371 #[cfg(feature = "test-hooks")]
372 {
373 let data =
374 bincode::serialize(record).map_err(|e| Error::Io(std::io::Error::other(e)))?;
375 self.notify_wal_hooks(&data, CrashTiming::Before)?;
376 self.notify_wal_hooks(&data, CrashTiming::During)?;
377 wal.append(record)?;
378 self.notify_wal_hooks(&data, CrashTiming::After)?;
379 Ok(())
380 }
381
382 #[cfg(not(feature = "test-hooks"))]
383 {
384 wal.append(record)
385 }
386 }
387
388 fn write_wal(&self, txn_id: TxnId, writes: &BTreeMap<Key, Option<Value>>) -> Result<()> {
389 if let Some(wal_lock) = &self.state.wal_writer {
390 let mut wal = wal_lock.write().unwrap();
391 self.append_wal_record(&mut wal, &WalRecord::Begin(txn_id))?;
392 for (key, value) in writes {
393 let record = match value {
394 Some(v) => WalRecord::Put(txn_id, key.clone(), v.clone()),
395 None => WalRecord::Delete(txn_id, key.clone()),
396 };
397 self.append_wal_record(&mut wal, &record)?;
398 }
399 self.append_wal_record(&mut wal, &WalRecord::Commit(txn_id))?;
400 wal.sync()?;
404 }
405 Ok(())
406 }
407
408 pub fn compact_in_memory(&self) -> Result<bool> {
410 #[cfg(feature = "test-hooks")]
411 self.notify_compaction(CrashTiming::Before);
412
413 let snapshot_bytes = {
414 let data = self.state.data.read().unwrap();
415 let mut bytes = 0usize;
416 for (k, (v, _)) in data.iter() {
417 bytes = bytes.saturating_add(k.len() + v.len());
418 }
419 bytes
420 };
421
422 let executed = self.compact_with_limit(snapshot_bytes, snapshot_bytes, || {
423 let data = self.state.data.read().unwrap();
424 let mut rebuilt = BTreeMap::new();
425 for (k, (v, version)) in data.iter() {
426 rebuilt.insert(k.clone(), (v.clone(), *version));
427 }
428 drop(data);
429
430 #[cfg(feature = "test-hooks")]
431 self.notify_compaction(CrashTiming::During);
432
433 let mut write_guard = self.state.data.write().unwrap();
434 *write_guard = rebuilt;
435 Ok(())
436 })?;
437
438 #[cfg(feature = "test-hooks")]
439 self.notify_compaction(CrashTiming::After);
440
441 Ok(executed)
442 }
443
444 pub fn flush(&self) -> Result<()> {
446 let Some(path) = self.state.sstable_path.as_ref() else {
447 return Ok(());
448 };
449
450 #[cfg(feature = "test-hooks")]
451 self.notify_compaction(CrashTiming::Before);
452
453 let data = self.state.data.read().unwrap();
454 let mut writer = SstableWriter::create(path)?;
455 for (key, (value, _version)) in data.iter() {
456 #[cfg(feature = "test-hooks")]
457 {
458 let mut record = Vec::with_capacity(key.len() + value.len());
459 record.extend_from_slice(key);
460 record.extend_from_slice(value);
461 self.trigger_crash(CrashOperation::SstWrite, CrashTiming::Before);
462 if let Some(hooks) = self.io_hooks() {
463 hooks.before_sst_write(&record).map_err(Error::Io)?;
464 }
465 self.trigger_crash(CrashOperation::SstWrite, CrashTiming::During);
466 }
467
468 writer.append(key, value)?;
469
470 #[cfg(feature = "test-hooks")]
471 self.trigger_crash(CrashOperation::SstWrite, CrashTiming::After);
472 }
473 drop(data);
474
475 #[cfg(feature = "test-hooks")]
476 self.trigger_crash(CrashOperation::SstFinalize, CrashTiming::Before);
477 let _footer = writer.finish()?;
478 #[cfg(feature = "test-hooks")]
479 self.trigger_crash(CrashOperation::SstFinalize, CrashTiming::After);
480 let reader = SstableReader::open(path)?;
481 let vec_path = path.with_extension("vec");
483 write_empty_vector_segment(&vec_path)?;
484
485 let mut slot = self.state.sstable.write().unwrap();
486 *slot = Some(reader);
487
488 #[cfg(feature = "test-hooks")]
489 self.notify_compaction(CrashTiming::After);
490 Ok(())
491 }
492
493 fn replay(&self) -> Result<()> {
495 let path = match &self.state.wal_path {
496 Some(p) => p,
497 None => return Ok(()),
498 };
499 if !path.exists() || std::fs::metadata(path)?.len() == 0 {
500 return Ok(());
501 }
502
503 let mut data = self.state.data.write().unwrap();
504 let mut max_txn_id = 0;
505 let mut max_version = self.state.commit_version.load(Ordering::Acquire);
506 let reader = WalReader::new(path)?;
507 let mut pending_txns: HashMap<TxnId, Vec<(Key, Option<Value>)>> = HashMap::new();
508
509 for record_result in reader {
510 match record_result? {
511 WalRecord::Begin(txn_id) => {
512 max_txn_id = max_txn_id.max(txn_id.0);
513 pending_txns.entry(txn_id).or_default();
514 }
515 WalRecord::Put(txn_id, key, value) => {
516 max_txn_id = max_txn_id.max(txn_id.0);
517 pending_txns
518 .entry(txn_id)
519 .or_default()
520 .push((key, Some(value)));
521 }
522 WalRecord::Delete(txn_id, key) => {
523 max_txn_id = max_txn_id.max(txn_id.0);
524 pending_txns.entry(txn_id).or_default().push((key, None));
525 }
526 WalRecord::Commit(txn_id) => {
527 if let Some(writes) = pending_txns.remove(&txn_id) {
528 max_version += 1;
529 for (key, value) in writes {
530 if let Some(v) = value {
531 data.insert(key, (v, max_version));
532 } else {
533 data.remove(&key);
534 }
535 }
536 }
537 }
538 }
539 }
540
541 self.state
542 .next_txn_id
543 .store(max_txn_id + 1, Ordering::SeqCst);
544 self.state
545 .commit_version
546 .store(max_version, Ordering::SeqCst);
547 Ok(())
548 }
549
550 fn load_sstable(&self) -> Result<()> {
551 let path = match &self.state.sstable_path {
552 Some(p) => p,
553 None => return Ok(()),
554 };
555 if !path.exists() {
556 return Ok(());
557 }
558
559 let mut reader = match SstableReader::open(path) {
560 Ok(reader) => reader,
561 Err(e @ (Error::InvalidFormat(_) | Error::ChecksumMismatch)) => {
568 warn!(
569 path = %path.display(),
570 error = %e,
571 "discarding unreadable SSTable during recovery; replaying WAL only"
572 );
573 return Ok(());
574 }
575 Err(Error::Io(io)) if io.kind() == std::io::ErrorKind::UnexpectedEof => {
576 warn!(
577 path = %path.display(),
578 "discarding truncated SSTable during recovery; replaying WAL only"
579 );
580 return Ok(());
581 }
582 Err(e) => return Err(e),
583 };
584 let mut data = self.state.data.write().unwrap();
585 let mut version = self.state.commit_version.load(Ordering::Acquire);
586
587 let keys: Vec<Key> = reader
588 .index()
589 .iter()
590 .map(|entry| entry.key.clone())
591 .collect();
592
593 for key in keys {
594 if let Some(value) = reader.get(&key)? {
595 version += 1;
596 data.insert(key, (value, version));
597 }
598 }
599
600 self.state.commit_version.store(version, Ordering::SeqCst);
601 let mut slot = self.state.sstable.write().unwrap();
602 *slot = Some(reader);
603 Ok(())
604 }
605
606 fn recover(&self) -> Result<()> {
608 self.load_sstable()?;
609 self.replay()?;
610 self.state.recompute_current_memory();
611 Ok(())
612 }
613
614 fn sstable_get(&self, key: &Key) -> Result<Option<Value>> {
615 let mut guard = self.state.sstable.write().unwrap();
616 if let Some(reader) = guard.as_mut() {
617 return reader.get(key);
618 }
619 Ok(None)
620 }
621
622 fn begin_internal(&self, mode: TxnMode) -> Result<MemoryTransaction<'_>> {
623 let txn_id = self.state.next_txn_id.fetch_add(1, Ordering::SeqCst);
624 let start_version = self.state.commit_version.load(Ordering::Acquire);
625 Ok(MemoryTransaction::new(
626 self,
627 TxnId(txn_id),
628 mode,
629 start_version,
630 ))
631 }
632}
633
634impl<'a> TxnManager<'a, MemoryTransaction<'a>> for &'a MemoryTxnManager {
635 fn begin(&'a self, mode: TxnMode) -> Result<MemoryTransaction<'a>> {
636 self.begin_internal(mode)
637 }
638
639 fn commit(&'a self, mut txn: MemoryTransaction<'a>) -> Result<()> {
640 if txn.state != TxnState::Active {
641 return Err(Error::TxnClosed);
642 }
643 if txn.mode == TxnMode::ReadOnly || txn.writes.is_empty() {
644 txn.state = TxnState::Committed;
645 return Ok(());
646 }
647
648 let mut data = self.state.data.write().unwrap();
649
650 for key in txn.read_set.keys() {
651 let current_version = data.get(key).map(|(_, v)| *v).unwrap_or(0);
652 if current_version > txn.start_version {
653 return Err(Error::TxnConflict);
654 }
655 }
656
657 for key in txn.writes.keys() {
659 let current_version = data.get(key).map(|(_, v)| *v).unwrap_or(0);
660 if current_version > txn.start_version {
661 return Err(Error::TxnConflict);
662 }
663 }
664
665 let mut delta: isize = 0;
667 for (key, value) in &txn.writes {
668 let current_size = data.get(key).map(|(v, _)| key.len() + v.len()).unwrap_or(0);
669 let new_size = match value {
670 Some(v) => key.len() + v.len(),
671 None => 0,
672 };
673 delta += new_size as isize - current_size as isize;
674 }
675
676 let current_mem = self.state.current_memory.load(Ordering::Relaxed);
677 let prospective = if delta >= 0 {
678 current_mem.saturating_add(delta as usize)
679 } else {
680 current_mem.saturating_sub(delta.unsigned_abs())
681 };
682
683 if delta > 0 {
684 self.state.check_memory_limit(delta as usize)?;
685 }
686
687 let commit_version = self.state.commit_version.fetch_add(1, Ordering::AcqRel) + 1;
688
689 self.write_wal(txn.id, &txn.writes)?;
690
691 for (key, value) in std::mem::take(&mut txn.writes) {
692 if let Some(v) = value {
693 data.insert(key, (v, commit_version));
694 } else {
695 data.remove(&key);
696 }
697 }
698
699 self.state
700 .current_memory
701 .store(prospective, Ordering::Relaxed);
702
703 txn.state = TxnState::Committed;
704 Ok(())
705 }
706
707 fn rollback(&'a self, mut txn: MemoryTransaction<'a>) -> Result<()> {
708 if txn.state != TxnState::Active {
709 return Err(Error::TxnClosed);
710 }
711 txn.state = TxnState::RolledBack;
712 Ok(())
713 }
714}
715
716pub struct MemoryTransaction<'a> {
718 manager: &'a MemoryTxnManager,
719 id: TxnId,
720 mode: TxnMode,
721 state: TxnState,
722 start_version: u64,
723 writes: BTreeMap<Key, Option<Value>>,
724 read_set: HashMap<Key, u64>,
725}
726
727impl<'a> MemoryTransaction<'a> {
728 fn new(manager: &'a MemoryTxnManager, id: TxnId, mode: TxnMode, start_version: u64) -> Self {
729 Self {
730 manager,
731 id,
732 mode,
733 state: TxnState::Active,
734 start_version,
735 writes: BTreeMap::new(),
736 read_set: HashMap::new(),
737 }
738 }
739
740 fn ensure_active(&self) -> Result<()> {
741 if self.state != TxnState::Active {
742 return Err(Error::TxnClosed);
743 }
744 Ok(())
745 }
746
747 pub(crate) fn rollback_in_place(&mut self) -> Result<()> {
749 if self.state != TxnState::Active {
750 return Err(Error::TxnClosed);
751 }
752 self.state = TxnState::RolledBack;
753 Ok(())
754 }
755
756 fn scan_range_internal(&mut self, start: &[u8], end: &[u8]) -> MergedScanIter<'_> {
757 let start_vec = start.to_vec();
758 let end_vec = end.to_vec();
759 let data_guard = self.manager.state.data.read().unwrap();
760 let data_ptr: *const BTreeMap<Key, VersionedValue> = &*data_guard;
761 let data_iter = unsafe {
762 (&*data_ptr).range((Included(start_vec.clone()), Excluded(end_vec.clone())))
764 };
765 let write_iter = self
766 .writes
767 .range((Included(start_vec.clone()), Excluded(end_vec.clone())));
768
769 MergedScanIter::new(
770 data_guard,
771 data_iter,
772 write_iter,
773 None,
774 Some(end_vec),
775 self.start_version,
776 &mut self.read_set,
777 )
778 }
779
780 fn scan_prefix_internal(&mut self, prefix: &[u8]) -> MergedScanIter<'_> {
781 let prefix_vec = prefix.to_vec();
782 let data_guard = self.manager.state.data.read().unwrap();
783 let data_ptr: *const BTreeMap<Key, VersionedValue> = &*data_guard;
784 let data_iter = unsafe {
785 (&*data_ptr).range(prefix_vec.clone()..)
787 };
788 let write_iter = self.writes.range(prefix_vec.clone()..);
789 MergedScanIter::new(
790 data_guard,
791 data_iter,
792 write_iter,
793 Some(prefix_vec),
794 None,
795 self.start_version,
796 &mut self.read_set,
797 )
798 }
799}
800
801impl<'a> KVTransaction<'a> for MemoryTransaction<'a> {
802 fn id(&self) -> TxnId {
803 self.id
804 }
805
806 fn mode(&self) -> TxnMode {
807 self.mode
808 }
809
810 fn get(&mut self, key: &Key) -> Result<Option<Value>> {
811 if self.state != TxnState::Active {
812 return Err(Error::TxnClosed);
813 }
814
815 if let Some(value) = self.writes.get(key) {
816 return Ok(value.clone());
817 }
818
819 let result = {
820 let data = self.manager.state.data.read().unwrap();
821 data.get(key).cloned()
822 };
823
824 if let Some((v, version)) = result {
825 self.read_set.insert(key.clone(), version);
826 return Ok(Some(v));
827 }
828
829 if let Some(value) = self.manager.sstable_get(key)? {
831 let version = self.manager.state.commit_version.load(Ordering::Acquire);
832 self.read_set.insert(key.clone(), version);
833 return Ok(Some(value));
834 }
835
836 Ok(None)
837 }
838
839 fn put(&mut self, key: Key, value: Value) -> Result<()> {
840 if self.state != TxnState::Active {
841 return Err(Error::TxnClosed);
842 }
843 if self.mode == TxnMode::ReadOnly {
844 return Err(Error::TxnReadOnly);
845 }
846 self.writes.insert(key, Some(value));
847 Ok(())
848 }
849
850 fn delete(&mut self, key: Key) -> Result<()> {
851 if self.state != TxnState::Active {
852 return Err(Error::TxnClosed);
853 }
854 if self.mode == TxnMode::ReadOnly {
855 return Err(Error::TxnReadOnly);
856 }
857 self.writes.insert(key, None);
858 Ok(())
859 }
860
861 fn scan_prefix(
862 &mut self,
863 prefix: &[u8],
864 ) -> Result<Box<dyn Iterator<Item = (Key, Value)> + '_>> {
865 self.ensure_active()?;
866 let iter = self
867 .scan_prefix_internal(prefix)
868 .filter_map(|(k, v)| v.map(|val| (k, val)));
869 Ok(Box::new(iter))
870 }
871
872 fn scan_range(
873 &mut self,
874 start: &[u8],
875 end: &[u8],
876 ) -> Result<Box<dyn Iterator<Item = (Key, Value)> + '_>> {
877 self.ensure_active()?;
878 let iter = self
879 .scan_range_internal(start, end)
880 .filter_map(|(k, v)| v.map(|val| (k, val)));
881 Ok(Box::new(iter))
882 }
883
884 fn commit_self(mut self) -> Result<()> {
885 if self.state != TxnState::Active {
886 return Err(Error::TxnClosed);
887 }
888 if self.mode == TxnMode::ReadOnly || self.writes.is_empty() {
889 self.state = TxnState::Committed;
890 return Ok(());
891 }
892
893 let mut data = self.manager.state.data.write().unwrap();
894
895 for key in self.read_set.keys() {
897 let current_version = data.get(key).map(|(_, v)| *v).unwrap_or(0);
898 if current_version > self.start_version {
899 return Err(Error::TxnConflict);
900 }
901 }
902
903 for key in self.writes.keys() {
905 let current_version = data.get(key).map(|(_, v)| *v).unwrap_or(0);
906 if current_version > self.start_version {
907 return Err(Error::TxnConflict);
908 }
909 }
910
911 let mut delta: isize = 0;
913 for (key, value) in &self.writes {
914 let current_size = data.get(key).map(|(v, _)| key.len() + v.len()).unwrap_or(0);
915 let new_size = match value {
916 Some(v) => key.len() + v.len(),
917 None => 0,
918 };
919 delta += new_size as isize - current_size as isize;
920 }
921
922 let current_mem = self.manager.state.current_memory.load(Ordering::Relaxed);
923 let prospective = if delta >= 0 {
924 current_mem.saturating_add(delta as usize)
925 } else {
926 current_mem.saturating_sub(delta.unsigned_abs())
927 };
928
929 if delta > 0 {
930 self.manager.state.check_memory_limit(delta as usize)?;
931 }
932
933 let commit_version = self
934 .manager
935 .state
936 .commit_version
937 .fetch_add(1, Ordering::AcqRel)
938 + 1;
939
940 self.manager.write_wal(self.id, &self.writes)?;
942
943 for (key, value) in std::mem::take(&mut self.writes) {
945 if let Some(v) = value {
946 data.insert(key, (v, commit_version));
947 } else {
948 data.remove(&key);
949 }
950 }
951
952 self.manager
953 .state
954 .current_memory
955 .store(prospective, Ordering::Relaxed);
956
957 self.state = TxnState::Committed;
958 Ok(())
959 }
960
961 fn rollback_self(mut self) -> Result<()> {
962 if self.state != TxnState::Active {
963 return Err(Error::TxnClosed);
964 }
965 self.state = TxnState::RolledBack;
966 Ok(())
967 }
968}
969
970struct MergedScanIter<'a> {
972 _data_guard: RwLockReadGuard<'a, BTreeMap<Key, VersionedValue>>,
973 data_iter: std::collections::btree_map::Range<'a, Key, VersionedValue>,
974 write_iter: std::collections::btree_map::Range<'a, Key, Option<Value>>,
975 data_peek: Option<(Key, (Value, u64))>,
976 write_peek: Option<(Key, Option<Value>)>,
977 prefix: Option<Vec<u8>>,
978 end: Option<Key>,
979 start_version: u64,
980 read_set: &'a mut HashMap<Key, u64>,
981}
982
983impl<'a> MergedScanIter<'a> {
984 #[allow(clippy::too_many_arguments)]
985 fn new(
986 data_guard: std::sync::RwLockReadGuard<'a, BTreeMap<Key, VersionedValue>>,
987 data_iter: std::collections::btree_map::Range<'a, Key, VersionedValue>,
988 write_iter: std::collections::btree_map::Range<'a, Key, Option<Value>>,
989 prefix: Option<Vec<u8>>,
990 end: Option<Key>,
991 start_version: u64,
992 read_set: &'a mut HashMap<Key, u64>,
993 ) -> Self {
994 let mut iter = Self {
995 _data_guard: data_guard,
996 data_iter,
997 write_iter,
998 data_peek: None,
999 write_peek: None,
1000 prefix,
1001 end,
1002 start_version,
1003 read_set,
1004 };
1005 iter.advance_data();
1006 iter.advance_write();
1007 iter
1008 }
1009
1010 fn advance_data(&mut self) {
1011 self.data_peek = None;
1012 while let Some((k, (v, ver))) = self.data_iter.next().map(|(k, v)| (k.clone(), v.clone())) {
1013 if let Some(end) = &self.end {
1014 if k >= *end {
1015 return;
1016 }
1017 }
1018 if let Some(prefix) = &self.prefix {
1019 if !k.starts_with(prefix) {
1020 return;
1021 }
1022 }
1023 if ver > self.start_version {
1024 continue;
1025 }
1026 self.data_peek = Some((k, (v, ver)));
1027 return;
1028 }
1029 }
1030
1031 fn advance_write(&mut self) {
1032 self.write_peek = None;
1033 if let Some((k, v)) = self.write_iter.next().map(|(k, v)| (k.clone(), v.clone())) {
1034 if let Some(end) = &self.end {
1035 if k >= *end {
1036 return;
1037 }
1038 }
1039 if let Some(prefix) = &self.prefix {
1040 if !k.starts_with(prefix) {
1041 return;
1042 }
1043 }
1044 self.write_peek = Some((k, v));
1045 }
1046 }
1047}
1048
1049impl<'a> Iterator for MergedScanIter<'a> {
1050 type Item = (Key, Option<Value>);
1051
1052 fn next(&mut self) -> Option<Self::Item> {
1053 let data_key = self.data_peek.as_ref().map(|(k, _)| k.clone());
1054 let write_key = self.write_peek.as_ref().map(|(k, _)| k.clone());
1055
1056 match (data_key, write_key) {
1057 (Some(dk), Some(wk)) => {
1058 if dk == wk {
1059 let (_, (_, ver)) = self.data_peek.take().unwrap();
1060 let (_, write_val) = self.write_peek.take().unwrap();
1061 self.read_set.insert(dk.clone(), ver);
1062 self.advance_data();
1063 self.advance_write();
1064 Some((dk, write_val))
1065 } else if dk < wk {
1066 let (k, (v, ver)) = self.data_peek.take().unwrap();
1067 self.read_set.insert(k.clone(), ver);
1068 self.advance_data();
1069 Some((k, Some(v)))
1070 } else {
1071 let (k, write_val) = self.write_peek.take().unwrap();
1072 self.advance_write();
1073 Some((k, write_val))
1074 }
1075 }
1076 (Some(_), None) => {
1077 let (k, (v, ver)) = self.data_peek.take().unwrap();
1078 self.read_set.insert(k.clone(), ver);
1079 self.advance_data();
1080 Some((k, Some(v)))
1081 }
1082 (None, Some(_)) => {
1083 let (k, write_val) = self.write_peek.take().unwrap();
1084 self.advance_write();
1085 Some((k, write_val))
1086 }
1087 (None, None) => None,
1088 }
1089 }
1090}
1091
1092impl<'a> Drop for MemoryTransaction<'a> {
1093 fn drop(&mut self) {
1094 if self.state == TxnState::Active {
1095 self.state = TxnState::RolledBack;
1096 }
1097 }
1098}
1099
1100#[cfg(all(test, not(target_arch = "wasm32")))]
1101mod tests {
1102 use super::*;
1103 use crate::{KVTransaction, TxnManager};
1104 use tempfile::tempdir;
1105 use tracing::Level;
1106
1107 fn key(s: &str) -> Key {
1108 s.as_bytes().to_vec()
1109 }
1110
1111 fn value(s: &str) -> Value {
1112 s.as_bytes().to_vec()
1113 }
1114
1115 fn committed_value_after_reopen(wal_path: &Path, key: Key) -> Option<Value> {
1116 let reopened = MemoryKV::open(wal_path).unwrap();
1117 let manager = reopened.txn_manager();
1118 let mut txn = manager.begin(TxnMode::ReadOnly).unwrap();
1119 txn.get(&key).unwrap()
1120 }
1121
1122 fn write_flush_and_corrupt_sstable<F>(corrupt: F) -> (tempfile::TempDir, PathBuf)
1123 where
1124 F: FnOnce(&Path),
1125 {
1126 let dir = tempdir().unwrap();
1127 let wal_path = dir.path().join("wal.log");
1128 {
1129 let store = MemoryKV::open(&wal_path).unwrap();
1130 let manager = store.txn_manager();
1131 let mut txn = manager.begin(TxnMode::ReadWrite).unwrap();
1132 txn.put(key("k1"), value("v1")).unwrap();
1133 manager.commit(txn).unwrap();
1134 store.flush().unwrap();
1135 }
1136
1137 corrupt(&wal_path.with_extension("sst"));
1138 (dir, wal_path)
1139 }
1140
1141 #[cfg(feature = "test-hooks")]
1142 struct FailsBeforeFsync;
1143
1144 #[cfg(feature = "test-hooks")]
1145 impl IoHooks for FailsBeforeFsync {
1146 fn before_fsync(&self) -> std::io::Result<()> {
1147 Err(std::io::Error::other("injected WAL fsync failure"))
1148 }
1149 }
1150
1151 #[cfg(feature = "test-hooks")]
1152 #[test]
1153 fn commit_self_wal_fsync_failure_does_not_ack_or_apply() {
1154 let dir = tempdir().unwrap();
1155 let wal_path = dir.path().join("wal.log");
1156 let store = MemoryKV::open_with_io_hooks(&wal_path, Arc::new(FailsBeforeFsync)).unwrap();
1157 let manager = store.txn_manager();
1158
1159 let mut txn = manager.begin(TxnMode::ReadWrite).unwrap();
1160 txn.put(key("not-acked"), value("value")).unwrap();
1161 let result = txn.commit_self();
1162 assert!(matches!(result, Err(Error::Io(_))));
1163
1164 let mut read_txn = manager.begin(TxnMode::ReadOnly).unwrap();
1165 assert_eq!(read_txn.get(&key("not-acked")).unwrap(), None);
1166 }
1167
1168 #[test]
1169 fn test_put_and_get_transient() {
1170 let store = MemoryKV::new();
1171 let manager = store.txn_manager();
1172 let mut txn = manager.begin(TxnMode::ReadWrite).unwrap();
1173 txn.put(key("hello"), value("world")).unwrap();
1174 let val = txn.get(&key("hello")).unwrap();
1175 assert_eq!(val, Some(value("world")));
1176 manager.commit(txn).unwrap();
1177
1178 let mut txn2 = manager.begin(TxnMode::ReadOnly).unwrap();
1179 let val2 = txn2.get(&key("hello")).unwrap();
1180 assert_eq!(val2, Some(value("world")));
1181 }
1182
1183 #[test]
1184 fn test_occ_conflict() {
1185 let store = MemoryKV::new();
1186 let manager = store.txn_manager();
1187
1188 let mut t1 = manager.begin(TxnMode::ReadWrite).unwrap();
1189 t1.get(&key("k1")).unwrap();
1190
1191 let mut t2 = manager.begin(TxnMode::ReadWrite).unwrap();
1192 t2.put(key("k1"), value("v2")).unwrap();
1193 assert!(manager.commit(t2).is_ok());
1194
1195 t1.put(key("k1"), value("v1")).unwrap();
1196 let result = manager.commit(t1);
1197 assert!(matches!(result, Err(Error::TxnConflict)));
1198 }
1199
1200 #[test]
1201 fn test_blind_write_conflict() {
1202 let store = MemoryKV::new();
1203 let manager = store.txn_manager();
1204
1205 let mut t1 = manager.begin(TxnMode::ReadWrite).unwrap();
1206 t1.put(key("k1"), value("v1")).unwrap();
1207
1208 let mut t2 = manager.begin(TxnMode::ReadWrite).unwrap();
1209 t2.put(key("k1"), value("v2")).unwrap();
1210 assert!(manager.commit(t2).is_ok());
1211
1212 let result = manager.commit(t1);
1213 assert!(matches!(result, Err(Error::TxnConflict)));
1214 }
1215
1216 #[test]
1217 fn test_read_only_write_fails() {
1218 let store = MemoryKV::new();
1219 let manager = store.txn_manager();
1220 let mut txn = manager.begin(TxnMode::ReadOnly).unwrap();
1221 assert!(matches!(
1222 txn.put(key("k1"), value("v1")),
1223 Err(Error::TxnReadOnly)
1224 ));
1225 assert!(matches!(txn.delete(key("k1")), Err(Error::TxnReadOnly)));
1226 }
1227
1228 #[test]
1229 fn test_txn_closed_error() {
1230 let store = MemoryKV::new();
1231 let manager = store.txn_manager();
1232 let txn = manager.begin(TxnMode::ReadWrite).unwrap();
1233 manager.commit(txn).unwrap();
1234
1235 let mut closed_txn = manager.begin(TxnMode::ReadWrite).unwrap();
1238 closed_txn.state = TxnState::Committed;
1239 assert!(matches!(closed_txn.get(&key("k1")), Err(Error::TxnClosed)));
1240 assert!(matches!(
1241 closed_txn.put(key("k1"), value("v1")),
1242 Err(Error::TxnClosed)
1243 ));
1244 }
1245
1246 #[test]
1247 fn test_get_not_found() {
1248 let store = MemoryKV::new();
1249 let manager = store.txn_manager();
1250 let mut txn = manager.begin(TxnMode::ReadOnly).unwrap();
1251 let res = txn.get(&key("non-existent"));
1252 assert!(res.is_ok());
1253 assert!(res.unwrap().is_none());
1254 }
1255
1256 #[test]
1257 fn flush_and_reopen_reads_from_sstable() {
1258 let dir = tempdir().unwrap();
1259 let wal_path = dir.path().join("wal.log");
1260 {
1261 let store = MemoryKV::open(&wal_path).unwrap();
1262 let manager = store.txn_manager();
1263 let mut txn = manager.begin(TxnMode::ReadWrite).unwrap();
1264 txn.put(key("k1"), value("v1")).unwrap();
1265 manager.commit(txn).unwrap();
1266 store.flush().unwrap();
1267 }
1268
1269 let reopened = MemoryKV::open(&wal_path).unwrap();
1270 let manager = reopened.txn_manager();
1271 let mut txn = manager.begin(TxnMode::ReadOnly).unwrap();
1272 assert_eq!(txn.get(&key("k1")).unwrap(), Some(value("v1")));
1273 }
1274
1275 #[test]
1276 fn corrupt_sstable_header_is_discarded_and_wal_recovers() {
1277 let (_dir, wal_path) = write_flush_and_corrupt_sstable(|sst_path| {
1278 let mut file = std::fs::OpenOptions::new()
1279 .write(true)
1280 .open(sst_path)
1281 .unwrap();
1282 use std::io::{Seek, Write};
1283 file.seek(std::io::SeekFrom::Start(0)).unwrap();
1284 file.write_all(b"BAD!").unwrap();
1285 file.sync_all().unwrap();
1286 });
1287 let err = SstableReader::open(&wal_path.with_extension("sst")).unwrap_err();
1288 assert!(matches!(err, Error::InvalidFormat(_)));
1289
1290 assert_eq!(
1291 committed_value_after_reopen(&wal_path, key("k1")),
1292 Some(value("v1"))
1293 );
1294 }
1295
1296 #[test]
1297 fn corrupt_sstable_payload_checksum_is_discarded_and_wal_recovers() {
1298 let (_dir, wal_path) = write_flush_and_corrupt_sstable(|sst_path| {
1299 let mut file = std::fs::OpenOptions::new()
1300 .read(true)
1301 .write(true)
1302 .open(sst_path)
1303 .unwrap();
1304 use std::io::{Read, Seek, Write};
1305 file.seek(std::io::SeekFrom::Start(16 + 8 + key("k1").len() as u64))
1306 .unwrap();
1307 let mut byte = [0u8; 1];
1308 file.read_exact(&mut byte).unwrap();
1309 file.seek(std::io::SeekFrom::Current(-1)).unwrap();
1310 file.write_all(&[byte[0] ^ 0xFF]).unwrap();
1311 file.sync_all().unwrap();
1312 });
1313 let err = SstableReader::open(&wal_path.with_extension("sst")).unwrap_err();
1314 assert!(matches!(err, Error::ChecksumMismatch));
1315
1316 assert_eq!(
1317 committed_value_after_reopen(&wal_path, key("k1")),
1318 Some(value("v1"))
1319 );
1320 }
1321
1322 #[test]
1323 fn truncated_sstable_is_discarded_and_wal_recovers() {
1324 let (_dir, wal_path) = write_flush_and_corrupt_sstable(|sst_path| {
1325 let file = std::fs::OpenOptions::new()
1326 .write(true)
1327 .open(sst_path)
1328 .unwrap();
1329 file.set_len(16).unwrap();
1330 file.sync_all().unwrap();
1331 });
1332 let err = SstableReader::open(&wal_path.with_extension("sst")).unwrap_err();
1333 assert!(matches!(err, Error::InvalidFormat(_)));
1334
1335 assert_eq!(
1336 committed_value_after_reopen(&wal_path, key("k1")),
1337 Some(value("v1"))
1338 );
1339 }
1340
1341 #[test]
1342 fn wal_recovers_committed_tombstone_on_reopen() {
1343 let dir = tempdir().unwrap();
1344 let wal_path = dir.path().join("wal.log");
1345 {
1346 let store = MemoryKV::open(&wal_path).unwrap();
1347 let manager = store.txn_manager();
1348 let mut put_txn = manager.begin(TxnMode::ReadWrite).unwrap();
1349 put_txn.put(key("deleted"), value("value")).unwrap();
1350 manager.commit(put_txn).unwrap();
1351
1352 let mut delete_txn = manager.begin(TxnMode::ReadWrite).unwrap();
1353 delete_txn.delete(key("deleted")).unwrap();
1354 manager.commit(delete_txn).unwrap();
1355 }
1356
1357 assert_eq!(
1358 committed_value_after_reopen(&wal_path, key("deleted")),
1359 None
1360 );
1361 }
1362
1363 #[test]
1364 fn wal_overlays_sstable_on_reopen() {
1365 let dir = tempdir().unwrap();
1366 let wal_path = dir.path().join("wal.log");
1367 {
1368 let store = MemoryKV::open(&wal_path).unwrap();
1369 let manager = store.txn_manager();
1370 let mut txn = manager.begin(TxnMode::ReadWrite).unwrap();
1371 txn.put(key("k1"), value("v1")).unwrap();
1372 manager.commit(txn).unwrap();
1373 store.flush().unwrap();
1374
1375 let mut txn2 = manager.begin(TxnMode::ReadWrite).unwrap();
1376 txn2.put(key("k1"), value("v2")).unwrap();
1377 manager.commit(txn2).unwrap();
1378 }
1379
1380 let reopened = MemoryKV::open(&wal_path).unwrap();
1381 let manager = reopened.txn_manager();
1382 let mut txn = manager.begin(TxnMode::ReadOnly).unwrap();
1383 assert_eq!(txn.get(&key("k1")).unwrap(), Some(value("v2")));
1384 }
1385
1386 #[test]
1387 fn scan_prefix_merges_snapshot_and_writes() {
1388 let store = MemoryKV::new();
1389 let manager = store.txn_manager();
1390
1391 let mut seed = manager.begin(TxnMode::ReadWrite).unwrap();
1392 seed.put(key("p:1"), value("old1")).unwrap();
1393 seed.put(key("p:2"), value("old2")).unwrap();
1394 seed.put(key("q:1"), value("other")).unwrap();
1395 manager.commit(seed).unwrap();
1396
1397 let mut txn = manager.begin(TxnMode::ReadWrite).unwrap();
1398 txn.put(key("p:1"), value("new1")).unwrap();
1399 txn.delete(key("p:2")).unwrap();
1400 txn.put(key("p:3"), value("new3")).unwrap();
1401
1402 let results: Vec<_> = txn.scan_prefix(b"p:").unwrap().collect();
1403 assert_eq!(
1404 results,
1405 vec![(key("p:1"), value("new1")), (key("p:3"), value("new3"))]
1406 );
1407 }
1408
1409 #[test]
1410 fn scan_range_skips_newer_versions() {
1411 let store = MemoryKV::new();
1412 let manager = store.txn_manager();
1413
1414 let mut seed = manager.begin(TxnMode::ReadWrite).unwrap();
1415 seed.put(key("b"), value("v1")).unwrap();
1416 manager.commit(seed).unwrap();
1417
1418 let mut txn1 = manager.begin(TxnMode::ReadWrite).unwrap();
1419
1420 let mut txn2 = manager.begin(TxnMode::ReadWrite).unwrap();
1421 txn2.put(key("ba"), value("v2")).unwrap();
1422 manager.commit(txn2).unwrap();
1423
1424 let results: Vec<_> = txn1.scan_range(b"b", b"c").unwrap().collect();
1425 assert_eq!(results, vec![(key("b"), value("v1"))]);
1426 }
1427
1428 #[test]
1429 fn scan_range_records_reads_for_conflict_detection() {
1430 let store = MemoryKV::new();
1431 let manager = store.txn_manager();
1432
1433 let mut seed = manager.begin(TxnMode::ReadWrite).unwrap();
1434 seed.put(key("k1"), value("v1")).unwrap();
1435 manager.commit(seed).unwrap();
1436
1437 let mut t1 = manager.begin(TxnMode::ReadWrite).unwrap();
1438 let results: Vec<_> = t1.scan_range(b"k0", b"kz").unwrap().collect();
1439 assert_eq!(results, vec![(key("k1"), value("v1"))]);
1440 t1.put(key("k_new"), value("v_new")).unwrap();
1441
1442 let mut t2 = manager.begin(TxnMode::ReadWrite).unwrap();
1443 t2.put(key("k1"), value("v2")).unwrap();
1444 manager.commit(t2).unwrap();
1445
1446 let result = manager.commit(t1);
1447 assert!(matches!(result, Err(Error::TxnConflict)));
1448 }
1449
1450 #[test]
1451 fn memory_stats_tracks_put_and_delete() {
1452 let store = MemoryKV::new();
1453 let manager = store.txn_manager();
1454
1455 let stats = manager.memory_stats();
1456 assert_eq!(stats.total_bytes, 0);
1457 assert_eq!(stats.kv_bytes, 0);
1458 assert_eq!(stats.index_bytes, 0);
1459
1460 let mut txn = manager.begin(TxnMode::ReadWrite).unwrap();
1462 txn.put(key("a"), value("1234")).unwrap(); manager.commit(txn).unwrap();
1464
1465 let stats = manager.memory_stats();
1466 assert_eq!(stats.total_bytes, 5);
1467 assert_eq!(stats.kv_bytes, 5);
1468 assert_eq!(stats.index_bytes, 0);
1469
1470 let mut txn = manager.begin(TxnMode::ReadWrite).unwrap();
1472 txn.delete(key("a")).unwrap();
1473 manager.commit(txn).unwrap();
1474
1475 let stats = manager.memory_stats();
1476 assert_eq!(stats.total_bytes, 0);
1477 assert_eq!(stats.kv_bytes, 0);
1478 }
1479
1480 #[test]
1481 fn memory_limit_error_does_not_break_reads() {
1482 let store = MemoryKV::new_with_limit(Some(10));
1483 let manager = store.txn_manager();
1484
1485 let mut txn = manager.begin_internal(TxnMode::ReadWrite).unwrap();
1487 txn.put(key("k1"), value("vvvv")).unwrap();
1488 manager.commit(txn).unwrap();
1489
1490 let mut txn2 = manager.begin_internal(TxnMode::ReadWrite).unwrap();
1492 txn2.put(key("k2"), value("vvvvvv")).unwrap();
1493 let result = manager.commit(txn2);
1494 assert!(matches!(result, Err(Error::MemoryLimitExceeded { .. })));
1495
1496 let mut read_txn = manager.begin_internal(TxnMode::ReadOnly).unwrap();
1498 let got = read_txn.get(&key("k1")).unwrap();
1499 assert_eq!(got, Some(value("vvvv")));
1500
1501 let stats = manager.memory_stats();
1503 assert_eq!(stats.total_bytes, 6);
1504 }
1505
1506 struct VecWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
1507
1508 impl std::io::Write for VecWriter {
1509 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1510 let mut guard = self.0.lock().unwrap();
1511 guard.extend_from_slice(buf);
1512 Ok(buf.len())
1513 }
1514
1515 fn flush(&mut self) -> std::io::Result<()> {
1516 Ok(())
1517 }
1518 }
1519
1520 #[test]
1521 fn compaction_skips_when_over_limit_and_logs_warning() {
1522 let store = MemoryKV::new_with_limit(Some(12));
1523 let manager = store.txn_manager();
1524
1525 let mut txn = manager.begin_internal(TxnMode::ReadWrite).unwrap();
1527 txn.put(key("k1"), value("123456")).unwrap();
1528 manager.commit(txn).unwrap();
1529
1530 let buffer = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1532 let make_writer = {
1533 let buf = buffer.clone();
1534 move || VecWriter(buf.clone())
1535 };
1536 let subscriber = tracing_subscriber::fmt()
1537 .with_max_level(Level::WARN)
1538 .with_writer(make_writer)
1539 .without_time()
1540 .finish();
1541 let _guard = tracing::subscriber::set_default(subscriber);
1542
1543 let ran = manager.compact_with_limit(2, 10, || Ok(())).unwrap();
1545 assert!(!ran);
1546
1547 assert_eq!(manager.memory_stats().total_bytes, 8);
1549
1550 let log = String::from_utf8(buffer.lock().unwrap().clone()).unwrap();
1552 assert!(
1553 log.contains("compaction skipped due to memory limit"),
1554 "expected warning log, got: {}",
1555 log
1556 );
1557 }
1558}