1use std::collections::HashMap;
9use std::fmt;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
12
13use crate::runtime::fs::FileIdentity;
14
15#[derive(Debug)]
17pub struct DiskQuota {
18 limit: u64,
19 used: AtomicU64,
20 unmeasured: bool,
22 open: Mutex<HashMap<FileIdentity, OpenFile>>,
24}
25
26#[derive(Debug, Default)]
27struct OpenFile {
28 readers: u32,
29 writer: bool,
30 pending: u64,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Holder {
38 Reader,
39 Writer,
40}
41
42impl DiskQuota {
43 pub fn new(limit: u64, used: u64) -> Self {
44 Self {
45 limit,
46 used: AtomicU64::new(used),
47 unmeasured: false,
48 open: Mutex::default(),
49 }
50 }
51
52 pub fn unmeasured(limit: u64) -> Self {
55 Self {
56 unmeasured: true,
57 ..Self::new(limit, 0)
58 }
59 }
60
61 pub fn limit(&self) -> u64 {
62 self.limit
63 }
64
65 pub fn used(&self) -> u64 {
66 self.used.load(Ordering::Acquire)
67 }
68
69 pub fn set_used(&self, bytes: u64) {
72 self.used.store(bytes, Ordering::Release);
73 }
74
75 pub fn is_unmeasured(&self) -> bool {
76 self.unmeasured
77 }
78
79 pub fn reserve(&self, bytes: u64) -> Result<(), QuotaExceeded> {
83 if bytes == 0 {
84 return Ok(());
85 }
86 if self.unmeasured {
87 return Err(QuotaExceeded::Unmeasured { limit: self.limit });
88 }
89 let mut used = self.used.load(Ordering::Acquire);
90 loop {
91 let Some(next) = used.checked_add(bytes).filter(|next| *next <= self.limit) else {
92 return Err(QuotaExceeded::Over {
93 limit: self.limit,
94 used,
95 requested: bytes,
96 });
97 };
98 match self
99 .used
100 .compare_exchange_weak(used, next, Ordering::AcqRel, Ordering::Acquire)
101 {
102 Ok(_) => return Ok(()),
103 Err(actual) => used = actual,
104 }
105 }
106 }
107
108 pub fn record(&self, bytes: u64) {
110 let _ = self
111 .used
112 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |used| {
113 Some(used.saturating_add(bytes))
114 });
115 }
116
117 pub fn release(&self, bytes: u64) {
118 let _ = self
119 .used
120 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |used| {
121 Some(used.saturating_sub(bytes))
122 });
123 }
124
125 pub fn hold(self: &Arc<Self>, file: FileIdentity, holder: Holder) -> OpenFileGuard {
127 let mut open = self.open_files();
128 let entry = open.entry(file).or_default();
129 match holder {
130 Holder::Reader => entry.readers = entry.readers.saturating_add(1),
131 Holder::Writer => entry.writer = true,
132 }
133 OpenFileGuard {
134 quota: Arc::clone(self),
135 file,
136 holder,
137 }
138 }
139
140 pub fn is_held(&self, file: FileIdentity) -> bool {
142 self.open_files().contains_key(&file)
143 }
144
145 fn is_writers_own(&self, file: FileIdentity) -> bool {
147 self.open_files().get(&file).is_some_and(|held| held.writer)
148 }
149
150 pub fn release_file(&self, file: FileIdentity, bytes: u64) {
154 let mut open = self.open_files();
155 match open.get_mut(&file) {
156 Some(held) if held.writer => {}
157 Some(held) => held.pending = held.pending.saturating_add(bytes),
158 None => {
159 drop(open);
160 self.release(bytes);
161 }
162 }
163 }
164
165 fn release_writers_own_file(&self, file: FileIdentity, bytes: u64) {
169 let mut open = self.open_files();
170 match open.get_mut(&file) {
171 Some(held) if held.readers > 0 => held.pending = held.pending.saturating_add(bytes),
172 _ => {
173 drop(open);
174 self.release(bytes);
175 }
176 }
177 }
178
179 fn open_files(&self) -> MutexGuard<'_, HashMap<FileIdentity, OpenFile>> {
180 self.open.lock().unwrap_or_else(PoisonError::into_inner)
183 }
184}
185
186#[derive(Debug)]
188pub struct OpenFileGuard {
189 quota: Arc<DiskQuota>,
190 file: FileIdentity,
191 holder: Holder,
192}
193
194impl Drop for OpenFileGuard {
195 fn drop(&mut self) {
196 let mut open = self.quota.open_files();
197 let Some(held) = open.get_mut(&self.file) else {
198 return;
199 };
200 match self.holder {
201 Holder::Reader => held.readers = held.readers.saturating_sub(1),
202 Holder::Writer => held.writer = false,
203 }
204 if held.readers == 0 && !held.writer {
205 let pending = held.pending;
206 open.remove(&self.file);
207 drop(open);
208 self.quota.release(pending);
209 }
210 }
211}
212
213#[derive(Debug)]
229pub struct QuotaCharge {
230 quota: Option<Arc<DiskQuota>>,
231 allowance: u64,
232 drawn: u64,
234 reserved: u64,
235 replaces: Replaces,
236}
237
238#[derive(Debug, Clone, Copy)]
240enum Replaces {
241 Nothing,
244 File(FileIdentity, u64),
247 InPlace,
249}
250
251impl Replaces {
252 fn file(replaced: Option<(FileIdentity, u64)>) -> Self {
253 replaced.map_or(Self::Nothing, |(file, bytes)| Self::File(file, bytes))
254 }
255}
256
257impl QuotaCharge {
258 pub fn new(quota: Option<Arc<DiskQuota>>, replaced: Option<(FileIdentity, u64)>) -> Self {
260 let allowance = match ("a, replaced) {
261 (Some(quota), Some((file, bytes))) if !quota.is_held(file) => bytes,
262 _ => 0,
263 };
264 Self {
265 quota,
266 allowance,
267 drawn: 0,
268 reserved: 0,
269 replaces: Replaces::file(replaced),
270 }
271 }
272
273 pub fn in_place(quota: Option<Arc<DiskQuota>>, rewritten: Option<(FileIdentity, u64)>) -> Self {
278 let (allowance, replaces) = match ("a, rewritten) {
279 (Some(quota), Some((file, bytes))) if !quota.is_writers_own(file) => {
280 (bytes, Replaces::InPlace)
281 }
282 _ => (0, Replaces::Nothing),
283 };
284 Self {
285 quota,
286 allowance,
287 drawn: 0,
288 reserved: 0,
289 replaces,
290 }
291 }
292
293 pub fn reserve(&mut self, bytes: u64) -> Result<(), QuotaExceeded> {
296 let from_allowance = bytes.min(self.allowance.saturating_sub(self.drawn));
297 let beyond = bytes.saturating_sub(from_allowance);
298 if let Some(quota) = &self.quota {
299 quota.reserve(beyond)?;
300 }
301 self.drawn = self.drawn.saturating_add(from_allowance);
302 self.reserved = self.reserved.saturating_add(beyond);
303 Ok(())
304 }
305
306 pub fn unreserve(&mut self, bytes: u64) {
309 let from_reserved = bytes.min(self.reserved);
310 if let Some(quota) = &self.quota {
311 quota.release(from_reserved);
312 }
313 self.reserved = self.reserved.saturating_sub(from_reserved);
314 self.drawn = self
315 .drawn
316 .saturating_sub(bytes.saturating_sub(from_reserved));
317 }
318
319 pub fn cover(&mut self, replaced: Option<(FileIdentity, u64)>) -> Result<(), QuotaExceeded> {
323 let replaced_bytes = replaced.map_or(0, |(_, bytes)| bytes);
324 let uncovered = self.drawn.saturating_sub(replaced_bytes);
325 if let Some(quota) = &self.quota {
326 quota.reserve(uncovered)?;
327 }
328 self.drawn = self.drawn.saturating_sub(uncovered);
329 self.reserved = self.reserved.saturating_add(uncovered);
330 self.replaces = Replaces::file(replaced);
331 Ok(())
332 }
333
334 pub fn commit(mut self) {
338 let Some(quota) = self.quota.take() else {
339 return;
340 };
341 match self.replaces {
342 Replaces::Nothing => {}
343 Replaces::File(file, bytes) => {
344 quota.release_file(file, bytes.saturating_sub(self.drawn));
345 }
346 Replaces::InPlace => quota.release(self.allowance.saturating_sub(self.drawn)),
347 }
348 }
349
350 pub fn settle_at(mut self, left: u64) {
354 let Some(quota) = self.quota.take() else {
355 return;
356 };
357 match self.replaces {
358 Replaces::InPlace => {
359 let counted = self.allowance.saturating_add(self.reserved);
360 if left < counted {
361 quota.release(counted - left);
362 } else {
363 quota.record(left - counted);
364 }
365 }
366 Replaces::Nothing | Replaces::File(..) => {
367 quota.release(self.reserved.saturating_sub(left));
368 }
369 }
370 }
371
372 pub fn keep(mut self) {
375 self.quota = None;
376 }
377
378 pub fn release_when_closed(mut self, written: FileIdentity) {
381 if let Some(quota) = self.quota.take() {
382 quota.release_writers_own_file(written, self.reserved);
383 }
384 }
385}
386
387impl Drop for QuotaCharge {
388 fn drop(&mut self) {
389 if let Some(quota) = self.quota.take() {
390 quota.release(self.reserved);
391 }
392 }
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398pub enum QuotaExceeded {
399 Over {
401 limit: u64,
402 used: u64,
403 requested: u64,
404 },
405 Unmeasured { limit: u64 },
408}
409
410impl fmt::Display for QuotaExceeded {
411 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412 match self {
413 Self::Over {
414 limit,
415 used,
416 requested,
417 } => write!(
418 f,
419 "the filesystem's size limit of {limit} bytes would be exceeded: {used} bytes are in use and this needs {requested} more — remove files you no longer need, or write less"
420 ),
421 Self::Unmeasured { limit } => write!(
422 f,
423 "the filesystem couldn't be measured against its size limit of {limit} bytes, so it is treated as full until a run can measure it"
424 ),
425 }
426 }
427}
428
429impl std::error::Error for QuotaExceeded {}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn quota_reserves_within_the_limit_and_refuses_past_it() {
437 let quota = DiskQuota::new(100, 40);
438 quota.reserve(60).expect("fits exactly");
439 let refused = quota.reserve(1).expect_err("one byte over");
440 assert_eq!(
441 refused,
442 QuotaExceeded::Over {
443 limit: 100,
444 used: 100,
445 requested: 1,
446 }
447 );
448 assert_eq!(
449 refused.to_string(),
450 "the filesystem's size limit of 100 bytes would be exceeded: 100 bytes are in use \
451 and this needs 1 more — remove files you no longer need, or write less"
452 );
453 quota.release(30);
454 assert_eq!(quota.used(), 70);
455 quota.release(1_000);
456 assert_eq!(quota.used(), 0, "release never underflows");
457 }
458
459 #[test]
460 fn a_dropped_charge_gives_back_what_it_reserved() {
461 let quota = Arc::new(DiskQuota::new(100, 0));
462 let mut charge = QuotaCharge::new(Some(Arc::clone("a)), None);
463 charge.reserve(70).expect("fits");
464 assert_eq!(quota.used(), 70);
465 drop(charge);
466 assert_eq!(quota.used(), 0);
467 }
468
469 #[test]
470 fn a_charge_covers_a_replaced_file_that_shrank() {
471 let quota = Arc::new(DiskQuota::new(100, 90));
472 let file = identity();
473 let mut charge = QuotaCharge::new(Some(Arc::clone("a)), Some((file, 90)));
474 charge.reserve(90).expect("drawn from the allowance");
475 assert_eq!(quota.used(), 90);
476 quota.release(90);
479 charge
480 .cover(Some((file, 0)))
481 .expect("fits once the file is empty");
482 charge.commit();
483 assert_eq!(quota.used(), 90);
484 let mut again = QuotaCharge::new(Some(Arc::clone("a)), Some((file, 90)));
485 again.reserve(90).expect("drawn from the allowance");
486 assert!(
487 again.cover(Some((file, 0))).is_err(),
488 "the file shrank and nothing freed its bytes"
489 );
490 }
491
492 #[test]
493 fn a_held_file_gives_no_allowance() {
494 let quota = Arc::new(DiskQuota::new(100, 60));
495 let file = identity();
496 let reader = quota.hold(file, Holder::Reader);
497 let mut charge = QuotaCharge::new(Some(Arc::clone("a)), Some((file, 60)));
498 assert!(
499 charge.reserve(60).is_err(),
500 "the replaced file stays on disk while the reader holds it"
501 );
502 charge.reserve(40).expect("the growth beyond it fits");
503 charge.commit();
504 assert_eq!(quota.used(), 100, "the old file is still counted");
505 drop(reader);
506 assert_eq!(quota.used(), 40, "and freed when the reader closes");
507 }
508
509 #[test]
510 fn a_file_rewritten_in_place_frees_its_old_bytes_while_held() {
511 let quota = Arc::new(DiskQuota::new(100, 60));
512 let file = identity();
513 let _reader = quota.hold(file, Holder::Reader);
514 let mut charge = QuotaCharge::in_place(Some(Arc::clone("a)), Some((file, 60)));
515 charge.reserve(30).expect("drawn from the old contents");
516 charge.commit();
517 assert_eq!(quota.used(), 30, "the old contents are gone, held or not");
518 }
519
520 #[test]
521 fn a_failed_rewrite_in_place_counts_what_it_left() {
522 let quota = Arc::new(DiskQuota::new(100, 40));
523 let mut charge = QuotaCharge::in_place(Some(Arc::clone("a)), Some((identity(), 40)));
524 charge.reserve(50).expect("room for the new contents");
525 assert_eq!(quota.used(), 50);
526 charge.settle_at(20);
527 assert_eq!(
528 quota.used(),
529 20,
530 "the old contents are gone, 20 new bytes landed"
531 );
532
533 let untouched = Arc::new(DiskQuota::new(100, 40));
534 let mut charge =
535 QuotaCharge::in_place(Some(Arc::clone(&untouched)), Some((identity(), 40)));
536 charge.reserve(10).expect("drawn from the old contents");
537 charge.settle_at(40);
538 assert_eq!(
539 untouched.used(),
540 40,
541 "a copy that failed before truncating left the file"
542 );
543 }
544
545 #[test]
546 fn a_writers_own_file_rewritten_in_place_is_left_for_the_writer() {
547 let quota = Arc::new(DiskQuota::new(100, 60));
548 let file = identity();
549 let _writer = quota.hold(file, Holder::Writer);
550 let mut charge = QuotaCharge::in_place(Some(Arc::clone("a)), Some((file, 60)));
551 charge.reserve(30).expect("room for the new contents");
552 charge.commit();
553 assert_eq!(
554 quota.used(),
555 90,
556 "the writer's charge still holds the old contents"
557 );
558 }
559
560 #[test]
561 fn a_removed_file_held_open_stays_counted_until_it_closes() {
562 let quota = Arc::new(DiskQuota::new(100, 60));
563 let file = identity();
564 let reader = quota.hold(file, Holder::Reader);
565 quota.release_file(file, 60);
566 assert_eq!(quota.used(), 60, "an open file's bytes are still on disk");
567 drop(reader);
568 assert_eq!(quota.used(), 0);
569 }
570
571 #[test]
572 fn a_writers_own_file_is_left_for_the_writer_to_settle() {
573 let quota = Arc::new(DiskQuota::new(100, 0));
574 let file = identity();
575 let writer = quota.hold(file, Holder::Writer);
576 quota.record(40);
577 quota.release_file(file, 40);
578 assert_eq!(quota.used(), 40);
579 drop(writer);
580 assert_eq!(
581 quota.used(),
582 40,
583 "releasing it is the writer's charge's job"
584 );
585 }
586
587 fn identity() -> FileIdentity {
589 let file = tempfile::tempfile().expect("tempfile");
590 let metadata = cap_std::fs::Metadata::from_file(&file).expect("metadata");
591 FileIdentity::of(&metadata).expect("identity")
592 }
593}