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