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"
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 quota.release(30);
449 assert_eq!(quota.used(), 70);
450 quota.release(1_000);
451 assert_eq!(quota.used(), 0, "release never underflows");
452 }
453
454 #[test]
455 fn a_dropped_charge_gives_back_what_it_reserved() {
456 let quota = Arc::new(DiskQuota::new(100, 0));
457 let mut charge = QuotaCharge::new(Some(Arc::clone("a)), None);
458 charge.reserve(70).expect("fits");
459 assert_eq!(quota.used(), 70);
460 drop(charge);
461 assert_eq!(quota.used(), 0);
462 }
463
464 #[test]
465 fn a_charge_covers_a_replaced_file_that_shrank() {
466 let quota = Arc::new(DiskQuota::new(100, 90));
467 let file = identity();
468 let mut charge = QuotaCharge::new(Some(Arc::clone("a)), Some((file, 90)));
469 charge.reserve(90).expect("drawn from the allowance");
470 assert_eq!(quota.used(), 90);
471 quota.release(90);
474 charge
475 .cover(Some((file, 0)))
476 .expect("fits once the file is empty");
477 charge.commit();
478 assert_eq!(quota.used(), 90);
479 let mut again = QuotaCharge::new(Some(Arc::clone("a)), Some((file, 90)));
480 again.reserve(90).expect("drawn from the allowance");
481 assert!(
482 again.cover(Some((file, 0))).is_err(),
483 "the file shrank and nothing freed its bytes"
484 );
485 }
486
487 #[test]
488 fn a_held_file_gives_no_allowance() {
489 let quota = Arc::new(DiskQuota::new(100, 60));
490 let file = identity();
491 let reader = quota.hold(file, Holder::Reader);
492 let mut charge = QuotaCharge::new(Some(Arc::clone("a)), Some((file, 60)));
493 assert!(
494 charge.reserve(60).is_err(),
495 "the replaced file stays on disk while the reader holds it"
496 );
497 charge.reserve(40).expect("the growth beyond it fits");
498 charge.commit();
499 assert_eq!(quota.used(), 100, "the old file is still counted");
500 drop(reader);
501 assert_eq!(quota.used(), 40, "and freed when the reader closes");
502 }
503
504 #[test]
505 fn a_file_rewritten_in_place_frees_its_old_bytes_while_held() {
506 let quota = Arc::new(DiskQuota::new(100, 60));
507 let file = identity();
508 let _reader = quota.hold(file, Holder::Reader);
509 let mut charge = QuotaCharge::in_place(Some(Arc::clone("a)), Some((file, 60)));
510 charge.reserve(30).expect("drawn from the old contents");
511 charge.commit();
512 assert_eq!(quota.used(), 30, "the old contents are gone, held or not");
513 }
514
515 #[test]
516 fn a_failed_rewrite_in_place_counts_what_it_left() {
517 let quota = Arc::new(DiskQuota::new(100, 40));
518 let mut charge = QuotaCharge::in_place(Some(Arc::clone("a)), Some((identity(), 40)));
519 charge.reserve(50).expect("room for the new contents");
520 assert_eq!(quota.used(), 50);
521 charge.settle_at(20);
522 assert_eq!(
523 quota.used(),
524 20,
525 "the old contents are gone, 20 new bytes landed"
526 );
527
528 let untouched = Arc::new(DiskQuota::new(100, 40));
529 let mut charge =
530 QuotaCharge::in_place(Some(Arc::clone(&untouched)), Some((identity(), 40)));
531 charge.reserve(10).expect("drawn from the old contents");
532 charge.settle_at(40);
533 assert_eq!(
534 untouched.used(),
535 40,
536 "a copy that failed before truncating left the file"
537 );
538 }
539
540 #[test]
541 fn a_writers_own_file_rewritten_in_place_is_left_for_the_writer() {
542 let quota = Arc::new(DiskQuota::new(100, 60));
543 let file = identity();
544 let _writer = quota.hold(file, Holder::Writer);
545 let mut charge = QuotaCharge::in_place(Some(Arc::clone("a)), Some((file, 60)));
546 charge.reserve(30).expect("room for the new contents");
547 charge.commit();
548 assert_eq!(
549 quota.used(),
550 90,
551 "the writer's charge still holds the old contents"
552 );
553 }
554
555 #[test]
556 fn a_removed_file_held_open_stays_counted_until_it_closes() {
557 let quota = Arc::new(DiskQuota::new(100, 60));
558 let file = identity();
559 let reader = quota.hold(file, Holder::Reader);
560 quota.release_file(file, 60);
561 assert_eq!(quota.used(), 60, "an open file's bytes are still on disk");
562 drop(reader);
563 assert_eq!(quota.used(), 0);
564 }
565
566 #[test]
567 fn a_writers_own_file_is_left_for_the_writer_to_settle() {
568 let quota = Arc::new(DiskQuota::new(100, 0));
569 let file = identity();
570 let writer = quota.hold(file, Holder::Writer);
571 quota.record(40);
572 quota.release_file(file, 40);
573 assert_eq!(quota.used(), 40);
574 drop(writer);
575 assert_eq!(
576 quota.used(),
577 40,
578 "releasing it is the writer's charge's job"
579 );
580 }
581
582 fn identity() -> FileIdentity {
584 let file = tempfile::tempfile().expect("tempfile");
585 let metadata = cap_std::fs::Metadata::from_file(&file).expect("metadata");
586 FileIdentity::of(&metadata).expect("identity")
587 }
588}