use super::Cache;
use crate::core::constants::NULL_SENTINEL;
use crate::error::{OxCacheError, OxCacheResult};
#[cfg(feature = "stale")]
use crate::i18n::messages::MSG_PANIC_STALE_STATE_PAYLOAD;
use crate::i18n::messages::{
MSG_DETAIL_GET_OR_LEADER_NOT_CACHED, MSG_DETAIL_GET_OR_OPTION_LEADER_NOT_CACHED, t,
};
#[cfg(all(feature = "stale", feature = "telemetry"))]
use crate::i18n::messages::{MSG_LOG_STALE_HIT_VIA_GET_OR, MSG_LOG_STALE_REVALIDATION_SCHEDULED};
use crate::macro_support::{AsyncSfGuard, shard_index as global_shard_index};
use crate::traits::CacheKey;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration;
use tokio::sync::watch;
const GET_OR_LOCK_SHARDS: usize = crate::macro_support::SF_SHARDS;
type GetOrShard = Mutex<HashMap<String, std::sync::Arc<watch::Sender<()>>>>;
static GET_OR_LOCKS: Lazy<[GetOrShard; GET_OR_LOCK_SHARDS]> =
Lazy::new(|| std::array::from_fn(|_| Mutex::new(HashMap::new())));
fn get_or_shard_index(key: &str) -> usize {
global_shard_index(key)
}
#[cfg(all(feature = "stale", feature = "telemetry"))]
#[inline]
fn telemetry_stale_downgrade(key: &str) {
tracing::debug!(
target: "oxcache::stale",
key,
"{}",
t(MSG_LOG_STALE_HIT_VIA_GET_OR, &[])
);
}
#[cfg(all(feature = "stale", not(feature = "telemetry")))]
#[inline]
fn telemetry_stale_downgrade(_key: &str) {}
#[cfg(all(feature = "stale", feature = "telemetry"))]
#[inline]
fn telemetry_stale_refresh(key: &str, spawned: bool) {
tracing::debug!(
target: "oxcache::stale",
key,
spawned,
"{}",
t(MSG_LOG_STALE_REVALIDATION_SCHEDULED, &[])
);
}
#[cfg(all(feature = "stale", not(feature = "telemetry")))]
#[inline]
fn telemetry_stale_refresh(_key: &str, _spawned: bool) {}
impl<K, V> Cache<K, V>
where
K: CacheKey,
V: serde::Serialize + for<'de> serde::Deserialize<'de>,
{
pub async fn get(&self, key: &K) -> OxCacheResult<Option<V>> {
#[cfg(feature = "metrics")]
let __start = std::time::Instant::now();
let key_str = key.to_key_string();
let bytes = self.backend.get(&key_str).await?;
#[cfg(feature = "metrics")]
{
let latency = __start.elapsed();
let layer = self.metrics_layer();
if bytes.is_some() {
self.metrics.record_hit(layer, latency);
} else {
self.metrics.record_miss(layer, latency);
}
self.record_backend_op();
}
#[cfg(feature = "audit")]
if let Some(publisher) = self.audit.as_ref() {
let action = if bytes.is_some() {
crate::features::audit::AuditAction::Hit
} else {
crate::features::audit::AuditAction::Miss
};
publisher.publish(
crate::features::audit::AuditEvent::new(action)
.with_key(crate::features::audit::redact_key_for_audit(&key_str)),
);
}
match bytes {
Some(data) if data.as_slice() == NULL_SENTINEL => Ok(None),
Some(data) => self.unified_serializer.deserialize(&data).map(Some),
None => Ok(None),
}
}
pub async fn get_by_str(&self, key: &str) -> OxCacheResult<Option<V>> {
#[cfg(feature = "metrics")]
let __start = std::time::Instant::now();
let bytes = self.backend.get(key).await?;
#[cfg(feature = "metrics")]
{
let latency = __start.elapsed();
let layer = self.metrics_layer();
if bytes.is_some() {
self.metrics.record_hit(layer, latency);
} else {
self.metrics.record_miss(layer, latency);
}
self.record_backend_op();
}
#[cfg(feature = "audit")]
if let Some(publisher) = self.audit.as_ref() {
let action = if bytes.is_some() {
crate::features::audit::AuditAction::Hit
} else {
crate::features::audit::AuditAction::Miss
};
publisher.publish(
crate::features::audit::AuditEvent::new(action)
.with_key(crate::features::audit::redact_key_for_audit(key)),
);
}
match bytes {
Some(data) if data.as_slice() == NULL_SENTINEL => Ok(None),
Some(data) => self.unified_serializer.deserialize(&data).map(Some),
None => Ok(None),
}
}
pub async fn set_by_str(
&self,
key: &str,
value: &V,
ttl: Option<Duration>,
) -> OxCacheResult<()> {
let bytes = self.unified_serializer.serialize(value)?;
#[cfg(feature = "metrics")]
let __start = std::time::Instant::now();
let result = self.backend.set(Arc::from(key), Arc::new(bytes), ttl).await;
#[cfg(feature = "metrics")]
{
self.metrics
.record_set(self.metrics_layer(), __start.elapsed());
self.record_backend_op();
}
result
}
pub async fn clear(&self) -> OxCacheResult<()> {
self.backend.clear().await
}
pub async fn keys(&self, pattern: &str) -> OxCacheResult<Vec<String>> {
self.backend.keys(pattern).await
}
pub async fn shutdown(&self) {
self.backend.shutdown().await
}
pub async fn health_check(&self) -> OxCacheResult<()> {
self.backend.health_check().await
}
pub async fn stats(&self) -> OxCacheResult<std::collections::HashMap<String, String>> {
self.backend.stats().await
}
pub async fn len(&self) -> OxCacheResult<u64> {
self.backend.len().await
}
pub async fn is_empty(&self) -> OxCacheResult<bool> {
self.backend.is_empty().await
}
pub async fn capacity(&self) -> OxCacheResult<u64> {
self.backend.capacity().await
}
pub async fn set(&self, key: &K, value: &V) -> OxCacheResult<()> {
self.set_with_ttl(key, value, None).await
}
pub async fn set_with_ttl(
&self,
key: &K,
value: &V,
ttl: Option<Duration>,
) -> OxCacheResult<()> {
let key_str = key.to_key_string();
let ttl = ttl.map(|t| self.apply_jitter(t));
#[cfg(feature = "audit")]
let __redacted_key = crate::features::audit::redact_key_for_audit(&key_str);
let bytes = self.unified_serializer.serialize(value)?;
#[cfg(feature = "metrics")]
let __start = std::time::Instant::now();
let result = self
.backend
.set(Arc::from(key_str), Arc::new(bytes), ttl)
.await;
#[cfg(feature = "metrics")]
{
self.metrics
.record_set(self.metrics_layer(), __start.elapsed());
self.record_backend_op();
}
#[cfg(feature = "audit")]
if result.is_ok()
&& let Some(publisher) = self.audit.as_ref()
{
publisher.publish(
crate::features::audit::AuditEvent::new(crate::features::audit::AuditAction::Set)
.with_key(__redacted_key),
);
}
result
}
pub async fn delete(&self, key: &K) -> OxCacheResult<()> {
let key_str = key.to_key_string();
#[cfg(feature = "metrics")]
let __start = std::time::Instant::now();
let result = self.backend.delete(&key_str).await;
#[cfg(feature = "metrics")]
{
self.metrics
.record_delete(self.metrics_layer(), __start.elapsed());
self.record_backend_op();
}
#[cfg(feature = "audit")]
if result.is_ok()
&& let Some(publisher) = self.audit.as_ref()
{
publisher.publish(
crate::features::audit::AuditEvent::new(
crate::features::audit::AuditAction::Delete,
)
.with_key(crate::features::audit::redact_key_for_audit(&key_str)),
);
}
result
}
pub async fn exists(&self, key: &K) -> OxCacheResult<bool> {
let key_str = key.to_key_string();
self.backend.exists(&key_str).await
}
pub async fn ttl(&self, key: &K) -> OxCacheResult<Option<Duration>> {
let key_str = key.to_key_string();
self.backend.ttl(&key_str).await
}
pub async fn expire(&self, key: &K, ttl: Duration) -> OxCacheResult<bool> {
let key_str = key.to_key_string();
self.backend.expire(&key_str, ttl).await
}
pub async fn get_or<F, Fut>(&self, key: &K, fallback: F) -> OxCacheResult<V>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = OxCacheResult<V>>,
{
self.get_or_core(key, None, fallback).await
}
pub async fn get_or_with_ttl<F, Fut>(
&self,
key: &K,
ttl: Option<Duration>,
fallback: F,
) -> OxCacheResult<V>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = OxCacheResult<V>>,
{
self.get_or_core(key, ttl, fallback).await
}
#[cfg(feature = "stale")]
async fn stale_step_basic(&self, key: &K) -> OxCacheResult<Option<V>> {
let Some(stale) = self.stale_backend.as_ref() else {
return Ok(None);
};
let key_str = key.to_key_string();
let (bytes, state) = stale.get_with_state(&key_str).await?;
if state != crate::features::stale::StaleState::Stale {
return Ok(None);
}
if bytes.as_deref() == Some(crate::core::constants::NULL_SENTINEL) {
return Ok(None);
}
let Some(raw) = bytes else {
return Ok(None);
};
match self.stale_policy {
crate::features::stale::StalePolicy::Return
| crate::features::stale::StalePolicy::OffloadRevalidate => {
telemetry_stale_downgrade(&key_str);
let old = self.unified_serializer.deserialize(&raw)?;
Ok(Some(old))
}
crate::features::stale::StalePolicy::Revalidate => {
let _ = crate::backend::CacheWriter::delete(stale.as_ref(), &key_str).await;
Ok(None)
}
}
}
#[cfg(feature = "stale")]
pub async fn get_or_refresh<F, Fut>(
&self,
key: &K,
ttl: Option<Duration>,
fallback: F,
) -> OxCacheResult<V>
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = OxCacheResult<V>> + Send + 'static,
V: Send + 'static,
{
if let Some(stale) = self.stale_backend.as_ref() {
let key_str = key.to_key_string();
let (bytes, state) = stale.get_with_state(&key_str).await?;
let stale_hit = state == crate::features::stale::StaleState::Stale
&& bytes.as_deref() != Some(crate::core::constants::NULL_SENTINEL);
if stale_hit {
let raw =
bytes.unwrap_or_else(|| panic!("{}", t(MSG_PANIC_STALE_STATE_PAYLOAD, &[])));
match self.stale_policy {
crate::features::stale::StalePolicy::Return => {
return self.unified_serializer.deserialize(&raw);
}
crate::features::stale::StalePolicy::OffloadRevalidate => {
let old = self.unified_serializer.deserialize(&raw)?;
if let Some(offload) = self.offload.as_ref() {
let backend = self.backend.clone();
let serializer = self.unified_serializer.clone();
let write_ttl = ttl.map(|t| self.apply_jitter(t));
let refresh_key: std::sync::Arc<str> = Arc::from(key_str.as_str());
let spawned = offload.spawn(refresh_key.clone(), async move {
if let Ok(value) = fallback().await
&& let Ok(bytes) = serializer.serialize(&value)
{
let _ =
backend.set(refresh_key, Arc::new(bytes), write_ttl).await;
}
});
telemetry_stale_refresh(&key_str, spawned);
return Ok(old);
}
return Ok(old);
}
crate::features::stale::StalePolicy::Revalidate => {
let _ = crate::backend::CacheWriter::delete(stale.as_ref(), &key_str).await;
}
}
}
}
self.get_or_core(key, ttl, fallback).await
}
async fn get_or_core<F, Fut>(
&self,
key: &K,
value_ttl: Option<Duration>,
fallback: F,
) -> OxCacheResult<V>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = OxCacheResult<V>>,
{
#[cfg(feature = "stale")]
if let Some(v) = self.stale_step_basic(key).await? {
return Ok(v);
}
if let Some(value) = self.get(key).await? {
return Ok(value);
}
let key_str = key.to_key_string();
let shard_index = get_or_shard_index(&key_str);
enum FlightReg {
Leader(Arc<watch::Sender<()>>),
Follower(watch::Receiver<()>),
}
let reg = {
let shard = &GET_OR_LOCKS[shard_index];
let mut map = shard
.lock()
.expect("GET_OR_LOCKS poisoned - concurrent operation panic detected");
match map.entry(key_str.clone()) {
std::collections::hash_map::Entry::Occupied(entry) => {
FlightReg::Follower(entry.get().subscribe())
}
std::collections::hash_map::Entry::Vacant(entry) => {
let (tx, _rx) = watch::channel(());
let tx = Arc::new(tx);
entry.insert(tx.clone());
FlightReg::Leader(tx)
}
}
};
match reg {
FlightReg::Follower(mut rx) => {
let _ = rx.changed().await;
self.get(key).await?.ok_or_else(|| {
OxCacheError::L1Error(t(MSG_DETAIL_GET_OR_LEADER_NOT_CACHED, &[]))
})
}
FlightReg::Leader(signal) => {
let mut guard =
AsyncSfGuard::new(&GET_OR_LOCKS, shard_index, key_str.clone(), signal);
if let Some(value) = self.get(key).await? {
guard.finish();
return Ok(value);
}
let result = fallback().await;
match result {
Ok(value) => {
match value_ttl {
Some(ttl) => self.set_with_ttl(key, &value, Some(ttl)).await?,
None => self.set(key, &value).await?,
}
guard.finish();
Ok(value)
}
Err(e) => {
guard.finish();
Err(e)
}
}
}
}
}
pub(super) fn apply_jitter(&self, ttl: Duration) -> Duration {
if self.ttl_jitter_factor <= 0.0 {
return ttl;
}
static JITTER_STATE: AtomicU64 = AtomicU64::new(0x9E37_79B9_7F4A_7C15);
let mut s = JITTER_STATE
.fetch_add(1, Ordering::Relaxed)
.wrapping_add(std::time::Instant::now().elapsed().subsec_nanos() as u64 | 1);
s ^= s >> 12;
s ^= s << 25;
s ^= s >> 27;
let uniform = (s % 20_001) as f64 / 10_000.0 - 1.0;
let jittered = ttl.as_millis() as f64 * (1.0 + self.ttl_jitter_factor * uniform);
Duration::from_millis(jittered.max(1.0) as u64)
}
pub async fn get_or_option<F, Fut>(&self, key: &K, fallback: F) -> OxCacheResult<Option<V>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = OxCacheResult<Option<V>>>,
{
self.get_or_option_core(key, None, false, fallback).await
}
pub async fn get_or_option_with_ttl<F, Fut>(
&self,
key: &K,
ttl: Option<Duration>,
fallback: F,
) -> OxCacheResult<Option<V>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = OxCacheResult<Option<V>>>,
{
self.get_or_option_core(key, ttl, true, fallback).await
}
async fn get_or_option_core<F, Fut>(
&self,
key: &K,
value_ttl: Option<Duration>,
jitter_sentinel: bool,
fallback: F,
) -> OxCacheResult<Option<V>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = OxCacheResult<Option<V>>>,
{
if let Some(value) = self.get(key).await? {
return Ok(Some(value));
}
let key_str = key.to_key_string();
if self.null_cache_ttl.is_some()
&& let Some(bytes) = self.backend.get(&key_str).await?
{
if bytes.as_slice() == NULL_SENTINEL {
return Ok(None);
}
return self.unified_serializer.deserialize(&bytes).map(Some);
}
let shard_index = get_or_shard_index(&key_str);
enum FlightReg {
Leader(Arc<watch::Sender<()>>),
Follower(watch::Receiver<()>),
}
let reg = {
let shard = &GET_OR_LOCKS[shard_index];
let mut map = shard
.lock()
.expect("GET_OR_LOCKS poisoned - concurrent operation panic detected");
match map.entry(key_str.clone()) {
std::collections::hash_map::Entry::Occupied(entry) => {
FlightReg::Follower(entry.get().subscribe())
}
std::collections::hash_map::Entry::Vacant(entry) => {
let (tx, _rx) = watch::channel(());
let tx = Arc::new(tx);
entry.insert(tx.clone());
FlightReg::Leader(tx)
}
}
};
match reg {
FlightReg::Follower(mut rx) => {
let _ = rx.changed().await;
if let Some(value) = self.get(key).await? {
return Ok(Some(value));
}
if self.null_cache_ttl.is_some()
&& let Some(bytes) = self.backend.get(&key_str).await?
&& bytes.as_slice() == NULL_SENTINEL
{
return Ok(None);
}
Err(OxCacheError::L1Error(t(
MSG_DETAIL_GET_OR_OPTION_LEADER_NOT_CACHED,
&[],
)))
}
FlightReg::Leader(signal) => {
let mut guard =
AsyncSfGuard::new(&GET_OR_LOCKS, shard_index, key_str.clone(), signal);
if let Some(value) = self.get(key).await? {
guard.finish();
return Ok(Some(value));
}
let result = fallback().await;
match result {
Ok(Some(value)) => {
match value_ttl {
Some(ttl) => self.set_with_ttl(key, &value, Some(ttl)).await?,
None => self.set(key, &value).await?,
}
guard.finish();
Ok(Some(value))
}
Ok(None) => {
if let Some(null_ttl) = self.null_cache_ttl {
let effective = if jitter_sentinel {
self.apply_jitter(null_ttl)
} else {
null_ttl
};
self.backend
.set(
Arc::from(key_str.as_str()),
Arc::new(NULL_SENTINEL.to_vec()),
Some(effective),
)
.await?;
}
guard.finish();
Ok(None)
}
Err(e) => {
guard.finish();
Err(e)
}
}
}
}
}
}
type SyncFlight = Arc<(Mutex<bool>, Condvar)>;
type GetOrSyncShard = Mutex<HashMap<String, SyncFlight>>;
static GET_OR_SYNC_LOCKS: Lazy<[GetOrSyncShard; GET_OR_LOCK_SHARDS]> =
Lazy::new(|| std::array::from_fn(|_| Mutex::new(HashMap::new())));
struct GetOrSyncGuard {
shard_index: usize,
map_key: String,
flight: SyncFlight,
removed: bool,
}
struct SyncLeaderCtx<'a> {
shard_index: usize,
key_str: &'a str,
flight: &'a SyncFlight,
}
impl Drop for GetOrSyncGuard {
fn drop(&mut self) {
if !self.removed {
{
let mut done = self.flight.0.lock().expect(
"GetOrSyncGuard: flight mutex poisoned - leader panicked during fallback",
);
*done = true;
}
self.flight.1.notify_all();
GET_OR_SYNC_LOCKS[self.shard_index]
.lock()
.expect("GET_OR_SYNC_LOCKS poisoned - concurrent operation panic detected")
.remove(&self.map_key);
}
}
}
impl<K, V> Cache<K, V>
where
K: CacheKey,
V: serde::Serialize + for<'de> serde::Deserialize<'de>,
{
pub(super) fn sync_backend(&self) -> OxCacheResult<&Arc<dyn crate::backend::SyncCacheBackend>> {
self.backend_sync.as_ref().ok_or_else(|| {
OxCacheError::NotSupported(
"sync API requires CacheBuilder::sync_mode(true); backend_sync is None".to_string(),
)
})
}
pub fn get_sync(&self, key: &K) -> OxCacheResult<Option<V>> {
let key_str = key.to_key_string();
let backend = self.sync_backend()?;
let bytes = backend.get(&key_str)?;
#[cfg(feature = "audit")]
if let Some(publisher) = self.audit.as_ref() {
let action = if bytes.is_some() {
crate::features::audit::AuditAction::Hit
} else {
crate::features::audit::AuditAction::Miss
};
publisher.publish(
crate::features::audit::AuditEvent::new(action)
.with_key(crate::features::audit::redact_key_for_audit(&key_str)),
);
}
match bytes {
Some(data) if data.as_slice() == NULL_SENTINEL => Ok(None),
Some(data) => self.unified_serializer.deserialize(&data).map(Some),
None => Ok(None),
}
}
pub fn set_sync(&self, key: &K, value: &V) -> OxCacheResult<()> {
self.set_with_ttl_sync(key, value, None)
}
pub fn set_with_ttl_sync(
&self,
key: &K,
value: &V,
ttl: Option<Duration>,
) -> OxCacheResult<()> {
let key_str = key.to_key_string();
let ttl = ttl.map(|t| self.apply_jitter(t));
let backend = self.sync_backend()?;
#[cfg(feature = "audit")]
let __redacted_key = crate::features::audit::redact_key_for_audit(&key_str);
let bytes = self.unified_serializer.serialize(value)?;
let result = backend.set(Arc::from(key_str), Arc::new(bytes), ttl);
#[cfg(feature = "audit")]
if result.is_ok()
&& let Some(publisher) = self.audit.as_ref()
{
publisher.publish(
crate::features::audit::AuditEvent::new(crate::features::audit::AuditAction::Set)
.with_key(__redacted_key),
);
}
result
}
pub fn delete_sync(&self, key: &K) -> OxCacheResult<()> {
let key_str = key.to_key_string();
let backend = self.sync_backend()?;
let result = backend.delete(&key_str);
#[cfg(feature = "audit")]
if result.is_ok()
&& let Some(publisher) = self.audit.as_ref()
{
publisher.publish(
crate::features::audit::AuditEvent::new(
crate::features::audit::AuditAction::Delete,
)
.with_key(crate::features::audit::redact_key_for_audit(&key_str)),
);
}
result
}
pub fn exists_sync(&self, key: &K) -> OxCacheResult<bool> {
let key_str = key.to_key_string();
let backend = self.sync_backend()?;
backend.exists(&key_str)
}
pub fn ttl_sync(&self, key: &K) -> OxCacheResult<Option<Duration>> {
let key_str = key.to_key_string();
let backend = self.sync_backend()?;
backend.ttl(&key_str)
}
pub fn expire_sync(&self, key: &K, ttl: Duration) -> OxCacheResult<bool> {
let key_str = key.to_key_string();
let backend = self.sync_backend()?;
backend.expire(&key_str, ttl)
}
pub fn get_or_sync<F>(&self, key: &K, fallback: F) -> OxCacheResult<V>
where
F: FnOnce() -> OxCacheResult<V>,
{
self.get_or_sync_core(key, None, fallback)
}
pub fn get_or_with_ttl_sync<F>(
&self,
key: &K,
ttl: Option<Duration>,
fallback: F,
) -> OxCacheResult<V>
where
F: FnOnce() -> OxCacheResult<V>,
{
self.get_or_sync_core(key, ttl, fallback)
}
fn get_or_sync_core<F>(
&self,
key: &K,
value_ttl: Option<Duration>,
fallback: F,
) -> OxCacheResult<V>
where
F: FnOnce() -> OxCacheResult<V>,
{
if let Some(value) = self.get_sync(key)? {
return Ok(value);
}
let key_str = key.to_key_string();
let shard_index = get_or_shard_index(&key_str);
let (is_follower, flight) = {
let shard = &GET_OR_SYNC_LOCKS[shard_index];
let mut map = shard
.lock()
.expect("GET_OR_SYNC_LOCKS poisoned - concurrent operation panic detected");
match map.entry(key_str.clone()) {
std::collections::hash_map::Entry::Occupied(entry) => {
(true, entry.get().clone())
}
std::collections::hash_map::Entry::Vacant(entry) => {
let f = Arc::new((Mutex::new(false), Condvar::new()));
entry.insert(f.clone());
(false, f)
}
}
};
if is_follower {
let mut done = flight
.0
.lock()
.expect("GET_OR_SYNC_LOCKS: follower flight mutex poisoned");
while !*done {
done = flight
.1
.wait(done)
.expect("GET_OR_SYNC_LOCKS: follower Condvar wait poisoned");
}
return self.get_sync(key)?.ok_or_else(|| {
OxCacheError::L1Error(
"get_or_sync: concurrent fetch leader failed to cache result".to_string(),
)
});
}
let mut guard = GetOrSyncGuard {
shard_index,
map_key: key_str.clone(),
flight: flight.clone(),
removed: false,
};
let leader = SyncLeaderCtx {
shard_index,
key_str: &key_str,
flight: &flight,
};
self.run_sync_fallback(key, leader, value_ttl, fallback, &mut guard)
}
fn run_sync_fallback<F>(
&self,
key: &K,
leader: SyncLeaderCtx<'_>,
value_ttl: Option<Duration>,
fallback: F,
guard: &mut GetOrSyncGuard,
) -> OxCacheResult<V>
where
F: FnOnce() -> OxCacheResult<V>,
{
if let Some(value) = self.get_sync(key)? {
Self::finish_sync_flight(leader.shard_index, leader.key_str, leader.flight, guard);
return Ok(value);
}
match fallback() {
Ok(value) => {
let cache_result = match value_ttl {
Some(ttl) => self.set_with_ttl_sync(key, &value, Some(ttl)),
None => self.set_sync(key, &value),
};
if let Err(e) = cache_result {
Self::finish_sync_flight(
leader.shard_index,
leader.key_str,
leader.flight,
guard,
);
return Err(e);
}
Self::finish_sync_flight(leader.shard_index, leader.key_str, leader.flight, guard);
Ok(value)
}
Err(e) => {
Self::finish_sync_flight(leader.shard_index, leader.key_str, leader.flight, guard);
Err(e)
}
}
}
fn finish_sync_flight(
shard_index: usize,
key_str: &str,
flight: &SyncFlight,
guard: &mut GetOrSyncGuard,
) {
{
let mut done = flight
.0
.lock()
.expect("GET_OR_SYNC_LOCKS: leader flight mutex poisoned");
*done = true;
}
flight.1.notify_all();
GET_OR_SYNC_LOCKS[shard_index]
.lock()
.expect("GET_OR_SYNC_LOCKS poisoned - concurrent operation panic detected")
.remove(key_str);
guard.removed = true;
}
pub fn get_or_option_sync<F>(&self, key: &K, fallback: F) -> OxCacheResult<Option<V>>
where
F: FnOnce() -> OxCacheResult<Option<V>>,
{
self.get_or_option_sync_core(key, None, false, fallback)
}
pub fn get_or_option_with_ttl_sync<F>(
&self,
key: &K,
ttl: Option<Duration>,
fallback: F,
) -> OxCacheResult<Option<V>>
where
F: FnOnce() -> OxCacheResult<Option<V>>,
{
self.get_or_option_sync_core(key, ttl, true, fallback)
}
fn get_or_option_sync_core<F>(
&self,
key: &K,
value_ttl: Option<Duration>,
jitter_sentinel: bool,
fallback: F,
) -> OxCacheResult<Option<V>>
where
F: FnOnce() -> OxCacheResult<Option<V>>,
{
if let Some(value) = self.get_sync(key)? {
return Ok(Some(value));
}
let key_str = key.to_key_string();
if self.null_cache_ttl.is_some() {
let backend = self.sync_backend()?;
if let Some(bytes) = backend.get(&key_str)? {
if bytes.as_slice() == NULL_SENTINEL {
return Ok(None);
}
return self.unified_serializer.deserialize(&bytes).map(Some);
}
}
let shard_index = get_or_shard_index(&key_str);
let (is_follower, flight) = {
let shard = &GET_OR_SYNC_LOCKS[shard_index];
let mut map = shard
.lock()
.expect("GET_OR_SYNC_LOCKS poisoned - concurrent operation panic detected");
match map.entry(key_str.clone()) {
std::collections::hash_map::Entry::Occupied(entry) => (true, entry.get().clone()),
std::collections::hash_map::Entry::Vacant(entry) => {
let f = Arc::new((Mutex::new(false), Condvar::new()));
entry.insert(f.clone());
(false, f)
}
}
};
if is_follower {
let mut done = flight
.0
.lock()
.expect("GET_OR_SYNC_LOCKS: follower flight mutex poisoned");
while !*done {
done = flight
.1
.wait(done)
.expect("GET_OR_SYNC_LOCKS: follower Condvar wait poisoned");
}
if let Some(value) = self.get_sync(key)? {
return Ok(Some(value));
}
if self.null_cache_ttl.is_some() {
let backend = self.sync_backend()?;
if let Some(bytes) = backend.get(&key_str)?
&& bytes.as_slice() == NULL_SENTINEL
{
return Ok(None);
}
}
return Err(OxCacheError::L1Error(
"get_or_option_sync: concurrent fetch leader failed to cache result".to_string(),
));
}
let mut guard = GetOrSyncGuard {
shard_index,
map_key: key_str.clone(),
flight: flight.clone(),
removed: false,
};
if let Some(value) = self.get_sync(key)? {
Self::finish_sync_flight(shard_index, &key_str, &flight, &mut guard);
return Ok(Some(value));
}
match fallback() {
Ok(Some(value)) => {
let cache_result = match value_ttl {
Some(ttl) => self.set_with_ttl_sync(key, &value, Some(ttl)),
None => self.set_sync(key, &value),
};
if let Err(e) = cache_result {
Self::finish_sync_flight(shard_index, &key_str, &flight, &mut guard);
return Err(e);
}
Self::finish_sync_flight(shard_index, &key_str, &flight, &mut guard);
Ok(Some(value))
}
Ok(None) => {
if let Some(null_ttl) = self.null_cache_ttl {
let effective = if jitter_sentinel {
self.apply_jitter(null_ttl)
} else {
null_ttl
};
let backend = self.sync_backend()?;
if let Err(e) = backend.set(
Arc::from(key_str.as_str()),
Arc::new(NULL_SENTINEL.to_vec()),
Some(effective),
) {
Self::finish_sync_flight(shard_index, &key_str, &flight, &mut guard);
return Err(e);
}
}
Self::finish_sync_flight(shard_index, &key_str, &flight, &mut guard);
Ok(None)
}
Err(e) => {
Self::finish_sync_flight(shard_index, &key_str, &flight, &mut guard);
Err(e)
}
}
}
pub fn clear_sync(&self) -> OxCacheResult<()> {
let backend = self.sync_backend()?;
backend.clear()
}
pub fn health_check_sync(&self) -> OxCacheResult<()> {
let backend = self.sync_backend()?;
backend.health_check()
}
pub fn shutdown_sync(&self) {
if let Some(backend) = &self.backend_sync {
backend.shutdown();
}
}
pub fn stats_sync(&self) -> OxCacheResult<std::collections::HashMap<String, String>> {
let backend = self.sync_backend()?;
backend.stats()
}
pub fn len_sync(&self) -> OxCacheResult<u64> {
let backend = self.sync_backend()?;
backend.len()
}
pub fn capacity_sync(&self) -> OxCacheResult<u64> {
let backend = self.sync_backend()?;
backend.capacity()
}
}
#[cfg(test)]
mod tests;