mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
feat(gateway): 重构 usage 数据层、迁移系统与系统导入
数据库迁移: - 引入 baseline v2 bootstrap,空库首次启动自动初始化 - 服务启动不再自动执行迁移,需显式 `--migrate` 运行 - 新增 pending migration 检测,schema 落后时拒绝启动 Usage 数据层: - usage body 存储外部化为独立 blob 表 - 新增 HTTP audit 表拆分存储请求/响应头与 body ref - 后台清理任务支持 legacy body ref 元数据迁移 - usage runtime 写入迁移到专用 tokio runtime(独立线程池, 8MB 栈) 系统导入/导出: - 支持用户、API Keys、钱包数据的完整导入 - 兼容 legacy 与 v1.3+ 两种导出格式 其他改进: - executor outcome 增加 runtime miss 诊断上下文 - 主 tokio runtime 栈大小调整为 8MB - 前端 provider 管理支持 base URL 配置 - dev.sh 支持 --migrate 参数
This commit is contained in:
@@ -1,17 +1,22 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use aether_contracts::{ExecutionPlan, ExecutionTelemetry};
|
||||
use aether_contracts::ExecutionTelemetry;
|
||||
use aether_data::redis::RedisStreamRunner;
|
||||
use aether_data_contracts::repository::usage::UpsertUsageRecord;
|
||||
use aether_data_contracts::DataLayerError;
|
||||
use async_trait::async_trait;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::executor::spawn_on_usage_background_runtime;
|
||||
use crate::{
|
||||
build_pending_usage_record, build_stream_terminal_usage_outcome, build_streaming_usage_record,
|
||||
build_sync_terminal_usage_outcome, build_terminal_usage_event_from_outcome,
|
||||
build_upsert_usage_record_from_event, build_usage_queue_worker, settle_usage_if_needed,
|
||||
GatewayStreamReportRequest, GatewaySyncReportRequest, UsageEvent, UsageQueue,
|
||||
UsageRecordWriter, UsageRuntimeConfig, UsageSettlementWriter, UsageTerminalState,
|
||||
build_pending_usage_record_from_seed, build_stream_terminal_usage_seed,
|
||||
build_streaming_usage_record_from_seed, build_sync_terminal_usage_seed,
|
||||
build_terminal_usage_event_from_seed, build_upsert_usage_record_from_event,
|
||||
build_usage_queue_worker, settle_usage_if_needed, LifecycleUsageSeed,
|
||||
StreamTerminalUsagePayloadSeed, SyncTerminalUsagePayloadSeed, TerminalUsageContextSeed,
|
||||
UsageEvent, UsageQueue, UsageRecordWriter, UsageRuntimeConfig, UsageSettlementWriter,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
@@ -32,6 +37,17 @@ pub struct UsageRuntime {
|
||||
config: UsageRuntimeConfig,
|
||||
}
|
||||
|
||||
struct SyncTerminalUsageTaskInput {
|
||||
context_seed: TerminalUsageContextSeed,
|
||||
payload_seed: SyncTerminalUsagePayloadSeed,
|
||||
}
|
||||
|
||||
struct StreamTerminalUsageTaskInput {
|
||||
context_seed: TerminalUsageContextSeed,
|
||||
payload_seed: StreamTerminalUsagePayloadSeed,
|
||||
cancelled: bool,
|
||||
}
|
||||
|
||||
impl Default for UsageRuntime {
|
||||
fn default() -> Self {
|
||||
Self::disabled()
|
||||
@@ -73,169 +89,197 @@ impl UsageRuntime {
|
||||
Some(worker.spawn())
|
||||
}
|
||||
|
||||
pub async fn record_pending<T>(
|
||||
&self,
|
||||
data: &T,
|
||||
plan: &ExecutionPlan,
|
||||
report_context: Option<&serde_json::Value>,
|
||||
) where
|
||||
T: UsageRuntimeAccess,
|
||||
pub fn record_pending<T>(&self, data: &T, seed: &LifecycleUsageSeed)
|
||||
where
|
||||
T: UsageRuntimeAccess + Clone + 'static,
|
||||
{
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
let now_unix_secs = now_unix_secs();
|
||||
match build_pending_usage_record(plan, report_context, now_unix_secs) {
|
||||
Ok(record) => {
|
||||
if let Err(err) = data.upsert_usage_record(record).await {
|
||||
let data = T::clone(data);
|
||||
let seed = seed.clone();
|
||||
let request_id = seed.request_id.clone();
|
||||
spawn_on_usage_background_runtime(boxed_usage_task(async move {
|
||||
let now_unix_secs = now_unix_secs();
|
||||
match build_pending_usage_record_offthread(&seed, now_unix_secs).await {
|
||||
Ok(record) => {
|
||||
if let Err(err) = data.upsert_usage_record(record).await {
|
||||
warn!(
|
||||
event_name = "usage_pending_record_failed",
|
||||
log_type = "event",
|
||||
request_id = %request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to record sync pending usage"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "usage_pending_record_failed",
|
||||
event_name = "usage_pending_build_failed",
|
||||
log_type = "event",
|
||||
request_id = %plan.request_id,
|
||||
request_id = %request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to record sync pending usage"
|
||||
);
|
||||
"usage runtime failed to build sync pending usage"
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "usage_pending_build_failed",
|
||||
log_type = "event",
|
||||
request_id = %plan.request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to build sync pending usage"
|
||||
)
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub async fn record_stream_started<T>(
|
||||
pub fn record_stream_started<T>(
|
||||
&self,
|
||||
data: &T,
|
||||
plan: &ExecutionPlan,
|
||||
report_context: Option<&serde_json::Value>,
|
||||
seed: &LifecycleUsageSeed,
|
||||
status_code: u16,
|
||||
headers: &std::collections::BTreeMap<String, String>,
|
||||
telemetry: Option<&ExecutionTelemetry>,
|
||||
) where
|
||||
T: UsageRuntimeAccess,
|
||||
T: UsageRuntimeAccess + Clone + 'static,
|
||||
{
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
let now_unix_secs = now_unix_secs();
|
||||
match build_streaming_usage_record(
|
||||
plan,
|
||||
report_context,
|
||||
status_code,
|
||||
headers,
|
||||
telemetry,
|
||||
now_unix_secs,
|
||||
) {
|
||||
Ok(record) => {
|
||||
if let Err(err) = data.upsert_usage_record(record).await {
|
||||
let data = T::clone(data);
|
||||
let seed = seed.clone();
|
||||
let telemetry = telemetry.cloned();
|
||||
let request_id = seed.request_id.clone();
|
||||
spawn_on_usage_background_runtime(boxed_usage_task(async move {
|
||||
let now_unix_secs = now_unix_secs();
|
||||
match build_streaming_usage_record_offthread(
|
||||
&seed,
|
||||
status_code,
|
||||
telemetry.as_ref(),
|
||||
now_unix_secs,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(record) => {
|
||||
if let Err(err) = data.upsert_usage_record(record).await {
|
||||
warn!(
|
||||
event_name = "usage_stream_record_failed",
|
||||
log_type = "event",
|
||||
request_id = %request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to record stream usage"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "usage_stream_record_failed",
|
||||
event_name = "usage_stream_build_failed",
|
||||
log_type = "event",
|
||||
request_id = %plan.request_id,
|
||||
request_id = %request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to record stream usage"
|
||||
);
|
||||
"usage runtime failed to build stream usage"
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "usage_stream_build_failed",
|
||||
log_type = "event",
|
||||
request_id = %plan.request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to build stream usage"
|
||||
)
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub async fn record_sync_terminal<T>(
|
||||
pub fn record_sync_terminal<T>(
|
||||
&self,
|
||||
data: &T,
|
||||
plan: &ExecutionPlan,
|
||||
report_context: Option<&serde_json::Value>,
|
||||
payload: &GatewaySyncReportRequest,
|
||||
context_seed: &TerminalUsageContextSeed,
|
||||
payload_seed: &SyncTerminalUsagePayloadSeed,
|
||||
) where
|
||||
T: UsageRuntimeAccess,
|
||||
T: UsageRuntimeAccess + Clone + 'static,
|
||||
{
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
match build_terminal_usage_event_from_outcome(build_sync_terminal_usage_outcome(
|
||||
plan,
|
||||
report_context,
|
||||
payload,
|
||||
)) {
|
||||
Ok(mut event) => {
|
||||
if let Err(err) = data.enrich_usage_event(&mut event).await {
|
||||
let runtime = self.clone();
|
||||
let data = T::clone(data);
|
||||
let request_id = context_seed.request_id.clone();
|
||||
let input = Box::new(SyncTerminalUsageTaskInput {
|
||||
context_seed: context_seed.clone(),
|
||||
payload_seed: payload_seed.clone(),
|
||||
});
|
||||
spawn_on_usage_background_runtime(boxed_usage_task(async move {
|
||||
match build_sync_terminal_usage_event_offthread(input).await {
|
||||
Ok(mut event) => {
|
||||
if let Err(err) = data.enrich_usage_event(&mut event).await {
|
||||
warn!(
|
||||
event_name = "usage_sync_terminal_billing_enrichment_failed",
|
||||
log_type = "event",
|
||||
request_id = %request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to enrich sync usage event with billing"
|
||||
);
|
||||
}
|
||||
runtime.enqueue_or_write_terminal(&data, event).await
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "usage_sync_terminal_billing_enrichment_failed",
|
||||
event_name = "usage_sync_terminal_build_failed",
|
||||
log_type = "event",
|
||||
request_id = %plan.request_id,
|
||||
request_id = %request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to enrich sync usage event with billing"
|
||||
);
|
||||
"usage runtime failed to build sync terminal usage event"
|
||||
)
|
||||
}
|
||||
self.enqueue_or_write_terminal(data, event).await
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "usage_sync_terminal_build_failed",
|
||||
log_type = "event",
|
||||
request_id = %plan.request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to build sync terminal usage event"
|
||||
)
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub async fn record_stream_terminal<T>(
|
||||
pub fn record_stream_terminal<T>(
|
||||
&self,
|
||||
data: &T,
|
||||
plan: &ExecutionPlan,
|
||||
report_context: Option<&serde_json::Value>,
|
||||
payload: &GatewayStreamReportRequest,
|
||||
context_seed: &TerminalUsageContextSeed,
|
||||
payload_seed: &StreamTerminalUsagePayloadSeed,
|
||||
cancelled: bool,
|
||||
) where
|
||||
T: UsageRuntimeAccess,
|
||||
T: UsageRuntimeAccess + Clone + 'static,
|
||||
{
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
let mut outcome = build_stream_terminal_usage_outcome(plan, report_context, payload);
|
||||
if cancelled {
|
||||
outcome.terminal_state = UsageTerminalState::Cancelled;
|
||||
}
|
||||
match build_terminal_usage_event_from_outcome(outcome) {
|
||||
Ok(mut event) => {
|
||||
if let Err(err) = data.enrich_usage_event(&mut event).await {
|
||||
let runtime = self.clone();
|
||||
let data = T::clone(data);
|
||||
let request_id = context_seed.request_id.clone();
|
||||
let input = Box::new(StreamTerminalUsageTaskInput {
|
||||
context_seed: context_seed.clone(),
|
||||
payload_seed: payload_seed.clone(),
|
||||
cancelled,
|
||||
});
|
||||
spawn_on_usage_background_runtime(boxed_usage_task(async move {
|
||||
match build_stream_terminal_usage_event_offthread(input).await {
|
||||
Ok(mut event) => {
|
||||
if let Err(err) = data.enrich_usage_event(&mut event).await {
|
||||
warn!(
|
||||
event_name = "usage_stream_terminal_billing_enrichment_failed",
|
||||
log_type = "event",
|
||||
request_id = %request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to enrich stream usage event with billing"
|
||||
);
|
||||
}
|
||||
runtime.enqueue_or_write_terminal(&data, event).await
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "usage_stream_terminal_billing_enrichment_failed",
|
||||
event_name = "usage_stream_terminal_build_failed",
|
||||
log_type = "event",
|
||||
request_id = %plan.request_id,
|
||||
request_id = %request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to enrich stream usage event with billing"
|
||||
);
|
||||
"usage runtime failed to build stream terminal usage event"
|
||||
)
|
||||
}
|
||||
self.enqueue_or_write_terminal(data, event).await
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
event_name = "usage_stream_terminal_build_failed",
|
||||
log_type = "event",
|
||||
request_id = %plan.request_id,
|
||||
error = %err,
|
||||
"usage runtime failed to build stream terminal usage event"
|
||||
)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn submit_terminal_event<T>(&self, data: &T, event: UsageEvent)
|
||||
where
|
||||
T: UsageRuntimeAccess + Clone + 'static,
|
||||
{
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
let runtime = self.clone();
|
||||
let data = T::clone(data);
|
||||
spawn_on_usage_background_runtime(boxed_usage_task(async move {
|
||||
runtime.record_terminal_event(&data, event).await;
|
||||
}));
|
||||
}
|
||||
|
||||
pub async fn record_terminal_event<T>(&self, data: &T, mut event: UsageEvent)
|
||||
@@ -326,6 +370,74 @@ impl UsageRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_pending_usage_record_offthread(
|
||||
seed: &LifecycleUsageSeed,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<UpsertUsageRecord, DataLayerError> {
|
||||
let seed = seed.clone();
|
||||
tokio::task::spawn_blocking(move || build_pending_usage_record_from_seed(&seed, now_unix_secs))
|
||||
.await
|
||||
.map_err(join_error_to_data_layer)?
|
||||
}
|
||||
|
||||
async fn build_streaming_usage_record_offthread(
|
||||
seed: &LifecycleUsageSeed,
|
||||
status_code: u16,
|
||||
telemetry: Option<&ExecutionTelemetry>,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<UpsertUsageRecord, DataLayerError> {
|
||||
let seed = seed.clone();
|
||||
let telemetry = telemetry.cloned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
build_streaming_usage_record_from_seed(
|
||||
&seed,
|
||||
status_code,
|
||||
telemetry.as_ref(),
|
||||
now_unix_secs,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(join_error_to_data_layer)?
|
||||
}
|
||||
|
||||
async fn build_sync_terminal_usage_event_offthread(
|
||||
input: Box<SyncTerminalUsageTaskInput>,
|
||||
) -> Result<UsageEvent, DataLayerError> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
build_terminal_usage_event_from_seed(build_sync_terminal_usage_seed(
|
||||
input.context_seed,
|
||||
input.payload_seed,
|
||||
))
|
||||
})
|
||||
.await
|
||||
.map_err(join_error_to_data_layer)?
|
||||
}
|
||||
|
||||
async fn build_stream_terminal_usage_event_offthread(
|
||||
input: Box<StreamTerminalUsageTaskInput>,
|
||||
) -> Result<UsageEvent, DataLayerError> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
build_terminal_usage_event_from_seed(build_stream_terminal_usage_seed(
|
||||
input.context_seed,
|
||||
input.payload_seed,
|
||||
input.cancelled,
|
||||
))
|
||||
})
|
||||
.await
|
||||
.map_err(join_error_to_data_layer)?
|
||||
}
|
||||
|
||||
fn join_error_to_data_layer(err: tokio::task::JoinError) -> DataLayerError {
|
||||
DataLayerError::UnexpectedValue(format!("usage builder task join failed: {err}"))
|
||||
}
|
||||
|
||||
fn boxed_usage_task<F>(task: F) -> Pin<Box<dyn Future<Output = ()> + Send>>
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
Box::pin(task)
|
||||
}
|
||||
|
||||
fn now_unix_secs() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
Reference in New Issue
Block a user