nodedb_mem/
collection_arena.rs1use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13
14use crate::error::{MemError, Result};
15
16#[derive(Debug, Clone)]
21pub struct CollectionArenaHandle {
22 arena_index: Option<u32>,
25 tag: String,
27}
28
29impl CollectionArenaHandle {
30 pub fn arena_index(&self) -> Option<u32> {
32 self.arena_index
33 }
34
35 pub fn tag(&self) -> &str {
37 &self.tag
38 }
39
40 pub fn resident_bytes(&self) -> Option<u64> {
44 let idx = self.arena_index?;
45 read_arena_resident(idx).ok().map(|v| v as u64)
46 }
47}
48
49#[derive(Debug, Default)]
54pub struct CollectionArenaRegistry {
55 inner: Mutex<RegistryInner>,
56}
57
58#[derive(Debug, Default)]
59struct RegistryInner {
60 handles: HashMap<(u64, String), CollectionArenaHandle>,
61}
62
63impl CollectionArenaRegistry {
64 pub fn new() -> Arc<Self> {
66 Arc::new(Self::default())
67 }
68
69 pub fn get_or_create(&self, tenant_id: u64, collection: &str) -> Result<CollectionArenaHandle> {
73 let key = (tenant_id, collection.to_string());
74 let mut guard = self
75 .inner
76 .lock()
77 .map_err(|_| MemError::Jemalloc("collection arena registry lock poisoned".into()))?;
78 if let Some(h) = guard.handles.get(&key) {
79 return Ok(h.clone());
80 }
81 let handle = allocate_handle(tenant_id, collection);
82 guard.handles.insert(key, handle.clone());
83 Ok(handle)
84 }
85
86 pub fn get(&self, tenant_id: u64, collection: &str) -> Option<CollectionArenaHandle> {
89 let guard = self.inner.lock().ok()?;
90 guard
91 .handles
92 .get(&(tenant_id, collection.to_string()))
93 .cloned()
94 }
95}
96
97fn allocate_handle(tenant_id: u64, collection: &str) -> CollectionArenaHandle {
103 let tag = format!("t{tenant_id}/{collection}");
104 let arena_index = create_arena()
105 .map_err(|e| {
106 tracing::debug!(tag, error = %e, "per-collection arena creation failed; using global allocator");
107 })
108 .ok();
109 CollectionArenaHandle { arena_index, tag }
110}
111
112fn create_arena() -> Result<u32> {
117 #[cfg(all(feature = "jemalloc", not(target_arch = "wasm32")))]
118 {
119 let arena_idx: u32 = unsafe { tikv_jemalloc_ctl::raw::read(b"arenas.create\0") }
122 .map_err(|e| MemError::Jemalloc(format!("failed to create collection arena: {e:?}")))?;
123 Ok(arena_idx)
124 }
125 #[cfg(any(not(feature = "jemalloc"), target_arch = "wasm32"))]
126 {
127 Err(MemError::Jemalloc(
128 "per-collection arenas need the jemalloc feature on a non-wasm32 target".into(),
129 ))
130 }
131}
132
133fn read_arena_resident(arena_index: u32) -> Result<usize> {
138 #[cfg(all(feature = "jemalloc", not(target_arch = "wasm32")))]
139 {
140 if let Ok(mib) = tikv_jemalloc_ctl::epoch::mib() {
142 let _ = mib.advance();
143 }
144 let key = format!("stats.arenas.{arena_index}.resident\0");
148 unsafe { tikv_jemalloc_ctl::raw::read::<usize>(key.as_bytes()) }
149 .map_err(|e| MemError::Jemalloc(format!("failed to read arena resident: {e:?}")))
150 }
151 #[cfg(any(not(feature = "jemalloc"), target_arch = "wasm32"))]
152 {
153 let _ = arena_index;
154 Err(MemError::Jemalloc(
155 "arena resident stats need the jemalloc feature on a non-wasm32 target".into(),
156 ))
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn registry_create_and_retrieve() {
166 let reg = CollectionArenaRegistry::new();
167 let h1 = reg.get_or_create(1, "embeddings").expect("create");
168 let h2 = reg.get_or_create(1, "embeddings").expect("re-fetch");
169 assert_eq!(h1.arena_index(), h2.arena_index(), "same key → same arena");
171 assert_eq!(h1.tag(), "t1/embeddings");
172 }
173
174 #[test]
175 fn different_collections_get_different_arenas() {
176 let reg = CollectionArenaRegistry::new();
177 let h1 = reg.get_or_create(1, "col_a").expect("create a");
178 let h2 = reg.get_or_create(1, "col_b").expect("create b");
179 if h1.arena_index().is_some() && h2.arena_index().is_some() {
180 assert_ne!(
181 h1.arena_index(),
182 h2.arena_index(),
183 "distinct collections must get distinct arenas"
184 );
185 }
186 }
187
188 #[test]
189 fn different_tenants_get_different_arenas() {
190 let reg = CollectionArenaRegistry::new();
191 let h1 = reg.get_or_create(1, "embeddings").expect("t1");
192 let h2 = reg.get_or_create(2, "embeddings").expect("t2");
193 if h1.arena_index().is_some() && h2.arena_index().is_some() {
194 assert_ne!(
195 h1.arena_index(),
196 h2.arena_index(),
197 "different tenants must get distinct arenas"
198 );
199 }
200 }
201
202 #[test]
203 fn get_returns_none_for_unknown_collection() {
204 let reg = CollectionArenaRegistry::new();
205 assert!(reg.get(99, "unknown").is_none());
206 }
207
208 #[test]
209 fn resident_bytes_does_not_panic() {
210 let reg = CollectionArenaRegistry::new();
211 let h = reg.get_or_create(1, "test_resident").expect("create");
212 let _ = h.resident_bytes();
214 }
215}