use std::sync::atomic::{AtomicUsize, Ordering};
use nodedb_types::{DatabaseId, TenantId};
use crate::engine::EngineId;
#[derive(Debug, Clone, Copy)]
pub(crate) struct ReleaseIdentity {
pub db: DatabaseId,
pub tenant: TenantId,
pub engine: EngineId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Layer {
Global,
Database,
Tenant,
Engine,
}
#[derive(Debug, Default)]
pub struct OverRelease {
global: AtomicUsize,
database: AtomicUsize,
tenant: AtomicUsize,
engine: AtomicUsize,
}
impl OverRelease {
pub fn new() -> Self {
Self::default()
}
pub fn record(&self, layer: Layer, shortfall: usize) {
if shortfall == 0 {
return;
}
self.counter(layer).fetch_add(1, Ordering::Relaxed);
}
pub fn global(&self) -> usize {
self.global.load(Ordering::Relaxed)
}
pub fn database(&self) -> usize {
self.database.load(Ordering::Relaxed)
}
pub fn tenant(&self) -> usize {
self.tenant.load(Ordering::Relaxed)
}
pub fn engine(&self) -> usize {
self.engine.load(Ordering::Relaxed)
}
pub fn total(&self) -> usize {
self.global() + self.database() + self.tenant() + self.engine()
}
fn counter(&self, layer: Layer) -> &AtomicUsize {
match layer {
Layer::Global => &self.global,
Layer::Database => &self.database,
Layer::Tenant => &self.tenant,
Layer::Engine => &self.engine,
}
}
}
pub(crate) fn release_layer(
counter: &AtomicUsize,
size: usize,
layer: Layer,
over_release: &OverRelease,
id: ReleaseIdentity,
) -> usize {
let shortfall = crate::budget::atomic_saturating_sub(counter, size);
if shortfall == 0 {
return 0;
}
over_release.record(layer, shortfall);
tracing::warn!(
layer = ?layer,
requested = size,
shortfall,
db = ?id.db,
tenant = ?id.tenant,
engine = ?id.engine,
"memory release exceeds allocation (WAL replay or accounting drift)"
);
shortfall
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_ignores_zero_shortfall() {
let counters = OverRelease::new();
counters.record(Layer::Engine, 0);
assert_eq!(counters.engine(), 0);
assert_eq!(counters.total(), 0);
}
#[test]
fn record_counts_events_not_bytes() {
let counters = OverRelease::new();
counters.record(Layer::Engine, 10);
counters.record(Layer::Engine, 9999);
assert_eq!(
counters.engine(),
2,
"record counts the number of over-release events, not shortfall bytes"
);
}
#[test]
fn each_layer_tracks_independently() {
let counters = OverRelease::new();
counters.record(Layer::Global, 1);
counters.record(Layer::Database, 1);
counters.record(Layer::Tenant, 1);
counters.record(Layer::Engine, 1);
assert_eq!(counters.global(), 1);
assert_eq!(counters.database(), 1);
assert_eq!(counters.tenant(), 1);
assert_eq!(counters.engine(), 1);
assert_eq!(counters.total(), 4);
}
}