use std::sync::atomic::{AtomicBool, Ordering};
use super::QuotaManager;
use crate::shard::quota::check::Resource;
use crate::shard::quota::config::QuotaLimits;
use crate::shard::quota::error::{QuotaError, QuotaResult};
use crate::shard::quota::status::QuotaExceeded;
#[derive(Debug, Default)]
pub struct ExceededVerdicts {
resident_memory: AtomicBool,
disk_usage: AtomicBool,
}
impl ExceededVerdicts {
pub fn clear(&self) {
self.resident_memory.store(false, Ordering::Relaxed);
self.disk_usage.store(false, Ordering::Relaxed);
}
}
impl QuotaManager {
pub fn check_capacity(&self) -> QuotaResult<()> {
self.check_update()
}
pub fn exceeded(&self) -> QuotaExceeded {
self.evaluate().0
}
pub fn check_update(&self) -> QuotaResult<()> {
match self.evaluate().1 {
Some(err) => Err(err),
None => Ok(()),
}
}
fn evaluate(&self) -> (QuotaExceeded, Option<QuotaError>) {
let QuotaLimits {
max_resident_memory_percent,
max_disk_usage_percent,
release_margin_percent,
} = self.config().limits();
let memory = evaluate(
Resource::ResidentMemory,
max_resident_memory_percent,
&self.exceeded.resident_memory,
release_margin_percent,
|threshold| self.resident_memory_percent(threshold),
);
let disk = evaluate(
Resource::DiskUsage,
max_disk_usage_percent,
&self.exceeded.disk_usage,
release_margin_percent,
|threshold| self.disk_usage_percent(&self.storage_path, threshold),
);
let exceeded = QuotaExceeded {
resident_memory: reported(&memory),
disk_usage: reported(&disk),
};
(exceeded, memory.err().or(disk.err()))
}
}
fn evaluate(
resource: Resource,
limit: Option<u8>,
was_exceeded: &AtomicBool,
release_margin_percent: u8,
measure: impl FnOnce(Option<u8>) -> Option<u8>,
) -> Result<Option<bool>, QuotaError> {
let Some(limit) = limit else {
was_exceeded.store(false, Ordering::Relaxed);
return Ok(None);
};
let threshold = threshold(
limit,
was_exceeded.load(Ordering::Relaxed),
release_margin_percent,
);
let Some(used_percent) = measure(Some(threshold)) else {
was_exceeded.store(false, Ordering::Relaxed);
return Ok(None);
};
let exceeded = used_percent >= threshold;
was_exceeded.store(exceeded, Ordering::Relaxed);
if exceeded {
return Err(resource.rejected(used_percent, limit, threshold));
}
Ok(Some(false))
}
fn reported(outcome: &Result<Option<bool>, QuotaError>) -> Option<bool> {
match outcome {
Ok(verdict) => *verdict,
Err(_) => Some(true),
}
}
fn threshold(limit: u8, was_exceeded: bool, release_margin_percent: u8) -> u8 {
if !was_exceeded {
return limit;
}
limit.saturating_sub(release_margin_percent).max(1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shard::quota::QuotaConfig;
use crate::shard::quota::config::DEFAULT_RELEASE_MARGIN_PERCENT;
#[test]
fn a_tripped_resource_clears_only_below_the_release_margin() {
const MARGIN: u8 = DEFAULT_RELEASE_MARGIN_PERCENT;
assert_eq!(threshold(90, false, MARGIN), 90);
assert_eq!(threshold(90, true, MARGIN), 85);
assert_eq!(threshold(5, true, MARGIN), 1);
assert_eq!(threshold(1, true, MARGIN), 1);
assert_eq!(threshold(90, true, 0), 90);
assert_eq!(threshold(90, true, 20), 70);
}
#[test]
fn a_tripped_resource_keeps_refusing_until_it_clears_the_margin() {
let was_exceeded = AtomicBool::new(false);
let judge = |used: u8| {
evaluate(
Resource::DiskUsage,
Some(90),
&was_exceeded,
DEFAULT_RELEASE_MARGIN_PERCENT,
|_| Some(used),
)
};
assert_eq!(judge(80).ok(), Some(Some(false)));
assert!(judge(90).is_err());
let err = judge(87).expect_err("still within the release margin");
assert!(err.to_string().contains("has to fall below 85%"), "{err}");
assert_eq!(judge(84).ok(), Some(Some(false)));
assert_eq!(judge(88).ok(), Some(Some(false)));
}
#[test]
fn a_resource_that_cannot_be_judged_holds_no_verdict() {
let was_exceeded = AtomicBool::new(true);
let outcome = evaluate(
Resource::DiskUsage,
Some(90),
&was_exceeded,
DEFAULT_RELEASE_MARGIN_PERCENT,
|_| None,
);
assert_eq!(outcome.ok(), Some(None));
assert!(
!was_exceeded.load(Ordering::Relaxed),
"an unreadable stat is not a statement about the resource, \
and must not leave the node refusing work forever",
);
let was_exceeded = AtomicBool::new(true);
let outcome = evaluate(
Resource::DiskUsage,
None,
&was_exceeded,
DEFAULT_RELEASE_MARGIN_PERCENT,
|_| unreachable!(),
);
assert_eq!(outcome.ok(), Some(None));
assert!(!was_exceeded.load(Ordering::Relaxed));
}
#[test]
fn limits_only_apply_while_the_quota_is_enabled() {
let dir = tempfile::Builder::new().tempdir().unwrap();
let settings = QuotaConfig {
enabled: false,
max_disk_usage_percent: Some(1),
..Default::default()
};
let manager = QuotaManager::load_or_init(dir.path(), settings).unwrap();
manager.check_update().unwrap();
assert_eq!(manager.exceeded().disk_usage, None);
manager
.set_config(QuotaConfig {
enabled: true,
..settings
})
.unwrap();
let err = manager.check_update().unwrap_err();
assert!(err.to_string().contains("global quota config"), "{err}");
assert_eq!(manager.exceeded().disk_usage, Some(true));
}
}