use std::future::Future;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use tokio::sync::watch;
use tokio::time::Instant;
use crate::error::{ClientError, ClientResult};
pub(super) fn is_fresh(now: Instant, refresh_after: Instant) -> bool {
now < refresh_after
}
#[derive(Default)]
pub(super) struct TokenCache {
state: Mutex<State>,
}
#[derive(Default)]
struct State {
token: Option<CachedToken>,
failure: Option<CachedFailure>,
flight: Option<Arc<watch::Sender<Outcome>>>,
}
struct CachedToken {
token: String,
refresh_after: Instant,
}
struct CachedFailure {
error: Arc<ClientError>,
until: Instant,
}
enum Role {
Wait(watch::Receiver<Outcome>),
Lead(Arc<watch::Sender<Outcome>>),
}
#[derive(Clone)]
enum Outcome {
Pending,
Done(Result<String, Arc<ClientError>>),
Abandoned,
}
impl TokenCache {
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(super) async fn get<F, Fut>(
&self,
failure_backoff: Duration,
refresh: F,
) -> ClientResult<String>
where
F: Fn() -> Fut,
Fut: Future<Output = ClientResult<(String, Duration)>>,
{
loop {
let role = {
let mut state = self.lock();
let now = Instant::now();
if let Some(t) = &state.token
&& is_fresh(now, t.refresh_after)
{
return Ok(t.token.clone());
}
if let Some(f) = &state.failure
&& now < f.until
{
return Err(replicate(&f.error));
}
if let Some(tx) = &state.flight {
Role::Wait(tx.subscribe())
} else {
let tx = Arc::new(watch::Sender::new(Outcome::Pending));
state.flight = Some(Arc::clone(&tx));
drop(state);
Role::Lead(tx)
}
};
let mut rx = match role {
Role::Wait(rx) => rx,
Role::Lead(tx) => {
let leader = Leader {
cache: self,
tx: Some(tx),
};
return leader.run(failure_backoff, refresh()).await;
}
};
let outcome = rx
.wait_for(|o| !matches!(o, Outcome::Pending))
.await
.map_or(Outcome::Abandoned, |o| o.clone());
match outcome {
Outcome::Done(Ok(token)) => return Ok(token),
Outcome::Done(Err(e)) => return Err(replicate(&e)),
Outcome::Pending | Outcome::Abandoned => {}
}
}
}
pub(super) fn invalidate(&self, token: &str) {
let mut state = self.lock();
if state.token.as_ref().is_some_and(|t| t.token == token) {
state.token = None;
}
}
#[cfg(test)]
pub(super) fn waiters(&self) -> usize {
self.lock()
.flight
.as_ref()
.map_or(0, |tx| tx.receiver_count())
}
}
struct Leader<'a> {
cache: &'a TokenCache,
tx: Option<Arc<watch::Sender<Outcome>>>,
}
impl Leader<'_> {
async fn run(
mut self,
failure_backoff: Duration,
refresh: impl Future<Output = ClientResult<(String, Duration)>>,
) -> ClientResult<String> {
let result = refresh.await;
let mut state = self.cache.lock();
state.flight = None;
let now = Instant::now();
let (outcome, returned) = match result {
Ok((token, ttl)) => {
state.token = Some(CachedToken {
token: token.clone(),
refresh_after: now + ttl,
});
(Ok(token.clone()), Ok(token))
}
Err(e) => {
let shared = Arc::new(replicate(&e));
state.failure = (!failure_backoff.is_zero()).then(|| CachedFailure {
error: Arc::clone(&shared),
until: now + failure_backoff,
});
(Err(shared), Err(e))
}
};
drop(state);
if let Some(tx) = self.tx.take() {
tx.send_replace(Outcome::Done(outcome));
}
returned
}
}
impl Drop for Leader<'_> {
fn drop(&mut self) {
if let Some(tx) = self.tx.take() {
self.cache.lock().flight = None;
tx.send_replace(Outcome::Abandoned);
}
}
}
pub(super) fn replicate(e: &ClientError) -> ClientError {
match e {
ClientError::Http(inner) => ClientError::HttpClient(inner.to_string()),
ClientError::HttpClient(s) => ClientError::HttpClient(s.clone()),
ClientError::Serialization(inner) => ClientError::Transport(inner.to_string()),
ClientError::Protocol(a) => ClientError::Protocol(a.clone()),
ClientError::Transport(s) => ClientError::Transport(s.clone()),
ClientError::InvalidEndpoint(s) => ClientError::InvalidEndpoint(s.clone()),
ClientError::UnexpectedStatus {
status,
body,
retry_after,
} => ClientError::UnexpectedStatus {
status: *status,
body: body.clone(),
retry_after: *retry_after,
},
ClientError::AuthRequired { task_id } => ClientError::AuthRequired {
task_id: task_id.clone(),
},
ClientError::Timeout(s) => ClientError::Timeout(s.clone()),
ClientError::TooManyPendingRequests { limit } => {
ClientError::TooManyPendingRequests { limit: *limit }
}
ClientError::ProtocolBindingMismatch(s) => ClientError::ProtocolBindingMismatch(s.clone()),
ClientError::IncompleteStream {
last_event_id,
detail,
} => ClientError::IncompleteStream {
last_event_id: last_event_id.clone(),
detail: detail.clone(),
},
}
}
#[cfg(test)]
mod tests;