use std::fmt;
use async_trait::async_trait;
use hydracache_core::Result;
use tokio::sync::broadcast;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CacheInvalidation {
Key {
key: String,
},
Tag {
tag: String,
},
Flush,
}
impl CacheInvalidation {
pub fn key(key: impl Into<String>) -> Self {
Self::Key { key: key.into() }
}
pub fn tag(tag: impl Into<String>) -> Self {
Self::Tag { tag: tag.into() }
}
pub fn flush() -> Self {
Self::Flush
}
pub fn key_value(&self) -> Option<&str> {
match self {
Self::Key { key } => Some(key),
Self::Tag { .. } | Self::Flush => None,
}
}
pub fn tag_value(&self) -> Option<&str> {
match self {
Self::Tag { tag } => Some(tag),
Self::Key { .. } | Self::Flush => None,
}
}
pub fn is_flush(&self) -> bool {
matches!(self, Self::Flush)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CacheInvalidationMessage {
source_id: String,
invalidation: CacheInvalidation,
}
impl CacheInvalidationMessage {
pub fn new(source_id: impl Into<String>, invalidation: CacheInvalidation) -> Self {
Self {
source_id: source_id.into(),
invalidation,
}
}
pub fn source_id(&self) -> &str {
&self.source_id
}
pub fn invalidation(&self) -> &CacheInvalidation {
&self.invalidation
}
pub(crate) fn into_parts(self) -> (String, CacheInvalidation) {
(self.source_id, self.invalidation)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CacheInvalidationReceive {
Message(CacheInvalidationMessage),
Lagged(u64),
Closed,
}
impl CacheInvalidationReceive {
pub fn message(message: CacheInvalidationMessage) -> Self {
Self::Message(message)
}
pub fn is_closed(&self) -> bool {
matches!(self, Self::Closed)
}
pub fn lagged_count(&self) -> Option<u64> {
match self {
Self::Lagged(count) => Some(*count),
Self::Message(_) | Self::Closed => None,
}
}
}
#[async_trait]
pub trait CacheInvalidationReceiver: Send + 'static {
async fn recv(&mut self) -> CacheInvalidationReceive;
}
#[async_trait]
pub trait CacheInvalidationBus: fmt::Debug + Send + Sync + 'static {
async fn publish(&self, message: CacheInvalidationMessage) -> Result<()>;
fn subscribe(&self) -> Box<dyn CacheInvalidationReceiver>;
}
#[derive(Debug, Clone)]
pub struct InMemoryInvalidationBus {
sender: broadcast::Sender<CacheInvalidationMessage>,
}
impl InMemoryInvalidationBus {
pub fn new(capacity: usize) -> Self {
let (sender, _) = broadcast::channel(capacity.max(1));
Self { sender }
}
pub fn receiver_count(&self) -> usize {
self.sender.receiver_count()
}
}
impl Default for InMemoryInvalidationBus {
fn default() -> Self {
Self::new(1024)
}
}
#[async_trait]
impl CacheInvalidationBus for InMemoryInvalidationBus {
async fn publish(&self, message: CacheInvalidationMessage) -> Result<()> {
let _ = self.sender.send(message);
Ok(())
}
fn subscribe(&self) -> Box<dyn CacheInvalidationReceiver> {
Box::new(InMemoryInvalidationReceiver {
receiver: self.sender.subscribe(),
})
}
}
struct InMemoryInvalidationReceiver {
receiver: broadcast::Receiver<CacheInvalidationMessage>,
}
#[async_trait]
impl CacheInvalidationReceiver for InMemoryInvalidationReceiver {
async fn recv(&mut self) -> CacheInvalidationReceive {
match self.receiver.recv().await {
Ok(message) => CacheInvalidationReceive::Message(message),
Err(broadcast::error::RecvError::Closed) => CacheInvalidationReceive::Closed,
Err(broadcast::error::RecvError::Lagged(count)) => {
CacheInvalidationReceive::Lagged(count)
}
}
}
}
#[cfg(test)]
mod tests {
use super::{
CacheInvalidation, CacheInvalidationBus, CacheInvalidationMessage,
CacheInvalidationReceive, InMemoryInvalidationBus,
};
#[test]
fn invalidation_helpers_expose_operation_metadata() {
let key = CacheInvalidation::key("user:42");
let tag = CacheInvalidation::tag("users");
let flush = CacheInvalidation::flush();
assert_eq!(key.key_value(), Some("user:42"));
assert_eq!(key.tag_value(), None);
assert_eq!(tag.tag_value(), Some("users"));
assert_eq!(tag.key_value(), None);
assert!(flush.is_flush());
assert_eq!(flush.key_value(), None);
assert_eq!(flush.tag_value(), None);
}
#[tokio::test]
async fn in_memory_bus_delivers_messages_to_subscribers() {
let bus = InMemoryInvalidationBus::new(8);
let mut subscriber = bus.subscribe();
bus.publish(CacheInvalidationMessage::new(
"node-a",
CacheInvalidation::tag("users"),
))
.await
.unwrap();
let CacheInvalidationReceive::Message(message) = subscriber.recv().await else {
panic!("expected invalidation message");
};
assert_eq!(message.source_id(), "node-a");
assert_eq!(message.invalidation().tag_value(), Some("users"));
}
#[test]
fn in_memory_bus_default_and_receiver_count_are_observable() {
let bus = InMemoryInvalidationBus::default();
assert_eq!(bus.receiver_count(), 0);
let first = bus.subscribe();
let second = bus.subscribe();
assert_eq!(bus.receiver_count(), 2);
drop(first);
drop(second);
assert_eq!(bus.receiver_count(), 0);
}
#[tokio::test]
async fn in_memory_receiver_returns_none_when_bus_is_closed() {
let bus = InMemoryInvalidationBus::new(1);
let mut subscriber = bus.subscribe();
drop(bus);
assert_eq!(subscriber.recv().await, CacheInvalidationReceive::Closed);
}
#[tokio::test]
async fn in_memory_receiver_reports_lagged_messages_before_latest() {
let bus = InMemoryInvalidationBus::new(1);
let mut subscriber = bus.subscribe();
for key in ["stale-1", "stale-2", "latest"] {
bus.publish(CacheInvalidationMessage::new(
"node-a",
CacheInvalidation::key(key),
))
.await
.unwrap();
}
let lag = subscriber.recv().await;
assert_eq!(lag.lagged_count(), Some(2));
let CacheInvalidationReceive::Message(message) = subscriber.recv().await else {
panic!("expected latest invalidation message after lag notification");
};
assert_eq!(message.invalidation().key_value(), Some("latest"));
}
#[test]
fn receive_helpers_describe_closed_and_message_states() {
let message = CacheInvalidationMessage::new("node-a", CacheInvalidation::key("user:42"));
let received = CacheInvalidationReceive::message(message.clone());
assert_eq!(received, CacheInvalidationReceive::Message(message));
assert!(!received.is_closed());
assert_eq!(received.lagged_count(), None);
assert!(CacheInvalidationReceive::Closed.is_closed());
assert_eq!(CacheInvalidationReceive::Closed.lagged_count(), None);
assert_eq!(CacheInvalidationReceive::Lagged(3).lagged_count(), Some(3));
}
}