didbot_policy_count/
counts.rs1use std::collections::HashMap;
4use std::sync::Mutex;
5
6use crate::statement::Window;
7
8pub const BUCKETS: usize = 24;
10
11const SECONDS_PER_BUCKET: i64 = 3_600;
13
14pub const HELD: usize = 65_536;
19
20#[must_use]
22pub fn hour_of(now: time::OffsetDateTime) -> i64 {
23 now.unix_timestamp().div_euclid(SECONDS_PER_BUCKET)
24}
25
26#[derive(Debug, Clone, Copy)]
28struct Ring {
29 head: i64,
30 counts: [u32; BUCKETS],
31}
32
33impl Ring {
34 fn new(at: i64) -> Self {
35 Self {
36 head: at,
37 counts: [0; BUCKETS],
38 }
39 }
40
41 fn advance(&mut self, at: i64) {
47 if at <= self.head {
48 return;
49 }
50 let steps = (at - self.head).min(BUCKETS as i64);
51 for step in 1..=steps {
52 self.counts[index(self.head + step)] = 0;
53 }
54 self.head = at;
55 }
56
57 fn read(&self, window: Window) -> u64 {
58 match window {
59 Window::Hour => u64::from(self.counts[index(self.head)]),
60 Window::Day => self.counts.iter().copied().map(u64::from).sum(),
61 }
62 }
63
64 fn add(&mut self) {
68 let slot = &mut self.counts[index(self.head)];
69 *slot = slot.saturating_add(1);
70 }
71
72 fn aged_out(&self, at: i64) -> bool {
73 self.head + (BUCKETS as i64) <= at
74 }
75}
76
77fn index(hour: i64) -> usize {
78 (hour.rem_euclid(BUCKETS as i64)) as usize
79}
80
81type Key = (String, Option<String>);
84
85#[derive(Default)]
96pub struct Counts {
97 rings: Mutex<HashMap<Key, Ring>>,
98}
99
100impl Counts {
101 #[must_use]
103 pub fn new() -> Self {
104 Self::default()
105 }
106
107 #[must_use]
109 pub fn read(&self, counter: &str, account: Option<&str>, window: Window, at: i64) -> u64 {
110 let key = (counter.to_owned(), account.map(str::to_owned));
111 let mut rings = self.lock();
112 match rings.get_mut(&key) {
113 Some(ring) => {
114 ring.advance(at);
115 ring.read(window)
116 }
117 None => 0,
118 }
119 }
120
121 pub fn add(&self, counter: &str, account: Option<&str>, at: i64) {
123 let key = (counter.to_owned(), account.map(str::to_owned));
124 let mut rings = self.lock();
125 if rings.len() >= HELD {
126 rings.retain(|_, ring| !ring.aged_out(at));
127 }
128 let ring = rings.entry(key).or_insert_with(|| Ring::new(at));
129 ring.advance(at);
130 ring.add();
131 }
132
133 pub fn forget(&self, account: &str) {
136 self.lock()
137 .retain(|(_, held), _| held.as_deref() != Some(account));
138 }
139
140 pub fn forget_everything(&self) {
143 self.lock().clear();
144 }
145
146 #[must_use]
148 pub fn held(&self) -> usize {
149 self.lock().len()
150 }
151
152 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<Key, Ring>> {
153 self.rings
154 .lock()
155 .unwrap_or_else(std::sync::PoisonError::into_inner)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 const COUNTER: &str = "a statement";
164
165 #[test]
166 fn the_hour_is_the_newest_bucket_and_the_day_is_the_ring() {
167 let counts = Counts::new();
168 for hour in 0..5 {
169 counts.add(COUNTER, Some("did:web:one"), hour);
170 }
171 assert_eq!(
172 counts.read(COUNTER, Some("did:web:one"), Window::Hour, 4),
173 1
174 );
175 assert_eq!(counts.read(COUNTER, Some("did:web:one"), Window::Day, 4), 5);
176 let later = 4 + BUCKETS as i64;
178 assert_eq!(
179 counts.read(COUNTER, Some("did:web:one"), Window::Day, later),
180 0
181 );
182 }
183
184 #[test]
185 fn accounts_and_statements_each_count_for_themselves() {
186 let counts = Counts::new();
187 counts.add(COUNTER, Some("did:web:one"), 10);
188 counts.add(COUNTER, Some("did:web:two"), 10);
189 counts.add("another statement", Some("did:web:one"), 10);
190 counts.add(COUNTER, None, 10);
191 assert_eq!(
192 counts.read(COUNTER, Some("did:web:one"), Window::Hour, 10),
193 1
194 );
195 assert_eq!(counts.read(COUNTER, None, Window::Hour, 10), 1);
196 assert_eq!(counts.held(), 4);
197 counts.forget("did:web:one");
198 assert_eq!(
199 counts.read(COUNTER, Some("did:web:one"), Window::Hour, 10),
200 0
201 );
202 assert_eq!(
203 counts.read(COUNTER, Some("did:web:two"), Window::Hour, 10),
204 1
205 );
206 assert_eq!(
207 counts.read(COUNTER, None, Window::Hour, 10),
208 1,
209 "a deleted account takes nothing of the deployment's with it"
210 );
211 }
212}