use crate::lease::Lease;
use rustlavel_core::Result;
use std::future::Future;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::sync::watch;
use tokio::task::JoinHandle;
pub const MIN_INTERVAL: Duration = Duration::from_secs(1);
pub struct Renewer {
stop: watch::Sender<bool>,
min_interval: Duration,
tasks: Mutex<Vec<JoinHandle<()>>>,
watching: std::sync::Arc<AtomicUsize>,
}
impl Default for Renewer {
fn default() -> Renewer {
Renewer::new()
}
}
impl Renewer {
pub fn new() -> Renewer {
Renewer {
stop: watch::channel(false).0,
min_interval: MIN_INTERVAL,
tasks: Mutex::new(Vec::new()),
watching: std::sync::Arc::new(AtomicUsize::new(0)),
}
}
pub fn min_interval(mut self, interval: Duration) -> Renewer {
self.min_interval = interval;
self
}
pub fn watching(&self) -> usize {
self.watching.load(Ordering::SeqCst)
}
pub fn watch<F, Fut>(&self, name: impl Into<String>, lease: Lease, renew: F)
where
F: Fn(Lease) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Lease>> + Send + 'static,
{
let name = name.into();
if !lease.renewable || !lease.exists() {
rustlavel_core::debug!("`{name}` carries no renewable lease; nothing to renew");
return;
}
let Ok(runtime) = tokio::runtime::Handle::try_current() else {
rustlavel_core::warn!(
"not renewing the lease on `{name}`: no tokio runtime is running, so there is \
nothing to renew it from. The credential will stop working in {}s.",
lease.remaining().as_secs()
);
return;
};
let stop = self.stop.subscribe();
let min_interval = self.min_interval;
let watching = self.watching.clone();
watching.fetch_add(1, Ordering::SeqCst);
let handle = runtime.spawn(async move {
renew_until_stopped(name, lease, renew, min_interval, stop).await;
watching.fetch_sub(1, Ordering::SeqCst);
});
self.tasks.lock().unwrap_or_else(|e| e.into_inner()).push(handle);
}
pub fn stop(&self) {
self.stop.send_replace(true);
}
pub async fn shutdown(&self) {
self.stop();
let tasks: Vec<JoinHandle<()>> =
self.tasks.lock().unwrap_or_else(|e| e.into_inner()).drain(..).collect();
for task in tasks {
let _ = task.await;
}
}
}
impl Drop for Renewer {
fn drop(&mut self) {
self.stop.send_replace(true);
}
}
impl std::fmt::Debug for Renewer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Renewer")
.field("watching", &self.watching())
.field("min_interval", &self.min_interval)
.finish()
}
}
async fn renew_until_stopped<F, Fut>(
name: String,
mut lease: Lease,
renew: F,
min_interval: Duration,
mut stop: watch::Receiver<bool>,
) where
F: Fn(Lease) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Lease>> + Send + 'static,
{
let mut wait = lease.renew_after().max(min_interval);
loop {
if sleep_or_stop(wait, &mut stop).await {
rustlavel_core::debug!("stopped renewing `{name}`");
return;
}
match renew(lease.clone()).await {
Ok(renewed) => {
rustlavel_core::debug!(
"renewed the lease on `{name}` for {}s",
renewed.duration.as_secs()
);
lease = renewed;
if !lease.renewable || !lease.exists() {
rustlavel_core::warn!(
"`{name}` came back from a renewal without a renewable lease. It stops \
working in {}s and renewing cannot prevent that — whatever issued it \
has to issue a new one.",
lease.remaining().as_secs()
);
return;
}
wait = lease.renew_after().max(min_interval);
}
Err(error) => {
if lease.is_expired() {
rustlavel_core::error!(
"lost the lease on `{name}`: {error}. Whatever it held open — a database \
account, a token — has expired, and this process is now using a \
credential the store no longer honours."
);
return;
}
rustlavel_core::warn!(
"could not renew `{name}`: {error}. Retrying; {}s left on the lease.",
lease.remaining().as_secs()
);
wait = (lease.remaining() / 4).max(min_interval);
}
}
}
}
async fn sleep_or_stop(wait: Duration, stop: &mut watch::Receiver<bool>) -> bool {
if *stop.borrow() {
return true;
}
tokio::select! {
_ = tokio::time::sleep(wait) => *stop.borrow(),
_ = stop.changed() => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rustlavel_core::Error;
use std::sync::Arc;
use std::time::Instant;
fn aged(duration: Duration, elapsed: Duration) -> Lease {
Lease {
id: "database/creds/app/abc".into(),
duration,
renewable: true,
issued: Instant::now() - elapsed,
}
}
async fn within(limit: Duration, mut done: impl FnMut() -> bool) -> bool {
let deadline = Instant::now() + limit;
while Instant::now() < deadline {
if done() {
return true;
}
tokio::time::sleep(Duration::from_millis(5)).await;
}
done()
}
#[tokio::test]
async fn renews_at_the_two_thirds_point_and_not_before() {
let renewals = Arc::new(AtomicUsize::new(0));
let counter = renewals.clone();
let renewer = Renewer::new().min_interval(Duration::from_millis(10));
renewer.watch("database", aged(Duration::from_millis(300), Duration::ZERO), move |lease| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
Ok(lease.renewed(Duration::from_millis(300), true))
}
});
tokio::time::sleep(Duration::from_millis(80)).await;
assert_eq!(renewals.load(Ordering::SeqCst), 0, "renewed far too early");
assert!(
within(Duration::from_secs(3), || renewals.load(Ordering::SeqCst) >= 1).await,
"the lease was never renewed"
);
renewer.shutdown().await;
}
#[tokio::test]
async fn retries_a_renewal_that_failed_within_the_time_that_is_left() {
let attempts = Arc::new(AtomicUsize::new(0));
let counter = attempts.clone();
let renewer = Renewer::new().min_interval(Duration::from_millis(10));
renewer.watch(
"database",
aged(Duration::from_secs(30), Duration::from_secs(20)),
move |lease| {
let counter = counter.clone();
async move {
if counter.fetch_add(1, Ordering::SeqCst) == 0 {
Err(Error::msg("the secret store is on a standby node"))
} else {
Ok(lease.renewed(Duration::from_secs(30), true))
}
}
},
);
assert!(
within(Duration::from_secs(3), || attempts.load(Ordering::SeqCst) >= 2).await,
"a failed renewal was never retried"
);
renewer.shutdown().await;
}
#[tokio::test]
async fn gives_up_when_the_lease_is_lost_rather_than_retrying_forever() {
let attempts = Arc::new(AtomicUsize::new(0));
let counter = attempts.clone();
let renewer = Renewer::new().min_interval(Duration::from_millis(10));
renewer.watch("database", aged(Duration::from_millis(60), Duration::ZERO), move |_| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
Err(Error::msg("permission denied"))
}
});
assert!(
within(Duration::from_secs(3), || renewer.watching() == 0).await,
"the renewer never gave up on an expired lease"
);
assert!(attempts.load(Ordering::SeqCst) >= 1, "it gave up without trying");
}
#[tokio::test]
async fn stops_cleanly_on_shutdown() {
let renewals = Arc::new(AtomicUsize::new(0));
let counter = renewals.clone();
let renewer = Renewer::new();
renewer.watch("token", aged(Duration::from_secs(3600), Duration::ZERO), move |lease| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
Ok(lease.renewed(Duration::from_secs(3600), true))
}
});
assert_eq!(renewer.watching(), 1);
let stopped = tokio::time::timeout(Duration::from_secs(5), renewer.shutdown()).await;
assert!(stopped.is_ok(), "shutdown waited for the sleep instead of interrupting it");
assert_eq!(renewer.watching(), 0);
assert_eq!(renewals.load(Ordering::SeqCst), 0, "nothing was due");
}
#[tokio::test]
async fn a_lease_handed_over_after_shutdown_stops_immediately() {
let renewer = Renewer::new().min_interval(Duration::from_millis(10));
renewer.stop();
renewer.watch("late", aged(Duration::from_secs(30), Duration::from_secs(29)), |lease| async move {
Ok(lease.renewed(Duration::from_secs(30), true))
});
assert!(
within(Duration::from_secs(3), || renewer.watching() == 0).await,
"a task started after shutdown kept running"
);
}
#[tokio::test]
async fn a_lease_that_cannot_be_renewed_is_not_watched_at_all() {
let renewer = Renewer::new();
let mut lease = aged(Duration::from_secs(60), Duration::ZERO);
lease.renewable = false;
renewer.watch("static", lease, |lease| async move { Ok(lease) });
renewer.watch("none", Lease::none(), |lease| async move { Ok(lease) });
assert_eq!(renewer.watching(), 0);
}
#[tokio::test]
async fn an_overdue_lease_does_not_become_a_busy_loop() {
let renewals = Arc::new(AtomicUsize::new(0));
let counter = renewals.clone();
let renewer = Renewer::new().min_interval(Duration::from_millis(50));
renewer.watch("database", aged(Duration::from_secs(30), Duration::from_secs(29)), move |_| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
Ok(Lease {
id: "database/creds/app/abc".into(),
duration: Duration::from_secs(30),
renewable: true,
issued: Instant::now() - Duration::from_secs(29),
})
}
});
tokio::time::sleep(Duration::from_millis(250)).await;
renewer.shutdown().await;
let count = renewals.load(Ordering::SeqCst);
assert!(count >= 1, "it should still have renewed");
assert!(count <= 10, "one renewal per 50ms at most, got {count} in 250ms");
}
}