use crate::config::FlowControlConfig;
use crate::error::LimiteronError;
use crate::governor::Governor;
use crate::storage::{BanStorage, MemoryBanStorage, MemoryStorage, Storage};
use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use oxcache::integrations::kit::OxcacheModule;
use trait_kit::prelude::*;
#[derive(Default, Clone)]
pub struct LimiteronStorageConfig {
storage: Option<Arc<dyn Storage>>,
ban_storage: Option<Arc<dyn BanStorage>>,
}
impl LimiteronStorageConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_storage(mut self, storage: Arc<dyn Storage>) -> Self {
self.storage = Some(storage);
self
}
pub fn with_ban_storage(mut self, ban_storage: Arc<dyn BanStorage>) -> Self {
self.ban_storage = Some(ban_storage);
self
}
pub fn storage(&self) -> Option<Arc<dyn Storage>> {
self.storage.clone()
}
pub fn ban_storage(&self) -> Option<Arc<dyn BanStorage>> {
self.ban_storage.clone()
}
}
pub struct LimiteronModule;
impl ModuleMeta for LimiteronModule {
const NAME: &'static str = "limiteron";
fn dependencies() -> &'static [(&'static str, TypeId)] {
static DEPS: std::sync::OnceLock<Vec<(&'static str, TypeId)>> = std::sync::OnceLock::new();
DEPS.get_or_init(|| vec![("oxcache", TypeId::of::<OxcacheModule>())])
}
}
impl AsyncAutoBuilder for LimiteronModule {
type Capability = Arc<Governor>;
type Error = LimiteronError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>> {
Box::pin(async move {
let config: FlowControlConfig = kit.config().map_err(|e| {
LimiteronError::ConfigError(format!("LimiteronModule: read config: {e}"))
})?;
let storage_override: Option<LimiteronStorageConfig> = kit.config().ok();
let storage: Arc<dyn Storage> = storage_override
.as_ref()
.and_then(|o| o.storage())
.unwrap_or_else(|| Arc::new(MemoryStorage::new()) as Arc<dyn Storage>);
let ban_storage: Arc<dyn BanStorage> = storage_override
.as_ref()
.and_then(|o| o.ban_storage())
.unwrap_or_else(|| Arc::new(MemoryBanStorage::new()) as Arc<dyn BanStorage>);
if let Ok(cache) = kit.require::<OxcacheModule>() {
const PROBE_KEY: &str = "limiteron:kit:probe";
let payload: Arc<Vec<u8>> = Arc::new(b"limiteron-cache-probe".to_vec());
let probe_ok = async {
cache
.set(Arc::from(PROBE_KEY), payload.clone(), None)
.await
.ok()?;
let got = cache.get(PROBE_KEY).await.ok().flatten()?;
cache.delete(PROBE_KEY).await.ok()?;
(got.as_slice() == payload.as_slice()).then_some(())
}
.await;
if probe_ok.is_none() {
log::warn!(
"LimiteronModule: OxcacheModule cache probe failed; continuing with in-memory L1"
);
}
}
let governor = Governor::builder()
.with_config(config)
.with_storage(storage)
.with_ban_storage(ban_storage)
.build()
.await?;
Ok(Arc::new(governor))
})
}
}
impl trait_kit::core::health::AsyncHealthCheck for LimiteronModule {
fn check(cap: &Self::Capability) -> trait_kit::core::health::HealthStatus {
use trait_kit::core::health::HealthStatus;
let hs = futures::executor::block_on(cap.health_status());
if hs.healthy() {
HealthStatus::Healthy
} else {
let mut detail = Vec::new();
if !hs.storage_healthy {
detail.push("storage");
}
if !hs.ban_storage_healthy {
detail.push("ban-storage");
}
if !hs.cache_healthy {
detail.push("l1-cache");
}
if !hs.background_tasks_alive {
detail.push("background-tasks");
}
HealthStatus::unhealthy(format!("unhealthy components: {}", detail.join(",")))
}
}
}
impl trait_kit::core::lifecycle::AsyncLifecycle for LimiteronModule {
fn on_ready<'a>(
kit: &'a trait_kit::kit::AsyncKit<trait_kit::kit::async_kit::Ready>,
) -> Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send + 'a>> {
Box::pin(async move {
let _governor: Arc<Governor> = kit.require::<LimiteronModule>().map_err(|e| {
LimiteronError::ConfigError(format!("LimiteronModule on_ready: {e}"))
})?;
Ok(())
})
}
fn on_shutdown<'a>(cap: &'a Self::Capability) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async move {
if let Err(e) = cap.shutdown().await {
log::warn!("LimiteronModule shutdown error: {e}");
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::FlowControlConfig;
#[cfg(feature = "ban-manager")]
use crate::error::StorageError;
use crate::governor::Governor;
#[cfg(feature = "ban-manager")]
use crate::matchers::Identifier;
#[cfg(feature = "ban-manager")]
use crate::storage::{BanHistory, BanRecord, BanTarget};
use oxcache::integrations::kit::OxcacheConfig;
#[cfg(feature = "ban-manager")]
use std::any::Any;
use std::any::TypeId;
use std::sync::Arc;
#[cfg(feature = "ban-manager")]
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(feature = "ban-manager")]
struct RecordingBanStorage {
inner: MemoryBanStorage,
saves: AtomicUsize,
}
#[cfg(feature = "ban-manager")]
impl RecordingBanStorage {
fn new() -> Self {
Self {
inner: MemoryBanStorage::new(),
saves: AtomicUsize::new(0),
}
}
fn save_count(&self) -> usize {
self.saves.load(Ordering::SeqCst)
}
}
#[cfg(feature = "ban-manager")]
#[async_trait::async_trait]
impl BanStorage for RecordingBanStorage {
async fn is_banned(&self, target: &BanTarget) -> Result<Option<BanRecord>, StorageError> {
self.inner.is_banned(target).await
}
async fn save(&self, record: &BanRecord) -> Result<(), StorageError> {
self.saves.fetch_add(1, Ordering::SeqCst);
self.inner.save(record).await
}
async fn get_history(
&self,
target: &BanTarget,
) -> Result<Option<BanHistory>, StorageError> {
self.inner.get_history(target).await
}
async fn increment_ban_times(&self, target: &BanTarget) -> Result<u64, StorageError> {
self.inner.increment_ban_times(target).await
}
async fn get_ban_times(&self, target: &BanTarget) -> Result<u64, StorageError> {
self.inner.get_ban_times(target).await
}
async fn remove_ban(&self, target: &BanTarget) -> Result<(), StorageError> {
self.inner.remove_ban(target).await
}
async fn cleanup_expired_bans(&self) -> Result<u64, StorageError> {
self.inner.cleanup_expired_bans().await
}
async fn list_bans(
&self,
active_only: bool,
offset: u64,
limit: u64,
) -> Result<Vec<BanRecord>, StorageError> {
self.inner.list_bans(active_only, offset, limit).await
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn make_minimal_valid_config() -> FlowControlConfig {
use crate::config::{Action, ActionConfig, LimiterConfig, Matcher, Rule};
let mut config = FlowControlConfig::default();
config.rules.push(Rule {
id: "default".to_string(),
name: "default rule".to_string(),
priority: 0,
matchers: vec![Matcher::User {
user_ids: vec!["user1".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 100,
refill_rate: 10,
}],
action: ActionConfig {
on_exceed: Action::Reject,
ban: None,
},
});
config
}
#[test]
fn limiteron_module_meta_name() {
assert_eq!(LimiteronModule::NAME, "limiteron");
}
#[test]
fn limiteron_module_meta_dependencies_declare_oxcache() {
let deps = LimiteronModule::dependencies();
assert_eq!(deps.len(), 1, "single upstream dep: oxcache");
assert_eq!(deps[0].0, "oxcache");
assert_eq!(deps[0].1, TypeId::of::<OxcacheModule>());
}
fn register_modules(kit: &mut AsyncKit) {
kit.set_config(OxcacheConfig::default());
kit.register::<OxcacheModule>()
.expect("register OxcacheModule");
}
#[tokio::test]
async fn limiteron_module_build_returns_governor_capability() {
let mut kit = AsyncKit::new();
register_modules(&mut kit);
kit.set_config(make_minimal_valid_config());
kit.register::<LimiteronModule>()
.expect("register LimiteronModule");
let kit = kit.build().await.expect("AsyncKit::build");
let governor: Arc<Governor> = kit
.require::<LimiteronModule>()
.expect("require LimiteronModule");
let _ = governor;
}
#[tokio::test]
async fn limiteron_module_build_reads_config_from_kit() {
let mut kit = AsyncKit::new();
register_modules(&mut kit);
kit.set_config(make_minimal_valid_config());
kit.register::<LimiteronModule>()
.expect("register LimiteronModule");
let kit = kit.build().await.expect("AsyncKit::build");
let _governor: Arc<Governor> = kit
.require::<LimiteronModule>()
.expect("require LimiteronModule");
}
#[tokio::test]
async fn limiteron_module_build_is_async() {
let kit = AsyncKit::new();
kit.set_config(make_minimal_valid_config());
let fut = <LimiteronModule as AsyncAutoBuilder>::build(&kit);
let _governor: Arc<Governor> = fut.await.expect("build future resolves");
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Arc<Governor>>();
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn limiteron_module_routes_to_injected_ban_storage() {
let recording = Arc::new(RecordingBanStorage::new());
let recording_for_assert = recording.clone();
let mut kit = AsyncKit::new();
register_modules(&mut kit);
kit.set_config(make_minimal_valid_config());
kit.set_config(
LimiteronStorageConfig::new()
.with_ban_storage(recording.clone() as Arc<dyn BanStorage>),
);
kit.register::<LimiteronModule>()
.expect("register LimiteronModule");
let kit = kit.build().await.expect("AsyncKit::build");
let governor: Arc<Governor> = kit
.require::<LimiteronModule>()
.expect("require LimiteronModule");
let identifier = Identifier::UserId("injected-route-user".to_string());
governor
.ban_identifier(&identifier, "test ban", None)
.await
.expect("ban via injected storage");
assert!(
recording_for_assert.save_count() > 0,
"the injected BanStorage must be routed by Governor (save)"
);
governor
.unban_identifier(&identifier)
.await
.expect("unban via injected storage");
}
#[cfg(feature = "ban-manager")]
#[tokio::test]
async fn limiteron_module_defaults_to_memory_storage() {
let mut kit = AsyncKit::new();
register_modules(&mut kit);
kit.set_config(make_minimal_valid_config());
kit.register::<LimiteronModule>()
.expect("register LimiteronModule");
let kit = kit.build().await.expect("AsyncKit::build");
let governor: Arc<Governor> = kit
.require::<LimiteronModule>()
.expect("require LimiteronModule");
let identifier = Identifier::UserId("default-memory-user".to_string());
governor
.ban_identifier(&identifier, "test ban", None)
.await
.expect("ban with default MemoryBanStorage");
}
#[test]
fn limiteron_storage_config_builders_roundtrip() {
let storage: Arc<dyn Storage> = Arc::new(MemoryStorage::new());
let ban_storage: Arc<dyn BanStorage> = Arc::new(MemoryBanStorage::new());
let config = LimiteronStorageConfig::new()
.with_storage(storage.clone())
.with_ban_storage(ban_storage.clone());
assert!(config.storage().is_some());
assert!(config.ban_storage().is_some());
let empty = LimiteronStorageConfig::new();
assert!(empty.storage().is_none());
assert!(empty.ban_storage().is_none());
}
#[tokio::test]
async fn t617_health_check_reports_healthy() {
use trait_kit::core::health::AsyncHealthCheck;
let mut kit = AsyncKit::new();
register_modules(&mut kit);
kit.set_config(make_minimal_valid_config());
kit.register::<LimiteronModule>().unwrap();
let kit = kit.build().await.unwrap();
let governor: Arc<Governor> = kit.require::<LimiteronModule>().unwrap();
let status = <LimiteronModule as AsyncHealthCheck>::check(&governor);
assert!(
matches!(status, trait_kit::core::health::HealthStatus::Healthy),
"the in-memory storage Governor should be Healthy, got: {status:?}"
);
}
#[tokio::test]
async fn t617_lifecycle_on_shutdown_stops_governor() {
use trait_kit::core::lifecycle::AsyncLifecycle;
let mut kit = AsyncKit::new();
register_modules(&mut kit);
kit.set_config(make_minimal_valid_config());
kit.register::<LimiteronModule>().unwrap();
let kit = kit.build().await.unwrap();
let governor: Arc<Governor> = kit.require::<LimiteronModule>().unwrap();
assert!(!governor.is_shutdown());
<LimiteronModule as AsyncLifecycle>::on_shutdown(&governor).await;
assert!(
governor.is_shutdown(),
"on_shutdown must stop the Governor (background task exit)"
);
}
#[tokio::test]
async fn t617_cache_probe_via_oxcache_module() {
let mut kit = AsyncKit::new();
register_modules(&mut kit);
kit.set_config(make_minimal_valid_config());
kit.register::<LimiteronModule>().unwrap();
let kit = kit.build().await.unwrap();
let cache = kit
.require::<OxcacheModule>()
.expect("OxcacheModule capability");
let health = cache.health_check().await;
assert!(
health.is_ok(),
"the injected cache backend should be healthy"
);
}
}