aion_server/worker/
quota_cache.rs1use std::collections::HashMap;
21use std::sync::{Arc, Mutex};
22use std::time::{Duration, Instant};
23
24use aion_store::NamespaceStore;
25
26#[derive(Clone, Copy)]
28struct CachedQuota {
29 ceiling: u32,
33 fetched_at: Instant,
34}
35
36#[derive(Clone)]
45pub struct QuotaCache {
46 store: Arc<dyn NamespaceStore>,
47 platform_default: u32,
48 ttl: Duration,
49 entries: Arc<Mutex<HashMap<String, CachedQuota>>>,
50}
51
52impl std::fmt::Debug for QuotaCache {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("QuotaCache")
55 .field("platform_default", &self.platform_default)
56 .field("ttl", &self.ttl)
57 .finish_non_exhaustive()
58 }
59}
60
61impl QuotaCache {
62 #[must_use]
65 pub fn new(store: Arc<dyn NamespaceStore>, platform_default: u32, ttl: Duration) -> Self {
66 Self {
67 store,
68 platform_default,
69 ttl,
70 entries: Arc::new(Mutex::new(HashMap::new())),
71 }
72 }
73
74 pub async fn ceiling(&self, namespace: &str) -> u32 {
81 if let Some(hit) = self.fresh_hit(namespace) {
82 return hit;
83 }
84 let ceiling = match self.store.get_namespace(namespace).await {
85 Ok(Some(record)) => record
86 .config
87 .max_in_flight_activities
88 .unwrap_or(self.platform_default),
89 Ok(None) | Err(_) => self.platform_default,
92 };
93 self.store_entry(namespace, ceiling);
94 ceiling
95 }
96
97 fn fresh_hit(&self, namespace: &str) -> Option<u32> {
100 let entries = self.entries.lock().ok()?;
101 let entry = entries.get(namespace)?;
102 if entry.fetched_at.elapsed() < self.ttl {
103 Some(entry.ceiling)
104 } else {
105 None
106 }
107 }
108
109 fn store_entry(&self, namespace: &str, ceiling: u32) {
112 if let Ok(mut entries) = self.entries.lock() {
113 entries.insert(
114 namespace.to_owned(),
115 CachedQuota {
116 ceiling,
117 fetched_at: Instant::now(),
118 },
119 );
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 #![allow(clippy::expect_used)]
127
128 use std::sync::Arc;
129 use std::time::Duration;
130
131 use aion_store::{InMemoryStore, NamespaceOrigin, NamespaceRecord, NamespaceStore};
132 use chrono::Utc;
133
134 use super::QuotaCache;
135
136 async fn register_with_quota(
138 store: &Arc<dyn NamespaceStore>,
139 namespace: &str,
140 quota: Option<u32>,
141 ) -> Result<(), Box<dyn std::error::Error>> {
142 let mut record =
143 NamespaceRecord::new_minted(namespace, NamespaceOrigin::Explicit, Utc::now());
144 record.config.max_in_flight_activities = quota;
145 store.put_namespace(record).await?;
146 Ok(())
147 }
148
149 #[tokio::test]
150 async fn explicit_override_is_read_from_store_and_cached()
151 -> Result<(), Box<dyn std::error::Error>> {
152 let store: Arc<dyn NamespaceStore> = Arc::new(InMemoryStore::default());
153 register_with_quota(&store, "capped", Some(7)).await?;
154 let cache = QuotaCache::new(Arc::clone(&store), 1024, Duration::from_secs(60));
155
156 assert_eq!(
157 cache.ceiling("capped").await,
158 7,
159 "the explicit override is read"
160 );
161
162 register_with_quota(&store, "capped", Some(99)).await?;
165 assert_eq!(
166 cache.ceiling("capped").await,
167 7,
168 "a fresh cache hit must not re-read the mutated durable record"
169 );
170 Ok(())
171 }
172
173 #[tokio::test]
174 async fn unset_override_resolves_to_platform_default() -> Result<(), Box<dyn std::error::Error>>
175 {
176 let store: Arc<dyn NamespaceStore> = Arc::new(InMemoryStore::default());
177 register_with_quota(&store, "uncapped", None).await?;
178 let cache = QuotaCache::new(store, 1024, Duration::from_secs(60));
179 assert_eq!(
180 cache.ceiling("uncapped").await,
181 1024,
182 "a namespace with no override resolves to the generous platform default"
183 );
184 Ok(())
185 }
186
187 #[tokio::test]
188 async fn absent_namespace_resolves_to_platform_default()
189 -> Result<(), Box<dyn std::error::Error>> {
190 let store: Arc<dyn NamespaceStore> = Arc::new(InMemoryStore::default());
191 let cache = QuotaCache::new(store, 512, Duration::from_secs(60));
192 assert_eq!(
193 cache.ceiling("never-seen").await,
194 512,
195 "an absent registry row resolves to the platform default, never zero"
196 );
197 Ok(())
198 }
199
200 #[tokio::test]
201 async fn refreshes_after_ttl_expiry() -> Result<(), Box<dyn std::error::Error>> {
202 let store: Arc<dyn NamespaceStore> = Arc::new(InMemoryStore::default());
203 let cache = QuotaCache::new(Arc::clone(&store), 1024, Duration::ZERO);
206 assert_eq!(
207 cache.ceiling("capped").await,
208 1024,
209 "an absent record resolves to the platform default"
210 );
211
212 register_with_quota(&store, "capped", Some(7)).await?;
215 assert_eq!(
216 cache.ceiling("capped").await,
217 7,
218 "an expired entry re-reads the durable record and sees the new override"
219 );
220 Ok(())
221 }
222}