use std::collections::hash_map::Entry;
use std::path::Path;
use std::sync::Arc;
use crate::common::disk_usage::DiskUsage;
use super::QuotaManager;
use crate::shard::quota::check::percent_of;
use crate::shard::quota::meter::{Meter, reusable};
use crate::shard::quota::status::QuotaUsage;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiskFit {
Fits { available: u64 },
TooLarge { available: u64, required: u64 },
Unknown,
}
impl QuotaManager {
pub fn usage(&self) -> QuotaUsage {
QuotaUsage {
resident_memory_percent: self.resident_memory_percent(None),
disk_usage_percent: self.disk_usage_percent(&self.storage_path, None),
}
}
pub fn available_bytes(&self, path: &Path, watch_below: u64) -> Option<u64> {
self.disk_usage(path, |usage| usage.available >= watch_below)
.map(|usage| usage.available)
}
pub fn disk_capacity_bytes(&self, path: &Path) -> Option<u64> {
self.disk_usage(path, |_| true).map(|usage| usage.total)
}
pub fn fits_on_disk(&self, path: &Path, required_bytes: u64) -> DiskFit {
let Some(available) = self.available_bytes(path, required_bytes) else {
return DiskFit::Unknown;
};
if available < required_bytes {
DiskFit::TooLarge {
available,
required: required_bytes,
}
} else {
DiskFit::Fits { available }
}
}
pub fn resident_memory_percent(&self, limit: Option<u8>) -> Option<u8> {
self.memory.measure(
|percent| reusable(percent, limit),
|| {
let resident = crate::common::memory_usage::resident_bytes()?;
percent_of(resident as u64, crate::segment::utils::mem::total_memory_bytes())
},
)
}
pub(super) fn disk_usage_percent(&self, path: &Path, limit: Option<u8>) -> Option<u8> {
let usage = self.disk_usage(path, |usage| {
reusable(percent_of(usage.used(), usage.total), limit)
})?;
percent_of(usage.used(), usage.total)
}
fn disk_usage(
&self,
path: &Path,
still_ample: impl Fn(DiskUsage) -> bool,
) -> Option<DiskUsage> {
let meter = match self.disk.lock().entry(path.to_path_buf()) {
Entry::Occupied(entry) => Arc::clone(entry.get()),
Entry::Vacant(entry) => Arc::clone(entry.insert(Arc::new(Meter::default()))),
};
meter.measure(
|usage| usage.is_none_or(&still_ample),
|| crate::common::disk_usage::disk_usage(path),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shard::quota::QuotaConfig;
#[test]
fn fits_on_disk_answers_from_free_space_not_the_quota() {
let dir = tempfile::Builder::new().tempdir().unwrap();
let manager = QuotaManager::load_or_init(
dir.path(),
QuotaConfig {
enabled: true,
max_disk_usage_percent: Some(1),
..Default::default()
},
)
.unwrap();
let fit = manager.fits_on_disk(dir.path(), 0);
assert!(matches!(fit, DiskFit::Fits { .. }), "{fit:?}");
let fit = manager.fits_on_disk(dir.path(), u64::MAX);
let DiskFit::TooLarge {
available,
required,
} = fit
else {
panic!("expected the disk to be too small, got {fit:?}");
};
assert_eq!(required, u64::MAX);
assert!(available < u64::MAX);
}
}