Skip to main content

jwks_cache/cache/
state.rs

1//! Cache state machine modelling JWKS lifecycle transitions.
2
3// crates.io
4use http_cache_semantics::CachePolicy;
5use jsonwebtoken::jwk::JwkSet;
6// self
7use crate::_prelude::*;
8
9/// Metadata captured for a cached JWKS payload.
10#[derive(Clone, Debug)]
11pub struct CachePayload {
12	/// JWKS document retained for the provider.
13	pub jwks: Arc<JwkSet>,
14	/// HTTP cache policy derived from the last response.
15	pub policy: CachePolicy,
16	/// Strong or weak validator supplied by the origin.
17	pub etag: Option<String>,
18	/// Last-Modified timestamp advertised by the origin.
19	pub last_modified: Option<DateTime<Utc>>,
20	/// UTC timestamp when the payload was most recently refreshed.
21	pub last_refresh_at: DateTime<Utc>,
22	/// Monotonic deadline after which the payload is considered expired.
23	pub expires_at: Instant,
24	/// Monotonic schedule for the next proactive refresh.
25	///
26	/// On refresh failures this is also repurposed as the cooldown deadline by
27	/// adding the computed retry backoff to the current `Instant`.
28	pub next_refresh_at: Instant,
29	/// Optional window permitting stale serving past expiry.
30	pub stale_deadline: Option<Instant>,
31	/// Exponential backoff duration before retrying a failed refresh.
32	///
33	/// This stores the most recent backoff duration; the cache manager combines
34	/// it with `next_refresh_at` to produce the absolute retry instant.
35	pub retry_backoff: Option<Duration>,
36	/// Count of consecutive refresh errors.
37	pub error_count: u32,
38}
39impl CachePayload {
40	/// Whether the payload has exceeded its freshness window.
41	pub fn is_expired(&self, now: Instant) -> bool {
42		now >= self.expires_at
43	}
44
45	/// Whether stale serving is still permitted at the given time.
46	pub fn can_serve_stale(&self, now: Instant) -> bool {
47		self.stale_deadline.map(|deadline| now <= deadline).unwrap_or(false)
48	}
49
50	/// Update retry bookkeeping after a failed refresh.
51	pub fn bump_error(&mut self, backoff: Option<Duration>) {
52		self.error_count = self.error_count.saturating_add(1);
53		self.retry_backoff = backoff;
54	}
55
56	/// Reset failure bookkeeping after a successful refresh.
57	pub fn reset_failures(&mut self) {
58		self.error_count = 0;
59		self.retry_backoff = None;
60	}
61}
62
63/// Cache lifecycle states.
64#[derive(Clone, Debug)]
65pub enum CacheState {
66	/// Cache has no payload and no work in progress.
67	Empty,
68	/// Initial fetch is underway and no payload is yet available.
69	Loading,
70	/// Fresh payload is ready for use.
71	Ready(CachePayload),
72	/// Payload is in use while a background refresh is running.
73	Refreshing(CachePayload),
74}
75impl CacheState {
76	/// Retrieve the current payload if available.
77	pub fn payload(&self) -> Option<&CachePayload> {
78		match self {
79			CacheState::Ready(payload) | CacheState::Refreshing(payload) => Some(payload),
80			_ => None,
81		}
82	}
83
84	/// Mutable access to payload when state carries one.
85	pub fn payload_mut(&mut self) -> Option<&mut CachePayload> {
86		match self {
87			CacheState::Ready(payload) | CacheState::Refreshing(payload) => Some(payload),
88			_ => None,
89		}
90	}
91
92	/// Whether the cached payload is immediately usable.
93	pub fn is_usable(&self) -> bool {
94		matches!(self, CacheState::Ready(_) | CacheState::Refreshing(_))
95	}
96}