Skip to main content

lsm_tree/compaction/
capacity.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026-present, Dmitry Prudnikov
3
4//! Room compactions take on the volumes they write to: reserved before a
5//! compaction writes, so that two compactions cannot both count on the same
6//! free bytes, and held until what it wrote is installed or removed.
7//!
8//! A reservation is checked against the free space a volume reports less what
9//! the reservations already on it have still to write. Every output file a
10//! compaction finishes lands on its volume, whose report counts it from then
11//! on, so the reservation holds that much less there. A compaction whose
12//! outputs outgrow its reservation, as a compaction filter or a merge operator
13//! can make them, takes the excess on as it lands, or stops when the volume
14//! keeps no room beyond it, its inputs left as they were.
15
16use crate::fs::Fs;
17use crate::path::Path;
18use crate::tree::inner::TreeId;
19use alloc::sync::Arc;
20use alloc::vec::Vec;
21use portable_atomic::{AtomicBool, Ordering::Relaxed};
22
23#[cfg(feature = "std")]
24use parking_lot::Mutex;
25// no-std: spin mirrors parking_lot's Mutex API without an allocator.
26#[cfg(not(feature = "std"))]
27use spin::Mutex;
28
29/// Where reserved bytes are counted.
30#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
31enum Pool {
32    /// A volume, by the id its backend reports for it.
33    Volume(u64),
34    /// Every folder whose backend reports no volume id: it is proven apart
35    /// from no other, so it is taken to share every volume.
36    Unproven,
37    /// A tree's quota.
38    Quota(TreeId),
39}
40
41impl Pool {
42    fn of(fs: &dyn Fs, path: &Path) -> Self {
43        fs.volume_id(path).map_or(Self::Unproven, Self::Volume)
44    }
45
46    /// Whether bytes held in `other` take room counted in `self`: every
47    /// volume shares the unproven pool, and a quota is its own.
48    fn shares(self, other: Self) -> bool {
49        match (self, other) {
50            (Self::Quota(_), _) | (_, Self::Quota(_)) => self == other,
51            (Self::Unproven, _) | (_, Self::Unproven) => true,
52            (Self::Volume(a), Self::Volume(b)) => a == b,
53        }
54    }
55}
56
57/// Whether a reservation may take the room the engine keeps free for flushes.
58#[derive(Clone, Copy, PartialEq, Eq, Debug)]
59pub enum Floor {
60    /// Only room the floor leaves: work that may as well wait.
61    Keep,
62    /// The floor as well, which breaks the deadlock of a compaction that
63    /// frees space and has none to start in.
64    MayConsume,
65}
66
67/// The room compactions in progress hold on the volumes they write to,
68/// shared by every tree given the same pool.
69///
70/// Every tree keeps a pool of its own unless it is given one with
71/// [`Config::compaction_capacity`](crate::Config::compaction_capacity). Trees
72/// whose files share volumes should share a pool: each then reserves against
73/// what the others have still to write instead of counting on the same free
74/// bytes. The account is kept in this process: room another process takes
75/// shows only in the free space the volume reports.
76///
77/// # Examples
78///
79/// ```
80/// use lsm_tree::{CapacityPool, Config, SequenceNumberCounter};
81/// use std::sync::Arc;
82///
83/// # let a_dir = tempfile::tempdir()?;
84/// # let b_dir = tempfile::tempdir()?;
85/// let pool = Arc::new(CapacityPool::default());
86/// let a = Config::new(&a_dir, SequenceNumberCounter::default(), SequenceNumberCounter::default())
87///     .compaction_capacity(Arc::clone(&pool));
88/// let b = Config::new(&b_dir, SequenceNumberCounter::default(), SequenceNumberCounter::default())
89///     .compaction_capacity(Arc::clone(&pool));
90/// # Ok::<(), Box<dyn std::error::Error>>(())
91/// ```
92#[derive(Default)]
93pub struct CapacityPool {
94    held: Mutex<crate::HashMap<Pool, u64>>,
95    /// The quota bytes each tree's reservations gave back, ever: what its
96    /// compactions installed into its footprint, or abandoned, counted as
97    /// installed. Locked inside `held`, never on its own before it.
98    quota_released: Mutex<crate::HashMap<TreeId, u64>>,
99    /// The bytes that landed past the reservations covering them, by pool.
100    #[cfg(test)]
101    excess: Mutex<crate::HashMap<Pool, u64>>,
102}
103
104impl core::fmt::Debug for CapacityPool {
105    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
106        f.debug_struct("CapacityPool").finish_non_exhaustive()
107    }
108}
109
110/// Bytes a compaction is about to write into the folder `path` of `fs`.
111pub struct Demand<'a> {
112    pub fs: &'a dyn Fs,
113    pub path: &'a Path,
114    pub bytes: u64,
115}
116
117/// How a reservation is checked.
118#[derive(Clone, Copy, Debug)]
119pub struct Check {
120    /// The room left under the tree's quota, `u64::MAX` without one.
121    pub quota_headroom: u64,
122    /// Whether the flush floor may be taken.
123    pub floor: Floor,
124}
125
126/// What the account holds that takes room counted in `pool`.
127fn held_against(held: &crate::HashMap<Pool, u64>, pool: Pool) -> u64 {
128    held.iter()
129        .filter(|(other, _)| pool.shares(**other))
130        .map(|(_, bytes)| *bytes)
131        .sum()
132}
133
134/// Whether `bytes` fit in `room`, leaving the flush floor unless it may be
135/// taken.
136fn fits(bytes: u64, room: u64, floor: Floor) -> bool {
137    const FLOOR: u64 = crate::tree::MIN_RESERVED_HEADROOM;
138    (room >= FLOOR && bytes <= room - FLOOR) || (floor == Floor::MayConsume && bytes <= room)
139}
140
141/// The bytes `demands` put into each pool, with the free space of the pool
142/// when `probe` asks for it (`u64::MAX` otherwise), and their total.
143///
144/// Called with the account locked: a probe read before it could predate an
145/// output that landed and gave its room back in between, and count that room
146/// free on the volume and free in the account both. A folder written nothing
147/// is left out: its volume holds none of the output, and a full one must not
148/// refuse what fits where it goes.
149fn gather(demands: &[Demand<'_>], probe: bool) -> (Vec<(Pool, u64, u64)>, u64) {
150    tally(
151        demands
152            .iter()
153            .filter(|demand| demand.bytes > 0)
154            .map(|demand| {
155                let free = if probe {
156                    demand.fs.available_space(demand.path).unwrap_or(u64::MAX)
157                } else {
158                    u64::MAX
159                };
160                (Pool::of(demand.fs, demand.path), demand.bytes, free)
161            }),
162    )
163}
164
165/// Sums `(pool, bytes, free)` per pool, keeping the least free space any of
166/// its folders reported: folders with no volume id share the unproven pool
167/// though they may lie on volumes apart, and each must hold what they take
168/// together.
169fn tally(probes: impl Iterator<Item = (Pool, u64, u64)>) -> (Vec<(Pool, u64, u64)>, u64) {
170    let mut volumes: Vec<(Pool, u64, u64)> = Vec::new();
171    let mut total = 0u64;
172    for (pool, bytes, free) in probes {
173        // Bounded by the bytes the volumes hold.
174        total += bytes;
175        if let Some(slot) = volumes.iter_mut().find(|(p, _, _)| *p == pool) {
176            slot.1 += bytes;
177            slot.2 = slot.2.min(free);
178            continue;
179        }
180        volumes.push((pool, bytes, free));
181    }
182    (volumes, total)
183}
184
185/// Whether `volumes`, `total` bytes in all, fit beside what `held` holds:
186/// each pool's room past what reservations sharing it hold takes what this
187/// compaction writes into every pool sharing it, and the tree's quota takes
188/// the total.
189fn admits(
190    held: &crate::HashMap<Pool, u64>,
191    tree: TreeId,
192    volumes: &[(Pool, u64, u64)],
193    total: u64,
194    check: Check,
195) -> bool {
196    volumes_fit(held, volumes, check.floor)
197        && total
198            <= check
199                .quota_headroom
200                .saturating_sub(held_against(held, Pool::Quota(tree)))
201}
202
203/// Whether each pool's room past what reservations sharing it hold takes what
204/// `volumes` write into every pool sharing it.
205fn volumes_fit(
206    held: &crate::HashMap<Pool, u64>,
207    volumes: &[(Pool, u64, u64)],
208    floor: Floor,
209) -> bool {
210    volumes.iter().all(|&(pool, _, free)| {
211        let own: u64 = volumes
212            .iter()
213            .filter(|(other, _, _)| pool.shares(*other))
214            .map(|(_, bytes, _)| bytes)
215            .sum();
216        // Room past what is reserved is all there is: none, not an error,
217        // once the reservations take it all.
218        fits(own, free.saturating_sub(held_against(held, pool)), floor)
219    })
220}
221
222impl CapacityPool {
223    /// The free space the folder `path` reports, less what the reservations
224    /// sharing its volume have still to write; `u64::MAX` when the backend
225    /// reports none.
226    pub(crate) fn available(&self, fs: &dyn Fs, path: &Path) -> u64 {
227        // Probed with the account locked, as `gather` probes.
228        let held = self.held.lock();
229        let Ok(free) = fs.available_space(path) else {
230            return u64::MAX;
231        };
232        // Free space past what is reserved is all there is to give: none,
233        // not an error, once the reservations take it all.
234        free.saturating_sub(held_against(&held, Pool::of(fs, path)))
235    }
236
237    /// What the compactions of `tree` in progress hold of its quota.
238    pub(crate) fn quota_held(&self, tree: TreeId) -> u64 {
239        self.held
240            .lock()
241            .get(&Pool::Quota(tree))
242            .copied()
243            .unwrap_or(0)
244    }
245
246    /// Whether `demands` of a compaction of `tree` would fit beside what the
247    /// pool holds, as [`Self::reserve`] checks them, reserving nothing.
248    pub(crate) fn would_fit(&self, tree: TreeId, demands: &[Demand<'_>], check: Check) -> bool {
249        let held = self.held.lock();
250        let (volumes, total) = gather(demands, true);
251        admits(&held, tree, &volumes, total, check)
252    }
253
254    /// Reserves `demands` for a compaction of `tree`, or `None` when `check`
255    /// finds no room for them. Without a check the room is reserved whatever
256    /// is free, so that checked compactions sharing the pool still count it.
257    pub(crate) fn reserve(
258        self: &Arc<Self>,
259        tree: TreeId,
260        demands: &[Demand<'_>],
261        check: Option<Check>,
262    ) -> Option<Reservation> {
263        let mut held = self.held.lock();
264        let (volumes, total) = gather(demands, check.is_some());
265        if let Some(check) = check
266            && !admits(&held, tree, &volumes, total, check)
267        {
268            return None;
269        }
270        // What it writes past its estimate leaves the flush floor, whatever
271        // the estimate itself was let to take.
272        let growth = check.map(|check| Check {
273            floor: Floor::Keep,
274            ..check
275        });
276        let room = self.take(&mut held, tree, volumes, total, growth);
277        drop(held);
278        Some(room)
279    }
280
281    /// What the reservations sharing the volume of the folder `path` hold.
282    pub(crate) fn held_on(&self, fs: &dyn Fs, path: &Path) -> u64 {
283        held_against(&self.held.lock(), Pool::of(fs, path))
284    }
285
286    /// Reserves `demands` for a compaction of `tree` when, on the volume of
287    /// each, the reservations sharing it hold no more than its `limit`,
288    /// checked and taken in one step: a pass that sized its work to the room
289    /// it measured, leaving the others that much, stops rather than write over
290    /// room another compaction took since. Its own writes are checked against
291    /// the tree's quota in `check`, not against the volumes' room it measured;
292    /// what it writes past the grant is checked against `check` as it lands.
293    pub(crate) fn reserve_within(
294        self: &Arc<Self>,
295        tree: TreeId,
296        demands: &[(Demand<'_>, u64)],
297        check: Check,
298    ) -> Option<Reservation> {
299        let mut held = self.held.lock();
300        let taken_since = demands.iter().any(|(demand, limit)| {
301            demand.bytes > 0 && held_against(&held, Pool::of(demand.fs, demand.path)) > *limit
302        });
303        if taken_since {
304            return None;
305        }
306        let demands: Vec<Demand<'_>> = demands
307            .iter()
308            .map(|(demand, _)| Demand {
309                fs: demand.fs,
310                path: demand.path,
311                bytes: demand.bytes,
312            })
313            .collect();
314        let (volumes, total) = gather(&demands, false);
315        // The quota is the tree's own limit, not room another took: what the
316        // slice writes has to fit what it leaves past the tree's holds, as it
317        // stands now.
318        if total
319            > check
320                .quota_headroom
321                .saturating_sub(held_against(&held, Pool::Quota(tree)))
322        {
323            return None;
324        }
325        let room = self.take(&mut held, tree, volumes, total, Some(check));
326        drop(held);
327        Some(room)
328    }
329
330    /// Takes `volumes`, `total` bytes in all, into `held`, the account
331    /// locked, as a reservation of `tree` growing under `check`.
332    fn take(
333        self: &Arc<Self>,
334        held: &mut crate::HashMap<Pool, u64>,
335        tree: TreeId,
336        volumes: Vec<(Pool, u64, u64)>,
337        total: u64,
338        check: Option<Check>,
339    ) -> Reservation {
340        let quota = Pool::Quota(tree);
341        let released_at_start = self.quota_released(tree);
342        for &(pool, bytes, _) in &volumes {
343            *held.entry(pool).or_default() += bytes;
344        }
345        *held.entry(quota).or_default() += total;
346        Reservation {
347            pool: Arc::clone(self),
348            check,
349            tree,
350            released_at_start,
351            grants: Mutex::new(Grants {
352                volumes: volumes
353                    .into_iter()
354                    .map(|(pool, granted, _)| Grant {
355                        pool,
356                        granted,
357                        landed: 0,
358                    })
359                    .collect(),
360                quota: (quota, total),
361            }),
362            refused: AtomicBool::new(false),
363        }
364    }
365
366    /// Reserves `demands` for a compaction of `tree` whatever is free, so that
367    /// checked compactions sharing the pool still count them.
368    #[cfg(test)]
369    pub(crate) fn hold(self: &Arc<Self>, tree: TreeId, demands: &[Demand<'_>]) -> Reservation {
370        #[expect(
371            clippy::expect_used,
372            reason = "a reservation without a check is never refused"
373        )]
374        self.reserve(tree, demands, None)
375            .expect("a reservation without a check is never refused")
376    }
377
378    /// Gives `bytes` of `pool` back.
379    #[expect(
380        clippy::significant_drop_tightening,
381        reason = "a quota hold and its count of releases change under the account's lock, so a reservation reads them in agreement"
382    )]
383    fn release(&self, pool: Pool, bytes: u64) {
384        if bytes == 0 {
385            return;
386        }
387        let mut held = self.held.lock();
388        if let Some(slot) = held.get_mut(&pool) {
389            debug_assert!(*slot >= bytes, "a pool gives back only what it held");
390            *slot -= bytes;
391            if *slot == 0 {
392                held.remove(&pool);
393            }
394            if let Pool::Quota(tree) = pool {
395                // Bounded by the bytes the tree's compactions ever wrote.
396                *self.quota_released.lock().entry(tree).or_default() += bytes;
397            }
398        }
399    }
400
401    /// The quota bytes `tree`'s reservations gave back, ever. Read with the
402    /// account locked, so that it agrees with what the account holds.
403    fn quota_released(&self, tree: TreeId) -> u64 {
404        self.quota_released.lock().get(&tree).copied().unwrap_or(0)
405    }
406
407    /// Drops the account of `tree`, which left the pool: its reservations went
408    /// with the compactions that held the tree, and a tree opens under a new
409    /// id, so nothing reads the account again.
410    pub(crate) fn forget(&self, tree: TreeId) {
411        let mut held = self.held.lock();
412        held.remove(&Pool::Quota(tree));
413        self.quota_released.lock().remove(&tree);
414        drop(held);
415    }
416
417    /// Whether the pool keeps any account of `tree`.
418    #[cfg(test)]
419    pub(crate) fn accounts(&self, tree: TreeId) -> bool {
420        self.held.lock().contains_key(&Pool::Quota(tree))
421            || self.quota_released.lock().contains_key(&tree)
422    }
423
424    /// The bytes that landed in the folder `path` of `fs`, or a folder on its
425    /// volume, past the reservations covering them.
426    #[cfg(test)]
427    pub(crate) fn excess_on(&self, fs: &dyn Fs, path: &Path) -> u64 {
428        self.excess
429            .lock()
430            .get(&Pool::of(fs, path))
431            .copied()
432            .unwrap_or(0)
433    }
434
435    /// What `pool` holds now.
436    #[cfg(test)]
437    fn held(&self, pool: Pool) -> u64 {
438        self.held.lock().get(&pool).copied().unwrap_or(0)
439    }
440}
441
442/// The room one volume gave a reservation, and how much of it has landed.
443struct Grant {
444    pool: Pool,
445    granted: u64,
446    landed: u64,
447}
448
449/// What a reservation holds: its grant on each volume, and on its tree's
450/// quota, which is held whole until the reservation goes since an output
451/// counts against the quota once it is installed, not once it lands.
452struct Grants {
453    volumes: Vec<Grant>,
454    quota: (Pool, u64),
455}
456
457/// The room one compaction holds: given back as its outputs land on their
458/// volumes, and wholly once it is dropped.
459pub struct Reservation {
460    pool: Arc<CapacityPool>,
461    check: Option<Check>,
462    /// The tree whose quota it holds.
463    tree: TreeId,
464    /// What the tree's reservations had given back of the quota when it was
465    /// made. Its quota headroom was measured then: what the others give back
466    /// since, installed, counts in the tree's footprint though nobody holds it
467    /// here, whether they held it then or started and finished since.
468    released_at_start: u64,
469    grants: Mutex<Grants>,
470    refused: AtomicBool,
471}
472
473impl Reservation {
474    /// Takes `demand` on as well when it fits beside what the pool holds, as
475    /// [`CapacityPool::reserve`] checks a reservation, in one step with the
476    /// check: no other compaction can take the room in between.
477    pub(crate) fn try_add(&self, demand: &Demand<'_>, check: Check) -> bool {
478        if demand.bytes == 0 {
479            return true;
480        }
481        let mut grants = self.grants.lock();
482        let mut held = self.pool.held.lock();
483        let (volumes, total) = gather(core::slice::from_ref(demand), true);
484        let (quota, granted) = grants.quota;
485        if !volumes_fit(&held, &volumes, check.floor)
486            || granted + total > self.quota_room(&held, grants.quota, check.quota_headroom)
487        {
488            return false;
489        }
490        let pool = Pool::of(demand.fs, demand.path);
491        if let Some(grant) = grants.volumes.iter_mut().find(|grant| grant.pool == pool) {
492            grant.granted += demand.bytes;
493        } else {
494            grants.volumes.push(Grant {
495                pool,
496                granted: demand.bytes,
497                landed: 0,
498            });
499        }
500        grants.quota.1 += demand.bytes;
501        drop(grants);
502        *held.entry(pool).or_default() += demand.bytes;
503        *held.entry(quota).or_default() += demand.bytes;
504        drop(held);
505        true
506    }
507
508    /// What the tree's quota leaves this reservation, its own `granted`
509    /// bytes included, against `headroom` measured when it was made or
510    /// since.
511    ///
512    /// The others' holds now are counted as taken, and so is every byte they
513    /// gave back since it was made, installed into the footprint the headroom
514    /// predates. A hold given back without an install counts too: the room is
515    /// never more than the quota leaves, and sometimes less.
516    fn quota_room(
517        &self,
518        held: &crate::HashMap<Pool, u64>,
519        (quota, granted): (Pool, u64),
520        headroom: u64,
521    ) -> u64 {
522        // Its own grant is among what its quota holds, and it gives nothing
523        // back before it goes, so what was given back since is the others'.
524        let others_now = held_against(held, quota) - granted;
525        let released_since = self.pool.quota_released(self.tree) - self.released_at_start;
526        // A quota the others fill leaves none: no room, not an error.
527        headroom.saturating_sub(others_now + released_since)
528    }
529
530    /// A file of `len` bytes was written into the folder `path` of `fs`.
531    ///
532    /// The volume counts it now, so its grant holds that much less. A file
533    /// the grant does not cover takes the excess on; a checked reservation
534    /// then stops its compaction when the volume, past what the other
535    /// reservations on it have still to write, keeps no more than the flush
536    /// floor (no room at all, for one its check lets into the floor), or the
537    /// tree's quota has no room for the excess.
538    ///
539    /// # Errors
540    ///
541    /// [`crate::Error::StorageFull`] when the compaction must stop.
542    pub(crate) fn landed(&self, fs: &dyn Fs, path: &Path, len: u64) -> crate::Result<()> {
543        let pool = Pool::of(fs, path);
544        let mut grants = self.grants.lock();
545        let (covered, excess) =
546            if let Some(grant) = grants.volumes.iter_mut().find(|grant| grant.pool == pool) {
547                let covered = len.min(grant.granted - grant.landed);
548                grant.landed += len;
549                grant.granted += len - covered;
550                (covered, len - covered)
551            } else {
552                // A volume nothing was reserved on: all of it is excess.
553                grants.volumes.push(Grant {
554                    pool,
555                    granted: len,
556                    landed: len,
557                });
558                (0, len)
559            };
560        drop(grants);
561        self.pool.release(pool, covered);
562        if excess == 0 {
563            return Ok(());
564        }
565        #[cfg(test)]
566        {
567            *self.pool.excess.lock().entry(pool).or_default() += excess;
568        }
569        self.grow(fs, path, pool, excess)
570    }
571
572    /// Takes on `excess` bytes landed in `pool` beyond its grant.
573    fn grow(&self, fs: &dyn Fs, path: &Path, pool: Pool, excess: u64) -> crate::Result<()> {
574        let Some(check) = self.check else {
575            return Ok(());
576        };
577        let mut grants = self.grants.lock();
578        let (quota, granted) = grants.quota;
579        let mut held = self.pool.held.lock();
580        // Probed with the account locked, as `gather` probes.
581        let free = fs.available_space(path).unwrap_or(u64::MAX);
582        // The excess lies on the volume already: what is asked is whether the
583        // volume keeps the floor beside it and what the others still write.
584        let room = free.saturating_sub(held_against(&held, pool));
585        let quota_room = self.quota_room(&held, grants.quota, check.quota_headroom);
586        // A reservation let into the floor stops only once nothing is left
587        // past what the others have still to write.
588        let keeps_room = match check.floor {
589            Floor::Keep => fits(0, room, Floor::Keep),
590            Floor::MayConsume => room > 0,
591        };
592        if keeps_room && granted + excess <= quota_room {
593            *held.entry(quota).or_default() += excess;
594            grants.quota.1 += excess;
595            return Ok(());
596        }
597        drop(held);
598        drop(grants);
599        self.refused.store(true, Relaxed);
600        Err(crate::Error::StorageFull {
601            used: granted + excess,
602            limit: quota_room,
603        })
604    }
605
606    /// Keeps `bytes` of its tree's quota held once it goes, or the whole of
607    /// its grant when they are not known: outputs it covered stay on disk,
608    /// unseen by a quota counted from the installed version, until they are
609    /// removed or the tree's next open sweeps them. An output that outgrew
610    /// the grant, refused or not, is held whole, past the grant too. The hold
611    /// is given back with [`QuotaHold::release_but`]; one never given back
612    /// ends with the tree that left it, which opens again under a new id.
613    pub(crate) fn strand(&self, bytes: Option<u64>) -> QuotaHold {
614        let mut grants = self.grants.lock();
615        let (quota, granted) = grants.quota;
616        let kept = bytes.unwrap_or(granted);
617        let from_grant = kept.min(granted);
618        grants.quota.1 -= from_grant;
619        let past_grant = kept - from_grant;
620        drop(grants);
621        if past_grant > 0 {
622            // Bounded by the bytes the outputs take on disk.
623            *self.pool.held.lock().entry(quota).or_default() += past_grant;
624        }
625        QuotaHold {
626            pool: Arc::clone(&self.pool),
627            quota,
628            bytes: kept,
629        }
630    }
631
632    /// Whether the reservation stopped its compaction.
633    pub(crate) fn refused(&self) -> bool {
634        self.refused.load(Relaxed)
635    }
636}
637
638/// Quota bytes a reservation kept held past its end, for outputs it left on
639/// disk ([`Reservation::strand`]).
640pub struct QuotaHold {
641    pool: Arc<CapacityPool>,
642    quota: Pool,
643    bytes: u64,
644}
645
646impl QuotaHold {
647    /// Gives the hold back, all but the `still` bytes its outputs take on
648    /// disk yet (all of it when that is not known).
649    pub(crate) fn release_but(self, still: Option<u64>) {
650        let keep = still.unwrap_or(self.bytes).min(self.bytes);
651        self.pool.release(self.quota, self.bytes - keep);
652    }
653}
654
655impl Drop for Reservation {
656    fn drop(&mut self) {
657        let grants = self.grants.get_mut();
658        for grant in grants.volumes.drain(..) {
659            self.pool.release(grant.pool, grant.granted - grant.landed);
660        }
661        self.pool.release(grants.quota.0, grants.quota.1);
662    }
663}
664
665#[cfg(test)]
666#[expect(clippy::expect_used, reason = "test code")]
667mod tests;
668
669#[cfg(all(test, feature = "std"))]
670#[expect(clippy::expect_used, reason = "test code")]
671mod tree_tests;