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
5.4 kB 177 lines
1use knot_resource::{BelowFloor, DiskGovernor, DiskReservation, FreeBytes}; 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 let free = 57 knot_resource::disk_free_bytes(self.root.as_path()).map_err(|source| LfsError::Io { 58 op: "probe free space under", 59 path: self.root.as_path().to_path_buf(), 60 source, 61 })?; 62 self.admit_against(free, declared) 63 } 64 65 fn max_object(&self) -> LfsSize { 66 self.max_object 67 } 68} 69 70impl StoreAdmission { 71 fn admit_against( 72 &self, 73 free: FreeBytes, 74 declared: ClaimedSize, 75 ) -> Result<UploadPermit, LfsError> { 76 match self 77 .governor 78 .reserve_against(free, knot_resource::ReserveBytes::new(declared.get())) 79 { 80 Ok(reservation) => Ok(UploadPermit { 81 _reservation: Some(reservation), 82 }), 83 Err(BelowFloor { free, .. }) => { 84 tracing::warn!( 85 declared = declared.get(), 86 free = free.get(), 87 floor = self.floor.get(), 88 "lfs upload denied below the free-space floor" 89 ); 90 Err(LfsError::FreeSpaceDenied { 91 free: LfsSize::new(free.get()), 92 floor: self.floor, 93 }) 94 } 95 } 96 } 97} 98 99#[cfg(test)] 100pub(crate) struct Unbounded; 101 102#[cfg(test)] 103impl UploadAdmission for Unbounded { 104 fn admit(&self, _declared: ClaimedSize) -> Result<UploadPermit, LfsError> { 105 Ok(UploadPermit::unreserved()) 106 } 107 108 fn max_object(&self) -> LfsSize { 109 LfsSize::new(u64::MAX) 110 } 111} 112 113#[cfg(test)] 114mod tests { 115 use super::*; 116 117 #[test] 118 fn the_size_limit_rejects_before_touching_the_disk() { 119 let gate = StoreAdmission::new( 120 LfsStorePath::new("/definitely/not/mounted"), 121 LfsSize::new(8), 122 FreeSpaceFloor::new(0), 123 ); 124 assert!(matches!( 125 gate.admit(ClaimedSize::new(9)), 126 Err(LfsError::SizeLimitExceeded { .. }) 127 )); 128 assert!(gate.admit(ClaimedSize::new(8)).is_ok()); 129 } 130 131 #[test] 132 fn an_absurd_floor_denies_and_a_zero_floor_opts_out() { 133 let dir = tempfile::tempdir().unwrap(); 134 let strict = StoreAdmission::new( 135 LfsStorePath::new(dir.path()), 136 LfsSize::new(u64::MAX), 137 FreeSpaceFloor::new(u64::MAX), 138 ); 139 assert!(matches!( 140 strict.admit(ClaimedSize::new(1)), 141 Err(LfsError::FreeSpaceDenied { .. }) 142 )); 143 let opted_out = StoreAdmission::new( 144 LfsStorePath::new(dir.path()), 145 LfsSize::new(u64::MAX), 146 FreeSpaceFloor::new(0), 147 ); 148 assert!(opted_out.admit(ClaimedSize::new(u64::MAX)).is_ok()); 149 } 150 151 #[test] 152 fn a_held_permit_reserves_against_the_next_admission() { 153 let dir = tempfile::tempdir().unwrap(); 154 let free = FreeBytes::new(10_240); 155 let gate = StoreAdmission::new( 156 LfsStorePath::new(dir.path()), 157 LfsSize::new(u64::MAX), 158 FreeSpaceFloor::new(4_096), 159 ); 160 let held = gate 161 .admit_against(free, ClaimedSize::new(4_096)) 162 .expect("a free-space reading the floor leaves room in admits a lone upload"); 163 assert!( 164 matches!( 165 gate.admit_against(free, ClaimedSize::new(4_096)), 166 Err(LfsError::FreeSpaceDenied { .. }) 167 ), 168 "a second upload cannot pass the floor while the first is in flight" 169 ); 170 drop(held); 171 assert!( 172 gate.admit_against(free, ClaimedSize::new(4_096)).is_ok(), 173 "the same reading must admit again once the first upload finishes, or the refusal \ 174 above was the floor rather than the reservation still being held" 175 ); 176 } 177}