This repository has no description
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}