Skip to main content

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//! **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; ordinary read
17//! hot paths gain no accounting hook.
18//!
19//! This sampler reports usage. `memory_admission` applies the growth checks
20//! against the shared budget (ADR 0073 §4).
21
22use crate::runtime::RedDBRuntime;
23use crate::storage::memory_pools::MemoryPool;
24
25impl RedDBRuntime {
26    /// Sample every governed pool and store its footprint into the shared
27    /// accounting. Called before the `red.stats` budget section is rendered.
28    ///
29    /// Samples store/segment inventories without scanning entity payloads.
30    /// Maintenance publication can delay a sample; ordinary reads do not call it.
31    pub fn refresh_memory_accounting(&self) {
32        let mut reservations = self.inner.memory_reservations.lock();
33        self.refresh_memory_accounting_with_reservations(&mut reservations);
34    }
35
36    /// Caller holds the reservation mutex across sampling and publication. A
37    /// completed guard cannot return headroom until this sample sees its writes;
38    /// an older sampler cannot overwrite this sample after headroom is returned.
39    pub(super) fn refresh_memory_accounting_with_reservations(
40        &self,
41        reservations: &mut super::memory_admission::MemoryReservations,
42    ) {
43        #[cfg(test)]
44        {
45            reservations.samples += 1;
46        }
47        let accounting = self.memory_accounting();
48        let store = self.db().store();
49
50        accounting.report(MemoryPool::PageCache, store.page_cache_bytes_in_use());
51        accounting.report(
52            MemoryPool::BlobCacheL1,
53            self.result_blob_cache().ram_bytes_in_use(),
54        );
55        accounting.report(MemoryPool::SegmentArena, store.segment_memory_bytes());
56        accounting.report(
57            MemoryPool::IndexMemory,
58            self.index_store_ref().memory_bytes(),
59        );
60        // Paged stores keep a resident group-commit writer and queue; embedded
61        // single-file stores append WAL payloads synchronously to the artifact
62        // and retain no live WAL buffer between mutations.
63        accounting.report(MemoryPool::WalBuffers, store.wal_buffer_bytes_in_use());
64        accounting.observe_total_used(accounting.total_used_bytes());
65        assert!(
66            reservations.completed_bytes <= reservations.reserved_bytes,
67            "invariant: only completed reservations can be reconciled"
68        );
69        reservations.reserved_bytes -= reservations.completed_bytes;
70        reservations.completed_bytes = 0;
71    }
72}