use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use super::RegisteredSchema;
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a schema cache",
note = "a cache answers `schema(id)` and `subject(name)` synchronously and takes new entries \
through `store`; fetching is `RegistryClient`'s job, so nothing here may await"
)]
pub trait SchemaCache: Send + Sync + 'static {
fn schema(&self, id: u32) -> Option<Arc<RegisteredSchema>>;
fn subject(&self, subject: &str) -> Option<Arc<RegisteredSchema>>;
fn store(&self, subject: Option<&str>, schema: &Arc<RegisteredSchema>);
fn evicted_ids(&self) -> Vec<u32> {
Vec::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SchemaCachePolicy {
Disabled,
Cached {
capacity: NonZeroUsize,
subject_ttl: Option<Duration>,
},
}
impl SchemaCachePolicy {
const DEFAULT_CAPACITY: usize = 1000;
}
impl Default for SchemaCachePolicy {
fn default() -> Self {
Self::Cached {
capacity: NonZeroUsize::new(Self::DEFAULT_CAPACITY).expect("a non-zero constant"),
subject_ttl: None,
}
}
}
struct SubjectEntry {
id: u32,
resolved: Instant,
}
pub struct MemorySchemaCache {
policy: SchemaCachePolicy,
by_id: Mutex<HashMap<u32, Arc<RegisteredSchema>>>,
id_order: Mutex<VecDeque<u32>>,
by_subject: Mutex<HashMap<String, SubjectEntry>>,
on_evict: Mutex<Vec<u32>>,
}
impl fmt::Debug for MemorySchemaCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MemorySchemaCache")
.field("policy", &self.policy)
.finish_non_exhaustive()
}
}
impl MemorySchemaCache {
#[must_use]
pub fn new(policy: SchemaCachePolicy) -> Self {
Self {
policy,
by_id: Mutex::new(HashMap::new()),
id_order: Mutex::new(VecDeque::new()),
by_subject: Mutex::new(HashMap::new()),
on_evict: Mutex::new(Vec::new()),
}
}
#[must_use]
pub fn policy(&self) -> SchemaCachePolicy {
self.policy
}
fn capacity(&self) -> Option<NonZeroUsize> {
match self.policy {
SchemaCachePolicy::Disabled => None,
SchemaCachePolicy::Cached { capacity, .. } => Some(capacity),
}
}
}
impl SchemaCache for MemorySchemaCache {
fn schema(&self, id: u32) -> Option<Arc<RegisteredSchema>> {
self.by_id
.lock()
.expect("schema cache mutex poisoned")
.get(&id)
.cloned()
}
fn subject(&self, subject: &str) -> Option<Arc<RegisteredSchema>> {
let ttl = match self.policy {
SchemaCachePolicy::Disabled => return None,
SchemaCachePolicy::Cached { subject_ttl, .. } => subject_ttl,
};
let by_subject = self
.by_subject
.lock()
.expect("subject cache mutex poisoned");
let fresh = by_subject.get(subject).and_then(|entry| {
ttl.is_none_or(|ttl| entry.resolved.elapsed() < ttl)
.then_some(entry.id)
});
drop(by_subject);
self.schema(fresh?)
}
fn store(&self, subject: Option<&str>, schema: &Arc<RegisteredSchema>) {
let Some(capacity) = self.capacity() else {
return;
};
let mut by_id = self.by_id.lock().expect("schema cache mutex poisoned");
let mut order = self
.id_order
.lock()
.expect("schema cache order mutex poisoned");
if by_id.insert(schema.id(), Arc::clone(schema)).is_none() {
order.push_back(schema.id());
}
let mut evicted = Vec::new();
while order.len() > capacity.get() {
let Some(oldest) = order.pop_front() else {
break;
};
by_id.remove(&oldest);
evicted.push(oldest);
}
drop(order);
drop(by_id);
if !evicted.is_empty() {
self.on_evict
.lock()
.expect("evicted list mutex poisoned")
.extend(evicted);
}
if let Some(subject) = subject {
self.by_subject
.lock()
.expect("subject cache mutex poisoned")
.insert(
subject.to_owned(),
SubjectEntry {
id: schema.id(),
resolved: Instant::now(),
},
);
}
}
fn evicted_ids(&self) -> Vec<u32> {
std::mem::take(&mut *self.on_evict.lock().expect("evicted list mutex poisoned"))
}
}