jwks_cache/cache/
entry.rs1use crate::{
5 _prelude::*,
6 cache::state::{CachePayload, CacheState},
7};
8
9#[derive(Clone, Debug)]
11pub struct CacheEntry {
12 tenant_id: Arc<str>,
13 provider_id: Arc<str>,
14 state: CacheState,
15}
16impl CacheEntry {
17 pub fn new(tenant_id: impl Into<Arc<str>>, provider_id: impl Into<Arc<str>>) -> Self {
19 Self {
20 tenant_id: tenant_id.into(),
21 provider_id: provider_id.into(),
22 state: CacheState::Empty,
23 }
24 }
25
26 pub fn tenant_id(&self) -> &str {
28 &self.tenant_id
29 }
30
31 pub fn provider_id(&self) -> &str {
33 &self.provider_id
34 }
35
36 pub fn state(&self) -> &CacheState {
38 &self.state
39 }
40
41 pub fn begin_load(&mut self) -> bool {
43 match self.state {
44 CacheState::Empty => {
45 self.state = CacheState::Loading;
46
47 true
48 },
49 _ => false,
50 }
51 }
52
53 pub fn load_success(&mut self, mut payload: CachePayload) {
55 payload.reset_failures();
56 self.state = CacheState::Ready(payload);
57 }
58
59 pub fn begin_refresh(&mut self, now: Instant) -> bool {
61 match &mut self.state {
62 CacheState::Ready(payload) =>
63 if now >= payload.next_refresh_at {
64 let next = payload.clone();
65 self.state = CacheState::Refreshing(next);
66
67 true
68 } else {
69 false
70 },
71 CacheState::Refreshing(_) | CacheState::Loading | CacheState::Empty => false,
72 }
73 }
74
75 pub fn refresh_success(&mut self, mut payload: CachePayload) {
77 payload.reset_failures();
78 self.state = CacheState::Ready(payload);
79 }
80
81 pub fn refresh_failure(&mut self, now: Instant, next_backoff: Option<Duration>) {
87 self.state = match std::mem::replace(&mut self.state, CacheState::Empty) {
88 CacheState::Refreshing(mut payload) => {
89 payload.bump_error(next_backoff);
90
91 if let Some(delay) = next_backoff {
92 payload.next_refresh_at = now + delay;
93 }
94
95 if payload.can_serve_stale(now) {
96 CacheState::Ready(payload)
97 } else {
98 CacheState::Empty
99 }
100 },
101 state => state,
102 };
103 }
104
105 pub fn invalidate(&mut self) {
107 self.state = CacheState::Empty;
108 }
109
110 pub fn snapshot(&self) -> Option<CachePayload> {
112 self.state.payload().cloned()
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use http::{Request, Response, StatusCode};
120 use http_cache_semantics::CachePolicy;
121 use jsonwebtoken::jwk::JwkSet;
122 use super::*;
124
125 fn sample_payload(now: Instant) -> CachePayload {
126 let request = Request::builder()
127 .method("GET")
128 .uri("https://example.com/.well-known/jwks.json")
129 .body(())
130 .expect("request");
131 let response = Response::builder().status(StatusCode::OK).body(()).expect("response");
132 let policy = CachePolicy::new(&request, &response);
133
134 CachePayload {
135 jwks: Arc::new(JwkSet { keys: Vec::new() }),
136 policy,
137 etag: Some("v1".to_string()),
138 last_modified: None,
139 last_refresh_at: Utc::now(),
140 expires_at: now + Duration::from_secs(60),
141 next_refresh_at: now + Duration::from_secs(30),
142 stale_deadline: Some(now + Duration::from_secs(120)),
143 retry_backoff: None,
144 error_count: 0,
145 }
146 }
147
148 #[test]
149 fn load_success_moves_entry_into_ready_state() {
150 let mut entry = CacheEntry::new("tenant", "provider");
151
152 assert!(matches!(entry.state(), CacheState::Empty));
153 assert!(entry.begin_load());
154
155 let now = Instant::now();
156 let payload = sample_payload(now);
157
158 entry.load_success(payload.clone());
159
160 match entry.state() {
161 CacheState::Ready(meta) => {
162 assert_eq!(meta.etag.as_deref(), Some("v1"));
163 assert_eq!(meta.error_count, 0);
164 assert!(meta.expires_at > now);
165 },
166 other => panic!("expected Ready state, got {:?}", other),
167 }
168 }
169
170 #[test]
171 fn begin_refresh_moves_ready_to_refreshing() {
172 let mut entry = CacheEntry::new("tenant", "provider");
173
174 entry.begin_load();
175
176 let now = Instant::now();
177
178 entry.load_success(sample_payload(now));
179
180 assert!(entry.begin_refresh(now + Duration::from_secs(31)));
181 matches!(entry.state(), CacheState::Refreshing(_));
182 }
183
184 #[test]
185 fn refresh_failure_without_stale_deadline_clears_entry() {
186 let mut entry = CacheEntry::new("tenant", "provider");
187
188 entry.begin_load();
189
190 let now = Instant::now();
191 let mut payload = sample_payload(now);
192
193 payload.stale_deadline = None;
194 entry.load_success(payload);
195
196 assert!(entry.begin_refresh(now + Duration::from_secs(31)));
197
198 entry.refresh_failure(now + Duration::from_secs(90), None);
199
200 assert!(matches!(entry.state(), CacheState::Empty));
201 }
202}