use std::sync::Mutex;
use std::time::{Duration, Instant};
use dynamic_config::Error;
pub const REFRESH_WITHIN: Duration = Duration::from_secs(60);
pub const SERVICE_ACCOUNT_TOKEN: &str = "/var/run/secrets/kubernetes.io/serviceaccount/token";
pub struct Issued<T> {
pub value: T,
pub ttl: Option<Duration>,
}
struct Held<T> {
value: T,
expires_at: Option<Instant>,
}
pub struct Cached<T> {
held: Mutex<Option<Held<T>>>,
margin: Duration,
}
impl<T> Cached<T> {
#[must_use]
pub const fn new() -> Self {
Self::with_margin(REFRESH_WITHIN)
}
#[must_use]
pub const fn with_margin(margin: Duration) -> Self {
Self {
held: Mutex::new(None),
margin,
}
}
pub fn invalidate(&self) {
*self.lock() = None;
}
fn is_stale(&self, held: &Held<T>) -> bool {
held.expires_at.is_some_and(|expires_at| {
expires_at.saturating_duration_since(Instant::now()) < self.margin
})
}
fn lock(&self) -> std::sync::MutexGuard<'_, Option<Held<T>>> {
self.held
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
impl<T: Clone> Cached<T> {
pub fn get(
&self,
obtain: impl FnOnce(Option<&T>) -> Result<Issued<T>, Error>,
) -> Result<T, Error> {
let mut held = self.lock();
if let Some(current) = held.as_ref() {
if !self.is_stale(current) {
return Ok(current.value.clone());
}
}
let issued = obtain(held.as_ref().map(|current| ¤t.value))?;
let value = issued.value.clone();
*held = Some(Held {
value: issued.value,
expires_at: issued.ttl.and_then(|ttl| Instant::now().checked_add(ttl)),
});
Ok(value)
}
}
impl<T> Default for Cached<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> std::fmt::Debug for Cached<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let held = match self.held.try_lock() {
Ok(held) => held,
Err(std::sync::TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
Err(std::sync::TryLockError::WouldBlock) => {
return f.debug_struct("Cached").finish_non_exhaustive();
}
};
f.debug_struct("Cached")
.field("value", &held.as_ref().map(|_| "***"))
.field(
"expires_at",
&held.as_ref().and_then(|held| held.expires_at),
)
.field("margin", &self.margin)
.finish()
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
fn counted(
calls: &AtomicUsize,
ttl: Option<Duration>,
) -> impl Fn(Option<&String>) -> Result<Issued<String>, Error> + '_ {
move |_| {
let count = calls.fetch_add(1, Ordering::SeqCst);
Ok(Issued {
value: format!("token-{count}"),
ttl,
})
}
}
#[test]
fn a_credential_is_obtained_once_and_then_reused() {
let calls = AtomicUsize::new(0);
let cached = Cached::new();
let obtain = counted(&calls, Some(Duration::from_secs(3600)));
assert_eq!(cached.get(&obtain).unwrap(), "token-0");
assert_eq!(cached.get(&obtain).unwrap(), "token-0");
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn a_credential_inside_the_margin_is_obtained_again() {
let calls = AtomicUsize::new(0);
let cached = Cached::new();
let obtain = counted(&calls, Some(REFRESH_WITHIN / 2));
assert_eq!(cached.get(&obtain).unwrap(), "token-0");
assert_eq!(cached.get(&obtain).unwrap(), "token-1");
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[test]
fn a_credential_with_no_ttl_is_never_refreshed() {
let calls = AtomicUsize::new(0);
let cached = Cached::new();
let obtain = counted(&calls, None);
assert_eq!(cached.get(&obtain).unwrap(), "token-0");
assert_eq!(cached.get(&obtain).unwrap(), "token-0");
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"a root token does not expire"
);
}
#[test]
fn a_ttl_too_large_to_represent_is_treated_as_no_expiry() {
let calls = AtomicUsize::new(0);
let cached = Cached::new();
let obtain = counted(&calls, Some(Duration::from_secs(u64::MAX)));
assert_eq!(cached.get(&obtain).unwrap(), "token-0");
assert_eq!(cached.get(&obtain).unwrap(), "token-0");
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn invalidating_forces_the_next_get_to_obtain() {
let calls = AtomicUsize::new(0);
let cached = Cached::new();
let obtain = counted(&calls, Some(Duration::from_secs(3600)));
assert_eq!(cached.get(&obtain).unwrap(), "token-0");
cached.invalidate();
assert_eq!(
cached.get(&obtain).unwrap(),
"token-1",
"a refusal must be able to force a fresh credential"
);
}
#[test]
fn a_stale_credential_is_offered_to_obtain_so_it_can_be_renewed() {
let cached = Cached::new();
assert_eq!(
cached
.get(|previous| {
assert!(previous.is_none(), "there is nothing to renew yet");
Ok(Issued {
value: "first".to_owned(),
ttl: Some(REFRESH_WITHIN / 2),
})
})
.unwrap(),
"first"
);
assert_eq!(
cached
.get(|previous| {
assert_eq!(previous.map(String::as_str), Some("first"));
Ok(Issued {
value: "renewed".to_owned(),
ttl: Some(Duration::from_secs(3600)),
})
})
.unwrap(),
"renewed"
);
}
#[test]
fn an_invalidated_credential_is_not_offered_to_obtain() {
let cached = Cached::new();
cached
.get(|_| {
Ok(Issued {
value: "first".to_owned(),
ttl: Some(Duration::from_secs(3600)),
})
})
.unwrap();
cached.invalidate();
cached
.get(|previous| {
assert!(previous.is_none(), "there is nothing left to renew");
Ok(Issued {
value: "second".to_owned(),
ttl: None,
})
})
.unwrap();
}
#[test]
fn a_failed_obtain_leaves_the_previous_credential_in_place() {
let cached = Cached::new();
assert_eq!(
cached
.get(|_| Ok(Issued {
value: "first".to_owned(),
ttl: Some(REFRESH_WITHIN / 2),
}))
.unwrap(),
"first"
);
let error = cached
.get(|_| Err::<Issued<String>, _>(Error::remote("the store is away")))
.expect_err("obtaining failed");
assert!(error.to_string().contains("the store is away"), "{error}");
cached
.get(|previous| {
assert_eq!(
previous.map(String::as_str),
Some("first"),
"a refresh that failed must not throw away a credential \
that still works"
);
Ok(Issued {
value: "second".to_owned(),
ttl: None,
})
})
.unwrap();
}
#[test]
fn concurrent_gets_obtain_once() {
const THREADS: usize = 8;
let calls = AtomicUsize::new(0);
let cached: Cached<String> = Cached::new();
std::thread::scope(|scope| {
for _ in 0..THREADS {
scope.spawn(|| {
let token = cached
.get(|_| {
calls.fetch_add(1, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(50));
Ok(Issued {
value: "shared".to_owned(),
ttl: Some(Duration::from_secs(3600)),
})
})
.unwrap();
assert_eq!(token, "shared");
});
}
});
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"eight readers finding an empty cache is one login, not eight"
);
}
#[test]
fn debug_never_prints_the_credential() {
let cached = Cached::new();
cached
.get(|_| {
Ok(Issued {
value: "hunter2-token".to_owned(),
ttl: Some(Duration::from_secs(3600)),
})
})
.unwrap();
let printed = format!("{cached:?}");
assert!(!printed.contains("hunter2"), "{printed}");
assert!(printed.contains("***"), "{printed}");
}
#[test]
fn debug_does_not_block_on_a_held_lock() {
let cached: Cached<String> = Cached::new();
cached
.get(|_| {
let printed = format!("{cached:?}");
assert!(!printed.contains("hunter2"), "{printed}");
Ok(Issued {
value: "hunter2-token".to_owned(),
ttl: None,
})
})
.unwrap();
}
}