1use std::collections::{HashMap, VecDeque};
14use std::sync::Mutex;
15use std::time::{Duration, Instant};
16
17use serde_json::Value;
18use uuid::Uuid;
19
20pub const DEFAULT_RING_CAPACITY: usize = 64;
22
23pub const DEFAULT_RING_TTL: Duration = Duration::from_secs(30 * 60);
25
26pub const DEFAULT_MAX_OUTER_KEYS: usize = 4096;
33
34#[derive(Clone, Debug)]
38pub struct RingEntry {
39 pub id: Uuid,
40 pub name: Option<String>,
41 pub touched_at: Instant,
42}
43
44type RingKey = (String, String);
45
46#[derive(Default)]
47struct RingState {
48 rings: HashMap<RingKey, VecDeque<RingEntry>>,
49}
50
51pub struct ReferenceRing {
57 state: Mutex<RingState>,
58 capacity: usize,
59 ttl: Duration,
60 max_outer_keys: usize,
61}
62
63impl Default for ReferenceRing {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl ReferenceRing {
70 pub fn new() -> Self {
71 Self::with_bounds(DEFAULT_RING_CAPACITY, DEFAULT_RING_TTL)
72 }
73
74 pub fn with_bounds(capacity: usize, ttl: Duration) -> Self {
75 Self::with_bounds_and_outer_limit(capacity, ttl, DEFAULT_MAX_OUTER_KEYS)
76 }
77
78 pub fn with_bounds_and_outer_limit(
79 capacity: usize,
80 ttl: Duration,
81 max_outer_keys: usize,
82 ) -> Self {
83 Self {
84 state: Mutex::new(RingState::default()),
85 capacity,
86 ttl,
87 max_outer_keys,
88 }
89 }
90
91 fn key(namespace: &str, actor: &str) -> RingKey {
92 (namespace.to_owned(), actor.to_owned())
93 }
94
95 fn lock_state(&self) -> std::sync::MutexGuard<'_, RingState> {
99 match self.state.lock() {
100 Ok(guard) => guard,
101 Err(poisoned) => {
102 tracing::warn!(
103 "reference ring mutex poisoned by a prior panic; recovering inner state \
104 (ring admission/lookup is best-effort and must never fail a dispatch)"
105 );
106 poisoned.into_inner()
107 }
108 }
109 }
110
111 fn evict_stale(ring: &mut VecDeque<RingEntry>, ttl: Duration, now: Instant) {
114 while let Some(front) = ring.front() {
115 if now.duration_since(front.touched_at) > ttl {
116 ring.pop_front();
117 } else {
118 break;
119 }
120 }
121 }
122
123 fn prune_outer_map(
128 rings: &mut HashMap<RingKey, VecDeque<RingEntry>>,
129 ttl: Duration,
130 max_outer_keys: usize,
131 now: Instant,
132 exempt: &RingKey,
133 ) {
134 rings.retain(|_, ring| {
135 Self::evict_stale(ring, ttl, now);
136 !ring.is_empty()
137 });
138 if rings.len() <= max_outer_keys {
139 return;
140 }
141 let mut by_recency: Vec<(RingKey, Instant)> = rings
142 .iter()
143 .filter(|(k, _)| *k != exempt)
144 .filter_map(|(k, ring)| ring.back().map(|e| (k.clone(), e.touched_at)))
145 .collect();
146 by_recency.sort_by_key(|(_, touched_at)| *touched_at);
147 let overflow = rings.len().saturating_sub(max_outer_keys);
148 for (key, _) in by_recency.into_iter().take(overflow) {
149 rings.remove(&key);
150 }
151 }
152
153 pub fn admit(&self, namespace: &str, actor: &str, id: Uuid, name: Option<String>) {
160 let now = Instant::now();
161 let mut state = self.lock_state();
162 let key = Self::key(namespace, actor);
163 {
164 let ring = state.rings.entry(key.clone()).or_default();
165 Self::evict_stale(ring, self.ttl, now);
166 ring.retain(|e| e.id != id);
167 ring.push_back(RingEntry {
168 id,
169 name,
170 touched_at: now,
171 });
172 while ring.len() > self.capacity {
173 ring.pop_front();
174 }
175 }
176 Self::prune_outer_map(&mut state.rings, self.ttl, self.max_outer_keys, now, &key);
177 }
178
179 pub fn snapshot(&self, namespace: &str, actor: &str) -> Vec<RingEntry> {
188 let now = Instant::now();
189 let mut state = self.lock_state();
190 let key = Self::key(namespace, actor);
191 let snap: Vec<RingEntry> = match state.rings.get_mut(&key) {
192 Some(ring) => {
193 Self::evict_stale(ring, self.ttl, now);
194 ring.iter().rev().cloned().collect()
195 }
196 None => Vec::new(),
197 };
198 Self::prune_outer_map(&mut state.rings, self.ttl, self.max_outer_keys, now, &key);
199 snap
200 }
201}
202
203fn display_name(result: &Value) -> Option<String> {
207 let name = result.get("name").and_then(Value::as_str)?;
208 let trimmed = name.trim();
209 (!trimmed.is_empty()).then(|| trimmed.to_string())
210}
211
212const ENTITY_KINDS: [&str; 9] = [
215 "concept", "document", "dataset", "project", "person", "org", "artifact", "service", "resource",
216];
217
218fn is_entity_kind_value(v: &str) -> bool {
219 v == "entity" || ENTITY_KINDS.contains(&v)
220}
221
222fn substrate_admits_as_entity(obj: &serde_json::Map<String, Value>) -> bool {
239 if matches!(
240 obj.get("kind").and_then(Value::as_str),
241 Some("edge") | Some("event")
242 ) {
243 return false;
244 }
245 if obj.contains_key("content") {
246 return false;
247 }
248 if obj.contains_key("entity_type") {
249 return true;
250 }
251 obj.get("kind")
252 .and_then(Value::as_str)
253 .is_some_and(is_entity_kind_value)
254}
255
256pub(crate) fn ring_admissions_for(verb: &str, result: &Value) -> Vec<(Uuid, Option<String>)> {
267 let Some(obj) = result.as_object() else {
268 return Vec::new();
269 };
270 if obj.contains_key("attempted") {
271 return Vec::new();
272 }
273 let parse_id = |key: &str| -> Option<Uuid> {
274 obj.get(key)
275 .and_then(Value::as_str)
276 .and_then(|s| Uuid::parse_str(s).ok())
277 };
278 match verb {
279 "create" | "get" | "update" | "delete" => {
280 if !substrate_admits_as_entity(obj) {
281 return Vec::new();
282 }
283 match parse_id("id") {
284 Some(id) => vec![(id, display_name(result))],
285 None => Vec::new(),
286 }
287 }
288 "merge" => match parse_id("kept_id") {
291 Some(id) => vec![(id, None)],
292 None => Vec::new(),
293 },
294 "link" => {
295 let mut out = Vec::new();
296 if let Some(id) = parse_id("source_id") {
297 out.push((id, None));
298 }
299 if let Some(id) = parse_id("target_id") {
300 out.push((id, None));
301 }
302 out
303 }
304 _ => Vec::new(),
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use serde_json::json;
312
313 #[test]
314 fn admits_by_id_ops_and_extracts_name() {
315 let ring = ReferenceRing::new();
316 ring.admit("local", "actor:a", Uuid::nil(), Some("Alpha".into()));
317 let snap = ring.snapshot("local", "actor:a");
318 assert_eq!(snap.len(), 1);
319 assert_eq!(snap[0].id, Uuid::nil());
320 assert_eq!(snap[0].name.as_deref(), Some("Alpha"));
321 }
322
323 #[test]
324 fn ring_admissions_for_search_and_list_is_empty() {
325 let result = json!([{"id": Uuid::nil().to_string(), "name": "hit"}]);
326 assert!(ring_admissions_for("search", &result).is_empty());
327 assert!(ring_admissions_for("list", &result).is_empty());
328 }
329
330 #[test]
331 fn ring_admissions_for_get_extracts_id_and_name() {
332 let id = Uuid::new_v4();
333 let result = json!({"id": id.to_string(), "name": "Concept", "entity_type": null});
337 let admissions = ring_admissions_for("get", &result);
338 assert_eq!(admissions, vec![(id, Some("Concept".to_string()))]);
339 }
340
341 #[test]
342 fn ring_admissions_for_note_result_is_empty() {
343 let id = Uuid::new_v4();
344 let result = json!({"id": id.to_string(), "name": "a note", "content": "body text"});
347 assert!(ring_admissions_for("create", &result).is_empty());
348 assert!(ring_admissions_for("get", &result).is_empty());
349 assert!(ring_admissions_for("update", &result).is_empty());
350 assert!(ring_admissions_for("delete", &result).is_empty());
351 }
352
353 #[test]
354 fn ring_admissions_for_edge_and_event_kind_is_empty() {
355 let id = Uuid::new_v4();
356 let edge_result = json!({"id": id.to_string(), "kind": "edge"});
357 assert!(ring_admissions_for("get", &edge_result).is_empty());
358 let event_result = json!({"id": id.to_string(), "kind": "event"});
359 assert!(ring_admissions_for("get", &event_result).is_empty());
360 }
361
362 #[test]
363 fn ring_admissions_for_delete_uses_echoed_kind_param() {
364 let id = Uuid::new_v4();
365 let entity_delete = json!({"deleted": true, "id": id.to_string(), "kind": "concept"});
368 assert_eq!(
369 ring_admissions_for("delete", &entity_delete),
370 vec![(id, None)]
371 );
372 let generic_entity_delete =
373 json!({"deleted": true, "id": id.to_string(), "kind": "entity"});
374 assert_eq!(
375 ring_admissions_for("delete", &generic_entity_delete),
376 vec![(id, None)]
377 );
378 let note_delete = json!({"deleted": true, "id": id.to_string(), "kind": "observation"});
379 assert!(ring_admissions_for("delete", ¬e_delete).is_empty());
380 let unspecified_delete = json!({"deleted": true, "id": id.to_string(), "kind": null});
381 assert!(ring_admissions_for("delete", &unspecified_delete).is_empty());
382 }
383
384 #[test]
385 fn display_name_never_falls_back_to_content() {
386 let result = json!({"id": Uuid::new_v4().to_string(), "content": "some note body"});
387 assert_eq!(display_name(&result), None);
388 }
389
390 #[test]
391 fn ring_admissions_for_link_extracts_both_endpoints() {
392 let source = Uuid::new_v4();
393 let target = Uuid::new_v4();
394 let result = json!({
395 "id": Uuid::new_v4().to_string(),
396 "source_id": source.to_string(),
397 "target_id": target.to_string(),
398 });
399 let admissions = ring_admissions_for("link", &result);
400 assert_eq!(admissions, vec![(source, None), (target, None)]);
401 }
402
403 #[test]
404 fn ring_admissions_for_merge_uses_kept_id() {
405 let kept = Uuid::new_v4();
406 let removed = Uuid::new_v4();
407 let result = json!({"kept_id": kept.to_string(), "removed_id": removed.to_string()});
408 let admissions = ring_admissions_for("merge", &result);
409 assert_eq!(admissions, vec![(kept, None)]);
410 }
411
412 #[test]
413 fn ring_admissions_for_bulk_shapes_is_empty() {
414 let bulk_create = json!({"attempted": 3, "created": 3});
415 assert!(ring_admissions_for("create", &bulk_create).is_empty());
416 let bulk_link = json!({"attempted": 2, "created": 2, "skipped": 0, "failed": 0});
417 assert!(ring_admissions_for("link", &bulk_link).is_empty());
418 }
419
420 #[test]
421 fn admission_bounds_by_size() {
422 let ring = ReferenceRing::with_bounds(3, DEFAULT_RING_TTL);
423 let ids: Vec<Uuid> = (0..5).map(|_| Uuid::new_v4()).collect();
424 for id in &ids {
425 ring.admit("local", "actor:a", *id, None);
426 }
427 let snap = ring.snapshot("local", "actor:a");
428 assert_eq!(snap.len(), 3);
429 assert_eq!(snap[0].id, ids[4]);
431 assert_eq!(snap[1].id, ids[3]);
432 assert_eq!(snap[2].id, ids[2]);
433 }
434
435 #[test]
436 fn admission_bounds_by_age() {
437 let ring = ReferenceRing::with_bounds(64, Duration::from_millis(20));
438 let old = Uuid::new_v4();
439 ring.admit("local", "actor:a", old, None);
440 std::thread::sleep(Duration::from_millis(40));
441 let fresh = Uuid::new_v4();
442 ring.admit("local", "actor:a", fresh, None);
443 let snap = ring.snapshot("local", "actor:a");
444 assert_eq!(snap.len(), 1);
445 assert_eq!(snap[0].id, fresh);
446 }
447
448 #[test]
449 fn actor_isolation_never_crosses_boundary() {
450 let ring = ReferenceRing::new();
451 let id_a = Uuid::new_v4();
452 ring.admit("local", "actor:a", id_a, Some("A-only".into()));
453 let snap_b = ring.snapshot("local", "actor:b");
454 assert!(snap_b.is_empty(), "actor b must never see actor a's ring");
455 let snap_a = ring.snapshot("local", "actor:a");
456 assert_eq!(snap_a.len(), 1);
457 }
458
459 #[test]
460 fn namespace_isolation_is_independent_of_actor_isolation() {
461 let ring = ReferenceRing::new();
462 let id = Uuid::new_v4();
463 ring.admit("tenant-a", "actor:a", id, None);
464 assert!(ring.snapshot("tenant-b", "actor:a").is_empty());
465 assert_eq!(ring.snapshot("tenant-a", "actor:a").len(), 1);
466 }
467
468 #[test]
469 fn re_admitting_an_id_moves_it_to_most_recent_without_duplicating() {
470 let ring = ReferenceRing::new();
471 let a = Uuid::new_v4();
472 let b = Uuid::new_v4();
473 ring.admit("local", "actor:a", a, Some("A".into()));
474 ring.admit("local", "actor:a", b, Some("B".into()));
475 ring.admit("local", "actor:a", a, Some("A-renamed".into()));
476 let snap = ring.snapshot("local", "actor:a");
477 assert_eq!(snap.len(), 2, "re-admission must not duplicate the entry");
478 assert_eq!(snap[0].id, a, "re-admitted id must be most-recent");
479 assert_eq!(snap[0].name.as_deref(), Some("A-renamed"));
480 }
481
482 #[test]
483 fn snapshot_prunes_a_key_that_ages_out_entirely() {
484 let ring = ReferenceRing::with_bounds(64, Duration::from_millis(20));
485 ring.admit("local", "actor:a", Uuid::new_v4(), None);
486 std::thread::sleep(Duration::from_millis(40));
487 assert!(ring.snapshot("local", "actor:a").is_empty());
489 let state = ring.state.lock().unwrap();
490 assert!(
491 !state
492 .rings
493 .contains_key(&("local".to_string(), "actor:a".to_string())),
494 "a key whose ring emptied via TTL eviction must not linger in the outer map"
495 );
496 }
497
498 #[test]
504 fn snapshot_prunes_other_stale_keys_it_did_not_query() {
505 let ring = ReferenceRing::with_bounds(64, Duration::from_millis(20));
506 ring.admit("local", "actor:queried", Uuid::new_v4(), None);
507 ring.admit("local", "actor:other", Uuid::new_v4(), None);
508 std::thread::sleep(Duration::from_millis(40));
509
510 assert!(ring.snapshot("local", "actor:queried").is_empty());
512
513 let state = ring.state.lock().unwrap();
514 assert!(
515 !state
516 .rings
517 .contains_key(&("local".to_string(), "actor:other".to_string())),
518 "snapshotting one actor must also prune OTHER actors' fully-stale keys, \
519 not just the one queried"
520 );
521 }
522
523 #[test]
524 fn outer_map_evicts_least_recently_touched_keys_over_budget() {
525 let ring = ReferenceRing::with_bounds_and_outer_limit(64, DEFAULT_RING_TTL, 3);
526 for i in 0..4 {
530 ring.admit(
531 "local",
532 &format!("actor:{i}"),
533 Uuid::new_v4(),
534 Some(format!("actor {i}")),
535 );
536 }
537 assert!(
538 ring.snapshot("local", "actor:0").is_empty(),
539 "the least-recently-touched key must be evicted once the outer-key budget is exceeded"
540 );
541 for i in 1..4 {
542 assert!(
543 !ring.snapshot("local", &format!("actor:{i}")).is_empty(),
544 "actor:{i} must survive the budget eviction"
545 );
546 }
547 }
548
549 #[test]
550 fn outer_map_budget_eviction_never_evicts_the_key_just_admitted() {
551 let ring = ReferenceRing::with_bounds_and_outer_limit(64, DEFAULT_RING_TTL, 1);
552 ring.admit("local", "actor:a", Uuid::new_v4(), None);
553 ring.admit("local", "actor:b", Uuid::new_v4(), None);
557 assert!(!ring.snapshot("local", "actor:b").is_empty());
558 }
559
560 #[test]
565 fn admit_and_snapshot_recover_from_poisoned_mutex() {
566 let ring = std::sync::Arc::new(ReferenceRing::new());
567 let poison_ring = ring.clone();
568 let _ = std::thread::spawn(move || {
569 let _guard = poison_ring.state.lock().unwrap();
570 panic!("deliberately poisoning the reference ring mutex for the recovery test");
571 })
572 .join();
573
574 ring.admit(
576 "local",
577 "actor:a",
578 Uuid::new_v4(),
579 Some("post-poison".into()),
580 );
581 let snap = ring.snapshot("local", "actor:a");
582 assert_eq!(snap.len(), 1);
583 assert_eq!(snap[0].name.as_deref(), Some("post-poison"));
584 }
585}