Skip to main content

rivetkit_core/actor/
kv.rs

1#[cfg(test)]
2use std::collections::BTreeMap;
3#[cfg(test)]
4use std::sync::Arc;
5
6use anyhow::Result;
7#[cfg(test)]
8use parking_lot::{Mutex, RwLock};
9use rivet_envoy_client::handle::EnvoyHandle;
10
11use crate::types::ListOpts;
12
13/// Narrow access to the actor's pre-SQLite KV namespace.
14///
15/// Production code may use this only for the one-time SQLite importer and the
16/// temporary inspector-token mirror. It intentionally has no default or
17/// unconfigured state. The in-memory backend exists only in unit tests.
18#[derive(Clone)]
19pub(crate) struct LegacyActorKv {
20	backend: LegacyActorKvBackend,
21}
22
23#[derive(Clone)]
24enum LegacyActorKvBackend {
25	Envoy {
26		handle: EnvoyHandle,
27		actor_id: String,
28	},
29	#[cfg(test)]
30	InMemory(Arc<TestLegacyActorKv>),
31}
32
33#[cfg(test)]
34struct TestLegacyActorKv {
35	store: RwLock<BTreeMap<Vec<u8>, Vec<u8>>>,
36	delete_range_after_write_lock: Mutex<Option<Arc<dyn Fn() + Send + Sync + 'static>>>,
37	list_limit_cap: Mutex<Option<u32>>,
38	range_start_inclusive: Mutex<bool>,
39}
40
41impl LegacyActorKv {
42	pub(crate) fn new(handle: EnvoyHandle, actor_id: impl Into<String>) -> Self {
43		Self {
44			backend: LegacyActorKvBackend::Envoy {
45				handle,
46				actor_id: actor_id.into(),
47			},
48		}
49	}
50
51	#[cfg(test)]
52	pub(crate) fn new_in_memory() -> Self {
53		Self {
54			backend: LegacyActorKvBackend::InMemory(Arc::new(TestLegacyActorKv {
55				store: RwLock::new(BTreeMap::new()),
56				delete_range_after_write_lock: Mutex::new(None),
57				list_limit_cap: Mutex::new(None),
58				range_start_inclusive: Mutex::new(true),
59			})),
60		}
61	}
62
63	#[cfg(test)]
64	pub(crate) async fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
65		Ok(self.batch_get(&[key]).await?.pop().flatten())
66	}
67
68	pub(crate) async fn put(&self, key: &[u8], value: &[u8]) -> Result<()> {
69		self.batch_put(&[(key, value)]).await
70	}
71
72	pub(crate) async fn batch_get(&self, keys: &[&[u8]]) -> Result<Vec<Option<Vec<u8>>>> {
73		match &self.backend {
74			LegacyActorKvBackend::Envoy { handle, actor_id } => {
75				handle
76					.kv_get(
77						actor_id.clone(),
78						keys.iter().map(|key| key.to_vec()).collect(),
79					)
80					.await
81			}
82			#[cfg(test)]
83			LegacyActorKvBackend::InMemory(store) => {
84				let store = store.store.read();
85				Ok(keys.iter().map(|key| store.get(*key).cloned()).collect())
86			}
87		}
88	}
89
90	pub(crate) async fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> Result<()> {
91		match &self.backend {
92			LegacyActorKvBackend::Envoy { handle, actor_id } => {
93				handle
94					.kv_put(
95						actor_id.clone(),
96						entries
97							.iter()
98							.map(|(key, value)| (key.to_vec(), value.to_vec()))
99							.collect(),
100					)
101					.await
102			}
103			#[cfg(test)]
104			LegacyActorKvBackend::InMemory(store) => {
105				let mut store = store.store.write();
106				for (key, value) in entries {
107					store.insert(key.to_vec(), value.to_vec());
108				}
109				Ok(())
110			}
111		}
112	}
113
114	pub(crate) async fn list_prefix(
115		&self,
116		prefix: &[u8],
117		opts: ListOpts,
118	) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
119		match &self.backend {
120			LegacyActorKvBackend::Envoy { handle, actor_id } => {
121				handle
122					.kv_list_prefix(
123						actor_id.clone(),
124						prefix.to_vec(),
125						Some(opts.reverse),
126						opts.limit.map(u64::from),
127					)
128					.await
129			}
130			#[cfg(test)]
131			LegacyActorKvBackend::InMemory(store) => {
132				let mut entries: Vec<_> = store
133					.store
134					.read()
135					.iter()
136					.filter(|(key, _)| key.starts_with(prefix))
137					.map(|(key, value)| (key.clone(), value.clone()))
138					.collect();
139				apply_list_opts(&mut entries, store.capped_opts(opts));
140				Ok(entries)
141			}
142		}
143	}
144
145	pub(crate) async fn list_range(
146		&self,
147		start: &[u8],
148		end: &[u8],
149		opts: ListOpts,
150	) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
151		match &self.backend {
152			LegacyActorKvBackend::Envoy { handle, actor_id } => {
153				handle
154					.kv_list_range(
155						actor_id.clone(),
156						start.to_vec(),
157						end.to_vec(),
158						true,
159						Some(opts.reverse),
160						opts.limit.map(u64::from),
161					)
162					.await
163			}
164			#[cfg(test)]
165			LegacyActorKvBackend::InMemory(store) => {
166				let entries_guard = store.store.read();
167				let mut entries: Vec<_> = entries_guard
168					.range(start.to_vec()..end.to_vec())
169					.filter(|(key, _)| {
170						*store.range_start_inclusive.lock() || key.as_slice() > start
171					})
172					.map(|(key, value)| (key.clone(), value.clone()))
173					.collect();
174				apply_list_opts(&mut entries, store.capped_opts(opts));
175				Ok(entries)
176			}
177		}
178	}
179
180	#[cfg(test)]
181	pub(crate) async fn batch_delete(&self, keys: &[&[u8]]) -> Result<()> {
182		match &self.backend {
183			LegacyActorKvBackend::Envoy { handle, actor_id } => {
184				handle
185					.kv_delete(
186						actor_id.clone(),
187						keys.iter().map(|key| key.to_vec()).collect(),
188					)
189					.await
190			}
191			LegacyActorKvBackend::InMemory(store) => {
192				let mut store = store.store.write();
193				for key in keys {
194					store.remove(*key);
195				}
196				Ok(())
197			}
198		}
199	}
200
201	#[cfg(test)]
202	pub(crate) async fn delete_range(&self, start: &[u8], end: &[u8]) -> Result<()> {
203		match &self.backend {
204			LegacyActorKvBackend::Envoy { handle, actor_id } => {
205				handle
206					.kv_delete_range(actor_id.clone(), start.to_vec(), end.to_vec())
207					.await
208			}
209			LegacyActorKvBackend::InMemory(store) => {
210				let mut entries = store.store.write();
211				if let Some(hook) = store.delete_range_after_write_lock.lock().clone() {
212					hook();
213				}
214				entries.retain(|key, _| key.as_slice() < start || key.as_slice() >= end);
215				Ok(())
216			}
217		}
218	}
219
220	#[cfg(test)]
221	pub(crate) fn test_identity(&self) -> usize {
222		match &self.backend {
223			LegacyActorKvBackend::Envoy { handle, .. } => handle.get_envoy_key().as_ptr() as usize,
224			LegacyActorKvBackend::InMemory(store) => Arc::as_ptr(store) as usize,
225		}
226	}
227
228	#[cfg(test)]
229	pub(crate) fn test_set_delete_range_after_write_lock_hook(
230		&self,
231		hook: impl Fn() + Send + Sync + 'static,
232	) {
233		if let LegacyActorKvBackend::InMemory(store) = &self.backend {
234			*store.delete_range_after_write_lock.lock() = Some(Arc::new(hook));
235		}
236	}
237
238	#[cfg(test)]
239	pub(crate) fn test_set_list_limit_cap(&self, cap: u32) {
240		if let LegacyActorKvBackend::InMemory(store) = &self.backend {
241			*store.list_limit_cap.lock() = Some(cap);
242		}
243	}
244
245	#[cfg(test)]
246	pub(crate) fn test_set_range_start_inclusive(&self, inclusive: bool) {
247		if let LegacyActorKvBackend::InMemory(store) = &self.backend {
248			*store.range_start_inclusive.lock() = inclusive;
249		}
250	}
251}
252
253#[cfg(test)]
254impl TestLegacyActorKv {
255	fn capped_opts(&self, opts: ListOpts) -> ListOpts {
256		let Some(cap) = *self.list_limit_cap.lock() else {
257			return opts;
258		};
259		ListOpts {
260			reverse: opts.reverse,
261			limit: Some(opts.limit.map_or(cap, |limit| limit.min(cap))),
262		}
263	}
264}
265
266#[cfg(test)]
267fn apply_list_opts(entries: &mut Vec<(Vec<u8>, Vec<u8>)>, opts: ListOpts) {
268	if opts.reverse {
269		entries.reverse();
270	}
271	if let Some(limit) = opts.limit {
272		entries.truncate(limit as usize);
273	}
274}
275
276#[cfg(test)]
277pub(crate) type Kv = LegacyActorKv;
278
279#[cfg(test)]
280#[path = "../../tests/kv.rs"]
281pub(crate) mod tests;