fix(adaptive): 修复 429 计数归零写入及 reset 语义

- sql.rs: UPDATE 时对 concurrent_429_count / rpm_429_count 加 COALESCE(, 0),避免 None 覆写为 NULL
- effects.rs: rpm_429_count 始终写入 Some(projection.rpm_429_count),不再过滤零值;新增 unknown-429 场景下零值持久化的单元测试
- adaptive.rs: reset 接口将两个计数字段置为 Some(0) 而非 None,与数据库默认语义对齐;同步修正集成测试断言
- docker-compose.build.yml: 补充 AETHER_GATEWAY_AUTO_PREPARE_DATABASE 默认开启
- README.md: 同步说明 build compose 也已默认开启自动迁移
This commit is contained in:
fawney19
2026-04-22 22:16:51 +08:00
parent c8d7dbd8d6
commit 4374f53315
6 changed files with 45 additions and 10 deletions

View File

@@ -113,7 +113,7 @@ client -> rust frontdoor (aether-gateway) -> execution_runtime/provider transpor
- `./dev.sh` 默认把 `AETHER_GATEWAY_VIDEO_TASK_TRUTH_SOURCE_MODE` 设为 `rust-authoritative`,避免本地还依赖 Python sync report 语义。
- 空库首次启动会自动初始化到当前 baseline。
- `aether-gateway` 默认启动不会自动应用后续 schema migration如果数据库版本落后服务会拒绝启动并提示先执行 `aether-gateway --migrate`
- 仓库自带的 `docker-compose.yml` 已把 `AETHER_GATEWAY_AUTO_PREPARE_DATABASE` 设为默认开启,因此 `docker compose up -d` 会在监听端口前自动执行挂起的 migration 和 backfill。
- 仓库自带的 `docker-compose.yml` `docker-compose.build.yml`已把 `AETHER_GATEWAY_AUTO_PREPARE_DATABASE` 设为默认开启,因此无论是预构建镜像部署还是 `./deploy.sh` / 本地构建 compose常规启动都会在监听端口前自动执行挂起的 migration 和 backfill。
## Aether Proxy (可选)
@@ -134,7 +134,7 @@ Aether Proxy 是配套的正向代理节点,部署在海外 VPS 上,为墙
- `APP_PORT``aether-gateway` 唯一监听端口,固定绑定 `0.0.0.0:${APP_PORT}`
- `DATABASE_URL` / `REDIS_URL``aether-gateway` 直接读取的共享后端连接串
- `AETHER_GATEWAY_AUTO_PREPARE_DATABASE`:常规启动前自动执行挂起的 schema migration 和 backfill仓库自带的 `docker-compose.yml` 默认开启
- `AETHER_GATEWAY_AUTO_PREPARE_DATABASE`:常规启动前自动执行挂起的 schema migration 和 backfill仓库自带的 `docker-compose.yml``docker-compose.build.yml` 默认开启
- `JWT_SECRET_KEY` / `ENCRYPTION_KEY`:认证和敏感数据加密所需密钥
- `API_KEY_PREFIX`:用户和管理员新建 API Key 时使用的前缀,默认 `sk`
- `PAYMENT_CALLBACK_SECRET`:支付回调公开入口的共享密钥;未配置时相关路由保持禁用
@@ -179,7 +179,7 @@ systemd 的 `.env` 必须保持简单 `KEY=VALUE` 形式,不要写 `export`、
**没有备份的情况:**
当前不应该再依赖旧的 `alembic downgrade` 路线。空库首次启动会自动初始化;如果开启 `AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true`(仓库自带 `docker-compose.yml` 默认如此),常规服务启动也会自动应用挂起的 migration/backfill。无论是否自动执行只要本次发布带来了不可逆的数据结构变化没有备份就不能保证安全回滚。因此升级前强烈建议先备份 `Postgres`
当前不应该再依赖旧的 `alembic downgrade` 路线。空库首次启动会自动初始化;如果开启 `AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true`(仓库自带的两份 compose 默认如此),常规服务启动也会自动应用挂起的 migration/backfill。无论是否自动执行只要本次发布带来了不可逆的数据结构变化没有备份就不能保证安全回滚。因此升级前强烈建议先备份 `Postgres`
---

