feat: 引入 aether-runtime/cache/data/http/testkit 基础 crate,完善并发门控与审计系统

新增 crate:
- aether-runtime: 服务运行时基础设施(并发门控、分布式并发、指标、队列、优雅关闭、tracing)
- aether-cache: 通用 TTL 缓存与命名空间抽象
- aether-data: 数据访问层(PostgreSQL/Redis 后端、repository 模式)
- aether-http: HTTP 客户端封装(重试、配置)
- aether-testkit: 集成测试工具集(gateway/executor/hub/proxy fixture、等待、负载测试)

gateway 扩展:
- 引入 audit 模块(shadow 执行审计、决策链路追踪、请求审计 bundle)
- 引入 cache 模块(AuthContext 缓存、direct-plan bypass 缓存)
- 引入 data 模块(auth/candidates/config/usage/video_tasks 数据访问)
- 集成 ConcurrencyGate/DistributedConcurrencyGate 请求门控
- 新增本地 auth 拒绝、过载响应构建器
- 补充 control/auth_cache/video/concurrency 集成测试

aether-proxy 扩展:
- AppState 集成 stream_gate / distributed_stream_gate 并发门控
- 新增 ProxyAdmissionError 及准入拒绝流程
- stream_handler 补充门控饱和/不可用场景测试
- 配置与注册客户端逻辑完善

aether-hub 扩展:
- main.rs 引入运行时初始化、指标端点、健康检查
- local_relay 重构为 lib.rs 暴露公共接口
This commit is contained in:
fawney19
2026-03-24 15:12:56 +08:00
parent eaf8475f9e
commit b5a0070023
157 changed files with 22097 additions and 448 deletions

View File

@@ -0,0 +1,9 @@
[package]
name = "aether-cache"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Shared in-memory cache primitives for Aether Rust services"
[dependencies]

View File

@@ -0,0 +1,5 @@
mod namespace;
mod ttl_map;
pub use namespace::CacheKeyNamespace;
pub use ttl_map::ExpiringMap;

View File

@@ -0,0 +1,51 @@
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKeyNamespace {
prefix: String,
}
impl CacheKeyNamespace {
pub fn new(prefix: impl Into<String>) -> Self {
Self {
prefix: prefix.into(),
}
}
pub fn child(&self, suffix: &str) -> Self {
if self.prefix.is_empty() {
return Self::new(suffix);
}
if suffix.is_empty() {
return self.clone();
}
Self::new(format!("{}:{}", self.prefix, suffix))
}
pub fn key(&self, raw_key: &str) -> String {
if self.prefix.is_empty() {
return raw_key.to_string();
}
if raw_key.is_empty() {
return self.prefix.clone();
}
format!("{}:{}", self.prefix, raw_key)
}
pub fn prefix(&self) -> &str {
&self.prefix
}
}
#[cfg(test)]
mod tests {
use super::CacheKeyNamespace;
#[test]
fn composes_scoped_keys() {
let root = CacheKeyNamespace::new("aether");
let child = root.child("auth");
assert_eq!(root.key("user-1"), "aether:user-1");
assert_eq!(child.key("user-1"), "aether:auth:user-1");
assert_eq!(child.prefix(), "aether:auth");
}
}

View File

@@ -0,0 +1,175 @@
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
struct TimedEntry<V> {
value: V,
inserted_at: Instant,
}
#[derive(Debug)]
pub struct ExpiringMap<K, V> {
entries: Mutex<HashMap<K, TimedEntry<V>>>,
}
impl<K, V> Default for ExpiringMap<K, V> {
fn default() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
}
}
}
impl<K, V> ExpiringMap<K, V>
where
K: Eq + Hash + Clone,
{
pub fn new() -> Self {
Self::default()
}
pub fn insert(&self, key: K, value: V, ttl: Duration, max_entries: usize) {
let Ok(mut entries) = self.entries.lock() else {
return;
};
prune_expired(&mut entries, ttl);
while max_entries > 0 && entries.len() >= max_entries {
let Some(oldest_key) = entries
.iter()
.min_by_key(|(_, entry)| entry.inserted_at)
.map(|(key, _)| key.clone())
else {
break;
};
entries.remove(&oldest_key);
}
entries.insert(
key,
TimedEntry {
value,
inserted_at: Instant::now(),
},
);
}
pub fn remove(&self, key: &K) -> Option<V> {
let Ok(mut entries) = self.entries.lock() else {
return None;
};
entries.remove(key).map(|entry| entry.value)
}
pub fn len(&self) -> usize {
self.entries
.lock()
.map(|entries| entries.len())
.unwrap_or(0)
}
}
impl<K, V> ExpiringMap<K, V>
where
K: Eq + Hash + Clone,
V: Clone,
{
pub fn get_fresh(&self, key: &K, ttl: Duration) -> Option<V> {
let Ok(mut entries) = self.entries.lock() else {
return None;
};
let Some(entry) = entries.get(key).cloned() else {
return None;
};
if entry.inserted_at.elapsed() > ttl {
entries.remove(key);
return None;
}
Some(entry.value)
}
pub fn contains_fresh(&self, key: &K, ttl: Duration) -> bool {
self.get_fresh(key, ttl).is_some()
}
}
fn prune_expired<K, V>(entries: &mut HashMap<K, TimedEntry<V>>, ttl: Duration)
where
K: Eq + Hash,
{
if ttl.is_zero() {
entries.clear();
return;
}
entries.retain(|_, entry| entry.inserted_at.elapsed() <= ttl);
}
#[cfg(test)]
mod tests {
use std::thread::sleep;
use super::ExpiringMap;
#[test]
fn evicts_expired_entries_on_read() {
let cache = ExpiringMap::new();
cache.insert(
"hello".to_string(),
42_u32,
std::time::Duration::from_millis(10),
16,
);
sleep(std::time::Duration::from_millis(20));
assert_eq!(
cache.get_fresh(&"hello".to_string(), std::time::Duration::from_millis(10)),
None
);
assert_eq!(cache.len(), 0);
}
#[test]
fn evicts_oldest_entry_when_capacity_is_hit() {
let cache = ExpiringMap::new();
cache.insert(
"one".to_string(),
1_u32,
std::time::Duration::from_secs(60),
2,
);
sleep(std::time::Duration::from_millis(2));
cache.insert(
"two".to_string(),
2_u32,
std::time::Duration::from_secs(60),
2,
);
sleep(std::time::Duration::from_millis(2));
cache.insert(
"three".to_string(),
3_u32,
std::time::Duration::from_secs(60),
2,
);
assert_eq!(
cache.get_fresh(&"one".to_string(), std::time::Duration::from_secs(60)),
None
);
assert_eq!(
cache.get_fresh(&"two".to_string(), std::time::Duration::from_secs(60)),
Some(2)
);
assert_eq!(
cache.get_fresh(&"three".to_string(), std::time::Duration::from_secs(60)),
Some(3)
);
}
}