This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-lfs / src / admission.rs
4.8 kB 160 lines
1use knot_resource::{DiskGovernor, DiskReservation, ReserveError}; 2 3use crate::{ClaimedSize, FreeSpaceFloor, LfsError, LfsSize, LfsStorePath}; 4 5pub trait UploadAdmission: Send + Sync { 6 fn admit(&self, declared: ClaimedSize) -> Result<UploadPermit, LfsError>; 7 8 fn max_object(&self) -> LfsSize; 9} 10 11pub struct UploadPermit { 12 _reservation: Option<DiskReservation>, 13} 14 15impl UploadPermit { 16 fn unreserved() -> Self { 17 Self { _reservation: None } 18 } 19} 20 21pub struct StoreAdmission { 22 root: LfsStorePath, 23 max_object: LfsSize, 24 floor: FreeSpaceFloor, 25 governor: DiskGovernor, 26} 27 28impl StoreAdmission { 29 pub fn new(root: LfsStorePath, max_object: LfsSize, floor: FreeSpaceFloor) -> Self { 30 let governor = DiskGovernor::new(knot_resource::DiskFloorBytes::new(floor.get())); 31 Self { 32 root, 33 max_object, 34 floor, 35 governor, 36 } 37 } 38} 39 40impl UploadAdmission for StoreAdmission { 41 fn admit(&self, declared: ClaimedSize) -> Result<UploadPermit, LfsError> { 42 if declared.get() > self.max_object.get() { 43 tracing::warn!( 44 declared = declared.get(), 45 limit = self.max_object.get(), 46 "lfs upload denied by the object size limit" 47 ); 48 return Err(LfsError::SizeLimitExceeded { 49 declared, 50 limit: self.max_object, 51 }); 52 } 53 if self.floor.get() == 0 { 54 return Ok(UploadPermit::unreserved()); 55 } 56 match self.governor.reserve( 57 self.root.as_path(), 58 knot_resource::ReserveBytes::new(declared.get()), 59 ) { 60 Ok(reservation) => Ok(UploadPermit { 61 _reservation: Some(reservation), 62 }), 63 Err(ReserveError::BelowFloor { free, .. }) => { 64 tracing::warn!( 65 declared = declared.get(), 66 free = free.get(), 67 floor = self.floor.get(), 68 "lfs upload denied below the free-space floor" 69 ); 70 Err(LfsError::FreeSpaceDenied { 71 free: LfsSize::new(free.get()), 72 floor: self.floor, 73 }) 74 } 75 Err(ReserveError::Probe(source)) => Err(LfsError::Io { 76 op: "probe free space under", 77 path: self.root.as_path().to_path_buf(), 78 source, 79 }), 80 } 81 } 82 83 fn max_object(&self) -> LfsSize { 84 self.max_object 85 } 86} 87 88#[cfg(test)] 89pub(crate) struct Unbounded; 90 91#[cfg(test)] 92impl UploadAdmission for Unbounded { 93 fn admit(&self, _declared: ClaimedSize) -> Result<UploadPermit, LfsError> { 94 Ok(UploadPermit::unreserved()) 95 } 96 97 fn max_object(&self) -> LfsSize { 98 LfsSize::new(u64::MAX) 99 } 100} 101 102#[cfg(test)] 103mod tests { 104 use super::*; 105 106 #[test] 107 fn the_size_limit_rejects_before_touching_the_disk() { 108 let gate = StoreAdmission::new( 109 LfsStorePath::new("/definitely/not/mounted"), 110 LfsSize::new(8), 111 FreeSpaceFloor::new(0), 112 ); 113 assert!(matches!( 114 gate.admit(ClaimedSize::new(9)), 115 Err(LfsError::SizeLimitExceeded { .. }) 116 )); 117 assert!(gate.admit(ClaimedSize::new(8)).is_ok()); 118 } 119 120 #[test] 121 fn an_absurd_floor_denies_and_a_zero_floor_opts_out() { 122 let dir = tempfile::tempdir().unwrap(); 123 let strict = StoreAdmission::new( 124 LfsStorePath::new(dir.path()), 125 LfsSize::new(u64::MAX), 126 FreeSpaceFloor::new(u64::MAX), 127 ); 128 assert!(matches!( 129 strict.admit(ClaimedSize::new(1)), 130 Err(LfsError::FreeSpaceDenied { .. }) 131 )); 132 let opted_out = StoreAdmission::new( 133 LfsStorePath::new(dir.path()), 134 LfsSize::new(u64::MAX), 135 FreeSpaceFloor::new(0), 136 ); 137 assert!(opted_out.admit(ClaimedSize::new(u64::MAX)).is_ok()); 138 } 139 140 #[test] 141 fn a_held_permit_reserves_against_the_next_admission() { 142 let dir = tempfile::tempdir().unwrap(); 143 let free = knot_resource::disk_free_bytes(dir.path()).unwrap(); 144 let gate = StoreAdmission::new( 145 LfsStorePath::new(dir.path()), 146 LfsSize::new(u64::MAX), 147 FreeSpaceFloor::new(free.get().saturating_sub(6_144)), 148 ); 149 let held = gate.admit(ClaimedSize::new(4_096)).unwrap(); 150 assert!( 151 matches!( 152 gate.admit(ClaimedSize::new(4_096)), 153 Err(LfsError::FreeSpaceDenied { .. }) 154 ), 155 "a second upload cannot pass the floor while the first is in flight" 156 ); 157 drop(held); 158 assert!(gate.admit(ClaimedSize::new(4_096)).is_ok()); 159 } 160}