Line data Source code
1 : use std::{
2 : collections::HashSet,
3 : convert::Infallible,
4 : sync::{atomic::AtomicU64, Arc},
5 : time::Duration,
6 : };
7 :
8 : use async_trait::async_trait;
9 : use dashmap::DashMap;
10 : use rand::{thread_rng, Rng};
11 : use smol_str::SmolStr;
12 : use tokio::sync::Mutex;
13 : use tokio::time::Instant;
14 : use tracing::{debug, info};
15 :
16 : use crate::{
17 : auth::IpPattern,
18 : config::ProjectInfoCacheOptions,
19 : console::AuthSecret,
20 : intern::{EndpointIdInt, ProjectIdInt, RoleNameInt},
21 : EndpointId, RoleName,
22 : };
23 :
24 : use super::{Cache, Cached};
25 :
26 : #[async_trait]
27 : pub(crate) trait ProjectInfoCache {
28 : fn invalidate_allowed_ips_for_project(&self, project_id: ProjectIdInt);
29 : fn invalidate_role_secret_for_project(&self, project_id: ProjectIdInt, role_name: RoleNameInt);
30 : async fn decrement_active_listeners(&self);
31 : async fn increment_active_listeners(&self);
32 : }
33 :
34 : struct Entry<T> {
35 : created_at: Instant,
36 : value: T,
37 : }
38 :
39 : impl<T> Entry<T> {
40 54 : pub(crate) fn new(value: T) -> Self {
41 54 : Self {
42 54 : created_at: Instant::now(),
43 54 : value,
44 54 : }
45 54 : }
46 : }
47 :
48 : impl<T> From<T> for Entry<T> {
49 54 : fn from(value: T) -> Self {
50 54 : Self::new(value)
51 54 : }
52 : }
53 :
54 : #[derive(Default)]
55 : struct EndpointInfo {
56 : secret: std::collections::HashMap<RoleNameInt, Entry<Option<AuthSecret>>>,
57 : allowed_ips: Option<Entry<Arc<Vec<IpPattern>>>>,
58 : }
59 :
60 : impl EndpointInfo {
61 66 : fn check_ignore_cache(ignore_cache_since: Option<Instant>, created_at: Instant) -> bool {
62 66 : match ignore_cache_since {
63 18 : None => false,
64 48 : Some(t) => t < created_at,
65 : }
66 66 : }
67 90 : pub(crate) fn get_role_secret(
68 90 : &self,
69 90 : role_name: RoleNameInt,
70 90 : valid_since: Instant,
71 90 : ignore_cache_since: Option<Instant>,
72 90 : ) -> Option<(Option<AuthSecret>, bool)> {
73 90 : if let Some(secret) = self.secret.get(&role_name) {
74 78 : if valid_since < secret.created_at {
75 42 : return Some((
76 42 : secret.value.clone(),
77 42 : Self::check_ignore_cache(ignore_cache_since, secret.created_at),
78 42 : ));
79 36 : }
80 12 : }
81 48 : None
82 90 : }
83 :
84 30 : pub(crate) fn get_allowed_ips(
85 30 : &self,
86 30 : valid_since: Instant,
87 30 : ignore_cache_since: Option<Instant>,
88 30 : ) -> Option<(Arc<Vec<IpPattern>>, bool)> {
89 30 : if let Some(allowed_ips) = &self.allowed_ips {
90 30 : if valid_since < allowed_ips.created_at {
91 24 : return Some((
92 24 : allowed_ips.value.clone(),
93 24 : Self::check_ignore_cache(ignore_cache_since, allowed_ips.created_at),
94 24 : ));
95 6 : }
96 0 : }
97 6 : None
98 30 : }
99 0 : pub(crate) fn invalidate_allowed_ips(&mut self) {
100 0 : self.allowed_ips = None;
101 0 : }
102 6 : pub(crate) fn invalidate_role_secret(&mut self, role_name: RoleNameInt) {
103 6 : self.secret.remove(&role_name);
104 6 : }
105 : }
106 :
107 : /// Cache for project info.
108 : /// This is used to cache auth data for endpoints.
109 : /// Invalidation is done by console notifications or by TTL (if console notifications are disabled).
110 : ///
111 : /// We also store endpoint-to-project mapping in the cache, to be able to access per-endpoint data.
112 : /// One may ask, why the data is stored per project, when on the user request there is only data about the endpoint available?
113 : /// On the cplane side updates are done per project (or per branch), so it's easier to invalidate the whole project cache.
114 : pub struct ProjectInfoCacheImpl {
115 : cache: DashMap<EndpointIdInt, EndpointInfo>,
116 :
117 : project2ep: DashMap<ProjectIdInt, HashSet<EndpointIdInt>>,
118 : config: ProjectInfoCacheOptions,
119 :
120 : start_time: Instant,
121 : ttl_disabled_since_us: AtomicU64,
122 : active_listeners_lock: Mutex<usize>,
123 : }
124 :
125 : #[async_trait]
126 : impl ProjectInfoCache for ProjectInfoCacheImpl {
127 0 : fn invalidate_allowed_ips_for_project(&self, project_id: ProjectIdInt) {
128 0 : info!("invalidating allowed ips for project `{}`", project_id);
129 0 : let endpoints = self
130 0 : .project2ep
131 0 : .get(&project_id)
132 0 : .map(|kv| kv.value().clone())
133 0 : .unwrap_or_default();
134 0 : for endpoint_id in endpoints {
135 0 : if let Some(mut endpoint_info) = self.cache.get_mut(&endpoint_id) {
136 0 : endpoint_info.invalidate_allowed_ips();
137 0 : }
138 : }
139 0 : }
140 6 : fn invalidate_role_secret_for_project(&self, project_id: ProjectIdInt, role_name: RoleNameInt) {
141 6 : info!(
142 0 : "invalidating role secret for project_id `{}` and role_name `{}`",
143 : project_id, role_name,
144 : );
145 6 : let endpoints = self
146 6 : .project2ep
147 6 : .get(&project_id)
148 6 : .map(|kv| kv.value().clone())
149 6 : .unwrap_or_default();
150 12 : for endpoint_id in endpoints {
151 6 : if let Some(mut endpoint_info) = self.cache.get_mut(&endpoint_id) {
152 6 : endpoint_info.invalidate_role_secret(role_name);
153 6 : }
154 : }
155 6 : }
156 0 : async fn decrement_active_listeners(&self) {
157 0 : let mut listeners_guard = self.active_listeners_lock.lock().await;
158 0 : if *listeners_guard == 0 {
159 0 : tracing::error!("active_listeners count is already 0, something is broken");
160 0 : return;
161 0 : }
162 0 : *listeners_guard -= 1;
163 0 : if *listeners_guard == 0 {
164 0 : self.ttl_disabled_since_us
165 0 : .store(u64::MAX, std::sync::atomic::Ordering::SeqCst);
166 0 : }
167 0 : }
168 :
169 12 : async fn increment_active_listeners(&self) {
170 12 : let mut listeners_guard = self.active_listeners_lock.lock().await;
171 12 : *listeners_guard += 1;
172 12 : if *listeners_guard == 1 {
173 12 : let new_ttl = (self.start_time.elapsed() + self.config.ttl).as_micros() as u64;
174 12 : self.ttl_disabled_since_us
175 12 : .store(new_ttl, std::sync::atomic::Ordering::SeqCst);
176 12 : }
177 12 : }
178 : }
179 :
180 : impl ProjectInfoCacheImpl {
181 18 : pub(crate) fn new(config: ProjectInfoCacheOptions) -> Self {
182 18 : Self {
183 18 : cache: DashMap::new(),
184 18 : project2ep: DashMap::new(),
185 18 : config,
186 18 : ttl_disabled_since_us: AtomicU64::new(u64::MAX),
187 18 : start_time: Instant::now(),
188 18 : active_listeners_lock: Mutex::new(0),
189 18 : }
190 18 : }
191 :
192 90 : pub(crate) fn get_role_secret(
193 90 : &self,
194 90 : endpoint_id: &EndpointId,
195 90 : role_name: &RoleName,
196 90 : ) -> Option<Cached<&Self, Option<AuthSecret>>> {
197 90 : let endpoint_id = EndpointIdInt::get(endpoint_id)?;
198 90 : let role_name = RoleNameInt::get(role_name)?;
199 90 : let (valid_since, ignore_cache_since) = self.get_cache_times();
200 90 : let endpoint_info = self.cache.get(&endpoint_id)?;
201 42 : let (value, ignore_cache) =
202 90 : endpoint_info.get_role_secret(role_name, valid_since, ignore_cache_since)?;
203 42 : if !ignore_cache {
204 24 : let cached = Cached {
205 24 : token: Some((
206 24 : self,
207 24 : CachedLookupInfo::new_role_secret(endpoint_id, role_name),
208 24 : )),
209 24 : value,
210 24 : };
211 24 : return Some(cached);
212 18 : }
213 18 : Some(Cached::new_uncached(value))
214 90 : }
215 30 : pub(crate) fn get_allowed_ips(
216 30 : &self,
217 30 : endpoint_id: &EndpointId,
218 30 : ) -> Option<Cached<&Self, Arc<Vec<IpPattern>>>> {
219 30 : let endpoint_id = EndpointIdInt::get(endpoint_id)?;
220 30 : let (valid_since, ignore_cache_since) = self.get_cache_times();
221 30 : let endpoint_info = self.cache.get(&endpoint_id)?;
222 30 : let value = endpoint_info.get_allowed_ips(valid_since, ignore_cache_since);
223 30 : let (value, ignore_cache) = value?;
224 24 : if !ignore_cache {
225 6 : let cached = Cached {
226 6 : token: Some((self, CachedLookupInfo::new_allowed_ips(endpoint_id))),
227 6 : value,
228 6 : };
229 6 : return Some(cached);
230 18 : }
231 18 : Some(Cached::new_uncached(value))
232 30 : }
233 42 : pub(crate) fn insert_role_secret(
234 42 : &self,
235 42 : project_id: ProjectIdInt,
236 42 : endpoint_id: EndpointIdInt,
237 42 : role_name: RoleNameInt,
238 42 : secret: Option<AuthSecret>,
239 42 : ) {
240 42 : if self.cache.len() >= self.config.size {
241 : // If there are too many entries, wait until the next gc cycle.
242 0 : return;
243 42 : }
244 42 : self.insert_project2endpoint(project_id, endpoint_id);
245 42 : let mut entry = self.cache.entry(endpoint_id).or_default();
246 42 : if entry.secret.len() < self.config.max_roles {
247 36 : entry.secret.insert(role_name, secret.into());
248 36 : }
249 42 : }
250 18 : pub(crate) fn insert_allowed_ips(
251 18 : &self,
252 18 : project_id: ProjectIdInt,
253 18 : endpoint_id: EndpointIdInt,
254 18 : allowed_ips: Arc<Vec<IpPattern>>,
255 18 : ) {
256 18 : if self.cache.len() >= self.config.size {
257 : // If there are too many entries, wait until the next gc cycle.
258 0 : return;
259 18 : }
260 18 : self.insert_project2endpoint(project_id, endpoint_id);
261 18 : self.cache.entry(endpoint_id).or_default().allowed_ips = Some(allowed_ips.into());
262 18 : }
263 60 : fn insert_project2endpoint(&self, project_id: ProjectIdInt, endpoint_id: EndpointIdInt) {
264 60 : if let Some(mut endpoints) = self.project2ep.get_mut(&project_id) {
265 42 : endpoints.insert(endpoint_id);
266 42 : } else {
267 18 : self.project2ep
268 18 : .insert(project_id, HashSet::from([endpoint_id]));
269 18 : }
270 60 : }
271 120 : fn get_cache_times(&self) -> (Instant, Option<Instant>) {
272 120 : let mut valid_since = Instant::now() - self.config.ttl;
273 120 : // Only ignore cache if ttl is disabled.
274 120 : let ttl_disabled_since_us = self
275 120 : .ttl_disabled_since_us
276 120 : .load(std::sync::atomic::Ordering::Relaxed);
277 120 : let ignore_cache_since = if ttl_disabled_since_us == u64::MAX {
278 42 : None
279 : } else {
280 78 : let ignore_cache_since = self.start_time + Duration::from_micros(ttl_disabled_since_us);
281 78 : // We are fine if entry is not older than ttl or was added before we are getting notifications.
282 78 : valid_since = valid_since.min(ignore_cache_since);
283 78 : Some(ignore_cache_since)
284 : };
285 120 : (valid_since, ignore_cache_since)
286 120 : }
287 :
288 0 : pub async fn gc_worker(&self) -> anyhow::Result<Infallible> {
289 0 : let mut interval =
290 0 : tokio::time::interval(self.config.gc_interval / (self.cache.shards().len()) as u32);
291 : loop {
292 0 : interval.tick().await;
293 0 : if self.cache.len() < self.config.size {
294 : // If there are not too many entries, wait until the next gc cycle.
295 0 : continue;
296 0 : }
297 0 : self.gc();
298 : }
299 : }
300 :
301 0 : fn gc(&self) {
302 0 : let shard = thread_rng().gen_range(0..self.project2ep.shards().len());
303 0 : debug!(shard, "project_info_cache: performing epoch reclamation");
304 :
305 : // acquire a random shard lock
306 0 : let mut removed = 0;
307 0 : let shard = self.project2ep.shards()[shard].write();
308 0 : for (_, endpoints) in shard.iter() {
309 0 : for endpoint in endpoints.get() {
310 0 : self.cache.remove(endpoint);
311 0 : removed += 1;
312 0 : }
313 : }
314 : // We can drop this shard only after making sure that all endpoints are removed.
315 0 : drop(shard);
316 0 : info!("project_info_cache: removed {removed} endpoints");
317 0 : }
318 : }
319 :
320 : /// Lookup info for project info cache.
321 : /// This is used to invalidate cache entries.
322 : pub(crate) struct CachedLookupInfo {
323 : /// Search by this key.
324 : endpoint_id: EndpointIdInt,
325 : lookup_type: LookupType,
326 : }
327 :
328 : impl CachedLookupInfo {
329 24 : pub(self) fn new_role_secret(endpoint_id: EndpointIdInt, role_name: RoleNameInt) -> Self {
330 24 : Self {
331 24 : endpoint_id,
332 24 : lookup_type: LookupType::RoleSecret(role_name),
333 24 : }
334 24 : }
335 6 : pub(self) fn new_allowed_ips(endpoint_id: EndpointIdInt) -> Self {
336 6 : Self {
337 6 : endpoint_id,
338 6 : lookup_type: LookupType::AllowedIps,
339 6 : }
340 6 : }
341 : }
342 :
343 : enum LookupType {
344 : RoleSecret(RoleNameInt),
345 : AllowedIps,
346 : }
347 :
348 : impl Cache for ProjectInfoCacheImpl {
349 : type Key = SmolStr;
350 : // Value is not really used here, but we need to specify it.
351 : type Value = SmolStr;
352 :
353 : type LookupInfo<Key> = CachedLookupInfo;
354 :
355 0 : fn invalidate(&self, key: &Self::LookupInfo<SmolStr>) {
356 0 : match &key.lookup_type {
357 0 : LookupType::RoleSecret(role_name) => {
358 0 : if let Some(mut endpoint_info) = self.cache.get_mut(&key.endpoint_id) {
359 0 : endpoint_info.invalidate_role_secret(*role_name);
360 0 : }
361 : }
362 : LookupType::AllowedIps => {
363 0 : if let Some(mut endpoint_info) = self.cache.get_mut(&key.endpoint_id) {
364 0 : endpoint_info.invalidate_allowed_ips();
365 0 : }
366 : }
367 : }
368 0 : }
369 : }
370 :
371 : #[cfg(test)]
372 : mod tests {
373 : use super::*;
374 : use crate::{scram::ServerSecret, ProjectId};
375 :
376 : #[tokio::test]
377 6 : async fn test_project_info_cache_settings() {
378 6 : tokio::time::pause();
379 6 : let cache = ProjectInfoCacheImpl::new(ProjectInfoCacheOptions {
380 6 : size: 2,
381 6 : max_roles: 2,
382 6 : ttl: Duration::from_secs(1),
383 6 : gc_interval: Duration::from_secs(600),
384 6 : });
385 6 : let project_id: ProjectId = "project".into();
386 6 : let endpoint_id: EndpointId = "endpoint".into();
387 6 : let user1: RoleName = "user1".into();
388 6 : let user2: RoleName = "user2".into();
389 6 : let secret1 = Some(AuthSecret::Scram(ServerSecret::mock([1; 32])));
390 6 : let secret2 = None;
391 6 : let allowed_ips = Arc::new(vec![
392 6 : "127.0.0.1".parse().unwrap(),
393 6 : "127.0.0.2".parse().unwrap(),
394 6 : ]);
395 6 : cache.insert_role_secret(
396 6 : (&project_id).into(),
397 6 : (&endpoint_id).into(),
398 6 : (&user1).into(),
399 6 : secret1.clone(),
400 6 : );
401 6 : cache.insert_role_secret(
402 6 : (&project_id).into(),
403 6 : (&endpoint_id).into(),
404 6 : (&user2).into(),
405 6 : secret2.clone(),
406 6 : );
407 6 : cache.insert_allowed_ips(
408 6 : (&project_id).into(),
409 6 : (&endpoint_id).into(),
410 6 : allowed_ips.clone(),
411 6 : );
412 6 :
413 6 : let cached = cache.get_role_secret(&endpoint_id, &user1).unwrap();
414 6 : assert!(cached.cached());
415 6 : assert_eq!(cached.value, secret1);
416 6 : let cached = cache.get_role_secret(&endpoint_id, &user2).unwrap();
417 6 : assert!(cached.cached());
418 6 : assert_eq!(cached.value, secret2);
419 6 :
420 6 : // Shouldn't add more than 2 roles.
421 6 : let user3: RoleName = "user3".into();
422 6 : let secret3 = Some(AuthSecret::Scram(ServerSecret::mock([3; 32])));
423 6 : cache.insert_role_secret(
424 6 : (&project_id).into(),
425 6 : (&endpoint_id).into(),
426 6 : (&user3).into(),
427 6 : secret3.clone(),
428 6 : );
429 6 : assert!(cache.get_role_secret(&endpoint_id, &user3).is_none());
430 6 :
431 6 : let cached = cache.get_allowed_ips(&endpoint_id).unwrap();
432 6 : assert!(cached.cached());
433 6 : assert_eq!(cached.value, allowed_ips);
434 6 :
435 6 : tokio::time::advance(Duration::from_secs(2)).await;
436 6 : let cached = cache.get_role_secret(&endpoint_id, &user1);
437 6 : assert!(cached.is_none());
438 6 : let cached = cache.get_role_secret(&endpoint_id, &user2);
439 6 : assert!(cached.is_none());
440 6 : let cached = cache.get_allowed_ips(&endpoint_id);
441 6 : assert!(cached.is_none());
442 6 : }
443 :
444 : #[tokio::test]
445 6 : async fn test_project_info_cache_invalidations() {
446 6 : tokio::time::pause();
447 6 : let cache = Arc::new(ProjectInfoCacheImpl::new(ProjectInfoCacheOptions {
448 6 : size: 2,
449 6 : max_roles: 2,
450 6 : ttl: Duration::from_secs(1),
451 6 : gc_interval: Duration::from_secs(600),
452 6 : }));
453 6 : cache.clone().increment_active_listeners().await;
454 6 : tokio::time::advance(Duration::from_secs(2)).await;
455 6 :
456 6 : let project_id: ProjectId = "project".into();
457 6 : let endpoint_id: EndpointId = "endpoint".into();
458 6 : let user1: RoleName = "user1".into();
459 6 : let user2: RoleName = "user2".into();
460 6 : let secret1 = Some(AuthSecret::Scram(ServerSecret::mock([1; 32])));
461 6 : let secret2 = Some(AuthSecret::Scram(ServerSecret::mock([2; 32])));
462 6 : let allowed_ips = Arc::new(vec![
463 6 : "127.0.0.1".parse().unwrap(),
464 6 : "127.0.0.2".parse().unwrap(),
465 6 : ]);
466 6 : cache.insert_role_secret(
467 6 : (&project_id).into(),
468 6 : (&endpoint_id).into(),
469 6 : (&user1).into(),
470 6 : secret1.clone(),
471 6 : );
472 6 : cache.insert_role_secret(
473 6 : (&project_id).into(),
474 6 : (&endpoint_id).into(),
475 6 : (&user2).into(),
476 6 : secret2.clone(),
477 6 : );
478 6 : cache.insert_allowed_ips(
479 6 : (&project_id).into(),
480 6 : (&endpoint_id).into(),
481 6 : allowed_ips.clone(),
482 6 : );
483 6 :
484 6 : tokio::time::advance(Duration::from_secs(2)).await;
485 6 : // Nothing should be invalidated.
486 6 :
487 6 : let cached = cache.get_role_secret(&endpoint_id, &user1).unwrap();
488 6 : // TTL is disabled, so it should be impossible to invalidate this value.
489 6 : assert!(!cached.cached());
490 6 : assert_eq!(cached.value, secret1);
491 6 :
492 6 : cached.invalidate(); // Shouldn't do anything.
493 6 : let cached = cache.get_role_secret(&endpoint_id, &user1).unwrap();
494 6 : assert_eq!(cached.value, secret1);
495 6 :
496 6 : let cached = cache.get_role_secret(&endpoint_id, &user2).unwrap();
497 6 : assert!(!cached.cached());
498 6 : assert_eq!(cached.value, secret2);
499 6 :
500 6 : // The only way to invalidate this value is to invalidate via the api.
501 6 : cache.invalidate_role_secret_for_project((&project_id).into(), (&user2).into());
502 6 : assert!(cache.get_role_secret(&endpoint_id, &user2).is_none());
503 6 :
504 6 : let cached = cache.get_allowed_ips(&endpoint_id).unwrap();
505 6 : assert!(!cached.cached());
506 6 : assert_eq!(cached.value, allowed_ips);
507 6 : }
508 :
509 : #[tokio::test]
510 6 : async fn test_increment_active_listeners_invalidate_added_before() {
511 6 : tokio::time::pause();
512 6 : let cache = Arc::new(ProjectInfoCacheImpl::new(ProjectInfoCacheOptions {
513 6 : size: 2,
514 6 : max_roles: 2,
515 6 : ttl: Duration::from_secs(1),
516 6 : gc_interval: Duration::from_secs(600),
517 6 : }));
518 6 :
519 6 : let project_id: ProjectId = "project".into();
520 6 : let endpoint_id: EndpointId = "endpoint".into();
521 6 : let user1: RoleName = "user1".into();
522 6 : let user2: RoleName = "user2".into();
523 6 : let secret1 = Some(AuthSecret::Scram(ServerSecret::mock([1; 32])));
524 6 : let secret2 = Some(AuthSecret::Scram(ServerSecret::mock([2; 32])));
525 6 : let allowed_ips = Arc::new(vec![
526 6 : "127.0.0.1".parse().unwrap(),
527 6 : "127.0.0.2".parse().unwrap(),
528 6 : ]);
529 6 : cache.insert_role_secret(
530 6 : (&project_id).into(),
531 6 : (&endpoint_id).into(),
532 6 : (&user1).into(),
533 6 : secret1.clone(),
534 6 : );
535 6 : cache.clone().increment_active_listeners().await;
536 6 : tokio::time::advance(Duration::from_millis(100)).await;
537 6 : cache.insert_role_secret(
538 6 : (&project_id).into(),
539 6 : (&endpoint_id).into(),
540 6 : (&user2).into(),
541 6 : secret2.clone(),
542 6 : );
543 6 :
544 6 : // Added before ttl was disabled + ttl should be still cached.
545 6 : let cached = cache.get_role_secret(&endpoint_id, &user1).unwrap();
546 6 : assert!(cached.cached());
547 6 : let cached = cache.get_role_secret(&endpoint_id, &user2).unwrap();
548 6 : assert!(cached.cached());
549 6 :
550 6 : tokio::time::advance(Duration::from_secs(1)).await;
551 6 : // Added before ttl was disabled + ttl should expire.
552 6 : assert!(cache.get_role_secret(&endpoint_id, &user1).is_none());
553 6 : assert!(cache.get_role_secret(&endpoint_id, &user2).is_none());
554 6 :
555 6 : // Added after ttl was disabled + ttl should not be cached.
556 6 : cache.insert_allowed_ips(
557 6 : (&project_id).into(),
558 6 : (&endpoint_id).into(),
559 6 : allowed_ips.clone(),
560 6 : );
561 6 : let cached = cache.get_allowed_ips(&endpoint_id).unwrap();
562 6 : assert!(!cached.cached());
563 6 :
564 6 : tokio::time::advance(Duration::from_secs(1)).await;
565 6 : // Added before ttl was disabled + ttl still should expire.
566 6 : assert!(cache.get_role_secret(&endpoint_id, &user1).is_none());
567 6 : assert!(cache.get_role_secret(&endpoint_id, &user2).is_none());
568 6 : // Shouldn't be invalidated.
569 6 :
570 6 : let cached = cache.get_allowed_ips(&endpoint_id).unwrap();
571 6 : assert!(!cached.cached());
572 6 : assert_eq!(cached.value, allowed_ips);
573 6 : }
574 : }
|