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//! 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}