mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor: extract runtime state backends
This commit is contained in:
@@ -2,6 +2,15 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn production_workspace_source(path: &Path) -> String {
|
||||
let source = std::fs::read_to_string(path).expect("source file should be readable");
|
||||
source
|
||||
.split("#[cfg(test)]")
|
||||
.next()
|
||||
.unwrap_or(&source)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_small_runtime_shims_stay_deleted() {
|
||||
for path in [
|
||||
@@ -79,6 +88,120 @@ fn gateway_small_runtime_shims_stay_deleted() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_state_owns_redis_runtime_boundaries() {
|
||||
let forbidden_business_patterns = [
|
||||
"use redis::",
|
||||
"redis::cmd",
|
||||
"::redis::cmd",
|
||||
"::redis::Script",
|
||||
"aether_data::driver::redis",
|
||||
"RedisKvRunner",
|
||||
"RedisLockRunner",
|
||||
"RedisStreamRunner",
|
||||
"redis_kv_runner(",
|
||||
];
|
||||
let mut violations = Vec::new();
|
||||
for root in [
|
||||
"apps/aether-gateway/src",
|
||||
"crates/aether-runtime/src",
|
||||
"crates/aether-usage-runtime/src",
|
||||
"crates/aether-provider-transport/src",
|
||||
] {
|
||||
for path in collect_workspace_rust_files(root) {
|
||||
if path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == "tests")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let source = production_workspace_source(&path);
|
||||
let hits = forbidden_business_patterns
|
||||
.iter()
|
||||
.filter(|pattern| source.contains(**pattern))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
if !hits.is_empty() {
|
||||
violations.push(format!("{} -> {}", path.display(), hits.join(", ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"business/runtime crates must use aether-runtime-state instead of Redis directly:\n{}",
|
||||
violations.join("\n")
|
||||
);
|
||||
|
||||
let mut runtime_state_violations = Vec::new();
|
||||
for path in collect_workspace_rust_files("crates/aether-runtime-state/src") {
|
||||
if path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == "redis")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let source = production_workspace_source(&path);
|
||||
let hits = [
|
||||
"use redis::",
|
||||
"redis::cmd",
|
||||
"::redis::cmd",
|
||||
"::redis::Script",
|
||||
]
|
||||
.iter()
|
||||
.filter(|pattern| source.contains(**pattern))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
if !hits.is_empty() {
|
||||
runtime_state_violations.push(format!("{} -> {}", path.display(), hits.join(", ")));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
runtime_state_violations.is_empty(),
|
||||
"only crates/aether-runtime-state/src/redis may depend on the redis crate directly:\n{}",
|
||||
runtime_state_violations.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aether_data_stays_free_of_redis_runtime_backends() {
|
||||
let cargo = read_workspace_file("crates/aether-data/Cargo.toml");
|
||||
assert!(
|
||||
!cargo.contains("redis.workspace"),
|
||||
"aether-data should not depend on redis; runtime Redis belongs to aether-runtime-state"
|
||||
);
|
||||
|
||||
for removed_path in [
|
||||
"crates/aether-data/src/backend/redis.rs",
|
||||
"crates/aether-data/src/backend/locks.rs",
|
||||
"crates/aether-data/src/backend/workers.rs",
|
||||
"crates/aether-data/src/driver/redis/mod.rs",
|
||||
] {
|
||||
assert!(
|
||||
!workspace_file_exists(removed_path),
|
||||
"{removed_path} should stay removed from aether-data"
|
||||
);
|
||||
}
|
||||
|
||||
for path in collect_workspace_rust_files("crates/aether-data/src") {
|
||||
let source = production_workspace_source(&path);
|
||||
for forbidden in [
|
||||
"pub mod redis",
|
||||
"driver::redis",
|
||||
"RedisBackend",
|
||||
"DataLockBackends",
|
||||
"DataWorkerBackends",
|
||||
"redis::cmd",
|
||||
"use redis::",
|
||||
] {
|
||||
assert!(
|
||||
!source.contains(forbidden),
|
||||
"{} should not keep Redis runtime backend surface {forbidden}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_request_candidate_trace_type_is_owned_by_aether_data_contracts() {
|
||||
let gateway_candidates = read_workspace_file("apps/aether-gateway/src/data/candidates.rs");
|
||||
|
||||
@@ -17,9 +17,18 @@ use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelect
|
||||
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data::repository::usage::InMemoryUsageReadRepository;
|
||||
use aether_runtime_state::{
|
||||
MemoryRuntimeStateConfig, RuntimeSemaphore, RuntimeSemaphoreConfig, RuntimeState,
|
||||
};
|
||||
|
||||
use crate::data::GatewayDataState;
|
||||
|
||||
fn memory_runtime_semaphore(gate: &'static str, limit: usize) -> RuntimeSemaphore {
|
||||
RuntimeState::memory(MemoryRuntimeStateConfig::default())
|
||||
.semaphore(gate, limit, RuntimeSemaphoreConfig::default())
|
||||
.expect("memory runtime semaphore should build")
|
||||
}
|
||||
|
||||
fn sample_decision() -> crate::control::GatewayControlDecision {
|
||||
crate::control::GatewayControlDecision {
|
||||
public_path: "/v1/chat/completions".to_string(),
|
||||
@@ -104,10 +113,7 @@ async fn gateway_rejects_second_in_flight_stream_request_with_distributed_overlo
|
||||
);
|
||||
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let distributed_gate = aether_runtime::DistributedConcurrencyGate::new_in_memory(
|
||||
"gateway_requests_distributed",
|
||||
1,
|
||||
);
|
||||
let distributed_gate = memory_runtime_semaphore("gateway_requests_distributed", 1);
|
||||
let gateway_a = build_router_with_state(
|
||||
build_local_openai_gateway_state(execution_runtime_url.clone())
|
||||
.with_distributed_request_concurrency_gate(distributed_gate.clone()),
|
||||
@@ -262,12 +268,10 @@ async fn gateway_exposes_request_concurrency_metrics() {
|
||||
AppState::new()
|
||||
.expect("gateway state should build")
|
||||
.with_request_concurrency_limit(3)
|
||||
.with_distributed_request_concurrency_gate(
|
||||
aether_runtime::DistributedConcurrencyGate::new_in_memory(
|
||||
"gateway_requests_distributed",
|
||||
5,
|
||||
),
|
||||
),
|
||||
.with_distributed_request_concurrency_gate(memory_runtime_semaphore(
|
||||
"gateway_requests_distributed",
|
||||
5,
|
||||
)),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ async fn gateway_clears_admin_external_models_cache_locally_with_trusted_admin_p
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["cleared"], false);
|
||||
assert_eq!(payload["message"], "Redis 未启用");
|
||||
assert_eq!(payload["message"], "缓存不存在");
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
|
||||
@@ -1307,9 +1307,11 @@ async fn gateway_handles_admin_monitoring_cache_redis_keys_delete_locally_with_t
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["detail"], json!("Redis 未启用"));
|
||||
assert_eq!(payload["status"], json!("ok"));
|
||||
assert_eq!(payload["category"], json!("upstream_models"));
|
||||
assert_eq!(payload["deleted_count"], json!(0));
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
@@ -1485,11 +1487,9 @@ async fn gateway_handles_admin_monitoring_model_mapping_stats_locally_with_trust
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["status"], json!("ok"));
|
||||
assert_eq!(payload["data"]["available"], json!(false));
|
||||
assert_eq!(
|
||||
payload["data"]["message"],
|
||||
json!("Redis 未启用,模型映射缓存不可用")
|
||||
);
|
||||
assert_eq!(payload["data"]["available"], json!(true));
|
||||
assert_eq!(payload["data"]["backend"], json!("memory"));
|
||||
assert_eq!(payload["data"]["total_keys"], json!(0));
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
@@ -1707,8 +1707,9 @@ async fn gateway_handles_admin_monitoring_redis_keys_locally_with_trusted_admin_
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["status"], json!("ok"));
|
||||
assert_eq!(payload["data"]["available"], json!(false));
|
||||
assert_eq!(payload["data"]["message"], json!("Redis 未启用"));
|
||||
assert_eq!(payload["data"]["available"], json!(true));
|
||||
assert_eq!(payload["data"]["backend"], json!("memory"));
|
||||
assert_eq!(payload["data"]["total_keys"], json!(0));
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
|
||||
@@ -1118,9 +1118,16 @@ async fn gateway_handles_admin_provider_oauth_start_key_locally_with_trusted_adm
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["detail"], "provider oauth redis unavailable");
|
||||
assert_eq!(payload["provider_type"], "codex");
|
||||
assert_eq!(
|
||||
payload["redirect_uri"],
|
||||
"http://localhost:1455/auth/callback"
|
||||
);
|
||||
assert!(payload["authorization_url"]
|
||||
.as_str()
|
||||
.is_some_and(|url| url.contains("state=")));
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
@@ -1174,9 +1181,16 @@ async fn gateway_handles_admin_provider_oauth_start_provider_locally_with_truste
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["detail"], "provider oauth redis unavailable");
|
||||
assert_eq!(payload["provider_type"], "codex");
|
||||
assert_eq!(
|
||||
payload["redirect_uri"],
|
||||
"http://localhost:1455/auth/callback"
|
||||
);
|
||||
assert!(payload["authorization_url"]
|
||||
.as_str()
|
||||
.is_some_and(|url| url.contains("state=")));
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
|
||||
@@ -9,6 +9,7 @@ use aether_crypto::{
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data::repository::proxy_nodes::InMemoryProxyNodeRepository;
|
||||
use aether_data_contracts::repository::provider_catalog::ProviderCatalogReadRepository;
|
||||
use aether_runtime_state::{RedisClientConfig, RuntimeState};
|
||||
use aether_testkit::ManagedRedisServer;
|
||||
use axum::body::to_bytes;
|
||||
use axum::body::Body;
|
||||
@@ -42,6 +43,23 @@ async fn start_managed_redis_or_skip() -> Option<ManagedRedisServer> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn redis_runtime_state_for_test(
|
||||
redis: &ManagedRedisServer,
|
||||
key_prefix: &str,
|
||||
) -> Arc<RuntimeState> {
|
||||
Arc::new(
|
||||
RuntimeState::redis(
|
||||
RedisClientConfig {
|
||||
url: redis.redis_url().to_string(),
|
||||
key_prefix: Some(key_prefix.to_string()),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("redis runtime state should build"),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_provider_ops_architectures_locally_with_trusted_admin_principal() {
|
||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||
@@ -3529,16 +3547,16 @@ async fn gateway_handles_admin_provider_ops_balance_cache_refresh_modes_with_red
|
||||
vec![],
|
||||
));
|
||||
let data_state = GatewayDataState::from_config(
|
||||
GatewayDataConfig::disabled()
|
||||
.with_redis_url(redis.redis_url(), Some("provider_ops_balance_cache"))
|
||||
.with_encryption_key(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
GatewayDataConfig::disabled().with_encryption_key(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
)
|
||||
.expect("data state should build")
|
||||
.attach_provider_catalog_repository_for_tests(Arc::clone(&provider_catalog_repository));
|
||||
let runtime_state = redis_runtime_state_for_test(&redis, "provider_ops_balance_cache").await;
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state),
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_runtime_state(runtime_state),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
let client = reqwest::Client::new();
|
||||
@@ -3683,19 +3701,17 @@ async fn gateway_handles_admin_provider_ops_balance_cache_miss_without_refresh_r
|
||||
vec![],
|
||||
));
|
||||
let data_state = GatewayDataState::from_config(
|
||||
GatewayDataConfig::disabled()
|
||||
.with_redis_url(
|
||||
redis.redis_url(),
|
||||
Some("provider_ops_balance_cache_sync_miss"),
|
||||
)
|
||||
.with_encryption_key(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
GatewayDataConfig::disabled().with_encryption_key(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
)
|
||||
.expect("data state should build")
|
||||
.attach_provider_catalog_repository_for_tests(Arc::clone(&provider_catalog_repository));
|
||||
let runtime_state =
|
||||
redis_runtime_state_for_test(&redis, "provider_ops_balance_cache_sync_miss").await;
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state),
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_runtime_state(runtime_state),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
let client = reqwest::Client::new();
|
||||
@@ -3841,19 +3857,17 @@ async fn gateway_clears_admin_provider_ops_balance_cache_after_config_save_with_
|
||||
vec![],
|
||||
));
|
||||
let data_state = GatewayDataState::from_config(
|
||||
GatewayDataConfig::disabled()
|
||||
.with_redis_url(
|
||||
redis.redis_url(),
|
||||
Some("provider_ops_balance_cache_config_save"),
|
||||
)
|
||||
.with_encryption_key(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
GatewayDataConfig::disabled().with_encryption_key(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
)
|
||||
.expect("data state should build")
|
||||
.attach_provider_catalog_repository_for_tests(Arc::clone(&provider_catalog_repository));
|
||||
let runtime_state =
|
||||
redis_runtime_state_for_test(&redis, "provider_ops_balance_cache_config_save").await;
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state),
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_runtime_state(runtime_state),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
let client = reqwest::Client::new();
|
||||
@@ -4058,19 +4072,17 @@ async fn gateway_verify_does_not_pollute_balance_cache_and_balance_uses_saved_ac
|
||||
vec![],
|
||||
));
|
||||
let data_state = GatewayDataState::from_config(
|
||||
GatewayDataConfig::disabled()
|
||||
.with_redis_url(
|
||||
redis.redis_url(),
|
||||
Some("provider_ops_verify_cache_isolation"),
|
||||
)
|
||||
.with_encryption_key(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
GatewayDataConfig::disabled().with_encryption_key(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
)
|
||||
.expect("data state should build")
|
||||
.attach_provider_catalog_repository_for_tests(Arc::clone(&provider_catalog_repository));
|
||||
let runtime_state =
|
||||
redis_runtime_state_for_test(&redis, "provider_ops_verify_cache_isolation").await;
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state),
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_runtime_state(runtime_state),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
let client = reqwest::Client::new();
|
||||
@@ -4232,16 +4244,16 @@ async fn gateway_handles_admin_provider_ops_batch_balance_with_pending_cache_hit
|
||||
vec![],
|
||||
));
|
||||
let data_state = GatewayDataState::from_config(
|
||||
GatewayDataConfig::disabled()
|
||||
.with_redis_url(redis.redis_url(), Some("provider_ops_batch_balance"))
|
||||
.with_encryption_key(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
GatewayDataConfig::disabled().with_encryption_key(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
)
|
||||
.expect("data state should build")
|
||||
.attach_provider_catalog_repository_for_tests(Arc::clone(&provider_catalog_repository));
|
||||
let runtime_state = redis_runtime_state_for_test(&redis, "provider_ops_batch_balance").await;
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state),
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_runtime_state(runtime_state),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
Reference in New Issue
Block a user