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;