mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor: 移除 shadow results 相关模块和接口
This commit is contained in:
@@ -5,10 +5,7 @@ use crate::async_task::{
|
|||||||
cancel_video_task, get_video_task_detail, get_video_task_stats, get_video_task_video,
|
cancel_video_task, get_video_task_detail, get_video_task_stats, get_video_task_video,
|
||||||
list_video_tasks,
|
list_video_tasks,
|
||||||
};
|
};
|
||||||
use crate::audit::{
|
use crate::audit::{get_auth_api_key_snapshot, get_decision_trace, get_request_candidate_trace};
|
||||||
get_auth_api_key_snapshot, get_decision_trace, get_request_candidate_trace,
|
|
||||||
list_recent_shadow_results,
|
|
||||||
};
|
|
||||||
use crate::hooks::{get_request_audit_bundle, get_request_usage_audit};
|
use crate::hooks::{get_request_audit_bundle, get_request_usage_audit};
|
||||||
use crate::router::metrics;
|
use crate::router::metrics;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
@@ -53,8 +50,4 @@ pub(crate) fn mount_operational_routes(router: Router<AppState>) -> Router<AppSt
|
|||||||
"/_gateway/audit/request-usage/{request_id}",
|
"/_gateway/audit/request-usage/{request_id}",
|
||||||
get(get_request_usage_audit),
|
get(get_request_usage_audit),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/_gateway/audit/shadow-results/recent",
|
|
||||||
get(list_recent_shadow_results),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,69 +8,6 @@ use serde_json::json;
|
|||||||
|
|
||||||
use crate::{AppState, GatewayError};
|
use crate::{AppState, GatewayError};
|
||||||
|
|
||||||
const DEFAULT_RECENT_LIMIT: usize = 20;
|
|
||||||
const MAX_RECENT_LIMIT: usize = 200;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct ListRecentShadowResultsQuery {
|
|
||||||
pub(crate) limit: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub(crate) struct ShadowResultStatusCounts {
|
|
||||||
pub(crate) pending: usize,
|
|
||||||
pub(crate) r#match: usize,
|
|
||||||
pub(crate) mismatch: usize,
|
|
||||||
pub(crate) error: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub(crate) struct ListRecentShadowResultsResponse {
|
|
||||||
pub(crate) items: Vec<aether_data::repository::shadow_results::StoredShadowResult>,
|
|
||||||
pub(crate) limit_applied: usize,
|
|
||||||
pub(crate) counts: ShadowResultStatusCounts,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn list_recent_shadow_results(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Query(query): Query<ListRecentShadowResultsQuery>,
|
|
||||||
) -> Result<Json<ListRecentShadowResultsResponse>, GatewayError> {
|
|
||||||
let limit = query
|
|
||||||
.limit
|
|
||||||
.unwrap_or(DEFAULT_RECENT_LIMIT)
|
|
||||||
.clamp(1, MAX_RECENT_LIMIT);
|
|
||||||
let items = state.list_recent_shadow_results(limit).await?;
|
|
||||||
|
|
||||||
let mut counts = ShadowResultStatusCounts {
|
|
||||||
pending: 0,
|
|
||||||
r#match: 0,
|
|
||||||
mismatch: 0,
|
|
||||||
error: 0,
|
|
||||||
};
|
|
||||||
for item in &items {
|
|
||||||
match item.match_status {
|
|
||||||
aether_data::repository::shadow_results::ShadowResultMatchStatus::Pending => {
|
|
||||||
counts.pending += 1
|
|
||||||
}
|
|
||||||
aether_data::repository::shadow_results::ShadowResultMatchStatus::Match => {
|
|
||||||
counts.r#match += 1
|
|
||||||
}
|
|
||||||
aether_data::repository::shadow_results::ShadowResultMatchStatus::Mismatch => {
|
|
||||||
counts.mismatch += 1
|
|
||||||
}
|
|
||||||
aether_data::repository::shadow_results::ShadowResultMatchStatus::Error => {
|
|
||||||
counts.error += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(ListRecentShadowResultsResponse {
|
|
||||||
items,
|
|
||||||
limit_applied: limit,
|
|
||||||
counts,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub(crate) struct GetRequestCandidateTraceQuery {
|
pub(crate) struct GetRequestCandidateTraceQuery {
|
||||||
pub(crate) attempted_only: Option<bool>,
|
pub(crate) attempted_only: Option<bool>,
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
mod admin;
|
mod admin;
|
||||||
mod http;
|
mod http;
|
||||||
mod shadow;
|
|
||||||
|
|
||||||
pub(crate) use admin::{attach_admin_audit_event, emit_admin_audit, AdminAuditEvent};
|
pub(crate) use admin::{attach_admin_audit_event, emit_admin_audit, AdminAuditEvent};
|
||||||
pub(crate) use http::get_auth_api_key_snapshot;
|
pub(crate) use http::get_auth_api_key_snapshot;
|
||||||
pub(crate) use http::get_decision_trace;
|
pub(crate) use http::get_decision_trace;
|
||||||
pub(crate) use http::get_request_candidate_trace;
|
pub(crate) use http::get_request_candidate_trace;
|
||||||
pub(crate) use http::list_recent_shadow_results;
|
|
||||||
pub(crate) use shadow::record_shadow_result_non_blocking;
|
|
||||||
|
|||||||
@@ -1,267 +0,0 @@
|
|||||||
use std::collections::hash_map::DefaultHasher;
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use aether_data::repository::shadow_results::{RecordShadowResultSample, ShadowResultSampleOrigin};
|
|
||||||
use axum::body::Body;
|
|
||||||
use axum::http::header::CONTENT_TYPE;
|
|
||||||
use axum::http::Response;
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::constants::{
|
|
||||||
CONTROL_CANDIDATE_ID_HEADER, CONTROL_REQUEST_ID_HEADER, EXECUTION_PATH_CONTROL_EXECUTE_STREAM,
|
|
||||||
EXECUTION_PATH_CONTROL_EXECUTE_SYNC,
|
|
||||||
};
|
|
||||||
use crate::control::GatewayControlDecision;
|
|
||||||
use crate::AppState;
|
|
||||||
|
|
||||||
pub(crate) fn record_shadow_result_non_blocking(
|
|
||||||
state: AppState,
|
|
||||||
trace_id: &str,
|
|
||||||
method: &http::Method,
|
|
||||||
path_and_query: &str,
|
|
||||||
control_decision: Option<&GatewayControlDecision>,
|
|
||||||
execution_path: &'static str,
|
|
||||||
response: &Response<Body>,
|
|
||||||
) {
|
|
||||||
let Some(decision) =
|
|
||||||
control_decision.filter(|decision| decision.route_class.as_deref() == Some("ai_public"))
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if !state.has_shadow_result_data_writer() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let route_family = decision.route_family.clone();
|
|
||||||
let route_kind = decision.route_kind.clone();
|
|
||||||
let status_code = response.status().as_u16();
|
|
||||||
let content_type = response
|
|
||||||
.headers()
|
|
||||||
.get(CONTENT_TYPE)
|
|
||||||
.and_then(|value| value.to_str().ok())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string();
|
|
||||||
let candidate_id = response
|
|
||||||
.headers()
|
|
||||||
.get(CONTROL_CANDIDATE_ID_HEADER)
|
|
||||||
.and_then(|value| value.to_str().ok())
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(ToOwned::to_owned);
|
|
||||||
let request_id = response
|
|
||||||
.headers()
|
|
||||||
.get(CONTROL_REQUEST_ID_HEADER)
|
|
||||||
.and_then(|value| value.to_str().ok())
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(ToOwned::to_owned);
|
|
||||||
let now_unix_secs = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
let sample = RecordShadowResultSample {
|
|
||||||
trace_id: trace_id.to_string(),
|
|
||||||
request_fingerprint: build_request_fingerprint(
|
|
||||||
method,
|
|
||||||
path_and_query,
|
|
||||||
route_family.as_deref(),
|
|
||||||
route_kind.as_deref(),
|
|
||||||
),
|
|
||||||
request_id,
|
|
||||||
route_family,
|
|
||||||
route_kind,
|
|
||||||
candidate_id,
|
|
||||||
origin: sample_origin_for_execution_path(execution_path),
|
|
||||||
result_digest: build_result_digest(status_code, &content_type),
|
|
||||||
status_code: Some(status_code),
|
|
||||||
error_message: (status_code >= 400)
|
|
||||||
.then(|| format!("gateway response status {status_code}")),
|
|
||||||
recorded_at_unix_secs: now_unix_secs,
|
|
||||||
};
|
|
||||||
let trace_id = trace_id.to_string();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = state.record_shadow_result_sample(sample).await {
|
|
||||||
warn!(trace_id = %trace_id, error = ?err, "gateway failed to record shadow result");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_request_fingerprint(
|
|
||||||
method: &http::Method,
|
|
||||||
path_and_query: &str,
|
|
||||||
route_family: Option<&str>,
|
|
||||||
route_kind: Option<&str>,
|
|
||||||
) -> String {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
method.as_str().hash(&mut hasher);
|
|
||||||
path_and_query.hash(&mut hasher);
|
|
||||||
route_family.unwrap_or_default().hash(&mut hasher);
|
|
||||||
route_kind.unwrap_or_default().hash(&mut hasher);
|
|
||||||
format!("{:x}", hasher.finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_result_digest(status_code: u16, content_type: &str) -> String {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
status_code.hash(&mut hasher);
|
|
||||||
content_type.hash(&mut hasher);
|
|
||||||
format!("{:x}", hasher.finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_origin_for_execution_path(execution_path: &str) -> ShadowResultSampleOrigin {
|
|
||||||
match execution_path {
|
|
||||||
EXECUTION_PATH_CONTROL_EXECUTE_SYNC | EXECUTION_PATH_CONTROL_EXECUTE_STREAM => {
|
|
||||||
ShadowResultSampleOrigin::Python
|
|
||||||
}
|
|
||||||
_ => ShadowResultSampleOrigin::Rust,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use aether_data::repository::shadow_results::{
|
|
||||||
InMemoryShadowResultRepository, ShadowResultMatchStatus, ShadowResultReadRepository,
|
|
||||||
};
|
|
||||||
use axum::body::Body;
|
|
||||||
use axum::http::header::CONTENT_TYPE;
|
|
||||||
use axum::http::{Method, Response, StatusCode};
|
|
||||||
|
|
||||||
use super::record_shadow_result_non_blocking;
|
|
||||||
use crate::constants::{
|
|
||||||
CONTROL_REQUEST_ID_HEADER, EXECUTION_PATH_CONTROL_EXECUTE_SYNC,
|
|
||||||
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
|
|
||||||
};
|
|
||||||
use crate::control::GatewayControlDecision;
|
|
||||||
use crate::AppState;
|
|
||||||
|
|
||||||
fn sample_decision() -> GatewayControlDecision {
|
|
||||||
GatewayControlDecision {
|
|
||||||
public_path: "/v1/chat/completions".to_string(),
|
|
||||||
public_query_string: Some("stream=true".to_string()),
|
|
||||||
route_class: Some("ai_public".to_string()),
|
|
||||||
route_family: Some("openai".to_string()),
|
|
||||||
route_kind: Some("chat".to_string()),
|
|
||||||
auth_endpoint_signature: Some("openai:chat".to_string()),
|
|
||||||
execution_runtime_candidate: true,
|
|
||||||
auth_context: None,
|
|
||||||
admin_principal: None,
|
|
||||||
local_auth_rejection: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn records_shadow_result_for_ai_public_response() {
|
|
||||||
let repository = Arc::new(InMemoryShadowResultRepository::default());
|
|
||||||
let state = AppState::new()
|
|
||||||
.expect("app state should build")
|
|
||||||
.with_shadow_result_data_writer_for_tests(repository.clone());
|
|
||||||
let response = Response::builder()
|
|
||||||
.status(StatusCode::OK)
|
|
||||||
.header(CONTENT_TYPE, "application/json")
|
|
||||||
.header(CONTROL_REQUEST_ID_HEADER, "req-shadow-123")
|
|
||||||
.body(Body::from("{}"))
|
|
||||||
.expect("response should build");
|
|
||||||
|
|
||||||
record_shadow_result_non_blocking(
|
|
||||||
state,
|
|
||||||
"trace-shadow-123",
|
|
||||||
&Method::POST,
|
|
||||||
"/v1/chat/completions?stream=true",
|
|
||||||
Some(&sample_decision()),
|
|
||||||
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
|
|
||||||
&response,
|
|
||||||
);
|
|
||||||
|
|
||||||
for _ in 0..30 {
|
|
||||||
if repository
|
|
||||||
.list_recent(1)
|
|
||||||
.await
|
|
||||||
.map(|rows| !rows.is_empty())
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stored = repository
|
|
||||||
.list_recent(1)
|
|
||||||
.await
|
|
||||||
.expect("list should succeed")
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.expect("stored result should exist");
|
|
||||||
assert_eq!(stored.trace_id, "trace-shadow-123");
|
|
||||||
assert_eq!(stored.request_id.as_deref(), Some("req-shadow-123"));
|
|
||||||
assert_eq!(stored.route_family.as_deref(), Some("openai"));
|
|
||||||
assert_eq!(stored.route_kind.as_deref(), Some("chat"));
|
|
||||||
assert_eq!(stored.match_status, ShadowResultMatchStatus::Pending);
|
|
||||||
assert_eq!(stored.status_code, Some(200));
|
|
||||||
assert!(stored.rust_result_digest.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn merges_rust_and_python_shadow_samples_into_match() {
|
|
||||||
let repository = Arc::new(InMemoryShadowResultRepository::default());
|
|
||||||
let state = AppState::new()
|
|
||||||
.expect("app state should build")
|
|
||||||
.with_shadow_result_data_repository_for_tests(repository.clone());
|
|
||||||
let response = Response::builder()
|
|
||||||
.status(StatusCode::OK)
|
|
||||||
.header(CONTENT_TYPE, "application/json")
|
|
||||||
.header(CONTROL_REQUEST_ID_HEADER, "req-shadow-compare-123")
|
|
||||||
.body(Body::from("{}"))
|
|
||||||
.expect("response should build");
|
|
||||||
|
|
||||||
record_shadow_result_non_blocking(
|
|
||||||
state.clone(),
|
|
||||||
"trace-shadow-compare-123",
|
|
||||||
&Method::POST,
|
|
||||||
"/v1/chat/completions?stream=true",
|
|
||||||
Some(&sample_decision()),
|
|
||||||
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
|
|
||||||
&response,
|
|
||||||
);
|
|
||||||
record_shadow_result_non_blocking(
|
|
||||||
state,
|
|
||||||
"trace-shadow-compare-123",
|
|
||||||
&Method::POST,
|
|
||||||
"/v1/chat/completions?stream=true",
|
|
||||||
Some(&sample_decision()),
|
|
||||||
EXECUTION_PATH_CONTROL_EXECUTE_SYNC,
|
|
||||||
&response,
|
|
||||||
);
|
|
||||||
|
|
||||||
for _ in 0..30 {
|
|
||||||
if repository
|
|
||||||
.list_recent(1)
|
|
||||||
.await
|
|
||||||
.map(|rows| {
|
|
||||||
rows.first()
|
|
||||||
.map(|row| row.match_status == ShadowResultMatchStatus::Match)
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stored = repository
|
|
||||||
.list_recent(1)
|
|
||||||
.await
|
|
||||||
.expect("list should succeed")
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.expect("stored result should exist");
|
|
||||||
assert_eq!(stored.request_id.as_deref(), Some("req-shadow-compare-123"));
|
|
||||||
assert_eq!(stored.match_status, ShadowResultMatchStatus::Match);
|
|
||||||
assert!(stored.rust_result_digest.is_some());
|
|
||||||
assert!(stored.python_result_digest.is_some());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -54,8 +54,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -94,8 +92,6 @@ impl GatewayDataState {
|
|||||||
let wallet_reader = backends.read().wallets();
|
let wallet_reader = backends.read().wallets();
|
||||||
let wallet_writer = backends.write().wallets();
|
let wallet_writer = backends.write().wallets();
|
||||||
let settlement_writer = backends.write().settlement();
|
let settlement_writer = backends.write().settlement();
|
||||||
let shadow_result_reader = backends.read().shadow_results();
|
|
||||||
let shadow_result_writer = backends.write().shadow_results();
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config,
|
config,
|
||||||
@@ -134,8 +130,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader,
|
wallet_reader,
|
||||||
wallet_writer,
|
wallet_writer,
|
||||||
settlement_writer,
|
settlement_writer,
|
||||||
shadow_result_reader,
|
|
||||||
shadow_result_writer,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -297,14 +291,6 @@ impl GatewayDataState {
|
|||||||
self.settlement_writer.is_some()
|
self.settlement_writer.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn has_shadow_result_writer(&self) -> bool {
|
|
||||||
self.shadow_result_writer.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn has_shadow_result_reader(&self) -> bool {
|
|
||||||
self.shadow_result_reader.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn encryption_key(&self) -> Option<&str> {
|
pub(crate) fn encryption_key(&self) -> Option<&str> {
|
||||||
self.config.encryption_key()
|
self.config.encryption_key()
|
||||||
|
|||||||
@@ -44,10 +44,6 @@ use aether_data::repository::proxy_nodes::{
|
|||||||
ProxyNodeRemoteConfigMutation, ProxyNodeTunnelStatusMutation, ProxyNodeWriteRepository,
|
ProxyNodeRemoteConfigMutation, ProxyNodeTunnelStatusMutation, ProxyNodeWriteRepository,
|
||||||
StoredProxyNode, StoredProxyNodeEvent,
|
StoredProxyNode, StoredProxyNodeEvent,
|
||||||
};
|
};
|
||||||
use aether_data::repository::shadow_results::{
|
|
||||||
merge_shadow_result_sample, RecordShadowResultSample, ShadowResultLookupKey,
|
|
||||||
ShadowResultReadRepository, ShadowResultWriteRepository, StoredShadowResult,
|
|
||||||
};
|
|
||||||
pub(crate) use aether_data::repository::system::{AdminSystemStats, StoredSystemConfigEntry};
|
pub(crate) use aether_data::repository::system::{AdminSystemStats, StoredSystemConfigEntry};
|
||||||
use aether_data::repository::users::{
|
use aether_data::repository::users::{
|
||||||
StoredUserAuthRecord, StoredUserExportRow, StoredUserSummary, UserReadRepository,
|
StoredUserAuthRecord, StoredUserExportRow, StoredUserSummary, UserReadRepository,
|
||||||
@@ -146,8 +142,6 @@ pub(crate) struct GatewayDataState {
|
|||||||
wallet_reader: Option<Arc<dyn WalletReadRepository>>,
|
wallet_reader: Option<Arc<dyn WalletReadRepository>>,
|
||||||
wallet_writer: Option<Arc<dyn WalletWriteRepository>>,
|
wallet_writer: Option<Arc<dyn WalletWriteRepository>>,
|
||||||
settlement_writer: Option<Arc<dyn SettlementWriteRepository>>,
|
settlement_writer: Option<Arc<dyn SettlementWriteRepository>>,
|
||||||
shadow_result_reader: Option<Arc<dyn ShadowResultReadRepository>>,
|
|
||||||
shadow_result_writer: Option<Arc<dyn ShadowResultWriteRepository>>,
|
|
||||||
system_config_values: Option<Arc<RwLock<BTreeMap<String, StoredSystemConfigEntry>>>>,
|
system_config_values: Option<Arc<RwLock<BTreeMap<String, StoredSystemConfigEntry>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,14 +243,6 @@ impl fmt::Debug for GatewayDataState {
|
|||||||
.field("has_wallet_reader", &self.wallet_reader.is_some())
|
.field("has_wallet_reader", &self.wallet_reader.is_some())
|
||||||
.field("has_wallet_writer", &self.wallet_writer.is_some())
|
.field("has_wallet_writer", &self.wallet_writer.is_some())
|
||||||
.field("has_settlement_writer", &self.settlement_writer.is_some())
|
.field("has_settlement_writer", &self.settlement_writer.is_some())
|
||||||
.field(
|
|
||||||
"has_shadow_result_reader",
|
|
||||||
&self.shadow_result_reader.is_some(),
|
|
||||||
)
|
|
||||||
.field(
|
|
||||||
"has_shadow_result_writer",
|
|
||||||
&self.shadow_result_writer.is_some(),
|
|
||||||
)
|
|
||||||
.field(
|
.field(
|
||||||
"has_system_config_values",
|
"has_system_config_values",
|
||||||
&self.system_config_values.is_some(),
|
&self.system_config_values.is_some(),
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
use super::{
|
use super::{
|
||||||
merge_shadow_result_sample, read_decision_trace, read_provider_transport_snapshot,
|
read_decision_trace, read_provider_transport_snapshot, read_request_candidate_trace,
|
||||||
read_request_candidate_trace, AdjustWalletBalanceInput, AdminPaymentOrderListQuery,
|
AdjustWalletBalanceInput, AdminPaymentOrderListQuery, AdminWalletLedgerQuery,
|
||||||
AdminWalletLedgerQuery, AdminWalletListQuery, AdminWalletRefundRequestListQuery,
|
AdminWalletListQuery, AdminWalletRefundRequestListQuery, AnnouncementListQuery,
|
||||||
AnnouncementListQuery, CompleteAdminWalletRefundInput, CreateAnnouncementRecord,
|
CompleteAdminWalletRefundInput, CreateAnnouncementRecord, CreateManualWalletRechargeInput,
|
||||||
CreateManualWalletRechargeInput, CreateWalletRechargeOrderInput,
|
CreateWalletRechargeOrderInput, CreateWalletRechargeOrderOutcome,
|
||||||
CreateWalletRechargeOrderOutcome, CreateWalletRefundRequestInput,
|
CreateWalletRefundRequestInput, CreateWalletRefundRequestOutcome, CreditAdminPaymentOrderInput,
|
||||||
CreateWalletRefundRequestOutcome, CreditAdminPaymentOrderInput, DataLayerError, DecisionTrace,
|
DataLayerError, DecisionTrace, FailAdminWalletRefundInput, GatewayDataState,
|
||||||
FailAdminWalletRefundInput, GatewayDataState, GatewayProviderTransportSnapshot,
|
GatewayProviderTransportSnapshot, LocalVideoTaskReadResponse, ProcessAdminWalletRefundInput,
|
||||||
LocalVideoTaskReadResponse, ProcessAdminWalletRefundInput, ProcessPaymentCallbackInput,
|
ProcessPaymentCallbackInput, ProcessPaymentCallbackOutcome, RedisStreamRunner,
|
||||||
ProcessPaymentCallbackOutcome, RecordShadowResultSample, RedisStreamRunner, RequestAuditBundle,
|
RequestAuditBundle, RequestCandidateTrace, StoredAdminPaymentCallbackPage,
|
||||||
RequestCandidateTrace, ShadowResultLookupKey, StoredAdminPaymentCallbackPage,
|
|
||||||
StoredAdminPaymentOrder, StoredAdminPaymentOrderPage, StoredAdminWalletLedgerPage,
|
StoredAdminPaymentOrder, StoredAdminPaymentOrderPage, StoredAdminWalletLedgerPage,
|
||||||
StoredAdminWalletListPage, StoredAdminWalletRefund, StoredAdminWalletRefundPage,
|
StoredAdminWalletListPage, StoredAdminWalletRefund, StoredAdminWalletRefundPage,
|
||||||
StoredAdminWalletRefundRequestPage, StoredAdminWalletTransaction,
|
StoredAdminWalletRefundRequestPage, StoredAdminWalletTransaction,
|
||||||
StoredAdminWalletTransactionPage, StoredAnnouncement, StoredAnnouncementPage,
|
StoredAdminWalletTransactionPage, StoredAnnouncement, StoredAnnouncementPage,
|
||||||
StoredBillingModelContext, StoredProviderQuotaSnapshot, StoredProviderUsageSummary,
|
StoredBillingModelContext, StoredProviderQuotaSnapshot, StoredProviderUsageSummary,
|
||||||
StoredRequestUsageAudit, StoredShadowResult, StoredUsageSettlement, StoredUserAuthRecord,
|
StoredRequestUsageAudit, StoredUsageSettlement, StoredUserAuthRecord, StoredUserExportRow,
|
||||||
StoredUserExportRow, StoredUserSummary, StoredVideoTask, StoredWalletDailyUsageLedger,
|
StoredUserSummary, StoredVideoTask, StoredWalletDailyUsageLedger,
|
||||||
StoredWalletDailyUsageLedgerPage, StoredWalletSnapshot, UpdateAnnouncementRecord,
|
StoredWalletDailyUsageLedgerPage, StoredWalletSnapshot, UpdateAnnouncementRecord,
|
||||||
UpsertUsageRecord, UpsertVideoTask, UsageSettlementInput, VideoTaskLookupKey,
|
UpsertUsageRecord, UpsertVideoTask, UsageSettlementInput, VideoTaskLookupKey,
|
||||||
VideoTaskModelCount, VideoTaskQueryFilter, VideoTaskStatusCount, WalletLookupKey,
|
VideoTaskModelCount, VideoTaskQueryFilter, VideoTaskStatusCount, WalletLookupKey,
|
||||||
@@ -25,12 +24,6 @@ use super::{
|
|||||||
use aether_data_contracts::repository::usage::UsageAuditListQuery;
|
use aether_data_contracts::repository::usage::UsageAuditListQuery;
|
||||||
use aether_video_tasks_core::read_data_backed_video_task_response;
|
use aether_video_tasks_core::read_data_backed_video_task_response;
|
||||||
|
|
||||||
fn is_missing_shadow_results_relation_error(error: &DataLayerError) -> bool {
|
|
||||||
error
|
|
||||||
.to_string()
|
|
||||||
.contains("relation \"gateway_shadow_results\" does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GatewayDataState {
|
impl GatewayDataState {
|
||||||
pub(crate) async fn list_announcements(
|
pub(crate) async fn list_announcements(
|
||||||
&self,
|
&self,
|
||||||
@@ -873,68 +866,6 @@ impl GatewayDataState {
|
|||||||
) -> Result<Option<LocalVideoTaskReadResponse>, DataLayerError> {
|
) -> Result<Option<LocalVideoTaskReadResponse>, DataLayerError> {
|
||||||
read_data_backed_video_task_response(self, route_family, request_path).await
|
read_data_backed_video_task_response(self, route_family, request_path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) async fn write_shadow_result(
|
|
||||||
&self,
|
|
||||||
result: aether_data::repository::shadow_results::UpsertShadowResult,
|
|
||||||
) -> Result<Option<StoredShadowResult>, DataLayerError> {
|
|
||||||
match &self.shadow_result_writer {
|
|
||||||
Some(repository) => match repository.upsert(result).await {
|
|
||||||
Ok(stored) => Ok(Some(stored)),
|
|
||||||
Err(err) if is_missing_shadow_results_relation_error(&err) => Ok(None),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
},
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn record_shadow_result_sample(
|
|
||||||
&self,
|
|
||||||
sample: RecordShadowResultSample,
|
|
||||||
) -> Result<Option<StoredShadowResult>, DataLayerError> {
|
|
||||||
let Some(writer) = &self.shadow_result_writer else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let existing = match &self.shadow_result_reader {
|
|
||||||
Some(reader) => {
|
|
||||||
match reader
|
|
||||||
.find(ShadowResultLookupKey::TraceFingerprint {
|
|
||||||
trace_id: &sample.trace_id,
|
|
||||||
request_fingerprint: &sample.request_fingerprint,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(existing) => existing,
|
|
||||||
Err(err) if is_missing_shadow_results_relation_error(&err) => return Ok(None),
|
|
||||||
Err(err) => return Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
let merged = merge_shadow_result_sample(existing.as_ref(), sample);
|
|
||||||
|
|
||||||
match writer.upsert(merged).await {
|
|
||||||
Ok(stored) => Ok(Some(stored)),
|
|
||||||
Err(err) if is_missing_shadow_results_relation_error(&err) => Ok(None),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn list_recent_shadow_results(
|
|
||||||
&self,
|
|
||||||
limit: usize,
|
|
||||||
) -> Result<Vec<StoredShadowResult>, DataLayerError> {
|
|
||||||
match &self.shadow_result_reader {
|
|
||||||
Some(repository) => match repository.list_recent(limit).await {
|
|
||||||
Ok(results) => Ok(results),
|
|
||||||
Err(err) if is_missing_shadow_results_relation_error(&err) => Ok(Vec::new()),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
},
|
|
||||||
None => Ok(Vec::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -942,9 +873,8 @@ mod tests {
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use aether_data::repository::users::{InMemoryUserReadRepository, StoredUserExportRow};
|
use aether_data::repository::users::{InMemoryUserReadRepository, StoredUserExportRow};
|
||||||
use aether_data::DataLayerError;
|
|
||||||
|
|
||||||
use super::{is_missing_shadow_results_relation_error, GatewayDataState};
|
use super::GatewayDataState;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn lists_non_admin_export_users_from_user_reader() {
|
async fn lists_non_admin_export_users_from_user_reader() {
|
||||||
@@ -983,19 +913,4 @@ mod tests {
|
|||||||
Some(serde_json::json!({"gpt-4.1": {"cache_1h": true}}))
|
Some(serde_json::json!({"gpt-4.1": {"cache_1h": true}}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn detects_missing_shadow_results_relation_error_text() {
|
|
||||||
let missing_relation = DataLayerError::UnexpectedValue(
|
|
||||||
"postgres error: error returned from database: relation \"gateway_shadow_results\" does not exist"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
let other = DataLayerError::UnexpectedValue(
|
|
||||||
"postgres error: error returned from database: relation \"usage\" does not exist"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(is_missing_shadow_results_relation_error(&missing_relation));
|
|
||||||
assert!(!is_missing_shadow_results_relation_error(&other));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,8 +96,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ use super::{
|
|||||||
OAuthProviderWriteRepository, ProviderCatalogReadRepository, ProviderCatalogWriteRepository,
|
OAuthProviderWriteRepository, ProviderCatalogReadRepository, ProviderCatalogWriteRepository,
|
||||||
ProviderQuotaReadRepository, ProviderQuotaWriteRepository, ProxyNodeReadRepository,
|
ProviderQuotaReadRepository, ProviderQuotaWriteRepository, ProxyNodeReadRepository,
|
||||||
ProxyNodeWriteRepository, RequestCandidateReadRepository, RequestCandidateWriteRepository,
|
ProxyNodeWriteRepository, RequestCandidateReadRepository, RequestCandidateWriteRepository,
|
||||||
SettlementWriteRepository, ShadowResultReadRepository, ShadowResultWriteRepository,
|
SettlementWriteRepository, StoredSystemConfigEntry, StoredUserPreferenceRecord,
|
||||||
StoredSystemConfigEntry, StoredUserPreferenceRecord, UsageReadRepository, UsageWriteRepository,
|
UsageReadRepository, UsageWriteRepository, UserReadRepository, VideoTaskReadRepository,
|
||||||
UserReadRepository, VideoTaskReadRepository, VideoTaskWriteRepository, WalletReadRepository,
|
VideoTaskWriteRepository, WalletReadRepository, WalletWriteRepository,
|
||||||
WalletWriteRepository,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mod announcements;
|
mod announcements;
|
||||||
@@ -80,8 +79,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,8 +128,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,8 +173,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,8 +306,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,8 +367,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,8 +421,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,8 +484,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -544,8 +529,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -592,8 +575,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -651,8 +632,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -712,8 +691,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -757,8 +734,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -817,8 +792,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -870,8 +843,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: Some(wallet_reader),
|
wallet_reader: Some(wallet_reader),
|
||||||
wallet_writer: Some(wallet_writer),
|
wallet_writer: Some(wallet_writer),
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -928,8 +899,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: Some(wallet_reader),
|
wallet_reader: Some(wallet_reader),
|
||||||
wallet_writer: Some(wallet_writer),
|
wallet_writer: Some(wallet_writer),
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -987,8 +956,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: Some(wallet_reader),
|
wallet_reader: Some(wallet_reader),
|
||||||
wallet_writer: Some(wallet_writer),
|
wallet_writer: Some(wallet_writer),
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1045,8 +1012,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1092,8 +1057,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1139,8 +1102,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1198,8 +1159,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1262,8 +1221,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1309,8 +1266,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1361,8 +1316,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1430,8 +1383,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1494,8 +1445,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1542,8 +1491,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1590,8 +1537,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1640,106 +1585,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn with_shadow_result_writer_for_tests(
|
|
||||||
repository: Arc<dyn ShadowResultWriteRepository>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
config: GatewayDataConfig::disabled(),
|
|
||||||
backends: None,
|
|
||||||
auth_api_key_reader: None,
|
|
||||||
auth_api_key_writer: None,
|
|
||||||
auth_module_reader: None,
|
|
||||||
auth_module_writer: None,
|
|
||||||
announcement_reader: None,
|
|
||||||
announcement_writer: None,
|
|
||||||
management_token_reader: None,
|
|
||||||
management_token_writer: None,
|
|
||||||
oauth_provider_reader: None,
|
|
||||||
oauth_provider_writer: None,
|
|
||||||
proxy_node_reader: None,
|
|
||||||
proxy_node_writer: None,
|
|
||||||
billing_reader: None,
|
|
||||||
gemini_file_mapping_reader: None,
|
|
||||||
gemini_file_mapping_writer: None,
|
|
||||||
global_model_reader: None,
|
|
||||||
global_model_writer: None,
|
|
||||||
minimal_candidate_selection_reader: None,
|
|
||||||
request_candidate_reader: None,
|
|
||||||
request_candidate_writer: None,
|
|
||||||
provider_catalog_reader: None,
|
|
||||||
provider_catalog_writer: None,
|
|
||||||
provider_quota_reader: None,
|
|
||||||
provider_quota_writer: None,
|
|
||||||
usage_reader: None,
|
|
||||||
usage_writer: None,
|
|
||||||
user_reader: None,
|
|
||||||
user_preferences: None,
|
|
||||||
usage_worker_runner: None,
|
|
||||||
video_task_reader: None,
|
|
||||||
video_task_writer: None,
|
|
||||||
wallet_reader: None,
|
|
||||||
wallet_writer: None,
|
|
||||||
settlement_writer: None,
|
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: Some(repository),
|
|
||||||
system_config_values: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn with_shadow_result_repository_for_tests<T>(repository: Arc<T>) -> Self
|
|
||||||
where
|
|
||||||
T: aether_data::repository::shadow_results::ShadowResultRepository + 'static,
|
|
||||||
{
|
|
||||||
let shadow_result_reader: Arc<dyn ShadowResultReadRepository> = repository.clone();
|
|
||||||
let shadow_result_writer: Arc<dyn ShadowResultWriteRepository> = repository;
|
|
||||||
|
|
||||||
Self {
|
|
||||||
config: GatewayDataConfig::disabled(),
|
|
||||||
backends: None,
|
|
||||||
auth_api_key_reader: None,
|
|
||||||
auth_api_key_writer: None,
|
|
||||||
auth_module_reader: None,
|
|
||||||
auth_module_writer: None,
|
|
||||||
announcement_reader: None,
|
|
||||||
announcement_writer: None,
|
|
||||||
management_token_reader: None,
|
|
||||||
management_token_writer: None,
|
|
||||||
oauth_provider_reader: None,
|
|
||||||
oauth_provider_writer: None,
|
|
||||||
proxy_node_reader: None,
|
|
||||||
proxy_node_writer: None,
|
|
||||||
billing_reader: None,
|
|
||||||
gemini_file_mapping_reader: None,
|
|
||||||
gemini_file_mapping_writer: None,
|
|
||||||
global_model_reader: None,
|
|
||||||
global_model_writer: None,
|
|
||||||
minimal_candidate_selection_reader: None,
|
|
||||||
request_candidate_reader: None,
|
|
||||||
request_candidate_writer: None,
|
|
||||||
provider_catalog_reader: None,
|
|
||||||
provider_catalog_writer: None,
|
|
||||||
provider_quota_reader: None,
|
|
||||||
provider_quota_writer: None,
|
|
||||||
usage_reader: None,
|
|
||||||
usage_writer: None,
|
|
||||||
user_reader: None,
|
|
||||||
user_preferences: None,
|
|
||||||
usage_worker_runner: None,
|
|
||||||
video_task_reader: None,
|
|
||||||
video_task_writer: None,
|
|
||||||
wallet_reader: None,
|
|
||||||
wallet_writer: None,
|
|
||||||
settlement_writer: None,
|
|
||||||
shadow_result_reader: Some(shadow_result_reader),
|
|
||||||
shadow_result_writer: Some(shadow_result_writer),
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1786,8 +1631,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1834,8 +1677,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1890,8 +1731,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1947,8 +1786,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2007,8 +1844,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2073,29 +1908,24 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn with_auth_candidate_selection_provider_catalog_request_candidates_and_shadow_results_for_tests<
|
pub(crate) fn with_auth_candidate_selection_provider_catalog_request_candidates_for_tests<
|
||||||
T,
|
T,
|
||||||
U,
|
U,
|
||||||
V,
|
|
||||||
>(
|
>(
|
||||||
auth_api_key_repository: Arc<dyn AuthApiKeyReadRepository>,
|
auth_api_key_repository: Arc<dyn AuthApiKeyReadRepository>,
|
||||||
candidate_selection_repository: Arc<dyn MinimalCandidateSelectionReadRepository>,
|
candidate_selection_repository: Arc<dyn MinimalCandidateSelectionReadRepository>,
|
||||||
provider_catalog_repository: Arc<U>,
|
provider_catalog_repository: Arc<U>,
|
||||||
request_candidate_repository: Arc<T>,
|
request_candidate_repository: Arc<T>,
|
||||||
shadow_result_repository: Arc<V>,
|
|
||||||
encryption_key: impl Into<String>,
|
encryption_key: impl Into<String>,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
T: RequestCandidateRepository + 'static,
|
T: RequestCandidateRepository + 'static,
|
||||||
U: ProviderCatalogReadRepository + ProviderCatalogWriteRepository + 'static,
|
U: ProviderCatalogReadRepository + ProviderCatalogWriteRepository + 'static,
|
||||||
V: aether_data::repository::shadow_results::ShadowResultRepository + 'static,
|
|
||||||
{
|
{
|
||||||
let request_candidate_reader: Arc<dyn RequestCandidateReadRepository> =
|
let request_candidate_reader: Arc<dyn RequestCandidateReadRepository> =
|
||||||
request_candidate_repository.clone();
|
request_candidate_repository.clone();
|
||||||
@@ -2105,9 +1935,6 @@ impl GatewayDataState {
|
|||||||
provider_catalog_repository.clone();
|
provider_catalog_repository.clone();
|
||||||
let provider_catalog_writer: Arc<dyn ProviderCatalogWriteRepository> =
|
let provider_catalog_writer: Arc<dyn ProviderCatalogWriteRepository> =
|
||||||
provider_catalog_repository;
|
provider_catalog_repository;
|
||||||
let shadow_result_reader: Arc<dyn ShadowResultReadRepository> =
|
|
||||||
shadow_result_repository.clone();
|
|
||||||
let shadow_result_writer: Arc<dyn ShadowResultWriteRepository> = shadow_result_repository;
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
config: GatewayDataConfig::disabled().with_encryption_key(encryption_key),
|
config: GatewayDataConfig::disabled().with_encryption_key(encryption_key),
|
||||||
@@ -2146,8 +1973,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: Some(shadow_result_reader),
|
|
||||||
shadow_result_writer: Some(shadow_result_writer),
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2217,8 +2042,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2295,8 +2118,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: Some(wallet_reader),
|
wallet_reader: Some(wallet_reader),
|
||||||
wallet_writer: Some(wallet_writer),
|
wallet_writer: Some(wallet_writer),
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2355,8 +2176,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2406,8 +2225,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2453,8 +2270,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2506,8 +2321,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: Some(wallet_reader),
|
wallet_reader: Some(wallet_reader),
|
||||||
wallet_writer: Some(wallet_writer),
|
wallet_writer: Some(wallet_writer),
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2563,8 +2376,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: Some(wallet_reader),
|
wallet_reader: Some(wallet_reader),
|
||||||
wallet_writer: Some(wallet_writer),
|
wallet_writer: Some(wallet_writer),
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2621,8 +2432,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: Some(wallet_reader),
|
wallet_reader: Some(wallet_reader),
|
||||||
wallet_writer: Some(wallet_writer),
|
wallet_writer: Some(wallet_writer),
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2672,8 +2481,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,8 +102,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,8 +151,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,8 +204,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,8 +261,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,8 +327,6 @@ impl GatewayDataState {
|
|||||||
wallet_reader: None,
|
wallet_reader: None,
|
||||||
wallet_writer: None,
|
wallet_writer: None,
|
||||||
settlement_writer: None,
|
settlement_writer: None,
|
||||||
shadow_result_reader: None,
|
|
||||||
shadow_result_writer: None,
|
|
||||||
system_config_values: None,
|
system_config_values: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ use aether_data::repository::auth::{
|
|||||||
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
||||||
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
||||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||||
use aether_data::repository::shadow_results::{
|
|
||||||
InMemoryShadowResultRepository, RecordShadowResultSample, ShadowResultLookupKey,
|
|
||||||
ShadowResultMatchStatus, ShadowResultReadRepository, ShadowResultSampleOrigin,
|
|
||||||
ShadowResultWriteRepository, StoredShadowResult, UpsertShadowResult,
|
|
||||||
};
|
|
||||||
use aether_data::repository::usage::InMemoryUsageReadRepository;
|
use aether_data::repository::usage::InMemoryUsageReadRepository;
|
||||||
use aether_data::repository::video_tasks::InMemoryVideoTaskRepository;
|
use aether_data::repository::video_tasks::InMemoryVideoTaskRepository;
|
||||||
use aether_data::DataLayerError;
|
use aether_data::DataLayerError;
|
||||||
@@ -32,7 +27,6 @@ use aether_scheduler_core::{
|
|||||||
build_minimal_candidate_selection, BuildMinimalCandidateSelectionInput,
|
build_minimal_candidate_selection, BuildMinimalCandidateSelectionInput,
|
||||||
SchedulerAuthConstraints, SchedulerPriorityMode,
|
SchedulerAuthConstraints, SchedulerPriorityMode,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::{GatewayDataConfig, GatewayDataState};
|
use super::{GatewayDataConfig, GatewayDataState};
|
||||||
@@ -52,8 +46,6 @@ fn disabled_gateway_data_state_has_no_backends() {
|
|||||||
assert!(!state.has_proxy_node_writer());
|
assert!(!state.has_proxy_node_writer());
|
||||||
assert!(!state.has_usage_reader());
|
assert!(!state.has_usage_reader());
|
||||||
assert!(!state.has_video_task_reader());
|
assert!(!state.has_video_task_reader());
|
||||||
assert!(!state.has_shadow_result_reader());
|
|
||||||
assert!(!state.has_shadow_result_writer());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -73,8 +65,6 @@ async fn postgres_gateway_data_state_builds_video_task_reader() {
|
|||||||
assert!(state.has_proxy_node_writer());
|
assert!(state.has_proxy_node_writer());
|
||||||
assert!(state.has_usage_reader());
|
assert!(state.has_usage_reader());
|
||||||
assert!(state.has_video_task_reader());
|
assert!(state.has_video_task_reader());
|
||||||
assert!(state.has_shadow_result_reader());
|
|
||||||
assert!(state.has_shadow_result_writer());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -152,8 +142,6 @@ async fn app_state_wires_gateway_data_state_from_config() {
|
|||||||
assert!(state.data.has_proxy_node_writer());
|
assert!(state.data.has_proxy_node_writer());
|
||||||
assert!(state.data.has_usage_reader());
|
assert!(state.data.has_usage_reader());
|
||||||
assert!(state.data.has_video_task_reader());
|
assert!(state.data.has_video_task_reader());
|
||||||
assert!(state.data.has_shadow_result_reader());
|
|
||||||
assert!(state.data.has_shadow_result_writer());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_auth_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
|
fn sample_auth_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
|
||||||
@@ -283,40 +271,6 @@ fn sample_request_usage(request_id: &str) -> StoredRequestUsageAudit {
|
|||||||
.expect("usage should build")
|
.expect("usage should build")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct MissingShadowResultsRelationRepository;
|
|
||||||
|
|
||||||
fn missing_shadow_results_relation_error() -> DataLayerError {
|
|
||||||
DataLayerError::UnexpectedValue(
|
|
||||||
"postgres error: error returned from database: relation \"gateway_shadow_results\" does not exist"
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ShadowResultReadRepository for MissingShadowResultsRelationRepository {
|
|
||||||
async fn find(
|
|
||||||
&self,
|
|
||||||
_key: ShadowResultLookupKey<'_>,
|
|
||||||
) -> Result<Option<StoredShadowResult>, DataLayerError> {
|
|
||||||
Err(missing_shadow_results_relation_error())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_recent(&self, _limit: usize) -> Result<Vec<StoredShadowResult>, DataLayerError> {
|
|
||||||
Err(missing_shadow_results_relation_error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ShadowResultWriteRepository for MissingShadowResultsRelationRepository {
|
|
||||||
async fn upsert(
|
|
||||||
&self,
|
|
||||||
_result: UpsertShadowResult,
|
|
||||||
) -> Result<StoredShadowResult, DataLayerError> {
|
|
||||||
Err(missing_shadow_results_relation_error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_minimal_candidate_selection_row(
|
fn sample_minimal_candidate_selection_row(
|
||||||
provider_id: &str,
|
provider_id: &str,
|
||||||
provider_name: &str,
|
provider_name: &str,
|
||||||
@@ -830,162 +784,6 @@ async fn maps_gemini_video_task_repository_row_into_read_response() {
|
|||||||
assert_eq!(response.body_json["done"], true);
|
assert_eq!(response.body_json["done"], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn data_state_write_uses_configured_shadow_result_writer() {
|
|
||||||
let repository = Arc::new(InMemoryShadowResultRepository::default());
|
|
||||||
let state = GatewayDataState::with_shadow_result_writer_for_tests(repository.clone());
|
|
||||||
|
|
||||||
let written = state
|
|
||||||
.write_shadow_result(UpsertShadowResult {
|
|
||||||
trace_id: "trace-1".to_string(),
|
|
||||||
request_fingerprint: "fp-1".to_string(),
|
|
||||||
request_id: Some("req-1".to_string()),
|
|
||||||
route_family: Some("openai".to_string()),
|
|
||||||
route_kind: Some("chat".to_string()),
|
|
||||||
candidate_id: None,
|
|
||||||
rust_result_digest: Some("rust-digest".to_string()),
|
|
||||||
python_result_digest: None,
|
|
||||||
match_status: ShadowResultMatchStatus::Pending,
|
|
||||||
status_code: Some(200),
|
|
||||||
error_message: None,
|
|
||||||
created_at_unix_ms: 100,
|
|
||||||
updated_at_unix_secs: 100,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("write should succeed");
|
|
||||||
|
|
||||||
assert!(written.is_some());
|
|
||||||
let stored = repository
|
|
||||||
.find(ShadowResultLookupKey::TraceFingerprint {
|
|
||||||
trace_id: "trace-1",
|
|
||||||
request_fingerprint: "fp-1",
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("find should succeed");
|
|
||||||
assert_eq!(
|
|
||||||
stored.expect("stored result should exist").match_status,
|
|
||||||
ShadowResultMatchStatus::Pending
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn data_state_records_shadow_result_samples_and_merges_match_status() {
|
|
||||||
let repository = Arc::new(InMemoryShadowResultRepository::default());
|
|
||||||
let state = GatewayDataState::with_shadow_result_repository_for_tests(repository);
|
|
||||||
|
|
||||||
let first = state
|
|
||||||
.record_shadow_result_sample(RecordShadowResultSample {
|
|
||||||
trace_id: "trace-1".to_string(),
|
|
||||||
request_fingerprint: "fp-1".to_string(),
|
|
||||||
request_id: Some("req-1".to_string()),
|
|
||||||
route_family: Some("openai".to_string()),
|
|
||||||
route_kind: Some("chat".to_string()),
|
|
||||||
candidate_id: None,
|
|
||||||
origin: ShadowResultSampleOrigin::Rust,
|
|
||||||
result_digest: "digest-1".to_string(),
|
|
||||||
status_code: Some(200),
|
|
||||||
error_message: None,
|
|
||||||
recorded_at_unix_secs: 100,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("first record should succeed")
|
|
||||||
.expect("first stored result should exist");
|
|
||||||
assert_eq!(first.match_status, ShadowResultMatchStatus::Pending);
|
|
||||||
|
|
||||||
let second = state
|
|
||||||
.record_shadow_result_sample(RecordShadowResultSample {
|
|
||||||
trace_id: "trace-1".to_string(),
|
|
||||||
request_fingerprint: "fp-1".to_string(),
|
|
||||||
request_id: Some("req-1".to_string()),
|
|
||||||
route_family: Some("openai".to_string()),
|
|
||||||
route_kind: Some("chat".to_string()),
|
|
||||||
candidate_id: None,
|
|
||||||
origin: ShadowResultSampleOrigin::Python,
|
|
||||||
result_digest: "digest-1".to_string(),
|
|
||||||
status_code: Some(200),
|
|
||||||
error_message: None,
|
|
||||||
recorded_at_unix_secs: 200,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("second record should succeed")
|
|
||||||
.expect("second stored result should exist");
|
|
||||||
|
|
||||||
assert_eq!(second.match_status, ShadowResultMatchStatus::Match);
|
|
||||||
assert_eq!(second.created_at_unix_ms, 100);
|
|
||||||
assert_eq!(second.updated_at_unix_secs, 200);
|
|
||||||
assert_eq!(second.request_id.as_deref(), Some("req-1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn data_state_lists_recent_shadow_results_from_reader() {
|
|
||||||
let repository = Arc::new(InMemoryShadowResultRepository::default());
|
|
||||||
let state = GatewayDataState::with_shadow_result_repository_for_tests(repository.clone());
|
|
||||||
|
|
||||||
state
|
|
||||||
.record_shadow_result_sample(RecordShadowResultSample {
|
|
||||||
trace_id: "trace-1".to_string(),
|
|
||||||
request_fingerprint: "fp-1".to_string(),
|
|
||||||
request_id: Some("req-shadow-1".to_string()),
|
|
||||||
route_family: Some("openai".to_string()),
|
|
||||||
route_kind: Some("chat".to_string()),
|
|
||||||
candidate_id: None,
|
|
||||||
origin: ShadowResultSampleOrigin::Rust,
|
|
||||||
result_digest: "digest-1".to_string(),
|
|
||||||
status_code: Some(200),
|
|
||||||
error_message: None,
|
|
||||||
recorded_at_unix_secs: 100,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("record should succeed");
|
|
||||||
|
|
||||||
let recent = state
|
|
||||||
.list_recent_shadow_results(5)
|
|
||||||
.await
|
|
||||||
.expect("list recent should succeed");
|
|
||||||
|
|
||||||
assert_eq!(recent.len(), 1);
|
|
||||||
assert_eq!(recent[0].trace_id, "trace-1");
|
|
||||||
assert_eq!(recent[0].request_id.as_deref(), Some("req-shadow-1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn data_state_ignores_missing_shadow_results_relation_when_recording_sample() {
|
|
||||||
let repository = Arc::new(MissingShadowResultsRelationRepository);
|
|
||||||
let state = GatewayDataState::with_shadow_result_repository_for_tests(repository);
|
|
||||||
|
|
||||||
let recorded = state
|
|
||||||
.record_shadow_result_sample(RecordShadowResultSample {
|
|
||||||
trace_id: "trace-missing-shadow".to_string(),
|
|
||||||
request_fingerprint: "fp-missing-shadow".to_string(),
|
|
||||||
request_id: Some("req-missing-shadow".to_string()),
|
|
||||||
route_family: Some("openai".to_string()),
|
|
||||||
route_kind: Some("chat".to_string()),
|
|
||||||
candidate_id: None,
|
|
||||||
origin: ShadowResultSampleOrigin::Rust,
|
|
||||||
result_digest: "digest-missing-shadow".to_string(),
|
|
||||||
status_code: Some(200),
|
|
||||||
error_message: None,
|
|
||||||
recorded_at_unix_secs: 100,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("missing shadow result table should be ignored");
|
|
||||||
|
|
||||||
assert!(recorded.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn data_state_returns_empty_shadow_results_when_relation_missing() {
|
|
||||||
let repository = Arc::new(MissingShadowResultsRelationRepository);
|
|
||||||
let state = GatewayDataState::with_shadow_result_repository_for_tests(repository);
|
|
||||||
|
|
||||||
let recent = state
|
|
||||||
.list_recent_shadow_results(5)
|
|
||||||
.await
|
|
||||||
.expect("missing shadow result table should be ignored");
|
|
||||||
|
|
||||||
assert!(recent.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_request_candidate(
|
fn sample_request_candidate(
|
||||||
id: &str,
|
id: &str,
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
|
|||||||
@@ -75,6 +75,83 @@ async fn admin_monitoring_trace_request_returns_local_payload() {
|
|||||||
assert_eq!(payload["candidates"][0]["status_code"], json!(502));
|
assert_eq!(payload["candidates"][0]["status_code"], json!(502));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn admin_monitoring_trace_request_hides_format_conversion_disabled_candidates() {
|
||||||
|
let mut hidden_candidate = sample_candidate(
|
||||||
|
"cand-hidden",
|
||||||
|
"request-1",
|
||||||
|
0,
|
||||||
|
RequestCandidateStatus::Skipped,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
hidden_candidate.skip_reason = Some("format_conversion_disabled".to_string());
|
||||||
|
|
||||||
|
let mut visible_skipped_candidate = sample_candidate(
|
||||||
|
"cand-visible-skipped",
|
||||||
|
"request-1",
|
||||||
|
1,
|
||||||
|
RequestCandidateStatus::Skipped,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
visible_skipped_candidate.skip_reason = Some("transport_unsupported".to_string());
|
||||||
|
|
||||||
|
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![
|
||||||
|
hidden_candidate,
|
||||||
|
visible_skipped_candidate,
|
||||||
|
sample_candidate(
|
||||||
|
"cand-used",
|
||||||
|
"request-1",
|
||||||
|
2,
|
||||||
|
RequestCandidateStatus::Failed,
|
||||||
|
Some(101),
|
||||||
|
Some(33),
|
||||||
|
Some(502),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
let provider_catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||||
|
vec![sample_provider()],
|
||||||
|
vec![sample_endpoint()],
|
||||||
|
vec![sample_key()],
|
||||||
|
));
|
||||||
|
let state = AppState::new()
|
||||||
|
.expect("state should build")
|
||||||
|
.with_decision_trace_data_readers_for_tests(request_candidates, provider_catalog);
|
||||||
|
let context = request_context(
|
||||||
|
http::Method::GET,
|
||||||
|
"/api/admin/monitoring/trace/request-1?attempted_only=false",
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = local_monitoring_response(&state, &context)
|
||||||
|
.await
|
||||||
|
.expect("handler should not error")
|
||||||
|
.expect("route should be handled locally");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), http::StatusCode::OK);
|
||||||
|
let body = to_bytes(response.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.expect("body should read");
|
||||||
|
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json body should parse");
|
||||||
|
|
||||||
|
assert_eq!(payload["total_candidates"], json!(2));
|
||||||
|
assert_eq!(
|
||||||
|
payload["candidates"]
|
||||||
|
.as_array()
|
||||||
|
.expect("candidates should be an array")
|
||||||
|
.iter()
|
||||||
|
.map(|item| item["id"].as_str().unwrap_or_default())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec!["cand-visible-skipped", "cand-used"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["candidates"][0]["skip_reason"],
|
||||||
|
json!("transport_unsupported")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn admin_monitoring_trace_provider_stats_returns_local_payload() {
|
async fn admin_monitoring_trace_provider_stats_returns_local_payload() {
|
||||||
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![
|
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![
|
||||||
|
|||||||
@@ -8,13 +8,29 @@ use aether_admin::observability::monitoring::{
|
|||||||
build_admin_monitoring_trace_provider_stats_payload_response,
|
build_admin_monitoring_trace_provider_stats_payload_response,
|
||||||
build_admin_monitoring_trace_request_payload_response, parse_admin_monitoring_attempted_only,
|
build_admin_monitoring_trace_request_payload_response, parse_admin_monitoring_attempted_only,
|
||||||
};
|
};
|
||||||
use aether_data_contracts::repository::candidates::RequestCandidateStatus;
|
use aether_data_contracts::repository::candidates::{DecisionTrace, RequestCandidateStatus};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
|
const HIDDEN_TRACE_SKIP_REASONS: &[&str] = &["format_conversion_disabled"];
|
||||||
|
|
||||||
|
fn filter_admin_monitoring_trace_candidates(mut trace: DecisionTrace) -> DecisionTrace {
|
||||||
|
trace.candidates.retain(|item| {
|
||||||
|
let skip_reason = item
|
||||||
|
.candidate
|
||||||
|
.skip_reason
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
!skip_reason.is_some_and(|reason| HIDDEN_TRACE_SKIP_REASONS.contains(&reason))
|
||||||
|
});
|
||||||
|
trace.total_candidates = trace.candidates.len();
|
||||||
|
trace
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) async fn build_admin_monitoring_trace_request_response(
|
pub(super) async fn build_admin_monitoring_trace_request_response(
|
||||||
state: &AdminAppState<'_>,
|
state: &AdminAppState<'_>,
|
||||||
request_context: &AdminRequestContext<'_>,
|
request_context: &AdminRequestContext<'_>,
|
||||||
@@ -51,6 +67,7 @@ pub(super) async fn build_admin_monitoring_trace_request_response(
|
|||||||
attempted_only,
|
attempted_only,
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
let trace = filter_admin_monitoring_trace_candidates(trace);
|
||||||
|
|
||||||
Ok(build_admin_monitoring_trace_request_payload_response(
|
Ok(build_admin_monitoring_trace_request_payload_response(
|
||||||
&trace,
|
&trace,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::audit::{emit_admin_audit, record_shadow_result_non_blocking};
|
use crate::audit::emit_admin_audit;
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
CONTROL_ENDPOINT_SIGNATURE_HEADER, CONTROL_EXECUTION_RUNTIME_HEADER, CONTROL_REQUEST_ID_HEADER,
|
CONTROL_ENDPOINT_SIGNATURE_HEADER, CONTROL_EXECUTION_RUNTIME_HEADER, CONTROL_REQUEST_ID_HEADER,
|
||||||
CONTROL_ROUTE_CLASS_HEADER, CONTROL_ROUTE_FAMILY_HEADER, CONTROL_ROUTE_KIND_HEADER,
|
CONTROL_ROUTE_CLASS_HEADER, CONTROL_ROUTE_FAMILY_HEADER, CONTROL_ROUTE_KIND_HEADER,
|
||||||
@@ -165,16 +165,6 @@ pub(super) fn finalize_gateway_response(
|
|||||||
}
|
}
|
||||||
response.extensions_mut().insert(RequestLogEmitted);
|
response.extensions_mut().insert(RequestLogEmitted);
|
||||||
|
|
||||||
record_shadow_result_non_blocking(
|
|
||||||
state.clone(),
|
|
||||||
trace_id,
|
|
||||||
method,
|
|
||||||
path_and_query,
|
|
||||||
control_decision,
|
|
||||||
execution_path,
|
|
||||||
&response,
|
|
||||||
);
|
|
||||||
|
|
||||||
maybe_hold_axum_response_permit(response, request_permit)
|
maybe_hold_axum_response_permit(response, request_permit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
pub(crate) use crate::audit::record_shadow_result_non_blocking;
|
|
||||||
pub(crate) use crate::usage::http::{get_request_audit_bundle, get_request_usage_audit};
|
pub(crate) use crate::usage::http::{get_request_audit_bundle, get_request_usage_audit};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ mod candidate_queries;
|
|||||||
mod gemini_files;
|
mod gemini_files;
|
||||||
mod payments;
|
mod payments;
|
||||||
mod security;
|
mod security;
|
||||||
mod shadow_results;
|
|
||||||
mod usage_queries;
|
mod usage_queries;
|
||||||
mod user_preferences;
|
mod user_preferences;
|
||||||
mod wallet;
|
mod wallet;
|
||||||
@@ -100,14 +99,6 @@ impl AppState {
|
|||||||
self.data.has_provider_quota_writer()
|
self.data.has_provider_quota_writer()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_shadow_result_data_writer(&self) -> bool {
|
|
||||||
self.data.has_shadow_result_writer()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_shadow_result_data_reader(&self) -> bool {
|
|
||||||
self.data.has_shadow_result_reader()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn count_active_admin_users(&self) -> Result<u64, GatewayError> {
|
pub(crate) async fn count_active_admin_users(&self) -> Result<u64, GatewayError> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if let Some(store) = self.auth_user_store.as_ref() {
|
if let Some(store) = self.auth_user_store.as_ref() {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
use crate::{AppState, GatewayError};
|
|
||||||
|
|
||||||
impl AppState {
|
|
||||||
pub(crate) async fn record_shadow_result_sample(
|
|
||||||
&self,
|
|
||||||
sample: aether_data::repository::shadow_results::RecordShadowResultSample,
|
|
||||||
) -> Result<Option<aether_data::repository::shadow_results::StoredShadowResult>, GatewayError>
|
|
||||||
{
|
|
||||||
self.data
|
|
||||||
.record_shadow_result_sample(sample)
|
|
||||||
.await
|
|
||||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn list_recent_shadow_results(
|
|
||||||
&self,
|
|
||||||
limit: usize,
|
|
||||||
) -> Result<Vec<aether_data::repository::shadow_results::StoredShadowResult>, GatewayError>
|
|
||||||
{
|
|
||||||
self.data
|
|
||||||
.list_recent_shadow_results(limit)
|
|
||||||
.await
|
|
||||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -578,27 +578,4 @@ impl AppState {
|
|||||||
.insert(provider_type.trim().to_ascii_lowercase(), token_url.into());
|
.insert(provider_type.trim().to_ascii_lowercase(), token_url.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_shadow_result_data_writer_for_tests(
|
|
||||||
mut self,
|
|
||||||
repository: Arc<dyn aether_data::repository::shadow_results::ShadowResultWriteRepository>,
|
|
||||||
) -> Self {
|
|
||||||
self.replace_data_state(Arc::new(
|
|
||||||
GatewayDataState::with_shadow_result_writer_for_tests(repository),
|
|
||||||
));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn with_shadow_result_data_repository_for_tests<T>(
|
|
||||||
mut self,
|
|
||||||
repository: Arc<T>,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
T: aether_data::repository::shadow_results::ShadowResultRepository + 'static,
|
|
||||||
{
|
|
||||||
self.replace_data_state(Arc::new(
|
|
||||||
GatewayDataState::with_shadow_result_repository_for_tests(repository),
|
|
||||||
));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ fn gateway_small_runtime_shims_stay_deleted() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hooks_mod = read_workspace_file("apps/aether-gateway/src/hooks/mod.rs");
|
let hooks_mod = read_workspace_file("apps/aether-gateway/src/hooks/mod.rs");
|
||||||
assert!(
|
|
||||||
hooks_mod.contains("pub(crate) use crate::audit::record_shadow_result_non_blocking;"),
|
|
||||||
"hooks/mod.rs should re-export shadow audit directly from crate::audit"
|
|
||||||
);
|
|
||||||
assert!(
|
assert!(
|
||||||
hooks_mod.contains("pub(crate) use crate::usage::http::{get_request_audit_bundle, get_request_usage_audit};"),
|
hooks_mod.contains("pub(crate) use crate::usage::http::{get_request_audit_bundle, get_request_usage_audit};"),
|
||||||
"hooks/mod.rs should re-export request audit helpers directly from usage/http"
|
"hooks/mod.rs should re-export request audit helpers directly from usage/http"
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ use aether_data::repository::auth::{
|
|||||||
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
||||||
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
||||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||||
use aether_data::repository::shadow_results::{
|
|
||||||
InMemoryShadowResultRepository, ShadowResultMatchStatus, ShadowResultReadRepository,
|
|
||||||
};
|
|
||||||
use aether_data::repository::usage::InMemoryUsageReadRepository;
|
use aether_data::repository::usage::InMemoryUsageReadRepository;
|
||||||
use aether_data_contracts::repository::candidate_selection::{
|
use aether_data_contracts::repository::candidate_selection::{
|
||||||
StoredMinimalCandidateSelectionRow, StoredProviderModelMapping,
|
StoredMinimalCandidateSelectionRow, StoredProviderModelMapping,
|
||||||
@@ -160,110 +157,6 @@ fn sample_local_openai_key() -> StoredProviderCatalogKey {
|
|||||||
.expect("key transport should build")
|
.expect("key transport should build")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn gateway_records_candidate_id_in_shadow_result_for_local_execution_response() {
|
|
||||||
let repository = Arc::new(InMemoryShadowResultRepository::default());
|
|
||||||
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
|
|
||||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
|
||||||
Some(hash_api_key("sk-client-openai-audit-local")),
|
|
||||||
sample_local_openai_auth_snapshot(
|
|
||||||
"api-key-openai-audit-local-1",
|
|
||||||
"user-openai-audit-local-1",
|
|
||||||
),
|
|
||||||
)]));
|
|
||||||
let candidate_selection_repository =
|
|
||||||
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
|
||||||
sample_local_openai_candidate_row(),
|
|
||||||
]));
|
|
||||||
|
|
||||||
let provider = Router::new().route(
|
|
||||||
"/v1/chat/completions",
|
|
||||||
any(|_request: Request| async move {
|
|
||||||
Json(json!({
|
|
||||||
"id": "chatcmpl-shadow-local-123",
|
|
||||||
"object": "chat.completion",
|
|
||||||
"model": "gpt-5-upstream",
|
|
||||||
"choices": [],
|
|
||||||
"usage": {
|
|
||||||
"prompt_tokens": 2,
|
|
||||||
"completion_tokens": 3,
|
|
||||||
"total_tokens": 5
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let (provider_url, provider_handle) = start_server(provider).await;
|
|
||||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
|
||||||
vec![sample_local_openai_provider()],
|
|
||||||
vec![sample_local_openai_endpoint(provider_url)],
|
|
||||||
vec![sample_local_openai_key()],
|
|
||||||
));
|
|
||||||
let gateway_state = AppState::new()
|
|
||||||
.expect("gateway state should build")
|
|
||||||
.with_data_state_for_tests(
|
|
||||||
crate::data::GatewayDataState::with_auth_candidate_selection_provider_catalog_request_candidates_and_shadow_results_for_tests(
|
|
||||||
auth_repository,
|
|
||||||
candidate_selection_repository,
|
|
||||||
provider_catalog_repository,
|
|
||||||
Arc::clone(&request_candidate_repository),
|
|
||||||
Arc::clone(&repository),
|
|
||||||
DEVELOPMENT_ENCRYPTION_KEY,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let gateway = build_router_with_state(gateway_state);
|
|
||||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
|
||||||
|
|
||||||
let response = reqwest::Client::new()
|
|
||||||
.post(format!("{gateway_url}/v1/chat/completions"))
|
|
||||||
.header(http::header::CONTENT_TYPE, "application/json")
|
|
||||||
.header(
|
|
||||||
http::header::AUTHORIZATION,
|
|
||||||
"Bearer sk-client-openai-audit-local",
|
|
||||||
)
|
|
||||||
.header(TRACE_ID_HEADER, "req-shadow-direct-123")
|
|
||||||
.body("{\"model\":\"gpt-5\",\"messages\":[]}")
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.expect("request should succeed");
|
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
|
||||||
let candidate_id = response
|
|
||||||
.headers()
|
|
||||||
.get(CONTROL_CANDIDATE_ID_HEADER)
|
|
||||||
.and_then(|value| value.to_str().ok())
|
|
||||||
.expect("candidate id header should exist")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
for _ in 0..50 {
|
|
||||||
if repository
|
|
||||||
.list_recent(1)
|
|
||||||
.await
|
|
||||||
.map(|rows| !rows.is_empty())
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stored = repository
|
|
||||||
.list_recent(1)
|
|
||||||
.await
|
|
||||||
.expect("list should succeed")
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.expect("stored result should exist");
|
|
||||||
assert_eq!(stored.request_id.as_deref(), Some("req-shadow-direct-123"));
|
|
||||||
assert_eq!(stored.candidate_id.as_deref(), Some(candidate_id.as_str()));
|
|
||||||
assert_eq!(stored.route_family.as_deref(), Some("openai"));
|
|
||||||
assert_eq!(stored.route_kind.as_deref(), Some("chat"));
|
|
||||||
assert_eq!(stored.match_status, ShadowResultMatchStatus::Pending);
|
|
||||||
|
|
||||||
gateway_handle.abort();
|
|
||||||
provider_handle.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_exposes_request_id_header_for_local_execution_response() {
|
async fn gateway_exposes_request_id_header_for_local_execution_response() {
|
||||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||||
|
|||||||
@@ -141,10 +141,8 @@ mod tests {
|
|||||||
assert!(backends.read().provider_catalog().is_none());
|
assert!(backends.read().provider_catalog().is_none());
|
||||||
assert!(backends.read().usage().is_none());
|
assert!(backends.read().usage().is_none());
|
||||||
assert!(backends.read().video_tasks().is_none());
|
assert!(backends.read().video_tasks().is_none());
|
||||||
assert!(backends.read().shadow_results().is_none());
|
|
||||||
assert!(backends.transactions().postgres().is_none());
|
assert!(backends.transactions().postgres().is_none());
|
||||||
assert!(backends.workers().redis().is_none());
|
assert!(backends.workers().redis().is_none());
|
||||||
assert!(backends.write().shadow_results().is_none());
|
|
||||||
assert!(backends.write().settlement().is_none());
|
assert!(backends.write().settlement().is_none());
|
||||||
assert!(backends.write().usage().is_none());
|
assert!(backends.write().usage().is_none());
|
||||||
}
|
}
|
||||||
@@ -184,9 +182,7 @@ mod tests {
|
|||||||
assert!(backends.read().usage().is_some());
|
assert!(backends.read().usage().is_some());
|
||||||
assert!(backends.read().video_tasks().is_some());
|
assert!(backends.read().video_tasks().is_some());
|
||||||
assert!(backends.read().wallets().is_some());
|
assert!(backends.read().wallets().is_some());
|
||||||
assert!(backends.read().shadow_results().is_some());
|
|
||||||
assert!(backends.transactions().postgres().is_some());
|
assert!(backends.transactions().postgres().is_some());
|
||||||
assert!(backends.write().shadow_results().is_some());
|
|
||||||
assert!(backends.write().auth_modules().is_some());
|
assert!(backends.write().auth_modules().is_some());
|
||||||
assert!(backends.write().gemini_file_mappings().is_some());
|
assert!(backends.write().gemini_file_mappings().is_some());
|
||||||
assert!(backends.write().management_tokens().is_some());
|
assert!(backends.write().management_tokens().is_some());
|
||||||
@@ -222,7 +218,6 @@ mod tests {
|
|||||||
assert!(backends.read().global_models().is_none());
|
assert!(backends.read().global_models().is_none());
|
||||||
assert!(backends.read().oauth_providers().is_none());
|
assert!(backends.read().oauth_providers().is_none());
|
||||||
assert!(backends.transactions().postgres().is_none());
|
assert!(backends.transactions().postgres().is_none());
|
||||||
assert!(backends.write().shadow_results().is_none());
|
|
||||||
assert!(backends.write().settlement().is_none());
|
assert!(backends.write().settlement().is_none());
|
||||||
assert!(backends.write().usage().is_none());
|
assert!(backends.write().usage().is_none());
|
||||||
assert!(backends.config().redis.is_some());
|
assert!(backends.config().redis.is_some());
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ use crate::repository::quota::{
|
|||||||
ProviderQuotaReadRepository, ProviderQuotaWriteRepository, SqlxProviderQuotaRepository,
|
ProviderQuotaReadRepository, ProviderQuotaWriteRepository, SqlxProviderQuotaRepository,
|
||||||
};
|
};
|
||||||
use crate::repository::settlement::{SettlementWriteRepository, SqlxSettlementRepository};
|
use crate::repository::settlement::{SettlementWriteRepository, SqlxSettlementRepository};
|
||||||
use crate::repository::shadow_results::{
|
|
||||||
ShadowResultReadRepository, ShadowResultWriteRepository, SqlxShadowResultRepository,
|
|
||||||
};
|
|
||||||
use crate::repository::system::{AdminSystemStats, StoredSystemConfigEntry};
|
use crate::repository::system::{AdminSystemStats, StoredSystemConfigEntry};
|
||||||
use crate::repository::usage::{
|
use crate::repository::usage::{
|
||||||
SqlxUsageReadRepository, UsageReadRepository, UsageWriteRepository,
|
SqlxUsageReadRepository, UsageReadRepository, UsageWriteRepository,
|
||||||
@@ -287,18 +284,10 @@ impl PostgresBackend {
|
|||||||
PostgresLeaseRunner::new(self.transaction_runner(), config)
|
PostgresLeaseRunner::new(self.transaction_runner(), config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shadow_result_write_repository(&self) -> Arc<dyn ShadowResultWriteRepository> {
|
|
||||||
Arc::new(SqlxShadowResultRepository::new(self.pool_clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn provider_quota_write_repository(&self) -> Arc<dyn ProviderQuotaWriteRepository> {
|
pub fn provider_quota_write_repository(&self) -> Arc<dyn ProviderQuotaWriteRepository> {
|
||||||
Arc::new(SqlxProviderQuotaRepository::new(self.pool_clone()))
|
Arc::new(SqlxProviderQuotaRepository::new(self.pool_clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shadow_result_read_repository(&self) -> Arc<dyn ShadowResultReadRepository> {
|
|
||||||
Arc::new(SqlxShadowResultRepository::new(self.pool_clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn find_system_config_value(
|
pub async fn find_system_config_value(
|
||||||
&self,
|
&self,
|
||||||
key: &str,
|
key: &str,
|
||||||
@@ -465,8 +454,6 @@ mod tests {
|
|||||||
let _lease_runner = backend
|
let _lease_runner = backend
|
||||||
.lease_runner(PostgresLeaseRunnerConfig::default())
|
.lease_runner(PostgresLeaseRunnerConfig::default())
|
||||||
.expect("lease runner should build");
|
.expect("lease runner should build");
|
||||||
let _shadow_result_reader = backend.shadow_result_read_repository();
|
|
||||||
let _shadow_result_writer = backend.shadow_result_write_repository();
|
|
||||||
let _provider_quota_writer = backend.provider_quota_write_repository();
|
let _provider_quota_writer = backend.provider_quota_write_repository();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ use crate::repository::oauth_providers::OAuthProviderReadRepository;
|
|||||||
use crate::repository::provider_catalog::ProviderCatalogReadRepository;
|
use crate::repository::provider_catalog::ProviderCatalogReadRepository;
|
||||||
use crate::repository::proxy_nodes::ProxyNodeReadRepository;
|
use crate::repository::proxy_nodes::ProxyNodeReadRepository;
|
||||||
use crate::repository::quota::ProviderQuotaReadRepository;
|
use crate::repository::quota::ProviderQuotaReadRepository;
|
||||||
use crate::repository::shadow_results::ShadowResultReadRepository;
|
|
||||||
use crate::repository::usage::UsageReadRepository;
|
use crate::repository::usage::UsageReadRepository;
|
||||||
use crate::repository::users::UserReadRepository;
|
use crate::repository::users::UserReadRepository;
|
||||||
use crate::repository::video_tasks::VideoTaskReadRepository;
|
use crate::repository::video_tasks::VideoTaskReadRepository;
|
||||||
@@ -40,7 +39,6 @@ pub struct DataReadRepositories {
|
|||||||
users: Option<Arc<dyn UserReadRepository>>,
|
users: Option<Arc<dyn UserReadRepository>>,
|
||||||
video_tasks: Option<Arc<dyn VideoTaskReadRepository>>,
|
video_tasks: Option<Arc<dyn VideoTaskReadRepository>>,
|
||||||
wallets: Option<Arc<dyn WalletReadRepository>>,
|
wallets: Option<Arc<dyn WalletReadRepository>>,
|
||||||
shadow_results: Option<Arc<dyn ShadowResultReadRepository>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for DataReadRepositories {
|
impl fmt::Debug for DataReadRepositories {
|
||||||
@@ -69,7 +67,6 @@ impl fmt::Debug for DataReadRepositories {
|
|||||||
.field("has_users", &self.users.is_some())
|
.field("has_users", &self.users.is_some())
|
||||||
.field("has_video_tasks", &self.video_tasks.is_some())
|
.field("has_video_tasks", &self.video_tasks.is_some())
|
||||||
.field("has_wallets", &self.wallets.is_some())
|
.field("has_wallets", &self.wallets.is_some())
|
||||||
.field("has_shadow_results", &self.shadow_results.is_some())
|
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +93,6 @@ impl DataReadRepositories {
|
|||||||
users: postgres.map(PostgresBackend::user_read_repository),
|
users: postgres.map(PostgresBackend::user_read_repository),
|
||||||
video_tasks: postgres.map(PostgresBackend::video_task_read_repository),
|
video_tasks: postgres.map(PostgresBackend::video_task_read_repository),
|
||||||
wallets: postgres.map(PostgresBackend::wallet_read_repository),
|
wallets: postgres.map(PostgresBackend::wallet_read_repository),
|
||||||
shadow_results: postgres.map(PostgresBackend::shadow_result_read_repository),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,10 +166,6 @@ impl DataReadRepositories {
|
|||||||
self.wallets.clone()
|
self.wallets.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shadow_results(&self) -> Option<Arc<dyn ShadowResultReadRepository>> {
|
|
||||||
self.shadow_results.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_any(&self) -> bool {
|
pub fn has_any(&self) -> bool {
|
||||||
self.auth_api_keys.is_some()
|
self.auth_api_keys.is_some()
|
||||||
|| self.announcements.is_some()
|
|| self.announcements.is_some()
|
||||||
@@ -192,7 +184,6 @@ impl DataReadRepositories {
|
|||||||
|| self.users.is_some()
|
|| self.users.is_some()
|
||||||
|| self.video_tasks.is_some()
|
|| self.video_tasks.is_some()
|
||||||
|| self.wallets.is_some()
|
|| self.wallets.is_some()
|
||||||
|| self.shadow_results.is_some()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +226,5 @@ mod tests {
|
|||||||
assert!(read.usage().is_some());
|
assert!(read.usage().is_some());
|
||||||
assert!(read.video_tasks().is_some());
|
assert!(read.video_tasks().is_some());
|
||||||
assert!(read.wallets().is_some());
|
assert!(read.wallets().is_some());
|
||||||
assert!(read.shadow_results().is_some());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use crate::repository::provider_catalog::ProviderCatalogWriteRepository;
|
|||||||
use crate::repository::proxy_nodes::ProxyNodeWriteRepository;
|
use crate::repository::proxy_nodes::ProxyNodeWriteRepository;
|
||||||
use crate::repository::quota::ProviderQuotaWriteRepository;
|
use crate::repository::quota::ProviderQuotaWriteRepository;
|
||||||
use crate::repository::settlement::SettlementWriteRepository;
|
use crate::repository::settlement::SettlementWriteRepository;
|
||||||
use crate::repository::shadow_results::ShadowResultWriteRepository;
|
|
||||||
use crate::repository::usage::UsageWriteRepository;
|
use crate::repository::usage::UsageWriteRepository;
|
||||||
use crate::repository::video_tasks::VideoTaskWriteRepository;
|
use crate::repository::video_tasks::VideoTaskWriteRepository;
|
||||||
use crate::repository::wallet::WalletWriteRepository;
|
use crate::repository::wallet::WalletWriteRepository;
|
||||||
@@ -24,7 +23,6 @@ pub struct DataWriteRepositories {
|
|||||||
announcements: Option<Arc<dyn AnnouncementWriteRepository>>,
|
announcements: Option<Arc<dyn AnnouncementWriteRepository>>,
|
||||||
auth_api_keys: Option<Arc<dyn AuthApiKeyWriteRepository>>,
|
auth_api_keys: Option<Arc<dyn AuthApiKeyWriteRepository>>,
|
||||||
auth_modules: Option<Arc<dyn AuthModuleWriteRepository>>,
|
auth_modules: Option<Arc<dyn AuthModuleWriteRepository>>,
|
||||||
shadow_results: Option<Arc<dyn ShadowResultWriteRepository>>,
|
|
||||||
request_candidates: Option<Arc<dyn RequestCandidateWriteRepository>>,
|
request_candidates: Option<Arc<dyn RequestCandidateWriteRepository>>,
|
||||||
gemini_file_mappings: Option<Arc<dyn GeminiFileMappingWriteRepository>>,
|
gemini_file_mappings: Option<Arc<dyn GeminiFileMappingWriteRepository>>,
|
||||||
global_models: Option<Arc<dyn GlobalModelWriteRepository>>,
|
global_models: Option<Arc<dyn GlobalModelWriteRepository>>,
|
||||||
@@ -45,7 +43,6 @@ impl fmt::Debug for DataWriteRepositories {
|
|||||||
.field("has_announcements", &self.announcements.is_some())
|
.field("has_announcements", &self.announcements.is_some())
|
||||||
.field("has_auth_api_keys", &self.auth_api_keys.is_some())
|
.field("has_auth_api_keys", &self.auth_api_keys.is_some())
|
||||||
.field("has_auth_modules", &self.auth_modules.is_some())
|
.field("has_auth_modules", &self.auth_modules.is_some())
|
||||||
.field("has_shadow_results", &self.shadow_results.is_some())
|
|
||||||
.field("has_request_candidates", &self.request_candidates.is_some())
|
.field("has_request_candidates", &self.request_candidates.is_some())
|
||||||
.field(
|
.field(
|
||||||
"has_gemini_file_mappings",
|
"has_gemini_file_mappings",
|
||||||
@@ -71,7 +68,6 @@ impl DataWriteRepositories {
|
|||||||
announcements: postgres.map(PostgresBackend::announcement_write_repository),
|
announcements: postgres.map(PostgresBackend::announcement_write_repository),
|
||||||
auth_api_keys: postgres.map(PostgresBackend::auth_api_key_write_repository),
|
auth_api_keys: postgres.map(PostgresBackend::auth_api_key_write_repository),
|
||||||
auth_modules: postgres.map(PostgresBackend::auth_module_write_repository),
|
auth_modules: postgres.map(PostgresBackend::auth_module_write_repository),
|
||||||
shadow_results: postgres.map(PostgresBackend::shadow_result_write_repository),
|
|
||||||
request_candidates: postgres.map(PostgresBackend::request_candidate_write_repository),
|
request_candidates: postgres.map(PostgresBackend::request_candidate_write_repository),
|
||||||
gemini_file_mappings: postgres
|
gemini_file_mappings: postgres
|
||||||
.map(PostgresBackend::gemini_file_mapping_write_repository),
|
.map(PostgresBackend::gemini_file_mapping_write_repository),
|
||||||
@@ -88,10 +84,6 @@ impl DataWriteRepositories {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shadow_results(&self) -> Option<Arc<dyn ShadowResultWriteRepository>> {
|
|
||||||
self.shadow_results.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn announcements(&self) -> Option<Arc<dyn AnnouncementWriteRepository>> {
|
pub fn announcements(&self) -> Option<Arc<dyn AnnouncementWriteRepository>> {
|
||||||
self.announcements.clone()
|
self.announcements.clone()
|
||||||
}
|
}
|
||||||
@@ -156,7 +148,6 @@ impl DataWriteRepositories {
|
|||||||
self.announcements.is_some()
|
self.announcements.is_some()
|
||||||
|| self.auth_api_keys.is_some()
|
|| self.auth_api_keys.is_some()
|
||||||
|| self.auth_modules.is_some()
|
|| self.auth_modules.is_some()
|
||||||
|| self.shadow_results.is_some()
|
|
||||||
|| self.request_candidates.is_some()
|
|| self.request_candidates.is_some()
|
||||||
|| self.gemini_file_mappings.is_some()
|
|| self.gemini_file_mappings.is_some()
|
||||||
|| self.global_models.is_some()
|
|| self.global_models.is_some()
|
||||||
@@ -179,7 +170,7 @@ mod tests {
|
|||||||
use crate::postgres::PostgresPoolConfig;
|
use crate::postgres::PostgresPoolConfig;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn builds_shadow_result_writer_from_postgres_backend() {
|
async fn builds_write_repositories_from_postgres_backend() {
|
||||||
let backend = PostgresBackend::from_config(PostgresPoolConfig {
|
let backend = PostgresBackend::from_config(PostgresPoolConfig {
|
||||||
database_url: "postgres://localhost/aether".to_string(),
|
database_url: "postgres://localhost/aether".to_string(),
|
||||||
min_connections: 1,
|
min_connections: 1,
|
||||||
@@ -198,7 +189,6 @@ mod tests {
|
|||||||
assert!(write.announcements().is_some());
|
assert!(write.announcements().is_some());
|
||||||
assert!(write.auth_api_keys().is_some());
|
assert!(write.auth_api_keys().is_some());
|
||||||
assert!(write.auth_modules().is_some());
|
assert!(write.auth_modules().is_some());
|
||||||
assert!(write.shadow_results().is_some());
|
|
||||||
assert!(write.request_candidates().is_some());
|
assert!(write.request_candidates().is_some());
|
||||||
assert!(write.gemini_file_mappings().is_some());
|
assert!(write.gemini_file_mappings().is_some());
|
||||||
assert!(write.global_models().is_some());
|
assert!(write.global_models().is_some());
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ pub mod provider_oauth;
|
|||||||
pub mod proxy_nodes;
|
pub mod proxy_nodes;
|
||||||
pub mod quota;
|
pub mod quota;
|
||||||
pub mod settlement;
|
pub mod settlement;
|
||||||
pub mod shadow_results;
|
|
||||||
pub mod system;
|
pub mod system;
|
||||||
pub mod usage;
|
pub mod usage;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
use std::sync::RwLock;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use super::types::{
|
|
||||||
ShadowResultLookupKey, ShadowResultReadRepository, ShadowResultWriteRepository,
|
|
||||||
StoredShadowResult, UpsertShadowResult,
|
|
||||||
};
|
|
||||||
use crate::DataLayerError;
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct InMemoryShadowResultRepository {
|
|
||||||
results: RwLock<BTreeMap<(String, String), StoredShadowResult>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ShadowResultReadRepository for InMemoryShadowResultRepository {
|
|
||||||
async fn find(
|
|
||||||
&self,
|
|
||||||
key: ShadowResultLookupKey<'_>,
|
|
||||||
) -> Result<Option<StoredShadowResult>, DataLayerError> {
|
|
||||||
let results = self.results.read().expect("shadow result repository lock");
|
|
||||||
Ok(match key {
|
|
||||||
ShadowResultLookupKey::TraceFingerprint {
|
|
||||||
trace_id,
|
|
||||||
request_fingerprint,
|
|
||||||
} => results
|
|
||||||
.get(&(trace_id.to_string(), request_fingerprint.to_string()))
|
|
||||||
.cloned(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_recent(&self, limit: usize) -> Result<Vec<StoredShadowResult>, DataLayerError> {
|
|
||||||
if limit == 0 {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut results = self
|
|
||||||
.results
|
|
||||||
.read()
|
|
||||||
.expect("shadow result repository lock")
|
|
||||||
.values()
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
results.sort_by(|left, right| right.updated_at_unix_secs.cmp(&left.updated_at_unix_secs));
|
|
||||||
results.truncate(limit);
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ShadowResultWriteRepository for InMemoryShadowResultRepository {
|
|
||||||
async fn upsert(
|
|
||||||
&self,
|
|
||||||
result: UpsertShadowResult,
|
|
||||||
) -> Result<StoredShadowResult, DataLayerError> {
|
|
||||||
let stored = result.into_stored();
|
|
||||||
let mut results = self.results.write().expect("shadow result repository lock");
|
|
||||||
results.insert(
|
|
||||||
(stored.trace_id.clone(), stored.request_fingerprint.clone()),
|
|
||||||
stored.clone(),
|
|
||||||
);
|
|
||||||
Ok(stored)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::InMemoryShadowResultRepository;
|
|
||||||
use crate::repository::shadow_results::{
|
|
||||||
ShadowResultLookupKey, ShadowResultMatchStatus, ShadowResultReadRepository,
|
|
||||||
ShadowResultWriteRepository, UpsertShadowResult,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn sample_result(
|
|
||||||
trace_id: &str,
|
|
||||||
request_fingerprint: &str,
|
|
||||||
updated_at_unix_secs: u64,
|
|
||||||
) -> UpsertShadowResult {
|
|
||||||
UpsertShadowResult {
|
|
||||||
trace_id: trace_id.to_string(),
|
|
||||||
request_fingerprint: request_fingerprint.to_string(),
|
|
||||||
request_id: Some(format!("req-{trace_id}")),
|
|
||||||
route_family: Some("openai".to_string()),
|
|
||||||
route_kind: Some("chat".to_string()),
|
|
||||||
candidate_id: Some("cand-1".to_string()),
|
|
||||||
rust_result_digest: Some("rust-digest".to_string()),
|
|
||||||
python_result_digest: Some("python-digest".to_string()),
|
|
||||||
match_status: ShadowResultMatchStatus::Match,
|
|
||||||
status_code: Some(200),
|
|
||||||
error_message: None,
|
|
||||||
created_at_unix_ms: updated_at_unix_secs.saturating_sub(10),
|
|
||||||
updated_at_unix_secs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn reads_result_by_trace_and_fingerprint() {
|
|
||||||
let repo = InMemoryShadowResultRepository::default();
|
|
||||||
repo.upsert(sample_result("trace-1", "fp-1", 100))
|
|
||||||
.await
|
|
||||||
.expect("upsert should succeed");
|
|
||||||
|
|
||||||
assert!(repo
|
|
||||||
.find(ShadowResultLookupKey::TraceFingerprint {
|
|
||||||
trace_id: "trace-1",
|
|
||||||
request_fingerprint: "fp-1",
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("find should succeed")
|
|
||||||
.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn list_recent_returns_results_in_descending_update_order() {
|
|
||||||
let repo = InMemoryShadowResultRepository::default();
|
|
||||||
repo.upsert(sample_result("trace-1", "fp-1", 100))
|
|
||||||
.await
|
|
||||||
.expect("upsert should succeed");
|
|
||||||
repo.upsert(sample_result("trace-2", "fp-2", 200))
|
|
||||||
.await
|
|
||||||
.expect("upsert should succeed");
|
|
||||||
|
|
||||||
let recent = repo
|
|
||||||
.list_recent(10)
|
|
||||||
.await
|
|
||||||
.expect("list recent should succeed");
|
|
||||||
assert_eq!(recent.len(), 2);
|
|
||||||
assert_eq!(recent[0].trace_id, "trace-2");
|
|
||||||
assert_eq!(recent[1].trace_id, "trace-1");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn upsert_replaces_existing_shadow_result() {
|
|
||||||
let repo = InMemoryShadowResultRepository::default();
|
|
||||||
repo.upsert(sample_result("trace-1", "fp-1", 100))
|
|
||||||
.await
|
|
||||||
.expect("upsert should succeed");
|
|
||||||
repo.upsert(UpsertShadowResult {
|
|
||||||
trace_id: "trace-1".to_string(),
|
|
||||||
request_fingerprint: "fp-1".to_string(),
|
|
||||||
request_id: Some("req-trace-1".to_string()),
|
|
||||||
route_family: Some("openai".to_string()),
|
|
||||||
route_kind: Some("chat".to_string()),
|
|
||||||
candidate_id: Some("cand-2".to_string()),
|
|
||||||
rust_result_digest: Some("rust-digest-2".to_string()),
|
|
||||||
python_result_digest: Some("python-digest-2".to_string()),
|
|
||||||
match_status: ShadowResultMatchStatus::Mismatch,
|
|
||||||
status_code: Some(502),
|
|
||||||
error_message: Some("mismatch".to_string()),
|
|
||||||
created_at_unix_ms: 100,
|
|
||||||
updated_at_unix_secs: 200,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("upsert should succeed");
|
|
||||||
|
|
||||||
let stored = repo
|
|
||||||
.find(ShadowResultLookupKey::TraceFingerprint {
|
|
||||||
trace_id: "trace-1",
|
|
||||||
request_fingerprint: "fp-1",
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("find should succeed")
|
|
||||||
.expect("stored result should exist");
|
|
||||||
assert_eq!(stored.request_id.as_deref(), Some("req-trace-1"));
|
|
||||||
assert_eq!(stored.candidate_id.as_deref(), Some("cand-2"));
|
|
||||||
assert_eq!(stored.match_status, ShadowResultMatchStatus::Mismatch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
mod memory;
|
|
||||||
mod record;
|
|
||||||
mod sql;
|
|
||||||
mod types;
|
|
||||||
|
|
||||||
pub use memory::InMemoryShadowResultRepository;
|
|
||||||
pub use record::{merge_shadow_result_sample, RecordShadowResultSample, ShadowResultSampleOrigin};
|
|
||||||
pub use sql::SqlxShadowResultRepository;
|
|
||||||
pub use types::{
|
|
||||||
ShadowResultLookupKey, ShadowResultMatchStatus, ShadowResultReadRepository,
|
|
||||||
ShadowResultRepository, ShadowResultWriteRepository, StoredShadowResult, UpsertShadowResult,
|
|
||||||
};
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
use super::types::{ShadowResultMatchStatus, StoredShadowResult, UpsertShadowResult};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum ShadowResultSampleOrigin {
|
|
||||||
Rust,
|
|
||||||
Python,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct RecordShadowResultSample {
|
|
||||||
pub trace_id: String,
|
|
||||||
pub request_fingerprint: String,
|
|
||||||
pub request_id: Option<String>,
|
|
||||||
pub route_family: Option<String>,
|
|
||||||
pub route_kind: Option<String>,
|
|
||||||
pub candidate_id: Option<String>,
|
|
||||||
pub origin: ShadowResultSampleOrigin,
|
|
||||||
pub result_digest: String,
|
|
||||||
pub status_code: Option<u16>,
|
|
||||||
pub error_message: Option<String>,
|
|
||||||
pub recorded_at_unix_secs: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn merge_shadow_result_sample(
|
|
||||||
existing: Option<&StoredShadowResult>,
|
|
||||||
sample: RecordShadowResultSample,
|
|
||||||
) -> UpsertShadowResult {
|
|
||||||
let RecordShadowResultSample {
|
|
||||||
trace_id,
|
|
||||||
request_fingerprint,
|
|
||||||
request_id,
|
|
||||||
route_family,
|
|
||||||
route_kind,
|
|
||||||
candidate_id,
|
|
||||||
origin,
|
|
||||||
result_digest,
|
|
||||||
status_code,
|
|
||||||
error_message,
|
|
||||||
recorded_at_unix_secs,
|
|
||||||
} = sample;
|
|
||||||
|
|
||||||
let (rust_result_digest, python_result_digest) = match origin {
|
|
||||||
ShadowResultSampleOrigin::Rust => (
|
|
||||||
Some(result_digest),
|
|
||||||
existing.and_then(|row| row.python_result_digest.clone()),
|
|
||||||
),
|
|
||||||
ShadowResultSampleOrigin::Python => (
|
|
||||||
existing.and_then(|row| row.rust_result_digest.clone()),
|
|
||||||
Some(result_digest),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let match_status = resolve_match_status(
|
|
||||||
rust_result_digest.as_deref(),
|
|
||||||
python_result_digest.as_deref(),
|
|
||||||
);
|
|
||||||
|
|
||||||
UpsertShadowResult {
|
|
||||||
trace_id,
|
|
||||||
request_fingerprint,
|
|
||||||
request_id: request_id.or_else(|| existing.and_then(|row| row.request_id.clone())),
|
|
||||||
route_family: route_family.or_else(|| existing.and_then(|row| row.route_family.clone())),
|
|
||||||
route_kind: route_kind.or_else(|| existing.and_then(|row| row.route_kind.clone())),
|
|
||||||
candidate_id: candidate_id.or_else(|| existing.and_then(|row| row.candidate_id.clone())),
|
|
||||||
rust_result_digest,
|
|
||||||
python_result_digest,
|
|
||||||
match_status,
|
|
||||||
status_code: status_code.or(existing.and_then(|row| row.status_code)),
|
|
||||||
error_message: resolve_error_message(existing, error_message, match_status),
|
|
||||||
created_at_unix_ms: existing
|
|
||||||
.map(|row| row.created_at_unix_ms)
|
|
||||||
.unwrap_or(recorded_at_unix_secs),
|
|
||||||
updated_at_unix_secs: recorded_at_unix_secs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_match_status(
|
|
||||||
rust_result_digest: Option<&str>,
|
|
||||||
python_result_digest: Option<&str>,
|
|
||||||
) -> ShadowResultMatchStatus {
|
|
||||||
match (rust_result_digest, python_result_digest) {
|
|
||||||
(Some(rust_digest), Some(python_digest)) if rust_digest == python_digest => {
|
|
||||||
ShadowResultMatchStatus::Match
|
|
||||||
}
|
|
||||||
(Some(_), Some(_)) => ShadowResultMatchStatus::Mismatch,
|
|
||||||
_ => ShadowResultMatchStatus::Pending,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_error_message(
|
|
||||||
existing: Option<&StoredShadowResult>,
|
|
||||||
error_message: Option<String>,
|
|
||||||
match_status: ShadowResultMatchStatus,
|
|
||||||
) -> Option<String> {
|
|
||||||
if match_status == ShadowResultMatchStatus::Mismatch {
|
|
||||||
error_message
|
|
||||||
.or_else(|| existing.and_then(|row| row.error_message.clone()))
|
|
||||||
.or_else(|| Some("shadow result digest mismatch".to_string()))
|
|
||||||
} else {
|
|
||||||
error_message.or_else(|| existing.and_then(|row| row.error_message.clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{merge_shadow_result_sample, RecordShadowResultSample, ShadowResultSampleOrigin};
|
|
||||||
use crate::repository::shadow_results::{ShadowResultMatchStatus, UpsertShadowResult};
|
|
||||||
|
|
||||||
fn rust_sample(result_digest: &str, recorded_at_unix_secs: u64) -> RecordShadowResultSample {
|
|
||||||
RecordShadowResultSample {
|
|
||||||
trace_id: "trace-1".to_string(),
|
|
||||||
request_fingerprint: "fp-1".to_string(),
|
|
||||||
request_id: Some("req-1".to_string()),
|
|
||||||
route_family: Some("openai".to_string()),
|
|
||||||
route_kind: Some("chat".to_string()),
|
|
||||||
candidate_id: None,
|
|
||||||
origin: ShadowResultSampleOrigin::Rust,
|
|
||||||
result_digest: result_digest.to_string(),
|
|
||||||
status_code: Some(200),
|
|
||||||
error_message: None,
|
|
||||||
recorded_at_unix_secs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn python_sample(result_digest: &str, recorded_at_unix_secs: u64) -> RecordShadowResultSample {
|
|
||||||
RecordShadowResultSample {
|
|
||||||
trace_id: "trace-1".to_string(),
|
|
||||||
request_fingerprint: "fp-1".to_string(),
|
|
||||||
request_id: Some("req-1".to_string()),
|
|
||||||
route_family: Some("openai".to_string()),
|
|
||||||
route_kind: Some("chat".to_string()),
|
|
||||||
candidate_id: None,
|
|
||||||
origin: ShadowResultSampleOrigin::Python,
|
|
||||||
result_digest: result_digest.to_string(),
|
|
||||||
status_code: Some(200),
|
|
||||||
error_message: None,
|
|
||||||
recorded_at_unix_secs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stored(upsert: UpsertShadowResult) -> crate::repository::shadow_results::StoredShadowResult {
|
|
||||||
upsert.into_stored()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn keeps_pending_until_both_samples_exist() {
|
|
||||||
let merged = merge_shadow_result_sample(None, rust_sample("digest-1", 100));
|
|
||||||
|
|
||||||
assert_eq!(merged.match_status, ShadowResultMatchStatus::Pending);
|
|
||||||
assert_eq!(merged.request_id.as_deref(), Some("req-1"));
|
|
||||||
assert_eq!(merged.rust_result_digest.as_deref(), Some("digest-1"));
|
|
||||||
assert!(merged.python_result_digest.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn marks_match_when_rust_and_python_digests_are_equal() {
|
|
||||||
let existing = stored(merge_shadow_result_sample(
|
|
||||||
None,
|
|
||||||
rust_sample("digest-1", 100),
|
|
||||||
));
|
|
||||||
let merged = merge_shadow_result_sample(Some(&existing), python_sample("digest-1", 200));
|
|
||||||
|
|
||||||
assert_eq!(merged.match_status, ShadowResultMatchStatus::Match);
|
|
||||||
assert_eq!(merged.created_at_unix_ms, 100);
|
|
||||||
assert_eq!(merged.updated_at_unix_secs, 200);
|
|
||||||
assert_eq!(merged.request_id.as_deref(), Some("req-1"));
|
|
||||||
assert_eq!(merged.rust_result_digest.as_deref(), Some("digest-1"));
|
|
||||||
assert_eq!(merged.python_result_digest.as_deref(), Some("digest-1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn marks_mismatch_when_rust_and_python_digests_differ() {
|
|
||||||
let existing = stored(merge_shadow_result_sample(
|
|
||||||
None,
|
|
||||||
rust_sample("digest-1", 100),
|
|
||||||
));
|
|
||||||
let merged = merge_shadow_result_sample(Some(&existing), python_sample("digest-2", 200));
|
|
||||||
|
|
||||||
assert_eq!(merged.match_status, ShadowResultMatchStatus::Mismatch);
|
|
||||||
assert_eq!(
|
|
||||||
merged.error_message.as_deref(),
|
|
||||||
Some("shadow result digest mismatch")
|
|
||||||
);
|
|
||||||
assert_eq!(merged.request_id.as_deref(), Some("req-1"));
|
|
||||||
assert_eq!(merged.rust_result_digest.as_deref(), Some("digest-1"));
|
|
||||||
assert_eq!(merged.python_result_digest.as_deref(), Some("digest-2"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
use futures_util::future::BoxFuture;
|
|
||||||
use futures_util::TryStreamExt;
|
|
||||||
use sqlx::{PgPool, Row};
|
|
||||||
|
|
||||||
use super::types::{
|
|
||||||
ShadowResultLookupKey, ShadowResultMatchStatus, ShadowResultReadRepository,
|
|
||||||
ShadowResultWriteRepository, StoredShadowResult, UpsertShadowResult,
|
|
||||||
};
|
|
||||||
use crate::postgres::PostgresTransactionRunner;
|
|
||||||
use crate::{error::SqlxResultExt, DataLayerError};
|
|
||||||
|
|
||||||
const FIND_BY_TRACE_FINGERPRINT_SQL: &str = r#"
|
|
||||||
SELECT
|
|
||||||
trace_id,
|
|
||||||
request_fingerprint,
|
|
||||||
NULL::TEXT AS request_id,
|
|
||||||
route_family,
|
|
||||||
route_kind,
|
|
||||||
candidate_id,
|
|
||||||
rust_result_digest,
|
|
||||||
python_result_digest,
|
|
||||||
match_status,
|
|
||||||
status_code,
|
|
||||||
error_message,
|
|
||||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_ms,
|
|
||||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
|
||||||
FROM gateway_shadow_results
|
|
||||||
WHERE trace_id = $1 AND request_fingerprint = $2
|
|
||||||
LIMIT 1
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const LIST_RECENT_SQL: &str = r#"
|
|
||||||
SELECT
|
|
||||||
trace_id,
|
|
||||||
request_fingerprint,
|
|
||||||
NULL::TEXT AS request_id,
|
|
||||||
route_family,
|
|
||||||
route_kind,
|
|
||||||
candidate_id,
|
|
||||||
rust_result_digest,
|
|
||||||
python_result_digest,
|
|
||||||
match_status,
|
|
||||||
status_code,
|
|
||||||
error_message,
|
|
||||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_ms,
|
|
||||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
|
||||||
FROM gateway_shadow_results
|
|
||||||
ORDER BY updated_at DESC
|
|
||||||
LIMIT $1
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const UPSERT_SQL: &str = r#"
|
|
||||||
INSERT INTO gateway_shadow_results (
|
|
||||||
trace_id,
|
|
||||||
request_fingerprint,
|
|
||||||
route_family,
|
|
||||||
route_kind,
|
|
||||||
candidate_id,
|
|
||||||
rust_result_digest,
|
|
||||||
python_result_digest,
|
|
||||||
match_status,
|
|
||||||
status_code,
|
|
||||||
error_message,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES (
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
$6,
|
|
||||||
$7,
|
|
||||||
$8,
|
|
||||||
$9,
|
|
||||||
$10,
|
|
||||||
TO_TIMESTAMP($11::double precision),
|
|
||||||
TO_TIMESTAMP($12::double precision)
|
|
||||||
)
|
|
||||||
ON CONFLICT (trace_id, request_fingerprint)
|
|
||||||
DO UPDATE SET
|
|
||||||
route_family = EXCLUDED.route_family,
|
|
||||||
route_kind = EXCLUDED.route_kind,
|
|
||||||
candidate_id = EXCLUDED.candidate_id,
|
|
||||||
rust_result_digest = EXCLUDED.rust_result_digest,
|
|
||||||
python_result_digest = EXCLUDED.python_result_digest,
|
|
||||||
match_status = EXCLUDED.match_status,
|
|
||||||
status_code = EXCLUDED.status_code,
|
|
||||||
error_message = EXCLUDED.error_message,
|
|
||||||
updated_at = EXCLUDED.updated_at
|
|
||||||
RETURNING
|
|
||||||
trace_id,
|
|
||||||
request_fingerprint,
|
|
||||||
NULL::TEXT AS request_id,
|
|
||||||
route_family,
|
|
||||||
route_kind,
|
|
||||||
candidate_id,
|
|
||||||
rust_result_digest,
|
|
||||||
python_result_digest,
|
|
||||||
match_status,
|
|
||||||
status_code,
|
|
||||||
error_message,
|
|
||||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_ms,
|
|
||||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs
|
|
||||||
"#;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SqlxShadowResultRepository {
|
|
||||||
pool: PgPool,
|
|
||||||
tx_runner: PostgresTransactionRunner,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SqlxShadowResultRepository {
|
|
||||||
pub fn new(pool: PgPool) -> Self {
|
|
||||||
let tx_runner = PostgresTransactionRunner::new(pool.clone());
|
|
||||||
Self { pool, tx_runner }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pool(&self) -> &PgPool {
|
|
||||||
&self.pool
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn transaction_runner(&self) -> &PostgresTransactionRunner {
|
|
||||||
&self.tx_runner
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn find(
|
|
||||||
&self,
|
|
||||||
key: ShadowResultLookupKey<'_>,
|
|
||||||
) -> Result<Option<StoredShadowResult>, DataLayerError> {
|
|
||||||
match key {
|
|
||||||
ShadowResultLookupKey::TraceFingerprint {
|
|
||||||
trace_id,
|
|
||||||
request_fingerprint,
|
|
||||||
} => {
|
|
||||||
self.find_by_trace_fingerprint(trace_id, request_fingerprint)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn find_by_trace_fingerprint(
|
|
||||||
&self,
|
|
||||||
trace_id: &str,
|
|
||||||
request_fingerprint: &str,
|
|
||||||
) -> Result<Option<StoredShadowResult>, DataLayerError> {
|
|
||||||
let row = sqlx::query(FIND_BY_TRACE_FINGERPRINT_SQL)
|
|
||||||
.bind(trace_id)
|
|
||||||
.bind(request_fingerprint)
|
|
||||||
.fetch_optional(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_postgres_err()?;
|
|
||||||
row.as_ref().map(map_shadow_result_row).transpose()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list_recent(
|
|
||||||
&self,
|
|
||||||
limit: usize,
|
|
||||||
) -> Result<Vec<StoredShadowResult>, DataLayerError> {
|
|
||||||
if limit == 0 {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rows = sqlx::query(LIST_RECENT_SQL)
|
|
||||||
.bind(i64::try_from(limit).map_err(|_| {
|
|
||||||
DataLayerError::UnexpectedValue(format!(
|
|
||||||
"invalid recent shadow result limit: {limit}"
|
|
||||||
))
|
|
||||||
})?)
|
|
||||||
.fetch(&self.pool);
|
|
||||||
let mut items = Vec::new();
|
|
||||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
|
||||||
items.push(map_shadow_result_row(&row)?);
|
|
||||||
}
|
|
||||||
Ok(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn upsert(
|
|
||||||
&self,
|
|
||||||
result: UpsertShadowResult,
|
|
||||||
) -> Result<StoredShadowResult, DataLayerError> {
|
|
||||||
self.tx_runner
|
|
||||||
.run_read_write(|tx| {
|
|
||||||
Box::pin(async move {
|
|
||||||
let row = sqlx::query(UPSERT_SQL)
|
|
||||||
.bind(&result.trace_id)
|
|
||||||
.bind(&result.request_fingerprint)
|
|
||||||
.bind(&result.route_family)
|
|
||||||
.bind(&result.route_kind)
|
|
||||||
.bind(&result.candidate_id)
|
|
||||||
.bind(&result.rust_result_digest)
|
|
||||||
.bind(&result.python_result_digest)
|
|
||||||
.bind(match_status_to_database(result.match_status))
|
|
||||||
.bind(result.status_code.map(i32::from))
|
|
||||||
.bind(&result.error_message)
|
|
||||||
.bind(result.created_at_unix_ms as f64)
|
|
||||||
.bind(result.updated_at_unix_secs as f64)
|
|
||||||
.fetch_one(&mut **tx)
|
|
||||||
.await
|
|
||||||
.map_postgres_err()?;
|
|
||||||
map_shadow_result_row(&row)
|
|
||||||
}) as BoxFuture<'_, Result<StoredShadowResult, DataLayerError>>
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ShadowResultReadRepository for SqlxShadowResultRepository {
|
|
||||||
async fn find(
|
|
||||||
&self,
|
|
||||||
key: ShadowResultLookupKey<'_>,
|
|
||||||
) -> Result<Option<StoredShadowResult>, DataLayerError> {
|
|
||||||
Self::find(self, key).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_recent(&self, limit: usize) -> Result<Vec<StoredShadowResult>, DataLayerError> {
|
|
||||||
Self::list_recent(self, limit).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ShadowResultWriteRepository for SqlxShadowResultRepository {
|
|
||||||
async fn upsert(
|
|
||||||
&self,
|
|
||||||
result: UpsertShadowResult,
|
|
||||||
) -> Result<StoredShadowResult, DataLayerError> {
|
|
||||||
Self::upsert(self, result).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn match_status_to_database(status: ShadowResultMatchStatus) -> &'static str {
|
|
||||||
match status {
|
|
||||||
ShadowResultMatchStatus::Pending => "pending",
|
|
||||||
ShadowResultMatchStatus::Match => "match",
|
|
||||||
ShadowResultMatchStatus::Mismatch => "mismatch",
|
|
||||||
ShadowResultMatchStatus::Error => "error",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_shadow_result_row(
|
|
||||||
row: &sqlx::postgres::PgRow,
|
|
||||||
) -> Result<StoredShadowResult, DataLayerError> {
|
|
||||||
let match_status = ShadowResultMatchStatus::from_database(
|
|
||||||
row.try_get::<String, _>("match_status")
|
|
||||||
.map_postgres_err()?
|
|
||||||
.as_str(),
|
|
||||||
)?;
|
|
||||||
StoredShadowResult::new(
|
|
||||||
row.try_get("trace_id").map_postgres_err()?,
|
|
||||||
row.try_get("request_fingerprint").map_postgres_err()?,
|
|
||||||
row.try_get("request_id").map_postgres_err()?,
|
|
||||||
row.try_get("route_family").map_postgres_err()?,
|
|
||||||
row.try_get("route_kind").map_postgres_err()?,
|
|
||||||
row.try_get("candidate_id").map_postgres_err()?,
|
|
||||||
row.try_get("rust_result_digest").map_postgres_err()?,
|
|
||||||
row.try_get("python_result_digest").map_postgres_err()?,
|
|
||||||
match_status,
|
|
||||||
row.try_get("status_code").map_postgres_err()?,
|
|
||||||
row.try_get("error_message").map_postgres_err()?,
|
|
||||||
row.try_get("created_at_unix_ms").map_postgres_err()?,
|
|
||||||
row.try_get("updated_at_unix_secs").map_postgres_err()?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::SqlxShadowResultRepository;
|
|
||||||
use crate::postgres::{PostgresPoolConfig, PostgresPoolFactory};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn repository_constructs_from_lazy_pool() {
|
|
||||||
let factory = PostgresPoolFactory::new(PostgresPoolConfig {
|
|
||||||
database_url: "postgres://localhost/aether".to_string(),
|
|
||||||
min_connections: 1,
|
|
||||||
max_connections: 4,
|
|
||||||
acquire_timeout_ms: 1_000,
|
|
||||||
idle_timeout_ms: 5_000,
|
|
||||||
max_lifetime_ms: 30_000,
|
|
||||||
statement_cache_capacity: 64,
|
|
||||||
require_ssl: false,
|
|
||||||
})
|
|
||||||
.expect("factory should build");
|
|
||||||
|
|
||||||
let pool = factory.connect_lazy().expect("pool should build");
|
|
||||||
let repository = SqlxShadowResultRepository::new(pool);
|
|
||||||
let _ = repository.pool();
|
|
||||||
let _ = repository.transaction_runner();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum ShadowResultMatchStatus {
|
|
||||||
Pending,
|
|
||||||
Match,
|
|
||||||
Mismatch,
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShadowResultMatchStatus {
|
|
||||||
pub fn from_database(value: &str) -> Result<Self, crate::DataLayerError> {
|
|
||||||
match value.trim().to_ascii_lowercase().as_str() {
|
|
||||||
"pending" => Ok(Self::Pending),
|
|
||||||
"match" => Ok(Self::Match),
|
|
||||||
"mismatch" => Ok(Self::Mismatch),
|
|
||||||
"error" => Ok(Self::Error),
|
|
||||||
other => Err(crate::DataLayerError::UnexpectedValue(format!(
|
|
||||||
"unsupported gateway_shadow_results.match_status: {other}"
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct StoredShadowResult {
|
|
||||||
pub trace_id: String,
|
|
||||||
pub request_fingerprint: String,
|
|
||||||
pub request_id: Option<String>,
|
|
||||||
pub route_family: Option<String>,
|
|
||||||
pub route_kind: Option<String>,
|
|
||||||
pub candidate_id: Option<String>,
|
|
||||||
pub rust_result_digest: Option<String>,
|
|
||||||
pub python_result_digest: Option<String>,
|
|
||||||
pub match_status: ShadowResultMatchStatus,
|
|
||||||
pub status_code: Option<u16>,
|
|
||||||
pub error_message: Option<String>,
|
|
||||||
pub created_at_unix_ms: u64,
|
|
||||||
pub updated_at_unix_secs: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StoredShadowResult {
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
|
||||||
trace_id: String,
|
|
||||||
request_fingerprint: String,
|
|
||||||
request_id: Option<String>,
|
|
||||||
route_family: Option<String>,
|
|
||||||
route_kind: Option<String>,
|
|
||||||
candidate_id: Option<String>,
|
|
||||||
rust_result_digest: Option<String>,
|
|
||||||
python_result_digest: Option<String>,
|
|
||||||
match_status: ShadowResultMatchStatus,
|
|
||||||
status_code: Option<i32>,
|
|
||||||
error_message: Option<String>,
|
|
||||||
created_at_unix_ms: i64,
|
|
||||||
updated_at_unix_secs: i64,
|
|
||||||
) -> Result<Self, crate::DataLayerError> {
|
|
||||||
let status_code = status_code
|
|
||||||
.map(|value| {
|
|
||||||
u16::try_from(value).map_err(|_| {
|
|
||||||
crate::DataLayerError::UnexpectedValue(format!("invalid status_code: {value}"))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.transpose()?;
|
|
||||||
let created_at_unix_ms = u64::try_from(created_at_unix_ms).map_err(|_| {
|
|
||||||
crate::DataLayerError::UnexpectedValue(format!(
|
|
||||||
"invalid created_at_unix_ms: {created_at_unix_ms}"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
let updated_at_unix_secs = u64::try_from(updated_at_unix_secs).map_err(|_| {
|
|
||||||
crate::DataLayerError::UnexpectedValue(format!(
|
|
||||||
"invalid updated_at_unix_secs: {updated_at_unix_secs}"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
trace_id,
|
|
||||||
request_fingerprint,
|
|
||||||
request_id,
|
|
||||||
route_family,
|
|
||||||
route_kind,
|
|
||||||
candidate_id,
|
|
||||||
rust_result_digest,
|
|
||||||
python_result_digest,
|
|
||||||
match_status,
|
|
||||||
status_code,
|
|
||||||
error_message,
|
|
||||||
created_at_unix_ms,
|
|
||||||
updated_at_unix_secs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct UpsertShadowResult {
|
|
||||||
pub trace_id: String,
|
|
||||||
pub request_fingerprint: String,
|
|
||||||
pub request_id: Option<String>,
|
|
||||||
pub route_family: Option<String>,
|
|
||||||
pub route_kind: Option<String>,
|
|
||||||
pub candidate_id: Option<String>,
|
|
||||||
pub rust_result_digest: Option<String>,
|
|
||||||
pub python_result_digest: Option<String>,
|
|
||||||
pub match_status: ShadowResultMatchStatus,
|
|
||||||
pub status_code: Option<u16>,
|
|
||||||
pub error_message: Option<String>,
|
|
||||||
pub created_at_unix_ms: u64,
|
|
||||||
pub updated_at_unix_secs: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UpsertShadowResult {
|
|
||||||
pub fn into_stored(self) -> StoredShadowResult {
|
|
||||||
StoredShadowResult {
|
|
||||||
trace_id: self.trace_id,
|
|
||||||
request_fingerprint: self.request_fingerprint,
|
|
||||||
request_id: self.request_id,
|
|
||||||
route_family: self.route_family,
|
|
||||||
route_kind: self.route_kind,
|
|
||||||
candidate_id: self.candidate_id,
|
|
||||||
rust_result_digest: self.rust_result_digest,
|
|
||||||
python_result_digest: self.python_result_digest,
|
|
||||||
match_status: self.match_status,
|
|
||||||
status_code: self.status_code,
|
|
||||||
error_message: self.error_message,
|
|
||||||
created_at_unix_ms: self.created_at_unix_ms,
|
|
||||||
updated_at_unix_secs: self.updated_at_unix_secs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum ShadowResultLookupKey<'a> {
|
|
||||||
TraceFingerprint {
|
|
||||||
trace_id: &'a str,
|
|
||||||
request_fingerprint: &'a str,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait ShadowResultReadRepository: Send + Sync {
|
|
||||||
async fn find(
|
|
||||||
&self,
|
|
||||||
key: ShadowResultLookupKey<'_>,
|
|
||||||
) -> Result<Option<StoredShadowResult>, crate::DataLayerError>;
|
|
||||||
|
|
||||||
async fn list_recent(
|
|
||||||
&self,
|
|
||||||
limit: usize,
|
|
||||||
) -> Result<Vec<StoredShadowResult>, crate::DataLayerError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait ShadowResultWriteRepository: Send + Sync {
|
|
||||||
async fn upsert(
|
|
||||||
&self,
|
|
||||||
result: UpsertShadowResult,
|
|
||||||
) -> Result<StoredShadowResult, crate::DataLayerError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ShadowResultRepository:
|
|
||||||
ShadowResultReadRepository + ShadowResultWriteRepository + Send + Sync
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> ShadowResultRepository for T where
|
|
||||||
T: ShadowResultReadRepository + ShadowResultWriteRepository + Send + Sync
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{ShadowResultMatchStatus, StoredShadowResult};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_match_status_from_database_text() {
|
|
||||||
assert_eq!(
|
|
||||||
ShadowResultMatchStatus::from_database("match").expect("status should parse"),
|
|
||||||
ShadowResultMatchStatus::Match
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rejects_invalid_database_status() {
|
|
||||||
assert!(ShadowResultMatchStatus::from_database("mystery").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rejects_invalid_numeric_fields() {
|
|
||||||
assert!(StoredShadowResult::new(
|
|
||||||
"trace-1".to_string(),
|
|
||||||
"fp-1".to_string(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
ShadowResultMatchStatus::Pending,
|
|
||||||
Some(-1),
|
|
||||||
None,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rejects_negative_updated_at_values() {
|
|
||||||
assert!(StoredShadowResult::new(
|
|
||||||
"trace-1".to_string(),
|
|
||||||
"fp-1".to_string(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
ShadowResultMatchStatus::Pending,
|
|
||||||
Some(200),
|
|
||||||
None,
|
|
||||||
1,
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
.is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user