1use futures::StreamExt;
2use std::fmt;
3use std::sync::Arc;
4use std::time::Duration;
5use tokio::sync::Notify;
6use tokio::sync::futures::Notified;
7use tokio::time::Instant;
8
9use arc_swap::ArcSwap;
10use relay_base_schema::project::ProjectKey;
11use relay_config::ConfigSnapshot;
12use relay_quotas::CachedRateLimits;
13use relay_statsd::metric;
14
15use crate::services::projects::project::{ProjectState, Revision};
16use crate::services::projects::source::SourceProjectState;
17use crate::statsd::{RelayDistributions, RelayTimers};
18use crate::utils::{RetryBackoff, UniqueScheduledQueue};
19
20pub struct ProjectStore {
31 config: Config,
32 shared: Arc<Shared>,
34 private: hashbrown::HashMap<ProjectKey, PrivateProjectState>,
36 evictions: UniqueScheduledQueue<ProjectKey>,
38 refreshes: UniqueScheduledQueue<ProjectKey>,
40}
41
42impl ProjectStore {
43 pub fn new(config: &ConfigSnapshot) -> Self {
44 Self {
45 config: Config::new(config),
46 shared: Default::default(),
47 private: Default::default(),
48 evictions: Default::default(),
49 refreshes: Default::default(),
50 }
51 }
52
53 pub fn shared(&self) -> Arc<Shared> {
55 Arc::clone(&self.shared)
56 }
57
58 pub fn try_begin_fetch(&mut self, project_key: ProjectKey) -> Option<Fetch> {
64 self.do_try_begin_fetch(project_key, false)
65 }
66
67 #[must_use = "an incomplete fetch must be retried"]
72 pub fn complete_fetch(&mut self, fetch: CompletedFetch) -> Option<Fetch> {
73 let project_key = fetch.project_key();
74
75 debug_assert!(self.shared.projects.pin().get(&project_key).is_some());
78 debug_assert!(self.private.get(&project_key).is_some());
79
80 let mut project = self.get_or_create(project_key);
81 let new_fetch = match project.complete_fetch(fetch) {
84 FetchResult::ReSchedule { refresh } => project.try_begin_fetch(refresh),
85 FetchResult::Done { expiry, refresh } => {
86 self.evictions.schedule(expiry.0, project_key);
87 if let Some(RefreshTime(refresh)) = refresh {
88 self.refreshes.schedule(refresh, project_key);
89 }
90 None
91 }
92 };
93
94 metric!(
95 distribution(RelayDistributions::ProjectStateCacheSize) =
96 self.shared.projects.len() as u64,
97 storage = "shared"
98 );
99 metric!(
100 distribution(RelayDistributions::ProjectStateCacheSize) = self.private.len() as u64,
101 storage = "private"
102 );
103
104 new_fetch
105 }
106
107 pub async fn poll(&mut self) -> Option<Action> {
114 let eviction = self.evictions.next();
115 let refresh = self.refreshes.next();
116
117 tokio::select! {
118 biased;
119
120 Some(e) = eviction => Some(Action::Eviction(Eviction(e))),
121 Some(r) = refresh => Some(Action::Refresh(Refresh(r))),
122 else => None,
123 }
124 }
125
126 pub fn refresh(&mut self, Refresh(project_key): Refresh) -> Option<Fetch> {
134 self.do_try_begin_fetch(project_key, true)
135 }
136
137 pub fn evict(&mut self, Eviction(project_key): Eviction) {
139 let Some(private) = self.private.remove(&project_key) else {
141 debug_assert!(false, "no private state for eviction");
143 return;
144 };
145
146 debug_assert!(
147 matches!(private.state, FetchState::Complete { .. }),
148 "private state must be completed"
149 );
150
151 let shared = self.shared.projects.pin();
153 let _removed = shared.remove(&project_key);
154 debug_assert!(
155 _removed.is_some(),
156 "an expired project must exist in the shared state"
157 );
158
159 self.refreshes.remove(&project_key);
162 }
163
164 fn do_try_begin_fetch(&mut self, project_key: ProjectKey, is_refresh: bool) -> Option<Fetch> {
167 let fetch = match is_refresh {
168 true => self.get(project_key)?,
174 false => self.get_or_create(project_key),
175 }
176 .try_begin_fetch(is_refresh);
177
178 if fetch.is_some() {
181 self.evictions.remove(&project_key);
182 }
185
186 fetch
187 }
188
189 fn get(&mut self, project_key: ProjectKey) -> Option<ProjectRef<'_>> {
193 let private = self.private.get_mut(&project_key)?;
194
195 debug_assert!(self.shared.projects.pin().contains_key(&project_key));
198
199 let shared = self
200 .shared
201 .projects
202 .pin()
203 .get_or_insert_with(project_key, Default::default)
204 .clone();
205
206 Some(ProjectRef {
207 private,
208 shared,
209 config: &self.config,
210 })
211 }
212
213 fn get_or_create(&mut self, project_key: ProjectKey) -> ProjectRef<'_> {
217 #[cfg(debug_assertions)]
218 if self.private.contains_key(&project_key) {
219 debug_assert!(self.shared.projects.pin().contains_key(&project_key));
225 }
226
227 let private = self
228 .private
229 .entry(project_key)
230 .or_insert_with(|| PrivateProjectState::new(project_key, &self.config));
231
232 let shared = self
233 .shared
234 .projects
235 .pin()
236 .get_or_insert_with(project_key, Default::default)
237 .clone();
238
239 ProjectRef {
240 private,
241 shared,
242 config: &self.config,
243 }
244 }
245}
246
247struct Config {
249 expiry: Duration,
253 grace_period: Duration,
258 refresh_interval: Option<Duration>,
265 max_retry_backoff: Duration,
267}
268
269impl Config {
270 fn new(config: &ConfigSnapshot) -> Self {
271 let expiry = config.project_cache_expiry();
272 let grace_period = config.project_grace_period();
273
274 let refresh_interval = config
278 .project_refresh_interval()
279 .filter(|rt| *rt < (expiry + grace_period))
280 .filter(|rt| *rt > expiry);
281
282 Self {
283 expiry: config.project_cache_expiry(),
284 grace_period: config.project_grace_period(),
285 refresh_interval,
286 max_retry_backoff: config.http_max_retry_interval(),
287 }
288 }
289}
290
291#[derive(Default)]
293pub struct Shared {
294 projects: papaya::HashMap<ProjectKey, SharedProjectState, ahash::RandomState>,
295}
296
297impl Shared {
298 pub fn get_or_create(&self, project_key: ProjectKey) -> SharedProject {
303 self.get_or_create_inner(project_key).to_shared_project()
304 }
305
306 fn get_or_create_inner(&self, project_key: ProjectKey) -> SharedProjectState {
307 let projects = self.projects.pin();
309 if let Some(project) = projects.get(&project_key) {
310 return project.clone();
311 }
312
313 match projects.try_insert(project_key, Default::default()) {
315 Ok(inserted) => inserted.clone(),
316 Err(occupied) => occupied.current.clone(),
317 }
318 }
319}
320
321#[cfg(test)]
323impl Shared {
324 pub fn test_set_project_state(&self, project_key: ProjectKey, state: ProjectState) {
328 self.projects
329 .pin()
330 .get_or_insert_with(project_key, Default::default)
331 .set_project_state(state);
332 }
333
334 pub fn test_has_project_created(&self, project_key: ProjectKey) -> bool {
336 self.projects.pin().contains_key(&project_key)
337 }
338}
339
340impl fmt::Debug for Shared {
341 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342 f.debug_struct("Shared")
343 .field("num_projects", &self.projects.len())
344 .finish()
345 }
346}
347
348pub struct SharedProject(Arc<SharedProjectStateInner>);
350
351impl SharedProject {
352 pub fn project_state(&self) -> &ProjectState {
354 &self.0.state
355 }
356
357 pub fn cached_rate_limits(&self) -> &CachedRateLimits {
359 &self.0.rate_limits
365 }
366
367 pub fn outdated(&self) -> Notified<'_> {
371 self.0.notify.notified()
372 }
373}
374
375#[cfg(test)]
377impl SharedProject {
378 pub fn for_test(state: ProjectState) -> Self {
380 Self(Arc::new(SharedProjectStateInner {
381 state,
382 ..Default::default()
383 }))
384 }
385}
386
387struct ProjectRef<'a> {
389 shared: SharedProjectState,
390 private: &'a mut PrivateProjectState,
391 config: &'a Config,
392}
393
394impl ProjectRef<'_> {
395 fn try_begin_fetch(&mut self, is_refresh: bool) -> Option<Fetch> {
396 let now = Instant::now();
397 self.private
398 .try_begin_fetch(now, is_refresh, self.config)
399 .map(|fetch| fetch.with_revision(self.shared.revision()))
400 }
401
402 fn complete_fetch(&mut self, fetch: CompletedFetch) -> FetchResult {
403 let now = Instant::now();
404
405 if let Some(latency) = fetch.latency() {
406 let delay = match fetch.delay() {
407 Some(delay) if delay.as_secs() <= 15 => "lte15s",
408 Some(delay) if delay.as_secs() <= 30 => "lte30s",
409 Some(delay) if delay.as_secs() <= 60 => "lte60s",
410 Some(delay) if delay.as_secs() <= 120 => "lte120",
411 Some(delay) if delay.as_secs() <= 300 => "lte300s",
412 Some(delay) if delay.as_secs() <= 600 => "lte600s",
413 Some(delay) if delay.as_secs() <= 1800 => "lte1800s",
414 Some(delay) if delay.as_secs() <= 3600 => "lte3600s",
415 Some(_) => "gt3600s",
416 None => "none",
417 };
418 metric!(
419 timer(RelayTimers::ProjectCacheUpdateLatency) = latency,
420 delay = delay
421 );
422 }
423
424 if !fetch.is_pending() {
425 let state = match fetch.state {
426 SourceProjectState::New(_) => "new",
427 SourceProjectState::NotModified => "not_modified",
428 };
429
430 metric!(
431 timer(RelayTimers::ProjectCacheFetchDuration) = fetch.duration(now),
432 state = state
433 );
434 }
435
436 let result = self.private.complete_fetch(&fetch, now, self.config);
438 match fetch.state {
439 SourceProjectState::New(state) if !state.is_pending() => {
442 self.shared.set_project_state(state);
443 }
444 _ => {}
445 }
446
447 result
448 }
449}
450
451pub enum Action {
452 Eviction(Eviction),
453 Refresh(Refresh),
454}
455
456#[derive(Debug)]
460#[must_use = "a refresh must be used"]
461pub struct Refresh(ProjectKey);
462
463impl Refresh {
464 pub fn project_key(&self) -> ProjectKey {
466 self.0
467 }
468}
469
470#[derive(Debug)]
474#[must_use = "an eviction must be used"]
475pub struct Eviction(ProjectKey);
476
477impl Eviction {
478 pub fn project_key(&self) -> ProjectKey {
480 self.0
481 }
482}
483
484#[must_use = "a fetch must be executed"]
488#[derive(Debug)]
489pub struct Fetch {
490 project_key: ProjectKey,
491 previous_fetch: Option<Instant>,
492 initiated: Instant,
493 when: Option<Instant>,
494 revision: Revision,
495}
496
497impl Fetch {
498 pub fn project_key(&self) -> ProjectKey {
500 self.project_key
501 }
502
503 pub fn when(&self) -> Option<Instant> {
508 self.when
509 }
510
511 pub fn revision(&self) -> Revision {
516 self.revision.clone()
517 }
518
519 pub fn complete(self, state: SourceProjectState) -> CompletedFetch {
521 CompletedFetch { fetch: self, state }
522 }
523
524 fn with_revision(mut self, revision: Revision) -> Self {
525 self.revision = revision;
526 self
527 }
528}
529
530#[must_use = "a completed fetch must be acted upon"]
532#[derive(Debug)]
533pub struct CompletedFetch {
534 fetch: Fetch,
535 state: SourceProjectState,
536}
537
538impl CompletedFetch {
539 pub fn project_key(&self) -> ProjectKey {
541 self.fetch.project_key()
542 }
543
544 fn delay(&self) -> Option<Duration> {
548 self.fetch
549 .previous_fetch
550 .map(|pf| self.fetch.initiated.duration_since(pf))
551 }
552
553 fn duration(&self, now: Instant) -> Duration {
555 now.duration_since(self.fetch.initiated)
556 }
557
558 fn latency(&self) -> Option<Duration> {
566 let is_first_fetch = self.fetch.revision().as_str().is_none();
570 if is_first_fetch {
571 return None;
572 }
573
574 let project_info = match &self.state {
575 SourceProjectState::New(ProjectState::Enabled(project_info)) => project_info,
576 _ => return None,
581 };
582
583 if project_info.rev == self.fetch.revision {
585 return None;
586 }
587
588 let elapsed = chrono::Utc::now() - project_info.last_change?;
589 elapsed.to_std().ok()
590 }
591
592 fn is_pending(&self) -> bool {
594 match &self.state {
595 SourceProjectState::New(state) => state.is_pending(),
596 SourceProjectState::NotModified => false,
597 }
598 }
599}
600
601#[derive(Debug, Default, Clone)]
605struct SharedProjectState(Arc<ArcSwap<SharedProjectStateInner>>);
606
607impl SharedProjectState {
608 fn set_project_state(&self, state: ProjectState) {
610 let prev = self.0.rcu(|stored| SharedProjectStateInner {
611 state: state.clone(),
612 rate_limits: Arc::clone(&stored.rate_limits),
613 notify: Arc::clone(&stored.notify),
614 });
615
616 prev.notify.notify_waiters();
618 }
619
620 fn revision(&self) -> Revision {
622 self.0.as_ref().load().state.revision().clone()
623 }
624
625 fn to_shared_project(&self) -> SharedProject {
627 SharedProject(self.0.as_ref().load_full())
628 }
629}
630
631#[derive(Debug, Default)]
636struct SharedProjectStateInner {
637 state: ProjectState,
638 rate_limits: Arc<CachedRateLimits>,
639 notify: Arc<Notify>,
640}
641
642#[derive(Debug)]
654enum FetchState {
655 InProgress {
657 is_refresh: bool,
662 },
663 Pending {
672 initiated: Option<Instant>,
680 next_fetch_attempt: Option<Instant>,
684 },
685 Complete {
687 when: LastFetch,
689 },
690}
691
692struct PrivateProjectState {
694 project_key: ProjectKey,
696
697 state: FetchState,
699 backoff: RetryBackoff,
703
704 last_fetch: Option<Instant>,
711
712 expiry: Option<Instant>,
716}
717
718impl PrivateProjectState {
719 fn new(project_key: ProjectKey, config: &Config) -> Self {
720 Self {
721 project_key,
722 state: FetchState::Pending {
723 initiated: None,
724 next_fetch_attempt: None,
725 },
726 backoff: RetryBackoff::new(config.max_retry_backoff),
727 last_fetch: None,
728 expiry: None,
729 }
730 }
731
732 fn try_begin_fetch(
733 &mut self,
734 now: Instant,
735 is_refresh: bool,
736 config: &Config,
737 ) -> Option<Fetch> {
738 let (initiated, when) = match &mut self.state {
739 FetchState::InProgress {
740 is_refresh: refresh_in_progress,
741 } => {
742 relay_log::trace!(
743 tags.project_key = self.project_key.as_str(),
744 "project fetch skipped, fetch in progress"
745 );
746 *refresh_in_progress = *refresh_in_progress && is_refresh;
748 return None;
749 }
750 FetchState::Pending {
751 initiated,
752 next_fetch_attempt,
753 } => {
754 (initiated.unwrap_or(now), *next_fetch_attempt)
756 }
757 FetchState::Complete { when } => {
758 debug_assert_eq!(Some(when.0), self.last_fetch);
760
761 if when.check_expiry(now, config).is_fresh() {
762 relay_log::trace!(
764 tags.project_key = self.project_key.as_str(),
765 "project fetch skipped, already up to date"
766 );
767 return None;
768 }
769
770 (now, None)
771 }
772 };
773
774 self.state = FetchState::InProgress { is_refresh };
776
777 relay_log::trace!(
778 tags.project_key = &self.project_key.as_str(),
779 attempts = self.backoff.attempt() + 1,
780 "project state {} scheduled in {:?}",
781 if is_refresh { "refresh" } else { "fetch" },
782 when.unwrap_or(now).saturating_duration_since(now),
783 );
784
785 Some(Fetch {
786 project_key: self.project_key,
787 previous_fetch: self.last_fetch,
788 initiated,
789 when,
790 revision: Revision::default(),
791 })
792 }
793
794 fn complete_fetch(
795 &mut self,
796 fetch: &CompletedFetch,
797 now: Instant,
798 config: &Config,
799 ) -> FetchResult {
800 let FetchState::InProgress { is_refresh } = self.state else {
801 debug_assert!(
802 false,
803 "fetch completed while there was no current fetch registered"
804 );
805 return FetchResult::ReSchedule { refresh: false };
807 };
808
809 if fetch.is_pending() {
810 let next_backoff = self.backoff.next_backoff();
811 let next_fetch_attempt = match next_backoff.is_zero() {
812 false => now.checked_add(next_backoff),
813 true => None,
814 };
815 self.state = FetchState::Pending {
816 next_fetch_attempt,
817 initiated: Some(fetch.fetch.initiated),
818 };
819 relay_log::trace!(
820 tags.project_key = &self.project_key.as_str(),
821 "project state {} completed but still pending",
822 if is_refresh { "refresh" } else { "fetch" },
823 );
824
825 FetchResult::ReSchedule {
826 refresh: is_refresh,
827 }
828 } else {
829 relay_log::trace!(
830 tags.project_key = &self.project_key.as_str(),
831 "project state {} completed with non-pending config",
832 if is_refresh { "refresh" } else { "fetch" },
833 );
834
835 self.backoff.reset();
836 self.last_fetch = Some(now);
837
838 let when = LastFetch(now);
839
840 let refresh = when.refresh_time(config);
841 let expiry = match self.expiry {
842 Some(expiry) if is_refresh => ExpiryTime(expiry),
843 Some(_) | None => when.expiry_time(config),
846 };
847 self.expiry = Some(expiry.0);
848
849 self.state = FetchState::Complete { when };
850 FetchResult::Done { expiry, refresh }
851 }
852 }
853}
854
855#[derive(Debug)]
857#[must_use = "fetch result must be used"]
858enum FetchResult {
859 ReSchedule {
861 refresh: bool,
863 },
864 Done {
866 expiry: ExpiryTime,
868 refresh: Option<RefreshTime>,
870 },
871}
872
873#[derive(Debug, Copy, Clone)]
875struct LastFetch(Instant);
876
877impl LastFetch {
878 fn check_expiry(&self, now: Instant, config: &Config) -> Expiry {
880 let elapsed = now.saturating_duration_since(self.0);
881
882 if elapsed >= config.expiry + config.grace_period {
883 Expiry::Expired
884 } else if elapsed >= config.expiry {
885 Expiry::Stale
886 } else {
887 Expiry::Fresh
888 }
889 }
890
891 fn refresh_time(&self, config: &Config) -> Option<RefreshTime> {
893 config
894 .refresh_interval
895 .map(|duration| self.0 + duration)
896 .map(RefreshTime)
897 }
898
899 fn expiry_time(&self, config: &Config) -> ExpiryTime {
901 ExpiryTime(self.0 + config.grace_period + config.expiry)
902 }
903}
904
905#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
907enum Expiry {
908 Fresh,
910 Stale,
913 Expired,
916}
917
918impl Expiry {
919 fn is_fresh(&self) -> bool {
921 matches!(self, Self::Fresh)
922 }
923}
924
925#[derive(Debug)]
927#[must_use = "an refresh time must be used to schedule a refresh"]
928struct RefreshTime(Instant);
929
930#[derive(Debug)]
932#[must_use = "an expiry time must be used to schedule an eviction"]
933struct ExpiryTime(Instant);
934
935#[cfg(test)]
936mod tests {
937 use std::time::Duration;
938
939 use relay_config::Config;
940
941 use super::*;
942
943 async fn collect_evicted(store: &mut ProjectStore) -> Vec<ProjectKey> {
944 let mut evicted = Vec::new();
945 while let Ok(Some(Action::Eviction(eviction))) =
947 tokio::time::timeout(Duration::from_nanos(5), store.poll()).await
948 {
949 evicted.push(eviction.0);
950 store.evict(eviction);
951 }
952 evicted
953 }
954
955 macro_rules! assert_state {
956 ($store:ident, $project_key:ident, $state:pat) => {
957 assert!(matches!(
958 $store.shared().get_or_create($project_key).project_state(),
959 $state
960 ));
961 };
962 }
963
964 #[tokio::test(start_paused = true)]
965 async fn test_store_fetch() {
966 let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
967 let mut store = ProjectStore::new(&Config::default().current());
968
969 let fetch = store.try_begin_fetch(project_key).unwrap();
970 assert_eq!(fetch.project_key(), project_key);
971 assert_eq!(fetch.when(), None);
972 assert_eq!(fetch.revision().as_str(), None);
973 assert_state!(store, project_key, ProjectState::Pending);
974
975 assert!(store.try_begin_fetch(project_key).is_none());
977
978 let fetch = fetch.complete(ProjectState::Pending.into());
980 let fetch = store.complete_fetch(fetch).unwrap();
981 assert_eq!(fetch.project_key(), project_key);
982 assert_eq!(fetch.when(), None);
984 assert_eq!(fetch.revision().as_str(), None);
985 assert_state!(store, project_key, ProjectState::Pending);
986
987 let fetch = fetch.complete(ProjectState::Pending.into());
989 let fetch = store.complete_fetch(fetch).unwrap();
990 assert_eq!(fetch.project_key(), project_key);
991 assert!(fetch.when() > Some(Instant::now()));
993 assert_eq!(fetch.revision().as_str(), None);
994 assert_state!(store, project_key, ProjectState::Pending);
995
996 let fetch = fetch.complete(ProjectState::Disabled.into());
998 assert!(store.complete_fetch(fetch).is_none());
999 assert_state!(store, project_key, ProjectState::Disabled);
1000
1001 assert!(store.try_begin_fetch(project_key).is_none());
1003 }
1004
1005 #[tokio::test(start_paused = true)]
1006 async fn test_store_fetch_pending_does_not_replace_state() {
1007 let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
1008 let mut store = ProjectStore::new(
1009 &Config::from_json_value(serde_json::json!({
1010 "cache": {
1011 "project_expiry": 5,
1012 "project_grace_period": 5,
1013 }
1014 }))
1015 .unwrap()
1016 .current(),
1017 );
1018
1019 let fetch = store.try_begin_fetch(project_key).unwrap();
1020 let fetch = fetch.complete(ProjectState::Disabled.into());
1021 assert!(store.complete_fetch(fetch).is_none());
1022 assert_state!(store, project_key, ProjectState::Disabled);
1023
1024 tokio::time::advance(Duration::from_secs(6)).await;
1025
1026 let fetch = store.try_begin_fetch(project_key).unwrap();
1027 let fetch = fetch.complete(ProjectState::Pending.into());
1028 let fetch = store.complete_fetch(fetch).unwrap();
1030 assert_state!(store, project_key, ProjectState::Disabled);
1032
1033 let fetch = fetch.complete(ProjectState::Dummy.into());
1034 assert!(store.complete_fetch(fetch).is_none());
1035 assert_state!(store, project_key, ProjectState::Dummy);
1036 }
1037
1038 #[tokio::test(start_paused = true)]
1039 async fn test_store_evict_projects() {
1040 let project_key1 = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
1041 let project_key2 = ProjectKey::parse("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
1042 let mut store = ProjectStore::new(
1043 &Config::from_json_value(serde_json::json!({
1044 "cache": {
1045 "project_expiry": 5,
1046 "project_grace_period": 0,
1047 }
1048 }))
1049 .unwrap()
1050 .current(),
1051 );
1052
1053 let fetch = store.try_begin_fetch(project_key1).unwrap();
1054 let fetch = fetch.complete(ProjectState::Disabled.into());
1055 assert!(store.complete_fetch(fetch).is_none());
1056
1057 assert_eq!(collect_evicted(&mut store).await, Vec::new());
1058 assert_state!(store, project_key1, ProjectState::Disabled);
1059
1060 tokio::time::advance(Duration::from_secs(3)).await;
1062
1063 assert_eq!(collect_evicted(&mut store).await, Vec::new());
1064 assert_state!(store, project_key1, ProjectState::Disabled);
1065
1066 let fetch = store.try_begin_fetch(project_key2).unwrap();
1067 let fetch = fetch.complete(ProjectState::Disabled.into());
1068 assert!(store.complete_fetch(fetch).is_none());
1069
1070 tokio::time::advance(Duration::from_secs(3)).await;
1072
1073 assert_eq!(collect_evicted(&mut store).await, vec![project_key1]);
1074 assert_state!(store, project_key1, ProjectState::Pending);
1075 assert_state!(store, project_key2, ProjectState::Disabled);
1076 }
1077
1078 #[tokio::test(start_paused = true)]
1079 async fn test_store_evict_projects_pending_not_expired() {
1080 let project_key1 = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
1081 let project_key2 = ProjectKey::parse("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
1082 let mut store = ProjectStore::new(
1083 &Config::from_json_value(serde_json::json!({
1084 "cache": {
1085 "project_expiry": 5,
1086 "project_grace_period": 0,
1087 }
1088 }))
1089 .unwrap()
1090 .current(),
1091 );
1092
1093 let fetch = store.try_begin_fetch(project_key1).unwrap();
1094 store.shared().get_or_create(project_key2);
1096
1097 tokio::time::advance(Duration::from_secs(6)).await;
1098
1099 assert_eq!(collect_evicted(&mut store).await, Vec::new());
1101
1102 let fetch = fetch.complete(ProjectState::Disabled.into());
1104 assert!(store.complete_fetch(fetch).is_none());
1105
1106 assert_eq!(collect_evicted(&mut store).await, Vec::new());
1108 tokio::time::advance(Duration::from_secs(4)).await;
1109 assert_eq!(collect_evicted(&mut store).await, Vec::new());
1110 assert_state!(store, project_key1, ProjectState::Disabled);
1111
1112 tokio::time::advance(Duration::from_millis(1001)).await;
1114 assert_eq!(collect_evicted(&mut store).await, vec![project_key1]);
1115 assert_state!(store, project_key1, ProjectState::Pending);
1116 assert_state!(store, project_key2, ProjectState::Pending);
1117 }
1118
1119 #[tokio::test(start_paused = true)]
1120 async fn test_store_evict_projects_stale() {
1121 let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
1122 let mut store = ProjectStore::new(
1123 &Config::from_json_value(serde_json::json!({
1124 "cache": {
1125 "project_expiry": 5,
1126 "project_grace_period": 5,
1127 }
1128 }))
1129 .unwrap()
1130 .current(),
1131 );
1132
1133 let fetch = store.try_begin_fetch(project_key).unwrap();
1134 let fetch = fetch.complete(ProjectState::Disabled.into());
1135 assert!(store.complete_fetch(fetch).is_none());
1136
1137 tokio::time::advance(Duration::from_millis(9500)).await;
1139
1140 assert_eq!(collect_evicted(&mut store).await, Vec::new());
1141 assert_state!(store, project_key, ProjectState::Disabled);
1142
1143 tokio::time::advance(Duration::from_secs(1)).await;
1145
1146 assert_eq!(collect_evicted(&mut store).await, vec![project_key]);
1147 assert_state!(store, project_key, ProjectState::Pending);
1148 }
1149
1150 #[tokio::test(start_paused = true)]
1151 async fn test_store_no_eviction_during_fetch() {
1152 let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
1153 let mut store = ProjectStore::new(
1154 &Config::from_json_value(serde_json::json!({
1155 "cache": {
1156 "project_expiry": 5,
1157 "project_grace_period": 5,
1158 }
1159 }))
1160 .unwrap()
1161 .current(),
1162 );
1163
1164 let fetch = store.try_begin_fetch(project_key).unwrap();
1165
1166 tokio::time::advance(Duration::from_millis(10500)).await;
1168 assert_eq!(collect_evicted(&mut store).await, Vec::new());
1170
1171 let fetch = fetch.complete(ProjectState::Disabled.into());
1173 assert!(store.complete_fetch(fetch).is_none());
1174 tokio::time::advance(Duration::from_millis(5001)).await;
1176 let fetch = store.try_begin_fetch(project_key).unwrap();
1177
1178 tokio::time::advance(Duration::from_millis(10500)).await;
1180 assert_eq!(collect_evicted(&mut store).await, Vec::new());
1182
1183 let fetch = fetch.complete(ProjectState::Disabled.into());
1185 assert!(store.complete_fetch(fetch).is_none());
1186
1187 tokio::time::advance(Duration::from_millis(9500)).await;
1189 assert_eq!(collect_evicted(&mut store).await, Vec::new());
1190 tokio::time::advance(Duration::from_millis(501)).await;
1192 assert_eq!(collect_evicted(&mut store).await, vec![project_key]);
1193 assert_state!(store, project_key, ProjectState::Pending);
1194 }
1195
1196 #[tokio::test(start_paused = true)]
1197 async fn test_store_refresh() {
1198 let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
1199 let mut store = ProjectStore::new(
1200 &Config::from_json_value(serde_json::json!({
1201 "cache": {
1202 "project_expiry": 5,
1203 "project_grace_period": 5,
1204 "project_refresh_interval": 7,
1205 }
1206 }))
1207 .unwrap()
1208 .current(),
1209 );
1210
1211 let fetch = store.try_begin_fetch(project_key).unwrap();
1212 let fetch = fetch.complete(ProjectState::Disabled.into());
1213 assert!(store.complete_fetch(fetch).is_none());
1214 assert_state!(store, project_key, ProjectState::Disabled);
1215
1216 let Some(Action::Refresh(refresh)) = store.poll().await else {
1218 panic!();
1219 };
1220 assert_eq!(refresh.project_key(), project_key);
1221
1222 let fetch = store.refresh(refresh).unwrap();
1223 assert!(store.try_begin_fetch(project_key).is_none());
1225 let fetch = fetch.complete(ProjectState::Disabled.into());
1226 assert!(store.complete_fetch(fetch).is_none());
1227
1228 let Some(Action::Refresh(refresh)) = store.poll().await else {
1232 panic!();
1233 };
1234 let fetch = store.refresh(refresh).unwrap();
1235 let fetch = fetch.complete(ProjectState::Disabled.into());
1236 assert!(store.complete_fetch(fetch).is_none());
1237
1238 let Some(Action::Eviction(eviction)) = store.poll().await else {
1241 panic!();
1242 };
1243 assert_eq!(eviction.project_key(), project_key);
1244 }
1245
1246 #[tokio::test(start_paused = true)]
1247 async fn test_store_refresh_overtaken_by_eviction() {
1248 let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
1249 let mut store = ProjectStore::new(
1250 &Config::from_json_value(serde_json::json!({
1251 "cache": {
1252 "project_expiry": 5,
1253 "project_grace_period": 5,
1254 "project_refresh_interval": 7,
1255 }
1256 }))
1257 .unwrap()
1258 .current(),
1259 );
1260
1261 let fetch = store.try_begin_fetch(project_key).unwrap();
1262 let fetch = fetch.complete(ProjectState::Disabled.into());
1263 assert!(store.complete_fetch(fetch).is_none());
1264 assert_state!(store, project_key, ProjectState::Disabled);
1265
1266 tokio::time::advance(Duration::from_secs(20)).await;
1268
1269 let Some(Action::Eviction(eviction)) = store.poll().await else {
1272 panic!();
1273 };
1274 assert_eq!(eviction.project_key(), project_key);
1275 store.evict(eviction);
1276
1277 assert!(
1281 tokio::time::timeout(Duration::from_secs(60), store.poll())
1282 .await
1283 .is_err()
1284 );
1285 }
1286
1287 #[tokio::test(start_paused = true)]
1288 async fn test_store_refresh_during_eviction() {
1289 let project_key = ProjectKey::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
1290 let mut store = ProjectStore::new(
1291 &Config::from_json_value(serde_json::json!({
1292 "cache": {
1293 "project_expiry": 5,
1294 "project_grace_period": 5,
1295 "project_refresh_interval": 7,
1296 }
1297 }))
1298 .unwrap()
1299 .current(),
1300 );
1301
1302 let fetch = store.try_begin_fetch(project_key).unwrap();
1303 let fetch = fetch.complete(ProjectState::Disabled.into());
1304 assert!(store.complete_fetch(fetch).is_none());
1305 assert_state!(store, project_key, ProjectState::Disabled);
1306
1307 tokio::time::advance(Duration::from_secs(20)).await;
1309
1310 let Some(Action::Eviction(eviction)) = store.poll().await else {
1314 panic!();
1315 };
1316 let Some(Action::Refresh(refresh)) = store.poll().await else {
1317 panic!();
1318 };
1319 assert_eq!(eviction.project_key(), project_key);
1320 assert_eq!(refresh.project_key(), project_key);
1321 store.evict(eviction);
1322
1323 assert!(store.refresh(refresh).is_none());
1324 }
1325
1326 #[tokio::test(start_paused = true)]
1327 async fn test_ready_state() {
1328 let shared = SharedProjectState::default();
1329
1330 let shared_project = shared.to_shared_project();
1331 assert!(shared_project.project_state().is_pending());
1332 let mut listener = std::pin::pin!(shared_project.outdated());
1333
1334 let result = tokio::time::timeout(Duration::from_secs(5), listener.as_mut()).await;
1336 assert!(result.is_err()); assert!(shared.to_shared_project().project_state().is_pending());
1338
1339 shared.set_project_state(ProjectState::Disabled);
1341
1342 let result = tokio::time::timeout(Duration::from_secs(1), listener).await;
1344 assert!(result.is_ok()); assert!(shared_project.project_state().is_pending());
1348
1349 assert!(matches!(
1351 shared.to_shared_project().project_state(),
1352 &ProjectState::Disabled
1353 ));
1354 }
1355}