reddb_server/runtime/memory_accounting.rs
1//! Live usage reporting into the shared accounting pool — ADR 0073 §2.
2//!
3//! Every governed pool already owns a counter for its own footprint: the page
4//! cache knows its resident slot count, the blob cache its `bytes_in_use`, the
5//! segments their resident estimates and retained lifetimes, the WAL queue its
6//! buffered bytes.
7//! This module is the seam that reads those counters and stores them into the
8//! one shared pool, so `red.stats` can show per-pool `share_bytes` /
9//! `used_bytes` against a single total.
10//!
11//! Storage pools use **pull, not push**. The alternative — every subsystem holding an
12//! `Arc<MemoryAccounting>` and updating it inline — would thread the pool into
13//! the page-cache hit path and the segment insert path, which is exactly the
14//! per-operation cost ADR 0073 §3 forbids. Instead the pools keep the atomics
15//! they already maintain and a reader samples them. A refresh is a handful of
16//! resident-counter reads under the segment publication locks. Direct output
17//! scans sample once before entering storage callbacks, never once per row.
18//!
19//! This sampler reports usage. `memory_admission` applies the growth checks
20//! against the shared budget (ADR 0073 §4).
21//!
22//! Query buffers are the exception: their owned reservations charge capacity
23//! before allocation and return it after the last owner drops. Never overwrite
24//! that pool with a sampled estimate.
25
26use crate::runtime::RedDBRuntime;
27use crate::storage::memory_pools::MemoryPool;
28
29impl RedDBRuntime {
30 /// Sample every governed pool and store its footprint into the shared
31 /// accounting. Called before the `red.stats` budget section is rendered.
32 ///
33 /// Samples store/segment inventories without scanning entity payloads.
34 /// Maintenance publication can delay a sample. Output admission must call
35 /// this outside storage callbacks and use the short ledger during a scan.
36 pub fn refresh_memory_accounting(&self) {
37 let mut reservations = self.inner.memory_reservations.lock();
38 self.refresh_memory_accounting_with_reservations(&mut reservations);
39 }
40
41 /// Caller holds the reservation mutex across sampling and publication. A
42 /// completed guard cannot return headroom until this sample sees its writes;
43 /// an older sampler cannot overwrite this sample after headroom is returned.
44 pub(super) fn refresh_memory_accounting_with_reservations(
45 &self,
46 reservations: &mut super::memory_admission::MemoryReservations,
47 ) {
48 #[cfg(test)]
49 {
50 reservations.samples += 1;
51 }
52 let accounting = self.memory_accounting();
53 let store = self.db().store();
54
55 accounting.report(MemoryPool::PageCache, store.page_cache_bytes_in_use());
56 accounting.report(
57 MemoryPool::BlobCacheL1,
58 self.result_blob_cache().ram_bytes_in_use(),
59 );
60 accounting.report(MemoryPool::SegmentArena, store.segment_memory_bytes());
61 accounting.report(
62 MemoryPool::IndexMemory,
63 self.index_store_ref().memory_bytes(),
64 );
65 // Paged stores keep a resident group-commit writer and queue; embedded
66 // single-file stores append WAL payloads synchronously to the artifact
67 // and retain no live WAL buffer between mutations.
68 accounting.report(MemoryPool::WalBuffers, store.wal_buffer_bytes_in_use());
69 accounting.observe_total_used(accounting.total_used_bytes());
70 assert!(
71 reservations.completed_bytes <= reservations.reserved_bytes,
72 "invariant: only completed reservations can be reconciled"
73 );
74 reservations.reserved_bytes -= reservations.completed_bytes;
75 reservations.completed_bytes = 0;
76 let non_query = crate::storage::memory_pools::MEMORY_POOLS
77 .iter()
78 .filter(|pool| **pool != MemoryPool::QueryBuffers)
79 .map(|pool| accounting.used_bytes(*pool))
80 .fold(reservations.reserved_bytes, u64::saturating_add);
81 accounting.reconcile_non_query_admission(non_query);
82 }
83}