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