use std::sync::Arc;
use std::time::Duration;
use cratefield_core::{KeyValue, ModuleContext};
use sha2::{Digest, Sha256};
pub(crate) const GENERATION_KEY: &str = "changelog:gen";
const KEY_PREFIX: &str = "changelog";
pub(crate) struct ReadCache {
kv: Option<Arc<dyn KeyValue>>,
ttl: Option<Duration>,
}
impl ReadCache {
pub(crate) fn of(ctx: &ModuleContext, ttl_secs: u64) -> Self {
Self {
kv: ctx.ports.kv.clone(),
ttl: (ttl_secs > 0).then(|| Duration::from_secs(ttl_secs)),
}
}
pub(crate) async fn get(&self, fingerprint: &str) -> Option<String> {
let kv = self.kv.as_deref()?;
self.ttl.as_ref()?;
let Some(generation) = generation(kv).await else {
tracing::warn!("changelog: could not read the cache generation; reading the database");
return None;
};
match kv.get(&key(&generation, fingerprint)).await {
Ok(body) => body,
Err(error) => {
tracing::warn!(error = %error, "changelog: cache read failed; reading the database");
None
}
}
}
pub(crate) async fn put(&self, fingerprint: &str, body: &str) {
let Some(kv) = self.kv.as_deref() else {
return;
};
let Some(ttl) = self.ttl else {
return;
};
let Some(generation) = generation(kv).await else {
return;
};
if let Err(error) = kv
.put(&key(&generation, fingerprint), body, Some(ttl))
.await
{
tracing::warn!(error = %error, "changelog: cache write failed; reads will use the database");
}
}
pub(crate) async fn publish_generation(&self, generation: &str) {
let Some(kv) = self.kv.as_deref() else {
return;
};
if let Err(error) = kv.put(GENERATION_KEY, generation, None).await {
tracing::warn!(
error = %error,
"changelog: could not publish the new cache generation; old entries live out their TTL"
);
}
}
}
fn key(generation: &str, fingerprint: &str) -> String {
format!("{KEY_PREFIX}:{generation}:{}", fingerprint_key(fingerprint))
}
async fn generation(kv: &dyn KeyValue) -> Option<String> {
match kv.get(GENERATION_KEY).await {
Ok(Some(generation)) => Some(generation),
Ok(None) => Some("0".to_owned()),
Err(_) => None,
}
}
fn fingerprint_key(fingerprint: &str) -> String {
let digest = Sha256::digest(fingerprint.as_bytes());
let mut out = String::with_capacity(digest.len() * 2);
for byte in digest {
out.push(char::from_digit(u32::from(byte >> 4), 16).expect("a hex digit"));
out.push(char::from_digit(u32::from(byte & 0x0F), 16).expect("a hex digit"));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fingerprints_key_by_their_whole_digest() {
assert_eq!(
fingerprint_key("list:1:20"),
"72955ba08ab9248bf24fae638721115ed519ee6f64d749cc4d4d3854e5f9799b"
);
assert_eq!(fingerprint_key("").len(), 64);
let long = "x".repeat(4096);
assert_eq!(fingerprint_key(&long).len(), 64);
let prefix = format!("release:v9.0.0-{}", "a".repeat(200));
let other = format!("{prefix}X");
assert_ne!(fingerprint_key(&prefix), fingerprint_key(&other));
}
}