use crate::fs::Fs;
use crate::path::Path;
use crate::tree::inner::TreeId;
use alloc::sync::Arc;
use alloc::vec::Vec;
use portable_atomic::{AtomicBool, Ordering::Relaxed};
#[cfg(feature = "std")]
use parking_lot::Mutex;
#[cfg(not(feature = "std"))]
use spin::Mutex;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum Pool {
Volume(u64),
Unproven,
Quota(TreeId),
}
impl Pool {
fn of(fs: &dyn Fs, path: &Path) -> Self {
fs.volume_id(path).map_or(Self::Unproven, Self::Volume)
}
fn shares(self, other: Self) -> bool {
match (self, other) {
(Self::Quota(_), _) | (_, Self::Quota(_)) => self == other,
(Self::Unproven, _) | (_, Self::Unproven) => true,
(Self::Volume(a), Self::Volume(b)) => a == b,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Floor {
Keep,
MayConsume,
}
#[derive(Default)]
pub struct CapacityPool {
held: Mutex<crate::HashMap<Pool, u64>>,
quota_released: Mutex<crate::HashMap<TreeId, u64>>,
#[cfg(test)]
excess: Mutex<crate::HashMap<Pool, u64>>,
}
impl core::fmt::Debug for CapacityPool {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("CapacityPool").finish_non_exhaustive()
}
}
pub struct Demand<'a> {
pub fs: &'a dyn Fs,
pub path: &'a Path,
pub bytes: u64,
}
#[derive(Clone, Copy, Debug)]
pub struct Check {
pub quota_headroom: u64,
pub floor: Floor,
}
fn held_against(held: &crate::HashMap<Pool, u64>, pool: Pool) -> u64 {
held.iter()
.filter(|(other, _)| pool.shares(**other))
.map(|(_, bytes)| *bytes)
.sum()
}
fn fits(bytes: u64, room: u64, floor: Floor) -> bool {
const FLOOR: u64 = crate::tree::MIN_RESERVED_HEADROOM;
(room >= FLOOR && bytes <= room - FLOOR) || (floor == Floor::MayConsume && bytes <= room)
}
fn gather(demands: &[Demand<'_>], probe: bool) -> (Vec<(Pool, u64, u64)>, u64) {
tally(
demands
.iter()
.filter(|demand| demand.bytes > 0)
.map(|demand| {
let free = if probe {
demand.fs.available_space(demand.path).unwrap_or(u64::MAX)
} else {
u64::MAX
};
(Pool::of(demand.fs, demand.path), demand.bytes, free)
}),
)
}
fn tally(probes: impl Iterator<Item = (Pool, u64, u64)>) -> (Vec<(Pool, u64, u64)>, u64) {
let mut volumes: Vec<(Pool, u64, u64)> = Vec::new();
let mut total = 0u64;
for (pool, bytes, free) in probes {
total += bytes;
if let Some(slot) = volumes.iter_mut().find(|(p, _, _)| *p == pool) {
slot.1 += bytes;
slot.2 = slot.2.min(free);
continue;
}
volumes.push((pool, bytes, free));
}
(volumes, total)
}
fn admits(
held: &crate::HashMap<Pool, u64>,
tree: TreeId,
volumes: &[(Pool, u64, u64)],
total: u64,
check: Check,
) -> bool {
volumes_fit(held, volumes, check.floor)
&& total
<= check
.quota_headroom
.saturating_sub(held_against(held, Pool::Quota(tree)))
}
fn volumes_fit(
held: &crate::HashMap<Pool, u64>,
volumes: &[(Pool, u64, u64)],
floor: Floor,
) -> bool {
volumes.iter().all(|&(pool, _, free)| {
let own: u64 = volumes
.iter()
.filter(|(other, _, _)| pool.shares(*other))
.map(|(_, bytes, _)| bytes)
.sum();
fits(own, free.saturating_sub(held_against(held, pool)), floor)
})
}
impl CapacityPool {
pub(crate) fn available(&self, fs: &dyn Fs, path: &Path) -> u64 {
let held = self.held.lock();
let Ok(free) = fs.available_space(path) else {
return u64::MAX;
};
free.saturating_sub(held_against(&held, Pool::of(fs, path)))
}
pub(crate) fn quota_held(&self, tree: TreeId) -> u64 {
self.held
.lock()
.get(&Pool::Quota(tree))
.copied()
.unwrap_or(0)
}
pub(crate) fn would_fit(&self, tree: TreeId, demands: &[Demand<'_>], check: Check) -> bool {
let held = self.held.lock();
let (volumes, total) = gather(demands, true);
admits(&held, tree, &volumes, total, check)
}
pub(crate) fn reserve(
self: &Arc<Self>,
tree: TreeId,
demands: &[Demand<'_>],
check: Option<Check>,
) -> Option<Reservation> {
let mut held = self.held.lock();
let (volumes, total) = gather(demands, check.is_some());
if let Some(check) = check
&& !admits(&held, tree, &volumes, total, check)
{
return None;
}
let growth = check.map(|check| Check {
floor: Floor::Keep,
..check
});
let room = self.take(&mut held, tree, volumes, total, growth);
drop(held);
Some(room)
}
pub(crate) fn held_on(&self, fs: &dyn Fs, path: &Path) -> u64 {
held_against(&self.held.lock(), Pool::of(fs, path))
}
pub(crate) fn reserve_within(
self: &Arc<Self>,
tree: TreeId,
demands: &[(Demand<'_>, u64)],
check: Check,
) -> Option<Reservation> {
let mut held = self.held.lock();
let taken_since = demands.iter().any(|(demand, limit)| {
demand.bytes > 0 && held_against(&held, Pool::of(demand.fs, demand.path)) > *limit
});
if taken_since {
return None;
}
let demands: Vec<Demand<'_>> = demands
.iter()
.map(|(demand, _)| Demand {
fs: demand.fs,
path: demand.path,
bytes: demand.bytes,
})
.collect();
let (volumes, total) = gather(&demands, false);
if total
> check
.quota_headroom
.saturating_sub(held_against(&held, Pool::Quota(tree)))
{
return None;
}
let room = self.take(&mut held, tree, volumes, total, Some(check));
drop(held);
Some(room)
}
fn take(
self: &Arc<Self>,
held: &mut crate::HashMap<Pool, u64>,
tree: TreeId,
volumes: Vec<(Pool, u64, u64)>,
total: u64,
check: Option<Check>,
) -> Reservation {
let quota = Pool::Quota(tree);
let released_at_start = self.quota_released(tree);
for &(pool, bytes, _) in &volumes {
*held.entry(pool).or_default() += bytes;
}
*held.entry(quota).or_default() += total;
Reservation {
pool: Arc::clone(self),
check,
tree,
released_at_start,
grants: Mutex::new(Grants {
volumes: volumes
.into_iter()
.map(|(pool, granted, _)| Grant {
pool,
granted,
landed: 0,
})
.collect(),
quota: (quota, total),
}),
refused: AtomicBool::new(false),
}
}
#[cfg(test)]
pub(crate) fn hold(self: &Arc<Self>, tree: TreeId, demands: &[Demand<'_>]) -> Reservation {
#[expect(
clippy::expect_used,
reason = "a reservation without a check is never refused"
)]
self.reserve(tree, demands, None)
.expect("a reservation without a check is never refused")
}
#[expect(
clippy::significant_drop_tightening,
reason = "a quota hold and its count of releases change under the account's lock, so a reservation reads them in agreement"
)]
fn release(&self, pool: Pool, bytes: u64) {
if bytes == 0 {
return;
}
let mut held = self.held.lock();
if let Some(slot) = held.get_mut(&pool) {
debug_assert!(*slot >= bytes, "a pool gives back only what it held");
*slot -= bytes;
if *slot == 0 {
held.remove(&pool);
}
if let Pool::Quota(tree) = pool {
*self.quota_released.lock().entry(tree).or_default() += bytes;
}
}
}
fn quota_released(&self, tree: TreeId) -> u64 {
self.quota_released.lock().get(&tree).copied().unwrap_or(0)
}
pub(crate) fn forget(&self, tree: TreeId) {
let mut held = self.held.lock();
held.remove(&Pool::Quota(tree));
self.quota_released.lock().remove(&tree);
drop(held);
}
#[cfg(test)]
pub(crate) fn accounts(&self, tree: TreeId) -> bool {
self.held.lock().contains_key(&Pool::Quota(tree))
|| self.quota_released.lock().contains_key(&tree)
}
#[cfg(test)]
pub(crate) fn excess_on(&self, fs: &dyn Fs, path: &Path) -> u64 {
self.excess
.lock()
.get(&Pool::of(fs, path))
.copied()
.unwrap_or(0)
}
#[cfg(test)]
fn held(&self, pool: Pool) -> u64 {
self.held.lock().get(&pool).copied().unwrap_or(0)
}
}
struct Grant {
pool: Pool,
granted: u64,
landed: u64,
}
struct Grants {
volumes: Vec<Grant>,
quota: (Pool, u64),
}
pub struct Reservation {
pool: Arc<CapacityPool>,
check: Option<Check>,
tree: TreeId,
released_at_start: u64,
grants: Mutex<Grants>,
refused: AtomicBool,
}
impl Reservation {
pub(crate) fn try_add(&self, demand: &Demand<'_>, check: Check) -> bool {
if demand.bytes == 0 {
return true;
}
let mut grants = self.grants.lock();
let mut held = self.pool.held.lock();
let (volumes, total) = gather(core::slice::from_ref(demand), true);
let (quota, granted) = grants.quota;
if !volumes_fit(&held, &volumes, check.floor)
|| granted + total > self.quota_room(&held, grants.quota, check.quota_headroom)
{
return false;
}
let pool = Pool::of(demand.fs, demand.path);
if let Some(grant) = grants.volumes.iter_mut().find(|grant| grant.pool == pool) {
grant.granted += demand.bytes;
} else {
grants.volumes.push(Grant {
pool,
granted: demand.bytes,
landed: 0,
});
}
grants.quota.1 += demand.bytes;
drop(grants);
*held.entry(pool).or_default() += demand.bytes;
*held.entry(quota).or_default() += demand.bytes;
drop(held);
true
}
fn quota_room(
&self,
held: &crate::HashMap<Pool, u64>,
(quota, granted): (Pool, u64),
headroom: u64,
) -> u64 {
let others_now = held_against(held, quota) - granted;
let released_since = self.pool.quota_released(self.tree) - self.released_at_start;
headroom.saturating_sub(others_now + released_since)
}
pub(crate) fn landed(&self, fs: &dyn Fs, path: &Path, len: u64) -> crate::Result<()> {
let pool = Pool::of(fs, path);
let mut grants = self.grants.lock();
let (covered, excess) =
if let Some(grant) = grants.volumes.iter_mut().find(|grant| grant.pool == pool) {
let covered = len.min(grant.granted - grant.landed);
grant.landed += len;
grant.granted += len - covered;
(covered, len - covered)
} else {
grants.volumes.push(Grant {
pool,
granted: len,
landed: len,
});
(0, len)
};
drop(grants);
self.pool.release(pool, covered);
if excess == 0 {
return Ok(());
}
#[cfg(test)]
{
*self.pool.excess.lock().entry(pool).or_default() += excess;
}
self.grow(fs, path, pool, excess)
}
fn grow(&self, fs: &dyn Fs, path: &Path, pool: Pool, excess: u64) -> crate::Result<()> {
let Some(check) = self.check else {
return Ok(());
};
let mut grants = self.grants.lock();
let (quota, granted) = grants.quota;
let mut held = self.pool.held.lock();
let free = fs.available_space(path).unwrap_or(u64::MAX);
let room = free.saturating_sub(held_against(&held, pool));
let quota_room = self.quota_room(&held, grants.quota, check.quota_headroom);
let keeps_room = match check.floor {
Floor::Keep => fits(0, room, Floor::Keep),
Floor::MayConsume => room > 0,
};
if keeps_room && granted + excess <= quota_room {
*held.entry(quota).or_default() += excess;
grants.quota.1 += excess;
return Ok(());
}
drop(held);
drop(grants);
self.refused.store(true, Relaxed);
Err(crate::Error::StorageFull {
used: granted + excess,
limit: quota_room,
})
}
pub(crate) fn strand(&self, bytes: Option<u64>) -> QuotaHold {
let mut grants = self.grants.lock();
let (quota, granted) = grants.quota;
let kept = bytes.unwrap_or(granted);
let from_grant = kept.min(granted);
grants.quota.1 -= from_grant;
let past_grant = kept - from_grant;
drop(grants);
if past_grant > 0 {
*self.pool.held.lock().entry(quota).or_default() += past_grant;
}
QuotaHold {
pool: Arc::clone(&self.pool),
quota,
bytes: kept,
}
}
pub(crate) fn refused(&self) -> bool {
self.refused.load(Relaxed)
}
}
pub struct QuotaHold {
pool: Arc<CapacityPool>,
quota: Pool,
bytes: u64,
}
impl QuotaHold {
pub(crate) fn release_but(self, still: Option<u64>) {
let keep = still.unwrap_or(self.bytes).min(self.bytes);
self.pool.release(self.quota, self.bytes - keep);
}
}
impl Drop for Reservation {
fn drop(&mut self) {
let grants = self.grants.get_mut();
for grant in grants.volumes.drain(..) {
self.pool.release(grant.pool, grant.granted - grant.landed);
}
self.pool.release(grants.quota.0, grants.quota.1);
}
}
#[cfg(test)]
#[expect(clippy::expect_used, reason = "test code")]
mod tests;
#[cfg(all(test, feature = "std"))]
#[expect(clippy::expect_used, reason = "test code")]
mod tree_tests;