Skip to main content

reifydb_store_multi/tier/read/
buffer.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (c) 2026 ReifyDB
3
4use std::{collections::HashMap, sync::Arc};
5
6use reifydb_core::{common::CommitVersion, encoded::key::EncodedKey};
7use reifydb_runtime::sync::mutex::Mutex;
8use reifydb_value::util::cowvec::CowVec;
9
10use crate::tier::VersionedGetResult;
11
12#[derive(Clone)]
13struct CacheEntry {
14	version: CommitVersion,
15	value: Option<CowVec<u8>>,
16	seq: u64,
17}
18
19struct Inner {
20	entries: HashMap<EncodedKey, CacheEntry>,
21	next_seq: u64,
22	capacity: usize,
23}
24
25#[derive(Clone)]
26pub struct MultiReadBufferTier {
27	inner: Arc<Mutex<Inner>>,
28}
29
30impl MultiReadBufferTier {
31	pub fn new(capacity: usize) -> Self {
32		Self {
33			inner: Arc::new(Mutex::new(Inner {
34				entries: HashMap::new(),
35				next_seq: 0,
36				capacity: capacity.max(1),
37			})),
38		}
39	}
40
41	pub fn set_capacity(&self, capacity: usize) {
42		let mut inner = self.inner.lock();
43		inner.capacity = capacity.max(1);
44		inner.entries.clear();
45	}
46
47	pub fn get(&self, key: &EncodedKey, version: CommitVersion) -> VersionedGetResult {
48		let mut inner = self.inner.lock();
49		let Some(entry) = inner.entries.get(key) else {
50			return VersionedGetResult::NotFound;
51		};
52		if entry.version > version {
53			return VersionedGetResult::NotFound;
54		}
55		let stored_version = entry.version;
56		let value = entry.value.clone();
57		let seq = inner.next_seq;
58		inner.next_seq += 1;
59		if let Some(e) = inner.entries.get_mut(key) {
60			e.seq = seq;
61		}
62		match value {
63			Some(v) => VersionedGetResult::Value {
64				value: v,
65				version: stored_version,
66			},
67			None => VersionedGetResult::Tombstone,
68		}
69	}
70
71	pub fn insert(&self, key: EncodedKey, version: CommitVersion, value: Option<CowVec<u8>>) {
72		let mut inner = self.inner.lock();
73		match inner.entries.get(&key) {
74			Some(existing) if existing.version > version => return,
75			_ => {}
76		}
77		let seq = inner.next_seq;
78		inner.next_seq += 1;
79		inner.entries.insert(
80			key,
81			CacheEntry {
82				version,
83				value,
84				seq,
85			},
86		);
87		while inner.entries.len() > inner.capacity {
88			let Some(oldest) = inner.entries.iter().min_by_key(|(_, e)| e.seq).map(|(k, _)| k.clone())
89			else {
90				break;
91			};
92			inner.entries.remove(&oldest);
93		}
94	}
95
96	pub fn invalidate(&self, key: &EncodedKey) {
97		self.inner.lock().entries.remove(key);
98	}
99
100	pub fn clear(&self) {
101		self.inner.lock().entries.clear();
102	}
103
104	#[cfg(test)]
105	pub fn len(&self) -> usize {
106		self.inner.lock().entries.len()
107	}
108}
109
110#[cfg(test)]
111mod tests {
112	use super::*;
113
114	fn key(s: &str) -> EncodedKey {
115		EncodedKey::new(s.as_bytes().to_vec())
116	}
117
118	fn val(s: &str) -> CowVec<u8> {
119		CowVec::new(s.as_bytes().to_vec())
120	}
121
122	#[test]
123	fn insert_then_get_returns_value_when_version_high_enough() {
124		let read = MultiReadBufferTier::new(8);
125		read.insert(key("k"), CommitVersion(5), Some(val("v5")));
126		match read.get(&key("k"), CommitVersion(5)) {
127			VersionedGetResult::Value {
128				value: v,
129				version: ver,
130			} => {
131				assert_eq!(v.as_ref(), b"v5");
132				assert_eq!(ver, CommitVersion(5));
133			}
134			VersionedGetResult::Tombstone => panic!("expected value, got tombstone"),
135			VersionedGetResult::NotFound => panic!("expected hit at exactly stored version"),
136		}
137		// A reader at a snapshot above the stored version still resolves to the cached value:
138		// no newer version exists for this key, so the latest committed value is correct.
139		assert!(matches!(read.get(&key("k"), CommitVersion(9)), VersionedGetResult::Value { .. }));
140	}
141
142	#[test]
143	fn get_below_stored_version_misses_so_caller_reads_through() {
144		// The read buffer only holds the LATEST committed value. A reader whose snapshot predates that
145		// commit must NOT be served the newer value - it must fall through to the persistent tier,
146		// which can resolve the correct historical version. Serving the cached value here would
147		// violate snapshot isolation.
148		let read = MultiReadBufferTier::new(8);
149		read.insert(key("k"), CommitVersion(5), Some(val("v5")));
150		assert!(
151			matches!(read.get(&key("k"), CommitVersion(4)), VersionedGetResult::NotFound),
152			"must miss below the stored version"
153		);
154	}
155
156	#[test]
157	fn tombstone_is_cached_and_served() {
158		let read = MultiReadBufferTier::new(8);
159		read.insert(key("k"), CommitVersion(3), None);
160		assert!(matches!(read.get(&key("k"), CommitVersion(3)), VersionedGetResult::Tombstone));
161	}
162
163	#[test]
164	fn invalidate_removes_the_key() {
165		let read = MultiReadBufferTier::new(8);
166		read.insert(key("k"), CommitVersion(1), Some(val("v1")));
167		read.invalidate(&key("k"));
168		assert!(
169			matches!(read.get(&key("k"), CommitVersion(1)), VersionedGetResult::NotFound),
170			"invalidated key must miss"
171		);
172	}
173
174	#[test]
175	fn newer_insert_overwrites_but_older_insert_is_ignored() {
176		let read = MultiReadBufferTier::new(8);
177		read.insert(key("k"), CommitVersion(5), Some(val("v5")));
178		// A stale insert for an older version (e.g. a late persistent-hit populate) must not clobber
179		// the newer cached value.
180		read.insert(key("k"), CommitVersion(2), Some(val("v2")));
181		match read.get(&key("k"), CommitVersion(5)) {
182			VersionedGetResult::Value {
183				value: v,
184				..
185			} => assert_eq!(v.as_ref(), b"v5", "older insert must not overwrite"),
186			VersionedGetResult::Tombstone => panic!("unexpected tombstone"),
187			VersionedGetResult::NotFound => panic!("unexpected miss"),
188		}
189		// A strictly newer insert replaces it.
190		read.insert(key("k"), CommitVersion(7), Some(val("v7")));
191		match read.get(&key("k"), CommitVersion(7)) {
192			VersionedGetResult::Value {
193				value: v,
194				version: ver,
195			} => {
196				assert_eq!(v.as_ref(), b"v7");
197				assert_eq!(ver, CommitVersion(7));
198			}
199			VersionedGetResult::Tombstone => panic!("unexpected tombstone"),
200			VersionedGetResult::NotFound => panic!("unexpected miss"),
201		}
202	}
203
204	#[test]
205	fn eviction_bounds_size_and_never_changes_correctness() {
206		// Eviction may drop any entry; a dropped entry simply forces a read-through. It must never
207		// turn a hit into a wrong answer - capacity is purely a RAM/CPU trade.
208		let read = MultiReadBufferTier::new(2);
209		read.insert(key("a"), CommitVersion(1), Some(val("a")));
210		read.insert(key("b"), CommitVersion(1), Some(val("b")));
211		read.insert(key("c"), CommitVersion(1), Some(val("c")));
212		assert!(read.len() <= 2, "read buffer must stay within capacity");
213		// Whatever survives must still answer correctly.
214		for k in ["a", "b", "c"] {
215			if let VersionedGetResult::Value {
216				value: v,
217				..
218			} = read.get(&key(k), CommitVersion(1))
219			{
220				assert_eq!(v.as_ref(), k.as_bytes());
221			}
222		}
223	}
224
225	#[test]
226	fn clone_shares_backing_storage() {
227		let a = MultiReadBufferTier::new(4);
228		let b = a.clone();
229		a.insert(key("k"), CommitVersion(1), Some(val("v")));
230		assert!(
231			matches!(b.get(&key("k"), CommitVersion(1)), VersionedGetResult::Value { .. }),
232			"clone observes writes from the original"
233		);
234	}
235}