use std::collections::HashMap;
use std::sync::Arc;
use arc_swap::ArcSwap;
use sqlx::{PgPool, postgres::PgListener};
use tokio_util::sync::CancellationToken;
use tracing::{debug, info};
use crate::config::ONWARDS_CONFIG_CHANGED_CHANNEL;
use crate::metrics::errors::component;
#[derive(Clone)]
pub struct ZdrKeyCache {
inner: Arc<ArcSwap<HashMap<String, bool>>>,
}
impl ZdrKeyCache {
pub fn empty() -> Self {
Self {
inner: Arc::new(ArcSwap::from_pointee(HashMap::new())),
}
}
pub fn is_zdr(&self, secret: &str) -> bool {
self.inner.load().get(secret).copied().unwrap_or(false)
}
fn replace(&self, map: HashMap<String, bool>) {
self.inner.store(Arc::new(map));
}
#[cfg(test)]
pub fn from_pairs<I: IntoIterator<Item = (String, bool)>>(pairs: I) -> Self {
let cache = Self::empty();
cache.replace(pairs.into_iter().collect());
cache
}
}
async fn load(pool: &PgPool) -> Result<HashMap<String, bool>, sqlx::Error> {
let rows = sqlx::query!(
r#"
SELECT ak.secret, u.zero_data_retention
FROM api_keys ak
JOIN users u ON u.id = ak.user_id
WHERE NOT ak.is_deleted
"#
)
.fetch_all(pool)
.await?;
Ok(rows.into_iter().map(|r| (r.secret, r.zero_data_retention)).collect())
}
pub async fn refresh(pool: &PgPool, cache: &ZdrKeyCache) -> Result<usize, sqlx::Error> {
let map = load(pool).await?;
let n = map.len();
cache.replace(map);
Ok(n)
}
pub async fn initial_cache(pool: &PgPool) -> Result<ZdrKeyCache, sqlx::Error> {
let cache = ZdrKeyCache::empty();
refresh(pool, &cache).await?;
Ok(cache)
}
pub async fn run(pool: PgPool, cache: ZdrKeyCache, fallback_interval_ms: u64, shutdown: CancellationToken) -> Result<(), anyhow::Error> {
const MIN_RELOAD_INTERVAL: std::time::Duration = std::time::Duration::from_millis(100);
let fallback = (fallback_interval_ms > 0).then(|| std::time::Duration::from_millis(fallback_interval_ms));
'outer: loop {
let mut listener = PgListener::connect_with(&pool).await?;
listener.listen(ONWARDS_CONFIG_CHANGED_CHANNEL).await?;
info!("Started ZDR key sync listener");
let mut last_reload = std::time::Instant::now();
let mut fallback_timer = fallback.map(|iv| {
let mut t = tokio::time::interval(iv);
t.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
t
});
loop {
let tick = async {
match fallback_timer.as_mut() {
Some(t) => t.tick().await,
None => std::future::pending().await,
}
};
tokio::select! {
_ = shutdown.cancelled() => break 'outer,
notif = listener.try_recv() => match notif {
Ok(Some(_)) => {
if last_reload.elapsed() < MIN_RELOAD_INTERVAL { continue; }
last_reload = std::time::Instant::now();
reload(&pool, &cache).await;
}
Ok(None) => {
debug!("ZDR key sync: connection lost, reconnecting");
break;
}
Err(e) => {
crate::background_error!(component::ZDR_KEY_SYNC, "listen", Error, error = %e, "ZDR key sync: listener error");
break;
}
},
_ = tick => {
if last_reload.elapsed() < MIN_RELOAD_INTERVAL { continue; }
last_reload = std::time::Instant::now();
reload(&pool, &cache).await;
}
}
}
}
Ok(())
}
async fn reload(pool: &PgPool, cache: &ZdrKeyCache) {
match refresh(pool, cache).await {
Ok(keys) => debug!(keys, "ZDR key sync: reloaded map"),
Err(e) => {
crate::background_error!(component::ZDR_KEY_SYNC, "load", Error, error = %e, "ZDR key sync: failed to reload map");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_cache_reads_every_key_as_non_zdr() {
let cache = ZdrKeyCache::empty();
assert!(!cache.is_zdr("sk-anything"));
}
#[test]
fn is_zdr_reflects_the_maps_flag() {
let cache = ZdrKeyCache::from_pairs([("sk-on".to_string(), true), ("sk-off".to_string(), false)]);
assert!(cache.is_zdr("sk-on"));
assert!(!cache.is_zdr("sk-off"));
assert!(!cache.is_zdr("sk-missing"));
}
#[test]
fn clones_share_one_map_so_a_replace_is_visible_to_all_handles() {
let cache = ZdrKeyCache::empty();
let handle = cache.clone();
assert!(!handle.is_zdr("sk-on"));
cache.replace([("sk-on".to_string(), true)].into_iter().collect());
assert!(handle.is_zdr("sk-on"));
}
}