mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 10:57:03 +08:00
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:
@@ -0,0 +1,373 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::Infallible;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use aether_contracts::{ExecutionPlan, ExecutionTimeouts, RequestBody};
|
||||
use aether_testkit::{
|
||||
init_test_runtime_for, run_http_load_probe, ExecutorHarness, ExecutorHarnessConfig,
|
||||
GatewayHarness, GatewayHarnessConfig, HttpLoadProbeConfig, HttpLoadProbeResponseMode,
|
||||
HttpLoadProbeResult, SpawnedServer,
|
||||
};
|
||||
use axum::body::{to_bytes, Body, Bytes};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::any;
|
||||
use axum::{extract::Request, Json, Router};
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SingleInstanceBaselineConfig {
|
||||
sync_requests: usize,
|
||||
sync_concurrency: usize,
|
||||
stream_requests: usize,
|
||||
stream_concurrency: usize,
|
||||
timeout: Duration,
|
||||
output_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for SingleInstanceBaselineConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sync_requests: 200,
|
||||
sync_concurrency: 20,
|
||||
stream_requests: 100,
|
||||
stream_concurrency: 10,
|
||||
timeout: Duration::from_secs(10),
|
||||
output_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct NamedBaselineResult {
|
||||
name: String,
|
||||
result: HttpLoadProbeResult,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SingleInstanceBaselineReport {
|
||||
suite: &'static str,
|
||||
scenarios: Vec<NamedBaselineResult>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
init_test_runtime_for("single-instance-baseline");
|
||||
let config = parse_args(std::env::args().skip(1).collect())?;
|
||||
let report = run_suite(&config).await?;
|
||||
let raw = serde_json::to_string_pretty(&report)?;
|
||||
println!("{raw}");
|
||||
if let Some(path) = config.output_path.as_ref() {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::write(path, format!("{raw}\n"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_suite(
|
||||
config: &SingleInstanceBaselineConfig,
|
||||
) -> Result<SingleInstanceBaselineReport, Box<dyn std::error::Error>> {
|
||||
let upstream = SpawnedServer::start(build_fake_upstream()).await?;
|
||||
let gateway = GatewayHarness::start(GatewayHarnessConfig::new(upstream.base_url())).await?;
|
||||
let executor = ExecutorHarness::start(ExecutorHarnessConfig::default()).await?;
|
||||
|
||||
let gateway_sync = run_http_load_probe(&gateway_sync_probe_config(gateway.base_url(), config))
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
let gateway_stream =
|
||||
run_http_load_probe(&gateway_stream_probe_config(gateway.base_url(), config))
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
let executor_sync = run_http_load_probe(&executor_sync_probe_config(
|
||||
executor.base_url(),
|
||||
upstream.base_url(),
|
||||
config,
|
||||
))
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
let executor_stream = run_http_load_probe(&executor_stream_probe_config(
|
||||
executor.base_url(),
|
||||
upstream.base_url(),
|
||||
config,
|
||||
))
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
Ok(SingleInstanceBaselineReport {
|
||||
suite: "single_instance_baseline",
|
||||
scenarios: vec![
|
||||
NamedBaselineResult {
|
||||
name: "gateway_proxy_sync".to_string(),
|
||||
result: gateway_sync,
|
||||
},
|
||||
NamedBaselineResult {
|
||||
name: "gateway_proxy_stream".to_string(),
|
||||
result: gateway_stream,
|
||||
},
|
||||
NamedBaselineResult {
|
||||
name: "executor_sync".to_string(),
|
||||
result: executor_sync,
|
||||
},
|
||||
NamedBaselineResult {
|
||||
name: "executor_stream".to_string(),
|
||||
result: executor_stream,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
fn gateway_sync_probe_config(
|
||||
gateway_base_url: &str,
|
||||
config: &SingleInstanceBaselineConfig,
|
||||
) -> HttpLoadProbeConfig {
|
||||
let mut probe = chat_probe_config(
|
||||
format!("{gateway_base_url}/v1/chat/completions"),
|
||||
false,
|
||||
config.sync_requests,
|
||||
config.sync_concurrency,
|
||||
config.timeout,
|
||||
);
|
||||
probe.response_mode = HttpLoadProbeResponseMode::FullBody;
|
||||
probe
|
||||
}
|
||||
|
||||
fn gateway_stream_probe_config(
|
||||
gateway_base_url: &str,
|
||||
config: &SingleInstanceBaselineConfig,
|
||||
) -> HttpLoadProbeConfig {
|
||||
let mut probe = chat_probe_config(
|
||||
format!("{gateway_base_url}/v1/chat/completions"),
|
||||
true,
|
||||
config.stream_requests,
|
||||
config.stream_concurrency,
|
||||
config.timeout,
|
||||
);
|
||||
probe.response_mode = HttpLoadProbeResponseMode::FullBody;
|
||||
probe
|
||||
}
|
||||
|
||||
fn executor_sync_probe_config(
|
||||
executor_base_url: &str,
|
||||
upstream_base_url: &str,
|
||||
config: &SingleInstanceBaselineConfig,
|
||||
) -> HttpLoadProbeConfig {
|
||||
execution_probe_config(
|
||||
format!("{executor_base_url}/v1/execute/sync"),
|
||||
execution_plan(format!("{upstream_base_url}/v1/chat/completions"), false),
|
||||
config.sync_requests,
|
||||
config.sync_concurrency,
|
||||
config.timeout,
|
||||
)
|
||||
}
|
||||
|
||||
fn executor_stream_probe_config(
|
||||
executor_base_url: &str,
|
||||
upstream_base_url: &str,
|
||||
config: &SingleInstanceBaselineConfig,
|
||||
) -> HttpLoadProbeConfig {
|
||||
execution_probe_config(
|
||||
format!("{executor_base_url}/v1/execute/stream"),
|
||||
execution_plan(format!("{upstream_base_url}/v1/chat/completions"), true),
|
||||
config.stream_requests,
|
||||
config.stream_concurrency,
|
||||
config.timeout,
|
||||
)
|
||||
}
|
||||
|
||||
fn execution_probe_config(
|
||||
url: String,
|
||||
plan: ExecutionPlan,
|
||||
total_requests: usize,
|
||||
concurrency: usize,
|
||||
timeout: Duration,
|
||||
) -> HttpLoadProbeConfig {
|
||||
HttpLoadProbeConfig {
|
||||
url,
|
||||
method: Method::POST,
|
||||
headers: BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
|
||||
body: Some(
|
||||
serde_json::to_vec(&plan).expect("execution plan should serialize for load probe"),
|
||||
),
|
||||
total_requests,
|
||||
concurrency,
|
||||
timeout,
|
||||
response_mode: HttpLoadProbeResponseMode::FullBody,
|
||||
}
|
||||
}
|
||||
|
||||
fn chat_probe_config(
|
||||
url: String,
|
||||
stream: bool,
|
||||
total_requests: usize,
|
||||
concurrency: usize,
|
||||
timeout: Duration,
|
||||
) -> HttpLoadProbeConfig {
|
||||
HttpLoadProbeConfig {
|
||||
url,
|
||||
method: Method::POST,
|
||||
headers: BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
|
||||
body: Some(
|
||||
serde_json::to_vec(&json!({
|
||||
"model": "gpt-5",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"stream": stream,
|
||||
}))
|
||||
.expect("chat body should serialize"),
|
||||
),
|
||||
total_requests,
|
||||
concurrency,
|
||||
timeout,
|
||||
response_mode: HttpLoadProbeResponseMode::FullBody,
|
||||
}
|
||||
}
|
||||
|
||||
fn execution_plan(url: String, stream: bool) -> ExecutionPlan {
|
||||
ExecutionPlan {
|
||||
request_id: if stream {
|
||||
"baseline-stream-request".to_string()
|
||||
} else {
|
||||
"baseline-sync-request".to_string()
|
||||
},
|
||||
candidate_id: Some(if stream {
|
||||
"baseline-stream-candidate".to_string()
|
||||
} else {
|
||||
"baseline-sync-candidate".to_string()
|
||||
}),
|
||||
provider_name: Some("openai".to_string()),
|
||||
provider_id: "provider-baseline".to_string(),
|
||||
endpoint_id: "endpoint-baseline".to_string(),
|
||||
key_id: "key-baseline".to_string(),
|
||||
method: "POST".to_string(),
|
||||
url,
|
||||
headers: BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
|
||||
content_type: Some("application/json".to_string()),
|
||||
content_encoding: None,
|
||||
body: RequestBody::from_json(json!({
|
||||
"model": "gpt-5",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"stream": stream,
|
||||
})),
|
||||
stream,
|
||||
client_api_format: "openai:chat".to_string(),
|
||||
provider_api_format: "openai:chat".to_string(),
|
||||
model_name: Some("gpt-5".to_string()),
|
||||
proxy: None,
|
||||
tls_profile: None,
|
||||
timeouts: Some(ExecutionTimeouts {
|
||||
connect_ms: Some(2_000),
|
||||
read_ms: Some(10_000),
|
||||
first_byte_ms: Some(5_000),
|
||||
total_ms: Some(10_000),
|
||||
..ExecutionTimeouts::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_fake_upstream() -> Router {
|
||||
Router::new().route(
|
||||
"/v1/chat/completions",
|
||||
any(|request: Request| async move {
|
||||
let (_parts, body) = request.into_parts();
|
||||
let raw_body = to_bytes(body, usize::MAX)
|
||||
.await
|
||||
.expect("fake upstream body should read");
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&raw_body).unwrap_or_else(|_| json!({}));
|
||||
let stream = payload
|
||||
.get("stream")
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false);
|
||||
if stream {
|
||||
let body = futures_util::stream::iter([
|
||||
Ok::<_, Infallible>(Bytes::from_static(
|
||||
b"data: {\"id\":\"chunk-1\",\"choices\":[{\"delta\":{\"content\":\"hel\"}}]}\n\n",
|
||||
)),
|
||||
Ok::<_, Infallible>(Bytes::from_static(
|
||||
b"data: {\"id\":\"chunk-2\",\"choices\":[{\"delta\":{\"content\":\"lo\"}}]}\n\n",
|
||||
)),
|
||||
Ok::<_, Infallible>(Bytes::from_static(b"data: [DONE]\n\n")),
|
||||
]);
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(http::header::CONTENT_TYPE, "text/event-stream")
|
||||
.body(Body::from_stream(body))
|
||||
.expect("fake upstream stream response should build")
|
||||
} else {
|
||||
Json(json!({
|
||||
"id": "chatcmpl-baseline",
|
||||
"object": "chat.completion",
|
||||
"model": payload.get("model").and_then(|value| value.as_str()).unwrap_or("gpt-5"),
|
||||
"choices": [{"message": {"role": "assistant", "content": "hello"}}]
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_args(
|
||||
args: Vec<String>,
|
||||
) -> Result<SingleInstanceBaselineConfig, Box<dyn std::error::Error>> {
|
||||
let mut config = SingleInstanceBaselineConfig::default();
|
||||
let mut iter = args.into_iter();
|
||||
while let Some(arg) = iter.next() {
|
||||
match arg.as_str() {
|
||||
"--sync-requests" => {
|
||||
config.sync_requests = next_value(&mut iter, "--sync-requests")?.parse()?
|
||||
}
|
||||
"--sync-concurrency" => {
|
||||
config.sync_concurrency = next_value(&mut iter, "--sync-concurrency")?.parse()?
|
||||
}
|
||||
"--stream-requests" => {
|
||||
config.stream_requests = next_value(&mut iter, "--stream-requests")?.parse()?
|
||||
}
|
||||
"--stream-concurrency" => {
|
||||
config.stream_concurrency =
|
||||
next_value(&mut iter, "--stream-concurrency")?.parse()?
|
||||
}
|
||||
"--timeout-ms" => {
|
||||
config.timeout =
|
||||
Duration::from_millis(next_value(&mut iter, "--timeout-ms")?.parse()?)
|
||||
}
|
||||
"--output" => {
|
||||
config.output_path = Some(PathBuf::from(next_value(&mut iter, "--output")?))
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
print_usage();
|
||||
std::process::exit(0);
|
||||
}
|
||||
other => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("unknown argument: {other}"),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn next_value(
|
||||
iter: &mut impl Iterator<Item = String>,
|
||||
flag: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
iter.next().ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("missing value for {flag}"),
|
||||
)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!(
|
||||
"usage: cargo run -p aether-testkit --bin single_instance_baseline -- [--sync-requests 200] [--sync-concurrency 20] [--stream-requests 100] [--stream-concurrency 10] [--timeout-ms 10000] [--output /tmp/baseline.json]"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user