1pub const TASK_CONTEXT_QUOTA_PERCENTS: [u8; 6] = [20, 25, 30, 10, 10, 5];
11
12pub const TASK_CONTEXT_QUOTA_NAMES: [&str; 6] = [
14 "atlas",
15 "surface",
16 "source",
17 "flow",
18 "verify",
19 "uncertainty",
20];
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct FilledBucket {
26 pub name: &'static str,
27 pub percent: u8,
28 pub quota: usize,
30 pub granted: usize,
32 pub truncated: usize,
34 pub rolled_forward: usize,
36}
37
38pub fn fill_with_rollover(total: usize, percents: &[u8], names: &[&'static str], used: &[usize]) -> Vec<FilledBucket> {
43 assert_eq!(percents.len(), used.len());
44 assert_eq!(percents.len(), names.len());
45 let sum: u16 = percents.iter().map(|p| *p as u16).sum();
46 assert_eq!(sum, 100, "quota percents must sum to 100");
47
48 let mut leftover = 0usize;
49 let mut granted_so_far = 0usize;
50 let mut out = Vec::with_capacity(percents.len());
51 for (i, (p, u)) in percents.iter().zip(used.iter()).enumerate() {
52 let base = if i + 1 == percents.len() {
53 total.saturating_sub(granted_so_far + leftover)
54 .saturating_add(leftover)
55 } else {
56 total * (*p as usize) / 100 + leftover
57 };
58 let take = (*u).min(base);
59 let truncated = u.saturating_sub(base);
60 let rolled = base.saturating_sub(*u);
61 granted_so_far += take;
62 leftover = rolled;
63 out.push(FilledBucket {
64 name: names[i],
65 percent: *p,
66 quota: base,
67 granted: take,
68 truncated,
69 rolled_forward: if i + 1 == percents.len() { 0 } else { rolled },
70 });
71 }
72 out
73}
74
75pub fn fill_task_context_quotas(total: usize, used: &[usize; 6]) -> Vec<FilledBucket> {
78 fill_with_rollover(
79 total,
80 &TASK_CONTEXT_QUOTA_PERCENTS,
81 &TASK_CONTEXT_QUOTA_NAMES,
82 used,
83 )
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn percents_sum_to_one_hundred() {
93 let sum: u16 = TASK_CONTEXT_QUOTA_PERCENTS.iter().map(|p| *p as u16).sum();
94 assert_eq!(sum, 100);
95 assert_eq!(TASK_CONTEXT_QUOTA_NAMES.len(), TASK_CONTEXT_QUOTA_PERCENTS.len());
96 }
97
98 #[test]
99 fn unused_quota_rolls_forward_and_truncation_is_disclosed() {
101 let filled = fill_with_rollover(
102 100,
103 &[40, 30, 30],
104 &["a", "b", "c"],
105 &[10, 50, 10],
106 );
107 assert_eq!(filled[0].granted, 10);
108 assert_eq!(filled[0].truncated, 0);
109 assert_eq!(filled[0].rolled_forward, 30);
110 assert_eq!(filled[1].quota, 60, "30% + 30 rolled");
111 assert_eq!(filled[1].granted, 50);
112 assert_eq!(filled[1].truncated, 0);
113 assert_eq!(filled[1].rolled_forward, 10);
114 assert_eq!(filled[2].granted, 10);
115 assert_eq!(filled[2].truncated, 0);
116 let over = fill_with_rollover(100, &[50, 50], &["x", "y"], &[80, 80]);
117 assert_eq!(over[0].granted, 50);
118 assert_eq!(over[0].truncated, 30);
119 assert_eq!(over[1].granted, 50);
120 assert_eq!(over[1].truncated, 30);
121 let task = fill_task_context_quotas(1000, &[0, 0, 0, 0, 0, 0]);
122 assert_eq!(task.iter().map(|b| b.granted).sum::<usize>(), 0);
123 assert!(task.iter().any(|b| b.name == "verify"));
124 }
125}