mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(usage): 完善 cache-affinity 时间线的用户名回退与模型空值兜底
- 当 auth 用户查询失败时回退到历史 username,避免时间线丢失用户信息 - SQL 投影对 usage.model 使用 COALESCE 兜底空值 - IntervalTimelineCard 在刷新间隔为 0 时跳过定时刷新 - Usage 页面关闭时间线卡片的自动轮询
This commit is contained in:
@@ -86,11 +86,20 @@ pub(super) async fn build_admin_usage_cache_affinity_interval_timeline_response(
|
|||||||
|
|
||||||
if include_user_info && user_id.is_none() {
|
if include_user_info && user_id.is_none() {
|
||||||
let user_ids: Vec<_> = grouped.keys().cloned().collect();
|
let user_ids: Vec<_> = grouped.keys().cloned().collect();
|
||||||
let user_map = load_usage_cache_affinity_usernames(state, &user_ids).await?;
|
let mut should_use_legacy_usernames = !state.has_auth_user_data_reader();
|
||||||
for (user_id, username) in user_map {
|
match load_usage_cache_affinity_usernames(state, &user_ids).await {
|
||||||
usernames_by_user_id.insert(user_id, username);
|
Ok(user_map) => {
|
||||||
|
usernames_by_user_id.extend(user_map);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!(
|
||||||
|
error = ?err,
|
||||||
|
"admin usage cache affinity interval timeline user lookup failed"
|
||||||
|
);
|
||||||
|
should_use_legacy_usernames = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !state.has_auth_user_data_reader() {
|
if should_use_legacy_usernames {
|
||||||
for (user_id, username) in legacy_usernames_by_user_id {
|
for (user_id, username) in legacy_usernames_by_user_id {
|
||||||
usernames_by_user_id.entry(user_id).or_insert(username);
|
usernames_by_user_id.entry(user_id).or_insert(username);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2605,6 +2605,90 @@ async fn gateway_handles_admin_usage_cache_affinity_interval_timeline_without_le
|
|||||||
upstream_handle.abort();
|
upstream_handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn gateway_handles_admin_usage_cache_affinity_interval_timeline_with_legacy_username_fallback_when_auth_user_lookup_fails(
|
||||||
|
) {
|
||||||
|
let (_upstream_url, upstream_hits, upstream_handle) =
|
||||||
|
start_usage_upstream("/api/admin/usage/cache-affinity/interval-timeline").await;
|
||||||
|
let mut usage_one = sample_usage_row(
|
||||||
|
"usage-auth-lookup-fail-1",
|
||||||
|
"req-auth-lookup-fail-1",
|
||||||
|
Some("user-1"),
|
||||||
|
Some("key-1"),
|
||||||
|
Some("primary"),
|
||||||
|
"OpenAI",
|
||||||
|
"gpt-5",
|
||||||
|
"completed",
|
||||||
|
10,
|
||||||
|
2,
|
||||||
|
0.01,
|
||||||
|
0.012,
|
||||||
|
recent_unix_secs(55),
|
||||||
|
);
|
||||||
|
usage_one.username = Some("stale-alice".to_string());
|
||||||
|
let mut usage_two = sample_usage_row(
|
||||||
|
"usage-auth-lookup-fail-2",
|
||||||
|
"req-auth-lookup-fail-2",
|
||||||
|
Some("user-1"),
|
||||||
|
Some("key-1"),
|
||||||
|
Some("primary"),
|
||||||
|
"OpenAI",
|
||||||
|
"gpt-5",
|
||||||
|
"completed",
|
||||||
|
12,
|
||||||
|
3,
|
||||||
|
0.01,
|
||||||
|
0.012,
|
||||||
|
recent_unix_secs(50),
|
||||||
|
);
|
||||||
|
usage_two.username = Some("stale-alice".to_string());
|
||||||
|
|
||||||
|
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![
|
||||||
|
usage_one, usage_two,
|
||||||
|
]));
|
||||||
|
let gateway = build_router_with_state(
|
||||||
|
AppState::new()
|
||||||
|
.expect("gateway should build")
|
||||||
|
.with_data_state_for_tests(GatewayDataState::with_usage_reader_for_tests(
|
||||||
|
usage_repository,
|
||||||
|
))
|
||||||
|
.with_auth_users_for_tests([StoredUserAuthRecord {
|
||||||
|
id: "user-1".to_string(),
|
||||||
|
email: None,
|
||||||
|
email_verified: false,
|
||||||
|
username: " ".to_string(),
|
||||||
|
password_hash: None,
|
||||||
|
role: "user".to_string(),
|
||||||
|
auth_source: "local".to_string(),
|
||||||
|
allowed_providers: None,
|
||||||
|
allowed_api_formats: None,
|
||||||
|
allowed_models: None,
|
||||||
|
is_active: true,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: None,
|
||||||
|
last_login_at: None,
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||||
|
|
||||||
|
let response = admin_request(reqwest::Client::new().get(format!(
|
||||||
|
"{gateway_url}/api/admin/usage/cache-affinity/interval-timeline?hours=24&limit=100&include_user_info=true"
|
||||||
|
)))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("request should succeed");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||||
|
assert_eq!(payload["total_points"], 1);
|
||||||
|
assert_eq!(payload["points"][0]["user_id"], "user-1");
|
||||||
|
assert_eq!(payload["users"]["user-1"], "stale-alice");
|
||||||
|
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||||
|
|
||||||
|
gateway_handle.abort();
|
||||||
|
upstream_handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_handles_admin_usage_cache_affinity_ttl_analysis_locally_with_trusted_admin_principal(
|
async fn gateway_handles_admin_usage_cache_affinity_ttl_analysis_locally_with_trusted_admin_principal(
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -2495,7 +2495,7 @@ WITH filtered_usage AS (
|
|||||||
builder.push(
|
builder.push(
|
||||||
r#" AS group_id,
|
r#" AS group_id,
|
||||||
"usage".username AS username,
|
"usage".username AS username,
|
||||||
"usage".model AS model,
|
COALESCE("usage".model, '') AS model,
|
||||||
"usage".created_at AS created_at,
|
"usage".created_at AS created_at,
|
||||||
"usage".id AS usage_id
|
"usage".id AS usage_id
|
||||||
FROM "usage"
|
FROM "usage"
|
||||||
@@ -6188,6 +6188,11 @@ mod tests {
|
|||||||
assert!(super::LIST_RECENT_USAGE_AUDITS_PREFIX.contains("FROM \"usage\""));
|
assert!(super::LIST_RECENT_USAGE_AUDITS_PREFIX.contains("FROM \"usage\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn usage_sql_cache_affinity_interval_query_coalesces_nullable_model_values() {
|
||||||
|
assert!(include_str!("sql.rs").contains("COALESCE(\"usage\".model, '') AS model"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn usage_sql_reads_http_audits_for_single_record_fetches() {
|
fn usage_sql_reads_http_audits_for_single_record_fetches() {
|
||||||
assert!(super::FIND_BY_REQUEST_ID_SQL.contains("LEFT JOIN usage_http_audits"));
|
assert!(super::FIND_BY_REQUEST_ID_SQL.contains("LEFT JOIN usage_http_audits"));
|
||||||
|
|||||||
@@ -347,6 +347,9 @@ function handleVisibilityChange() {
|
|||||||
stopRefreshTimer()
|
stopRefreshTimer()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!props.refreshIntervalMs || props.refreshIntervalMs <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
void loadData()
|
void loadData()
|
||||||
scheduleNextRefresh()
|
scheduleNextRefresh()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
:title="intervalTimelineTitle"
|
:title="intervalTimelineTitle"
|
||||||
:is-admin="isAdminPage"
|
:is-admin="isAdminPage"
|
||||||
:hours="intervalTimelineHours"
|
:hours="intervalTimelineHours"
|
||||||
:refresh-interval-ms="30000"
|
:refresh-interval-ms="0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user