1use serde::{Deserialize, Serialize};
9
10use crate::abi::InstanceId;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15#[non_exhaustive]
16pub enum QuotaReductionPolicy {
17 #[default]
19 Reject,
20 GrandfatherExisting,
23 ThrottleProportional,
27}
28
29#[non_exhaustive]
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum QuotaReductionError {
34 WouldViolateChildren {
37 current_usage: u64,
40 new_quota: u64,
42 },
43}
44
45impl core::fmt::Display for QuotaReductionError {
46 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47 match self {
48 Self::WouldViolateChildren {
49 current_usage,
50 new_quota,
51 } => write!(
52 f,
53 "quota reduction would violate children: current_usage={}, new_quota={}",
54 current_usage, new_quota
55 ),
56 }
57 }
58}
59
60impl std::error::Error for QuotaReductionError {}
61
62#[must_use = "policy result determines whether the reduction can proceed"]
87pub fn apply_quota_reduction(
88 policy: QuotaReductionPolicy,
89 new_quota: u64,
90 children: &[(InstanceId, u64)],
91) -> Result<Vec<(InstanceId, u64)>, QuotaReductionError> {
92 debug_assert!(
93 children.windows(2).all(|w| w[0].0 <= w[1].0),
94 "apply_quota_reduction: children must be sorted by InstanceId ascending"
95 );
96
97 let current_total: u64 = children
98 .iter()
99 .map(|(_, u)| *u)
100 .fold(0u64, u64::saturating_add);
101
102 match policy {
103 QuotaReductionPolicy::Reject => {
104 if current_total > new_quota {
105 Err(QuotaReductionError::WouldViolateChildren {
106 current_usage: current_total,
107 new_quota,
108 })
109 } else {
110 Ok(children.to_vec())
111 }
112 }
113 QuotaReductionPolicy::GrandfatherExisting => Ok(children.to_vec()),
114 QuotaReductionPolicy::ThrottleProportional => {
115 if children.is_empty() || current_total == 0 || new_quota >= current_total {
119 return Ok(children.to_vec());
120 }
121 let total = current_total as u128;
122 let target = new_quota as u128;
123 let mut out: Vec<(InstanceId, u64)> = Vec::with_capacity(children.len());
124 let mut allocated: u128 = 0;
125 for (id, current) in children {
126 let scaled = (*current as u128).saturating_mul(target) / total;
127 let scaled_u64 = scaled.min(u64::MAX as u128) as u64;
128 allocated = allocated.saturating_add(scaled);
129 out.push((*id, scaled_u64));
130 }
131 let mut remainder = target.saturating_sub(allocated);
132 for entry in out.iter_mut() {
133 if remainder == 0 {
134 break;
135 }
136 entry.1 = entry.1.saturating_add(1);
137 remainder -= 1;
138 }
139 Ok(out)
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn policy_default_is_reject() {
150 assert_eq!(
151 QuotaReductionPolicy::default(),
152 QuotaReductionPolicy::Reject
153 );
154 }
155
156 #[test]
157 fn policy_three_distinct_variants() {
158 let a = QuotaReductionPolicy::Reject;
159 let b = QuotaReductionPolicy::GrandfatherExisting;
160 let c = QuotaReductionPolicy::ThrottleProportional;
161 assert_ne!(a, b);
162 assert_ne!(b, c);
163 assert_ne!(a, c);
164 }
165
166 #[test]
167 fn error_display_includes_numbers() {
168 let e = QuotaReductionError::WouldViolateChildren {
169 current_usage: 100,
170 new_quota: 50,
171 };
172 let s = format!("{}", e);
173 assert!(s.contains("current_usage=100"));
174 assert!(s.contains("new_quota=50"));
175 }
176
177 #[test]
178 fn error_implements_std_error() {
179 fn assert_err<E: std::error::Error>() {}
180 assert_err::<QuotaReductionError>();
181 }
182
183 fn id(n: u64) -> InstanceId {
186 InstanceId::new(n).unwrap()
187 }
188
189 #[test]
190 fn reject_allows_under_quota() {
191 let cs = vec![(id(1), 25), (id(2), 25)];
192 let result = apply_quota_reduction(QuotaReductionPolicy::Reject, 100, &cs).unwrap();
193 assert_eq!(result, cs);
194 }
195
196 #[test]
197 fn reject_denies_over_quota() {
198 let cs = vec![(id(1), 100), (id(2), 100)];
199 let err = apply_quota_reduction(QuotaReductionPolicy::Reject, 100, &cs).unwrap_err();
200 assert_eq!(
201 err,
202 QuotaReductionError::WouldViolateChildren {
203 current_usage: 200,
204 new_quota: 100,
205 }
206 );
207 }
208
209 #[test]
210 fn reject_at_exact_limit_allows() {
211 let cs = vec![(id(1), 50), (id(2), 50)];
212 let result = apply_quota_reduction(QuotaReductionPolicy::Reject, 100, &cs).unwrap();
213 assert_eq!(result, cs);
214 }
215
216 #[test]
217 fn grandfather_returns_children_unchanged() {
218 let cs = vec![(id(1), 100), (id(2), 100)];
219 let result =
220 apply_quota_reduction(QuotaReductionPolicy::GrandfatherExisting, 100, &cs).unwrap();
221 assert_eq!(result, cs);
222 }
223
224 #[test]
225 fn throttle_basic() {
226 let cs = vec![(id(1), 100), (id(2), 200), (id(3), 300)];
228 let result =
229 apply_quota_reduction(QuotaReductionPolicy::ThrottleProportional, 300, &cs).unwrap();
230 assert_eq!(result, vec![(id(1), 50), (id(2), 100), (id(3), 150)]);
231 }
232
233 #[test]
234 fn throttle_remainder_distributes_ascending() {
235 let cs = vec![(id(1), 100), (id(2), 100), (id(3), 100)];
238 let result =
239 apply_quota_reduction(QuotaReductionPolicy::ThrottleProportional, 100, &cs).unwrap();
240 assert_eq!(result, vec![(id(1), 34), (id(2), 33), (id(3), 33)]);
241 let sum: u64 = result.iter().map(|(_, q)| *q).sum();
242 assert_eq!(sum, 100);
243 }
244
245 #[test]
246 fn throttle_does_not_scale_up_when_quota_exceeds_total() {
247 let cs = vec![(id(1), 100), (id(2), 200)];
251 let result =
252 apply_quota_reduction(QuotaReductionPolicy::ThrottleProportional, 600, &cs).unwrap();
253 assert_eq!(result, cs);
254 }
255
256 #[test]
257 fn throttle_at_exact_total_is_identity() {
258 let cs = vec![(id(1), 100), (id(2), 200)];
261 let result =
262 apply_quota_reduction(QuotaReductionPolicy::ThrottleProportional, 300, &cs).unwrap();
263 assert_eq!(result, cs);
264 }
265
266 #[test]
267 fn throttle_zero_total_idempotent() {
268 let cs = vec![(id(1), 0), (id(2), 0)];
269 let result =
270 apply_quota_reduction(QuotaReductionPolicy::ThrottleProportional, 100, &cs).unwrap();
271 assert_eq!(result, cs);
272 }
273
274 #[test]
275 fn throttle_deterministic() {
276 let cs = vec![(id(1), 100), (id(2), 100), (id(3), 100)];
277 let r1 =
278 apply_quota_reduction(QuotaReductionPolicy::ThrottleProportional, 100, &cs).unwrap();
279 let r2 =
280 apply_quota_reduction(QuotaReductionPolicy::ThrottleProportional, 100, &cs).unwrap();
281 assert_eq!(r1, r2);
282 }
283
284 #[test]
285 fn apply_to_empty_children_returns_empty() {
286 let cs: Vec<(InstanceId, u64)> = vec![];
287 for policy in [
288 QuotaReductionPolicy::Reject,
289 QuotaReductionPolicy::GrandfatherExisting,
290 QuotaReductionPolicy::ThrottleProportional,
291 ] {
292 let result = apply_quota_reduction(policy, 100, &cs).unwrap();
293 assert!(
294 result.is_empty(),
295 "policy {:?} should pass empty through",
296 policy
297 );
298 }
299 }
300}