use std::sync::Arc;
use crate::error::CacheError;
use crate::key::KeyRef;
use crate::tier::TierId;
use crate::tier::tier_trait::{BackendKind, CacheTier, TierHealth};
pub struct HotRungTier<V> {
rung: TierId,
inner: std::sync::RwLock<Arc<dyn CacheTier<V>>>,
}
impl<V> std::fmt::Debug for HotRungTier<V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HotRungTier")
.field("rung", &self.rung)
.finish_non_exhaustive()
}
}
impl<V> HotRungTier<V> {
pub fn new(rung: TierId, initial: Arc<dyn CacheTier<V>>) -> Result<Self, CacheError> {
if initial.tier_id() != rung {
return Err(CacheError::ConfigurationError);
}
Ok(HotRungTier {
rung,
inner: std::sync::RwLock::new(initial),
})
}
pub fn hot_swap(
&self,
replacement: Arc<dyn CacheTier<V>>,
) -> Result<Arc<dyn CacheTier<V>>, CacheError> {
if replacement.tier_id() != self.rung {
return Err(CacheError::ConfigurationError);
}
let Ok(mut guard) = self.inner.write() else {
return Err(CacheError::ConfigurationError);
};
Ok(std::mem::replace(&mut guard, replacement))
}
fn current(&self) -> Result<Arc<dyn CacheTier<V>>, CacheError> {
self.inner
.read()
.map_err(|_| CacheError::ConfigurationError)
.map(|g| Arc::clone(&g))
}
#[must_use]
pub const fn rung(&self) -> TierId {
self.rung
}
}
#[async_trait::async_trait]
impl<V: Clone + Send + Sync + 'static> CacheTier<V> for HotRungTier<V> {
fn name(&self) -> String {
match self.current() {
Ok(inner) => inner.name(),
Err(_) => "hot-rung-poisoned".to_string(),
}
}
fn backend(&self) -> BackendKind {
match self.current() {
Ok(inner) => inner.backend(),
Err(_) => BackendKind::Unavailable,
}
}
fn capability(&self) -> crate::capability::TierCapability {
match self.current() {
Ok(inner) => inner.capability(),
Err(_) => crate::capability::TierCapability::unbound(),
}
}
fn eviction_candidate(&self) -> Option<crate::integrity::KeyAddress> {
self.current().ok()?.eviction_candidate()
}
fn capacity(&self) -> Option<usize> {
self.current().ok()?.capacity()
}
fn remove_if_address(&self, address: crate::integrity::KeyAddress) -> Result<bool, CacheError> {
self.current()?.remove_if_address(address)
}
async fn get(&self, key: &KeyRef<'_>) -> Result<Option<V>, CacheError> {
let inner = self.current()?;
inner.get(key).await
}
async fn set(
&self,
key: &KeyRef<'_>,
value: V,
ttl: Option<std::time::Duration>,
) -> Result<(), CacheError> {
let inner = self.current()?;
inner.set(key, value, ttl).await
}
async fn remove(&self, key: &KeyRef<'_>) -> Result<(), CacheError> {
let inner = self.current()?;
inner.remove(key).await
}
async fn contains(&self, key: &KeyRef<'_>) -> Result<bool, CacheError> {
let inner = self.current()?;
inner.contains(key).await
}
fn health(&self) -> TierHealth {
match self.current() {
Ok(inner) => inner.health(),
Err(_) => TierHealth::default(),
}
}
fn tier_id(&self) -> TierId {
self.rung
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
use crate::integrity::IntegrityCheck;
use crate::tier::test::TestTier;
fn a_test_tier<V: Clone + Send + Sync + 'static + IntegrityCheck>(
rung: TierId,
) -> Arc<dyn CacheTier<V>> {
Arc::new(TestTier::<V>::new(rung))
}
#[test]
fn a_swap_refuses_a_tier_claiming_another_rung() {
testkit::proves!("autonomy.rebind.rebind_refuses_a_foreign_rung");
let hot =
HotRungTier::<Vec<u8>>::new(TierId::L1, a_test_tier(TierId::L1)).expect("same rung");
assert!(
hot.hot_swap(a_test_tier(TierId::L1)).is_ok(),
"a rung-honest tier must be accepted"
);
assert!(
hot.hot_swap(a_test_tier(TierId::L2)).is_err(),
"a tier claiming another rung must be refused"
);
}
#[test]
fn the_wrapper_forwards_the_inner_tier_identity() {
testkit::proves!("autonomy.rebind.rebind_preserves_capability_honesty");
let hot = HotRungTier::<Vec<u8>>::new(TierId::L1, a_test_tier(TierId::L1))
.expect("stub claims L1");
let before = hot.capability();
let backend_before = before.backend;
let state_before = before.state;
let displaced = hot
.hot_swap(a_test_tier(TierId::L1))
.expect("rebind with a rung-matching tier");
let after = hot.capability();
assert_eq!(hot.backend(), BackendKind::Test);
assert_eq!(hot.tier_id(), TierId::L1);
assert_eq!(after.backend, backend_before);
assert_eq!(
after.state, state_before,
"a rebound rung must keep reporting the identity it actually has"
);
assert_eq!(displaced.tier_id(), TierId::L1);
assert_eq!(hot.rung(), TierId::L1);
}
#[tokio::test]
async fn a_swap_lands_around_in_flight_operations_not_mid_write() {
testkit::proves!("autonomy.rebind.rebind_preserves_capability_honesty");
let hot = HotRungTier::<Vec<u8>>::new(TierId::L2, a_test_tier(TierId::L2))
.expect("stub claims L2");
let key = KeyRef(b"hot-rung-key");
hot.set(&key, b"v1".to_vec(), None).await.expect("set v1");
let _ = hot
.hot_swap(a_test_tier(TierId::L2))
.expect("rebind with a rung-matching tier");
let read_after_swap = hot.get(&key).await.expect("get on the new inner");
assert_eq!(
read_after_swap, None,
"the old inner's value leaked through"
);
hot.set(&key, b"v2".to_vec(), None).await.expect("set v2");
assert_eq!(hot.get(&key).await.expect("get"), Some(b"v2".to_vec()));
}
#[tokio::test]
async fn concurrent_reads_survive_a_concurrent_swap() {
testkit::proves!("autonomy.rebind.rebind_preserves_capability_honesty");
let hot = Arc::new(
HotRungTier::<Vec<u8>>::new(TierId::L1, a_test_tier(TierId::L1)).expect("same rung"),
);
let done = Arc::new(AtomicUsize::new(0));
let readers = 8;
let mut handles = Vec::new();
for _ in 0..readers {
let hot = Arc::clone(&hot);
let done = Arc::clone(&done);
handles.push(tokio::spawn(async move {
for _ in 0..50 {
let key = KeyRef(b"race-key");
let r = hot.get(&key).await;
assert!(
r.is_ok(),
"a read must not fail because another task swapped"
);
let _ = hot.hot_swap(a_test_tier(TierId::L1));
done.fetch_add(1, Ordering::Relaxed);
}
}));
}
for handle in handles {
handle.await.expect("reader task");
}
assert_eq!(done.load(Ordering::Relaxed), readers * 50);
}
}