macula_rust/station_link/
admission.rs1use std::collections::HashMap;
16use std::hash::Hash;
17use std::sync::{Arc, Mutex, MutexGuard};
18
19use crate::frame::VerifiedRequest;
20
21use super::LinkError;
22
23const DEADLINE_PAST_TOLERANCE_MS: i64 = 5 * 60_000;
24const DEADLINE_AHEAD_MAX_MS: i64 = 10 * 60_000;
25const KEPT_PAST_DEADLINE_MS: i64 = 5 * 60_000;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct AdmissionLimits {
32 pub caller_quota: usize,
33 pub share: usize,
34 pub cap: usize,
35 pub reply_bytes: usize,
36 pub reply_bytes_total: usize,
37 pub sessions_per_caller: usize,
38 pub sessions: usize,
39 pub inbox_bytes_per_caller: usize,
40 pub inbox_bytes: usize,
41}
42
43impl Default for AdmissionLimits {
44 fn default() -> Self {
48 AdmissionLimits {
49 caller_quota: 256,
50 share: 1024,
51 cap: 1024,
52 reply_bytes: 256 * 1024,
53 reply_bytes_total: 16 * 1024 * 1024,
54 sessions_per_caller: 16,
55 sessions: 1000,
56 inbox_bytes_per_caller: 16 * 1024 * 1024,
57 inbox_bytes: 256 * 1024 * 1024,
58 }
59 }
60}
61
62impl AdmissionLimits {
63 pub fn validate(&self) -> Result<(), LinkError> {
66 let l = self;
67 let valid = l.caller_quota > 0
68 && l.share > 0
69 && l.cap > 0
70 && l.reply_bytes > 0
71 && l.reply_bytes_total > 0
72 && l.caller_quota <= l.share
73 && l.reply_bytes <= l.reply_bytes_total
74 && l.sessions_per_caller > 0
75 && l.sessions >= l.sessions_per_caller
76 && l.inbox_bytes_per_caller > 0
77 && l.inbox_bytes >= l.inbox_bytes_per_caller;
78 if valid {
79 Ok(())
80 } else {
81 Err(LinkError::InvalidConfig(
82 "admission limits must be positive, each per-caller bound within its total".into(),
83 ))
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
91pub(super) enum Verdict {
92 Refused(&'static str),
93 Copy(Option<Vec<u8>>),
94 New,
95}
96
97struct Entry {
98 hash: [u8; 48],
99 expires_at: i64,
100 share: String,
101 answered: bool,
102 reply: Option<Vec<u8>>,
104}
105
106#[derive(Default)]
107struct Held {
108 entries: HashMap<([u8; 32], [u8; 16]), Entry>,
109 callers: HashMap<[u8; 32], usize>,
110 shares: HashMap<String, usize>,
111 reply_bytes: HashMap<[u8; 32], usize>,
112 reply_total: usize,
113 sessions: HashMap<[u8; 32], usize>,
114 sessions_total: usize,
115 inbox: HashMap<[u8; 32], usize>,
116 inbox_total: usize,
117}
118
119pub struct Admission {
121 limits: AdmissionLimits,
122 held: Mutex<Held>,
123}
124
125impl Admission {
126 pub fn new(limits: AdmissionLimits) -> Admission {
129 Admission {
130 limits,
131 held: Mutex::new(Held::default()),
132 }
133 }
134
135 pub fn limits(&self) -> AdmissionLimits {
137 self.limits
138 }
139
140 fn lock(&self) -> MutexGuard<'_, Held> {
141 self.held.lock().unwrap_or_else(|p| p.into_inner())
142 }
143
144 pub(super) fn admit(&self, request: &VerifiedRequest, share: &str, now_ms: i64) -> Verdict {
147 let deadline = request.deadline as i64;
148 if deadline < now_ms - DEADLINE_PAST_TOLERANCE_MS {
149 return Verdict::Refused("expired");
150 }
151 if deadline > now_ms + DEADLINE_AHEAD_MAX_MS {
152 return Verdict::Refused("not_yet_valid");
153 }
154 let mut held = self.lock();
155 held.sweep(now_ms);
156 let key = (request.caller, request.request_id);
157 if let Some(entry) = held.entries.get(&key) {
158 if entry.hash != request.request_hash {
159 return Verdict::Refused("request_id_reused");
160 }
161 if entry.answered && entry.reply.is_none() {
162 return Verdict::Refused("reply_not_kept");
163 }
164 return Verdict::Copy(entry.reply.clone());
165 }
166 if held.callers.get(&request.caller).copied().unwrap_or(0) >= self.limits.caller_quota {
167 return Verdict::Refused("caller_quota");
168 }
169 if held.shares.get(share).copied().unwrap_or(0) >= self.limits.share {
170 return Verdict::Refused("share_full");
171 }
172 if held.entries.len() >= self.limits.cap {
173 return Verdict::Refused("admission_full");
174 }
175 held.entries.insert(
176 key,
177 Entry {
178 hash: request.request_hash,
179 expires_at: deadline + KEPT_PAST_DEADLINE_MS,
180 share: share.to_string(),
181 answered: false,
182 reply: None,
183 },
184 );
185 *held.callers.entry(request.caller).or_default() += 1;
186 *held.shares.entry(share.to_string()).or_default() += 1;
187 Verdict::New
188 }
189
190 pub(super) fn store(&self, request: &VerifiedRequest, reply: Vec<u8>) {
193 let mut held = self.lock();
194 let held = &mut *held;
195 let Some(entry) = held.entries.get_mut(&(request.caller, request.request_id)) else {
196 return;
197 };
198 if entry.answered || entry.hash != request.request_hash {
199 return;
200 }
201 entry.answered = true;
202 let caller_bytes = held.reply_bytes.get(&request.caller).copied().unwrap_or(0);
203 if caller_bytes + reply.len() > self.limits.reply_bytes
204 || held.reply_total + reply.len() > self.limits.reply_bytes_total
205 {
206 return;
207 }
208 *held.reply_bytes.entry(request.caller).or_default() += reply.len();
209 held.reply_total += reply.len();
210 entry.reply = Some(reply);
211 }
212
213 pub(super) fn open_session(self: &Arc<Self>, caller: [u8; 32]) -> Option<SessionPlace> {
216 let mut held = self.lock();
217 if held.sessions.get(&caller).copied().unwrap_or(0) >= self.limits.sessions_per_caller
218 || held.sessions_total >= self.limits.sessions
219 {
220 return None;
221 }
222 *held.sessions.entry(caller).or_default() += 1;
223 held.sessions_total += 1;
224 Some(SessionPlace {
225 admission: self.clone(),
226 caller,
227 })
228 }
229
230 pub(super) fn charge_inbox(&self, caller: [u8; 32], n: usize) -> bool {
233 let mut held = self.lock();
234 if held.inbox.get(&caller).copied().unwrap_or(0) + n > self.limits.inbox_bytes_per_caller
235 || held.inbox_total + n > self.limits.inbox_bytes
236 {
237 return false;
238 }
239 *held.inbox.entry(caller).or_default() += n;
240 held.inbox_total += n;
241 true
242 }
243
244 pub(super) fn release_inbox(&self, caller: [u8; 32], n: usize) {
246 let mut held = self.lock();
247 decrement(&mut held.inbox, caller, n);
248 held.inbox_total = held.inbox_total.saturating_sub(n);
249 }
250}
251
252pub(super) struct SessionPlace {
254 admission: Arc<Admission>,
255 caller: [u8; 32],
256}
257
258impl Drop for SessionPlace {
259 fn drop(&mut self) {
260 let mut held = self.admission.lock();
261 decrement(&mut held.sessions, self.caller, 1);
262 held.sessions_total = held.sessions_total.saturating_sub(1);
263 }
264}
265
266impl Held {
267 fn sweep(&mut self, now_ms: i64) {
270 let expired: Vec<_> = self
271 .entries
272 .iter()
273 .filter(|(_, e)| e.expires_at < now_ms)
274 .map(|(k, _)| *k)
275 .collect();
276 for key in expired {
277 let Some(entry) = self.entries.remove(&key) else {
278 continue;
279 };
280 decrement(&mut self.callers, key.0, 1);
281 decrement(&mut self.shares, entry.share, 1);
282 if let Some(reply) = entry.reply {
283 decrement(&mut self.reply_bytes, key.0, reply.len());
284 self.reply_total = self.reply_total.saturating_sub(reply.len());
285 }
286 }
287 }
288}
289
290fn decrement<K: Eq + Hash>(m: &mut HashMap<K, usize>, k: K, n: usize) {
292 if let Some(v) = m.get_mut(&k) {
293 *v = v.saturating_sub(n);
294 if *v == 0 {
295 m.remove(&k);
296 }
297 }
298}