use std::sync::Arc;
use std::time::{Duration, Instant};
use dashmap::{DashMap, Entry};
use super::provider::PermissionProvider;
use super::types::RolePolicy;
const DEFAULT_TTL: Duration = Duration::from_secs(300);
const DEFAULT_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[derive(Clone, Debug)]
struct CacheEntry {
value: RolePolicy,
inserted_at: Instant,
}
#[derive(Clone, Debug)]
pub struct PermissionCacheConfig {
pub ttl: Duration,
pub refresh_interval: Duration,
pub stale_while_revalidate: bool,
}
impl Default for PermissionCacheConfig {
fn default() -> Self {
Self {
ttl: DEFAULT_TTL,
refresh_interval: DEFAULT_REFRESH_INTERVAL,
stale_while_revalidate: true,
}
}
}
pub struct PermissionCache {
inner: Arc<DashMap<String, CacheEntry>>,
config: PermissionCacheConfig,
provider: Option<Arc<dyn PermissionProvider>>,
last_refresh: Arc<DashMap<String, Instant>>,
}
impl std::fmt::Debug for PermissionCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PermissionCache")
.field("entry_count", &self.inner.len())
.field("config", &self.config)
.field("has_provider", &self.provider.is_some())
.finish_non_exhaustive()
}
}
impl Default for PermissionCache {
fn default() -> Self {
Self::new()
}
}
fn try_reserve_refresh(
last_refresh: &DashMap<String, Instant>,
key: &str,
interval: Duration,
) -> bool {
match last_refresh.entry(key.to_string()) {
Entry::Occupied(mut occupied) => {
if occupied.get().elapsed() < interval {
return false;
}
*occupied.get_mut() = Instant::now();
true
}
Entry::Vacant(vacant) => {
vacant.insert(Instant::now());
true
}
}
}
impl PermissionCache {
pub fn new() -> Self {
Self {
inner: Arc::new(DashMap::new()),
config: PermissionCacheConfig::default(),
provider: None,
last_refresh: Arc::new(DashMap::new()),
}
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.config.ttl = ttl;
self
}
pub fn with_refresh_interval(mut self, interval: Duration) -> Self {
self.config.refresh_interval = interval;
self
}
pub fn with_stale_while_revalidate(mut self, enabled: bool) -> Self {
self.config.stale_while_revalidate = enabled;
self
}
pub fn with_provider(mut self, provider: Arc<dyn PermissionProvider>) -> Self {
self.provider = Some(provider);
self
}
pub fn config(&self) -> &PermissionCacheConfig {
&self.config
}
pub fn insert(&self, key: &str, value: RolePolicy) {
let entry = CacheEntry {
value,
inserted_at: Instant::now(),
};
self.inner.insert(key.to_string(), entry);
}
pub fn invalidate(&self, key: &str) {
self.inner.remove(key);
self.last_refresh.remove(key);
}
pub fn clear(&self) {
self.inner.clear();
self.last_refresh.clear();
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn get(&self, key: &str) -> Option<RolePolicy> {
if let Some(entry) = self.inner.get(key) {
let elapsed = entry.inserted_at.elapsed();
if elapsed < self.config.ttl {
return Some(entry.value.clone());
}
self.maybe_spawn_refresh(key);
if self.config.stale_while_revalidate {
Some(entry.value.clone())
} else {
None
}
} else {
None
}
}
pub fn is_expired(&self, key: &str) -> bool {
if let Some(entry) = self.inner.get(key) {
entry.inserted_at.elapsed() >= self.config.ttl
} else {
true
}
}
pub async fn refresh(&self, key: &str) {
if !try_reserve_refresh(&self.last_refresh, key, self.config.refresh_interval) {
return;
}
let provider = match &self.provider {
Some(p) => p.clone(),
None => {
return;
}
};
let key_owned = key.to_string();
match provider.get_role_policy(&key_owned) {
Some(new_policy) => {
self.insert(&key_owned, new_policy);
}
None => {
}
}
}
fn maybe_spawn_refresh(&self, key: &str) {
if self.provider.is_none() {
return;
}
if !try_reserve_refresh(&self.last_refresh, key, self.config.refresh_interval) {
return;
}
let key_owned = key.to_string();
let provider = self.provider.clone().unwrap();
let inner = self.inner.clone();
tokio::spawn(async move {
match provider.get_role_policy(&key_owned) {
Some(new_policy) => {
let entry = CacheEntry {
value: new_policy,
inserted_at: Instant::now(),
};
inner.insert(key_owned, entry);
}
None => {
}
}
});
}
}
impl Clone for PermissionCache {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
config: self.config.clone(),
provider: self.provider.clone(),
last_refresh: self.last_refresh.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::access::permission::PermissionProviderError;
use crate::access::{PermissionAction, TablePermission};
fn sample_policy(table: &str) -> RolePolicy {
RolePolicy {
tables: vec![TablePermission {
name: table.to_string(),
operations: vec![PermissionAction::Select],
}],
}
}
#[tokio::test]
async fn test_insert_and_get_fresh() {
let cache = PermissionCache::new().with_ttl(Duration::from_secs(60));
cache.insert("admin", sample_policy("users"));
let got = cache.get("admin");
assert!(got.is_some());
assert_eq!(got.unwrap().tables.len(), 1);
}
#[tokio::test]
async fn test_get_missing_returns_none() {
let cache = PermissionCache::new();
assert!(cache.get("ghost").is_none());
}
#[tokio::test]
async fn test_expired_without_swr_returns_none() {
let cache = PermissionCache::new()
.with_ttl(Duration::from_millis(1))
.with_stale_while_revalidate(false);
cache.insert("admin", sample_policy("users"));
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(cache.get("admin").is_none());
}
#[tokio::test]
async fn test_expired_with_swr_returns_stale() {
let cache = PermissionCache::new()
.with_ttl(Duration::from_millis(1))
.with_stale_while_revalidate(true);
cache.insert("admin", sample_policy("users"));
tokio::time::sleep(Duration::from_millis(20)).await;
let got = cache.get("admin");
assert!(got.is_some(), "SWR should return stale value");
}
#[tokio::test]
async fn test_is_expired() {
let cache = PermissionCache::new().with_ttl(Duration::from_millis(10));
cache.insert("admin", sample_policy("users"));
assert!(!cache.is_expired("admin"));
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(cache.is_expired("admin"));
assert!(cache.is_expired("ghost"));
}
#[tokio::test]
async fn test_invalidate_and_clear() {
let cache = PermissionCache::new();
cache.insert("a", sample_policy("t1"));
cache.insert("b", sample_policy("t2"));
assert_eq!(cache.len(), 2);
cache.invalidate("a");
assert_eq!(cache.len(), 1);
assert!(cache.get("a").is_none());
cache.clear();
assert!(cache.is_empty());
}
struct CountingProvider {
calls: AtomicUsize,
delay: Duration,
}
impl CountingProvider {
fn new() -> Self {
Self::with_delay(Duration::from_millis(50))
}
fn with_delay(delay: Duration) -> Self {
Self {
calls: AtomicUsize::new(0),
delay,
}
}
}
impl PermissionProvider for CountingProvider {
fn get_role_policy(&self, _role: &str) -> Option<RolePolicy> {
self.calls.fetch_add(1, Ordering::SeqCst);
std::thread::sleep(self.delay);
Some(sample_policy("users"))
}
fn check_access(
&self,
role: &str,
table: &str,
operation: PermissionAction,
) -> Result<bool, PermissionProviderError> {
Ok(self.get_role_policy(role).is_some_and(|p| {
p.tables
.iter()
.any(|t| t.name == table && t.operations.contains(&operation))
}))
}
fn get_roles(&self) -> Vec<String> {
vec!["admin".to_string()]
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_concurrent_refresh_single_provider_call() {
let provider = Arc::new(CountingProvider::new());
let cache = PermissionCache::new()
.with_ttl(Duration::from_millis(1))
.with_refresh_interval(Duration::from_secs(60))
.with_provider(provider.clone());
cache.insert("admin", sample_policy("users"));
tokio::time::sleep(Duration::from_millis(20)).await;
let mut handles = Vec::new();
for _ in 0..8 {
let cache = cache.clone();
handles.push(tokio::spawn(async move {
cache.refresh("admin").await;
}));
}
for h in handles {
h.await.unwrap();
}
assert_eq!(provider.calls.load(Ordering::SeqCst), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_concurrent_get_single_background_refresh() {
let provider = Arc::new(CountingProvider::new());
let cache = PermissionCache::new()
.with_ttl(Duration::from_millis(1))
.with_refresh_interval(Duration::from_secs(60))
.with_stale_while_revalidate(true)
.with_provider(provider.clone());
cache.insert("admin", sample_policy("users"));
tokio::time::sleep(Duration::from_millis(20)).await;
let mut handles = Vec::new();
for _ in 0..8 {
let cache = cache.clone();
handles.push(tokio::spawn(async move {
let _ = cache.get("admin");
}));
}
for h in handles {
h.await.unwrap();
}
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(provider.calls.load(Ordering::SeqCst), 1);
}
const RACE_TASKS: usize = 16;
const RACE_ROUNDS: usize = 24;
async fn race_start(barrier: &tokio::sync::Barrier, latch: &AtomicUsize) {
barrier.wait().await;
latch.fetch_sub(1, Ordering::AcqRel);
while latch.load(Ordering::Acquire) != 0 {
for _ in 0..4096 {
std::hint::spin_loop();
if latch.load(Ordering::Acquire) == 0 {
return;
}
}
tokio::task::yield_now().await;
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 16)]
async fn test_barrier_refresh_racers_single_provider_call() {
let provider = Arc::new(CountingProvider::with_delay(Duration::from_millis(20)));
let cache = PermissionCache::new()
.with_refresh_interval(Duration::from_secs(60))
.with_provider(provider.clone());
let barrier = Arc::new(tokio::sync::Barrier::new(RACE_TASKS));
for _ in 0..RACE_ROUNDS {
cache.invalidate("admin");
let latch = Arc::new(AtomicUsize::new(RACE_TASKS));
let mut handles = Vec::with_capacity(RACE_TASKS);
for _ in 0..RACE_TASKS {
let cache = cache.clone();
let barrier = barrier.clone();
let latch = latch.clone();
handles.push(tokio::spawn(async move {
race_start(&barrier, &latch).await;
cache.refresh("admin").await;
}));
}
for h in handles {
h.await.unwrap();
}
}
assert_eq!(
provider.calls.load(Ordering::SeqCst),
RACE_ROUNDS,
"锁步放行后每轮并发 refresh 仍只允许 1 次 provider 调用"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 16)]
async fn test_barrier_mixed_refresh_and_get_single_provider_call() {
let provider = Arc::new(CountingProvider::with_delay(Duration::from_millis(20)));
let cache = PermissionCache::new()
.with_ttl(Duration::from_millis(1))
.with_refresh_interval(Duration::from_secs(60))
.with_stale_while_revalidate(true)
.with_provider(provider.clone());
let barrier = Arc::new(tokio::sync::Barrier::new(RACE_TASKS));
for _ in 0..RACE_ROUNDS {
cache.invalidate("admin");
cache.insert("admin", sample_policy("users"));
tokio::time::sleep(Duration::from_millis(3)).await;
let latch = Arc::new(AtomicUsize::new(RACE_TASKS));
let mut handles = Vec::with_capacity(RACE_TASKS);
for i in 0..RACE_TASKS {
let cache = cache.clone();
let barrier = barrier.clone();
let latch = latch.clone();
handles.push(tokio::spawn(async move {
race_start(&barrier, &latch).await;
if i % 2 == 0 {
cache.refresh("admin").await;
} else {
let _ = cache.get("admin");
}
}));
}
for h in handles {
h.await.unwrap();
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
provider.calls.load(Ordering::SeqCst),
RACE_ROUNDS,
"refresh 与 get 两条路径并发时每轮仍只允许 1 次 provider 调用"
);
}
}