sa_token_core/service/
grant_cache.rs1use std::collections::HashMap;
15use std::future::Future;
16use std::sync::{Arc, RwLock};
17use std::time::{Duration, Instant};
18
19use crate::config::SaTokenConfig;
20use crate::error::SaTokenResult;
21
22const SHARD_COUNT: usize = 8;
25
26const SHARD_MASK: usize = SHARD_COUNT - 1;
28
29const KEY_SEP: char = '\u{1}';
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum GrantKind {
36 Permission,
38 Role,
40}
41
42impl GrantKind {
43 #[inline]
45 fn tag(self) -> char {
46 match self {
47 Self::Permission => 'p',
48 Self::Role => 'r',
49 }
50 }
51}
52
53struct CacheEntry {
55 value: Arc<[String]>,
56 expires_at: Instant,
57}
58
59type FlightMap = HashMap<String, Arc<tokio::sync::Mutex<()>>>;
60type FlightShards = Box<[tokio::sync::Mutex<FlightMap>]>;
61
62pub struct GrantCache {
64 shards: Box<[RwLock<HashMap<String, CacheEntry>>]>,
65 flights: FlightShards,
68 ttl: Duration,
69 max_per_shard: usize,
70 single_flight: bool,
71}
72
73impl std::fmt::Debug for GrantCache {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("GrantCache")
76 .field("ttl", &self.ttl)
77 .field("max_per_shard", &self.max_per_shard)
78 .field("single_flight", &self.single_flight)
79 .finish()
80 }
81}
82
83impl GrantCache {
84 pub fn new(ttl: Duration, max_entries: usize, single_flight: bool) -> Self {
86 let max_per_shard = (max_entries / SHARD_COUNT).max(1);
87 let shards = (0..SHARD_COUNT)
88 .map(|_| RwLock::new(HashMap::new()))
89 .collect::<Vec<_>>()
90 .into_boxed_slice();
91 let flights = (0..SHARD_COUNT)
92 .map(|_| tokio::sync::Mutex::new(HashMap::new()))
93 .collect::<Vec<_>>()
94 .into_boxed_slice();
95 Self {
96 shards,
97 flights,
98 ttl,
99 max_per_shard,
100 single_flight,
101 }
102 }
103
104 pub fn from_config(config: &SaTokenConfig) -> Option<Arc<Self>> {
107 config.grant_cache_duration().map(|ttl| {
108 Arc::new(Self::new(
109 ttl,
110 config.grant_cache_max_entries,
111 config.grant_cache_single_flight,
112 ))
113 })
114 }
115
116 pub fn cache_key(kind: GrantKind, login_type: &str, login_id: &str) -> String {
118 let mut key = String::with_capacity(2 + login_type.len() + 1 + login_id.len());
119 key.push(kind.tag());
120 key.push(KEY_SEP);
121 key.push_str(login_type);
122 key.push(KEY_SEP);
123 key.push_str(login_id);
124 key
125 }
126
127 #[inline]
130 fn fnv1a(key: &str) -> usize {
131 let mut hash = 0xcbf29ce484222325u64;
132 for byte in key.as_bytes() {
133 hash ^= u64::from(*byte);
134 hash = hash.wrapping_mul(0x100000001b3);
135 }
136 hash as usize
137 }
138
139 #[inline]
141 fn shard_of(&self, key: &str) -> &RwLock<HashMap<String, CacheEntry>> {
142 match self.shards.get(Self::fnv1a(key) & SHARD_MASK) {
143 Some(s) => s,
144 None => unreachable!("fnv1a masked to SHARD_COUNT"),
145 }
146 }
147
148 #[inline]
149 fn flight_shard(&self, key: &str) -> &tokio::sync::Mutex<FlightMap> {
150 match self.flights.get(Self::fnv1a(key) & SHARD_MASK) {
151 Some(s) => s,
152 None => unreachable!("fnv1a masked to SHARD_COUNT"),
153 }
154 }
155
156 fn peek(&self, key: &str) -> Option<Arc<[String]>> {
158 let guard = self.shard_of(key).read().ok()?;
159 let entry = guard.get(key)?;
160 if entry.expires_at > Instant::now() {
161 Some(Arc::clone(&entry.value))
162 } else {
163 None
164 }
165 }
166
167 fn put(&self, key: String, value: Arc<[String]>) {
169 let Ok(mut guard) = self.shard_of(&key).write() else {
170 return;
171 };
172 let now = Instant::now();
173
174 if guard.len() >= self.max_per_shard && !guard.contains_key(&key) {
175 guard.retain(|_, entry| entry.expires_at > now);
176
177 if guard.len() >= self.max_per_shard {
178 let victim = guard
179 .iter()
180 .min_by_key(|(_, entry)| entry.expires_at)
181 .map(|(k, _)| k.clone());
182 if let Some(victim) = victim {
183 guard.remove(&victim);
184 }
185 }
186 }
187
188 guard.insert(
189 key,
190 CacheEntry {
191 value,
192 expires_at: now + self.ttl,
193 },
194 );
195 }
196
197 pub async fn get_or_load<F, Fut>(&self, key: String, loader: F) -> SaTokenResult<Arc<[String]>>
199 where
200 F: FnOnce() -> Fut,
201 Fut: Future<Output = SaTokenResult<Vec<String>>>,
202 {
203 if let Some(hit) = self.peek(&key) {
204 return Ok(hit);
205 }
206
207 if !self.single_flight {
208 let value: Arc<[String]> = loader().await?.into();
209 self.put(key, Arc::clone(&value));
210 return Ok(value);
211 }
212
213 let gate = {
214 let mut flights = self.flight_shard(&key).lock().await;
215 Arc::clone(
216 flights
217 .entry(key.clone())
218 .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))),
219 )
220 };
221 let _permit = gate.lock().await;
222
223 if let Some(hit) = self.peek(&key) {
224 self.release_flight(&key).await;
225 return Ok(hit);
226 }
227
228 match loader().await {
229 Ok(list) => {
230 let value: Arc<[String]> = list.into();
231 self.put(key.clone(), Arc::clone(&value));
232 self.release_flight(&key).await;
233 Ok(value)
234 }
235 Err(err) => {
236 self.release_flight(&key).await;
237 Err(err)
238 }
239 }
240 }
241
242 async fn release_flight(&self, key: &str) {
244 self.flight_shard(key).lock().await.remove(key);
245 }
246
247 pub fn invalidate(&self, key: &str) {
249 if let Ok(mut guard) = self.shard_of(key).write() {
250 guard.remove(key);
251 }
252 }
253
254 pub fn invalidate_account(&self, login_type: &str, login_id: &str) {
257 self.invalidate(&Self::cache_key(
258 GrantKind::Permission,
259 login_type,
260 login_id,
261 ));
262 self.invalidate(&Self::cache_key(GrantKind::Role, login_type, login_id));
263 }
264
265 pub fn clear(&self) {
267 for shard in self.shards.iter() {
268 if let Ok(mut guard) = shard.write() {
269 guard.clear();
270 }
271 }
272 }
273
274 pub fn len(&self) -> usize {
276 self.shards
277 .iter()
278 .filter_map(|shard| shard.read().ok().map(|g| g.len()))
279 .sum()
280 }
281
282 pub fn is_empty(&self) -> bool {
284 self.len() == 0
285 }
286}