View File

@@ -134,8 +134,8 @@ impl<'a> AdminAppState<'a> {
return Ok(admin_adaptive_key_not_found_response(key_id));
};
key.learned_rpm_limit = None;
key.concurrent_429_count = None;
key.rpm_429_count = None;
key.concurrent_429_count = Some(0);
key.rpm_429_count = Some(0);
key.last_429_at_unix_secs = None;
key.last_429_type = None;
key.adjustment_history = None;

View File

@@ -300,7 +300,7 @@ async fn record_adaptive_rate_limit_effect(
};
let mut updated_key = current_key.clone();
updated_key.rpm_429_count = Some(projection.rpm_429_count).filter(|value| *value > 0);
updated_key.rpm_429_count = Some(projection.rpm_429_count);
updated_key.learned_rpm_limit = projection.learned_rpm_limit;
updated_key.last_429_at_unix_secs = Some(projection.last_429_at_unix_secs);
updated_key.last_429_type = Some(projection.last_429_type);
@@ -1315,6 +1315,40 @@ mod tests {
assert_eq!(stored_key.last_429_type, None);
}
#[tokio::test]
async fn adaptive_rate_limit_effect_persists_zero_rpm_count_for_unknown_429() {
let mut key = sample_health_key();
key.rpm_limit = None;
key.learned_rpm_limit = Some(20);
let state = adaptive_state_with_request_candidates(key, Vec::new());
let plan = sample_plan();
apply_local_execution_effect(
&state,
LocalExecutionEffectContext {
plan: &plan,
report_context: None,
},
LocalExecutionEffect::AdaptiveRateLimit(LocalAdaptiveRateLimitEffect {
status_code: 429,
classification: LocalFailoverClassification::RetryStatusCode,
headers: None,
}),
)
.await;
let stored_key = state
.read_provider_catalog_keys_by_ids(std::slice::from_ref(&plan.key_id))
.await
.expect("provider catalog keys should load")
.into_iter()
.next()
.expect("stored key should exist");
assert_eq!(stored_key.rpm_429_count, Some(0));
assert_eq!(stored_key.learned_rpm_limit, Some(19));
assert_eq!(stored_key.last_429_type.as_deref(), Some("unknown"));
}
#[tokio::test]
async fn adaptive_success_effect_expands_limit_from_recent_rpm_usage() {
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;

View File

@@ -440,8 +440,8 @@ async fn gateway_resets_admin_adaptive_learning_locally_with_trusted_admin_princ
.await
.expect("repository query should succeed");
assert_eq!(updated[0].learned_rpm_limit, None);
assert_eq!(updated[0].concurrent_429_count, None);
assert_eq!(updated[0].rpm_429_count, None);
assert_eq!(updated[0].concurrent_429_count, Some(0));
assert_eq!(updated[0].rpm_429_count, Some(0));
assert_eq!(updated[0].adjustment_history, None);
gateway_handle.abort();

View File

@@ -1738,8 +1738,8 @@ SET
END,
oauth_invalid_reason = $26,
status_snapshot = $27,
concurrent_429_count = $28,
rpm_429_count = $29,
concurrent_429_count = COALESCE($28, 0),
rpm_429_count = COALESCE($29, 0),
last_429_at = CASE
WHEN $30::double precision IS NULL THEN NULL
ELSE TO_TIMESTAMP($30::double precision)

View File

@@ -51,6 +51,7 @@ services:
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
TZ: Asia/Shanghai
APP_PORT: ${APP_PORT:-8084}
AETHER_GATEWAY_AUTO_PREPARE_DATABASE: ${AETHER_GATEWAY_AUTO_PREPARE_DATABASE:-true}
depends_on:
postgres:
condition: service_healthy