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}