#![deny(missing_docs)]
use nidus_core::NidusError;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, CacheError>;
#[derive(Debug, Error)]
pub enum CacheError {
#[error(transparent)]
Nidus(#[from] NidusError),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CacheConfig {
namespace: Option<String>,
time_to_live: Option<std::time::Duration>,
max_capacity: Option<u64>,
}
impl CacheConfig {
pub fn new() -> Self {
Self {
namespace: None,
time_to_live: None,
max_capacity: None,
}
}
pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
self.namespace = Some(namespace.into());
self
}
pub fn time_to_live(mut self, time_to_live: std::time::Duration) -> Self {
self.time_to_live = Some(time_to_live);
self
}
pub fn max_capacity(mut self, max_capacity: u64) -> Self {
self.max_capacity = Some(max_capacity);
self
}
pub fn namespace_value(&self) -> Option<&str> {
self.namespace.as_deref()
}
pub fn time_to_live_value(&self) -> Option<std::time::Duration> {
self.time_to_live
}
pub fn max_capacity_value(&self) -> Option<u64> {
self.max_capacity
}
}
impl Default for CacheConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CacheKey(String);
impl CacheKey {
pub fn new(namespace: Option<&str>, key: impl AsRef<str>) -> Self {
match namespace {
Some(namespace) if !namespace.is_empty() => {
Self(format!("{namespace}:{}", key.as_ref()))
}
_ => Self(key.as_ref().to_owned()),
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
#[cfg(feature = "moka")]
mod moka_backend {
#[cfg(feature = "observability")]
use std::time::Instant;
use nidus_core::{Container, ProviderRegistrant, Result as NidusResult};
use super::{CacheConfig, CacheKey, Result};
#[derive(Clone, Debug, Default)]
pub struct MokaCacheBuilder {
config: CacheConfig,
#[cfg(feature = "observability")]
observer: Option<nidus_observability::ObservabilityAdapterObserver>,
}
impl MokaCacheBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn config(mut self, config: CacheConfig) -> Self {
self.config = config;
self
}
pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
self.config = self.config.namespace(namespace);
self
}
pub fn time_to_live(mut self, time_to_live: std::time::Duration) -> Self {
self.config = self.config.time_to_live(time_to_live);
self
}
pub fn max_capacity(mut self, max_capacity: u64) -> Self {
self.config = self.config.max_capacity(max_capacity);
self
}
#[cfg(feature = "observability")]
pub fn observability(
mut self,
observer: nidus_observability::ObservabilityAdapterObserver,
) -> Self {
self.observer = Some(observer);
self
}
pub fn build(self) -> MokaCacheProvider {
let mut builder = moka::future::Cache::builder();
if let Some(time_to_live) = self.config.time_to_live {
builder = builder.time_to_live(time_to_live);
}
if let Some(max_capacity) = self.config.max_capacity {
builder = builder.max_capacity(max_capacity);
}
MokaCacheProvider {
namespace: self.config.namespace,
cache: builder.build(),
#[cfg(feature = "observability")]
observer: self.observer,
}
}
pub fn register(self, container: &mut Container) -> Result<()> {
container.register_singleton(self.build())?;
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct MokaCacheProvider {
namespace: Option<String>,
cache: moka::future::Cache<String, Vec<u8>>,
#[cfg(feature = "observability")]
observer: Option<nidus_observability::ObservabilityAdapterObserver>,
}
impl MokaCacheProvider {
pub fn builder() -> MokaCacheBuilder {
MokaCacheBuilder::new()
}
pub fn from_cache(
cache: moka::future::Cache<String, Vec<u8>>,
namespace: Option<String>,
) -> Self {
Self {
namespace,
cache,
#[cfg(feature = "observability")]
observer: None,
}
}
pub async fn insert(&self, key: impl AsRef<str>, value: Vec<u8>) {
#[cfg(feature = "observability")]
let started_at = Instant::now();
self.cache
.insert(self.cache_key(key).into_string(), value)
.await;
#[cfg(feature = "observability")]
self.record(
"insert",
nidus_observability::OperationStatus::Success,
started_at,
);
}
pub async fn get(&self, key: impl AsRef<str>) -> Option<Vec<u8>> {
#[cfg(feature = "observability")]
let started_at = Instant::now();
let result = self.cache.get(self.cache_key(key).as_str()).await;
#[cfg(feature = "observability")]
self.record(
"get",
nidus_observability::OperationStatus::Success,
started_at,
);
result
}
pub async fn invalidate(&self, key: impl AsRef<str>) {
#[cfg(feature = "observability")]
let started_at = Instant::now();
self.cache.invalidate(self.cache_key(key).as_str()).await;
#[cfg(feature = "observability")]
self.record(
"invalidate",
nidus_observability::OperationStatus::Success,
started_at,
);
}
pub fn inner(&self) -> &moka::future::Cache<String, Vec<u8>> {
&self.cache
}
pub fn namespace(&self) -> Option<&str> {
self.namespace.as_deref()
}
#[cfg(feature = "health")]
pub fn health_status(&self) -> nidus_http::health::HealthStatus {
#[cfg(feature = "observability")]
let started_at = Instant::now();
#[cfg(feature = "observability")]
self.record(
"health",
nidus_observability::OperationStatus::Success,
started_at,
);
nidus_http::health::HealthStatus::up()
}
#[cfg(feature = "health")]
pub fn register_ready_check(
self: std::sync::Arc<Self>,
registry: nidus_http::health::HealthRegistry,
name: impl Into<String>,
) -> nidus_http::health::HealthRegistry {
registry.ready_check_sync(name, move || self.health_status())
}
fn cache_key(&self, key: impl AsRef<str>) -> CacheKey {
CacheKey::new(self.namespace.as_deref(), key)
}
#[cfg(feature = "observability")]
fn record(
&self,
operation: &'static str,
status: nidus_observability::OperationStatus,
started_at: Instant,
) {
if let Some(observer) = &self.observer {
observer.record("nidus-cache", operation, status, started_at.elapsed());
}
}
}
impl ProviderRegistrant for MokaCacheProvider {
fn register_provider(container: &mut Container) -> NidusResult<()> {
container.register_singleton(Self::builder().build())?;
Ok(())
}
}
}
#[cfg(feature = "moka")]
pub use moka_backend::{MokaCacheBuilder, MokaCacheProvider};