mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: 提前记录 pending 用量、优化流遥测时序与前端活跃请求发现机制
- 将 record_pending 调用移至执行开始前(sync/stream 两路),确保请求在执行前即有 pending 记录 - stream_pump 在收到第一个数据块前优先 yield 遥测帧,保证 ttfb 早于 data 帧到达 - stream execution 增加 should_refresh_stream_usage_telemetry,在遥测帧携带新 ttfb/elapsed 时及时更新 record_stream_started - access_log 对高频轮询路径(usage/active、usage/records 等)降级为 TRACE 日志,减少日志噪音 - 前端新增 reconcileActiveRequestDiscovery 工具函数及 discoverActiveRequests 逻辑,活跃请求发现与全局自动刷新解耦,空闲时降频为 5 秒扫描 - RequestDetailDrawer 调整:进行中请求不再自动开启轮询,由用户手动触发;刷新按钮 title 动态适配状态
This commit is contained in:
@@ -70,6 +70,10 @@ pub(crate) async fn execute_execution_runtime_stream(
|
||||
mut report_context: Option<serde_json::Value>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
ensure_execution_request_candidate_slot(state, &mut plan, &mut report_context).await;
|
||||
state
|
||||
.usage_runtime
|
||||
.record_pending(state.data.as_ref(), &plan, report_context.as_ref())
|
||||
.await;
|
||||
let plan_request_id_for_log = short_request_id(plan.request_id.as_str());
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
@@ -235,6 +239,19 @@ where
|
||||
read_next_frame(lines).await
|
||||
}
|
||||
|
||||
fn should_refresh_stream_usage_telemetry(
|
||||
previous: Option<&ExecutionTelemetry>,
|
||||
next: &ExecutionTelemetry,
|
||||
) -> bool {
|
||||
let previous_ttfb = previous.and_then(|telemetry| telemetry.ttfb_ms);
|
||||
let previous_elapsed = previous.and_then(|telemetry| telemetry.elapsed_ms);
|
||||
let next_ttfb = next.ttfb_ms;
|
||||
let next_elapsed = next.elapsed_ms;
|
||||
|
||||
(next_ttfb.is_some() && next_ttfb != previous_ttfb)
|
||||
|| (next_elapsed.is_some() && next_elapsed != previous_elapsed)
|
||||
}
|
||||
|
||||
async fn probe_local_stream_success_failover_text<R>(
|
||||
buffered_frames: &mut VecDeque<StreamFrame>,
|
||||
lines: &mut FramedRead<R, LinesCodec>,
|
||||
@@ -740,10 +757,6 @@ async fn execute_stream_from_frame_stream(
|
||||
}
|
||||
|
||||
let candidate_started_unix_secs = current_request_candidate_unix_ms();
|
||||
state
|
||||
.usage_runtime
|
||||
.record_pending(state.data.as_ref(), &plan, report_context.as_ref())
|
||||
.await;
|
||||
state
|
||||
.usage_runtime
|
||||
.record_stream_started(
|
||||
@@ -794,7 +807,8 @@ async fn execute_stream_from_frame_stream(
|
||||
tokio::spawn(async move {
|
||||
let mut provider_buffered_body = provider_prefetched_body_for_report;
|
||||
let mut buffered_body = prefetched_body_for_report;
|
||||
let mut telemetry: Option<ExecutionTelemetry> = initial_telemetry;
|
||||
let mut telemetry: Option<ExecutionTelemetry> = initial_telemetry.clone();
|
||||
let mut usage_stream_telemetry: Option<ExecutionTelemetry> = initial_telemetry;
|
||||
let reached_eof = initial_reached_eof;
|
||||
let mut downstream_dropped = false;
|
||||
let mut terminal_failure: Option<StreamFailureReport> = None;
|
||||
@@ -928,7 +942,25 @@ async fn execute_stream_from_frame_stream(
|
||||
StreamFramePayload::Telemetry {
|
||||
telemetry: frame_telemetry,
|
||||
} => {
|
||||
telemetry = Some(frame_telemetry);
|
||||
let should_refresh_stream_usage = should_refresh_stream_usage_telemetry(
|
||||
usage_stream_telemetry.as_ref(),
|
||||
&frame_telemetry,
|
||||
);
|
||||
telemetry = Some(frame_telemetry.clone());
|
||||
if should_refresh_stream_usage {
|
||||
state_for_report
|
||||
.usage_runtime
|
||||
.record_stream_started(
|
||||
state_for_report.data.as_ref(),
|
||||
&plan_for_report,
|
||||
report_context_owned.as_ref(),
|
||||
status_code,
|
||||
&headers_for_report,
|
||||
Some(&frame_telemetry),
|
||||
)
|
||||
.await;
|
||||
usage_stream_telemetry = Some(frame_telemetry);
|
||||
}
|
||||
}
|
||||
StreamFramePayload::Eof { .. } => {
|
||||
break;
|
||||
|
||||
@@ -42,6 +42,7 @@ pub(crate) fn build_direct_execution_frame_stream(
|
||||
|
||||
let mut upstream_bytes = 0u64;
|
||||
let mut ttfb_ms = None;
|
||||
let mut first_chunk_telemetry_emitted = false;
|
||||
let mut bytes_stream = response.bytes_stream();
|
||||
while let Some(item) = bytes_stream.next().await {
|
||||
match item {
|
||||
@@ -49,6 +50,26 @@ pub(crate) fn build_direct_execution_frame_stream(
|
||||
if ttfb_ms.is_none() {
|
||||
ttfb_ms = Some(started_at.elapsed().as_millis() as u64);
|
||||
}
|
||||
if !first_chunk_telemetry_emitted {
|
||||
let telemetry_frame = StreamFrame {
|
||||
frame_type: StreamFrameType::Telemetry,
|
||||
payload: StreamFramePayload::Telemetry {
|
||||
telemetry: ExecutionTelemetry {
|
||||
ttfb_ms,
|
||||
elapsed_ms: ttfb_ms,
|
||||
upstream_bytes: Some(upstream_bytes),
|
||||
},
|
||||
},
|
||||
};
|
||||
match encode_stream_frame_ndjson(&telemetry_frame) {
|
||||
Ok(frame) => yield Ok(frame),
|
||||
Err(err) => {
|
||||
yield Err(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
first_chunk_telemetry_emitted = true;
|
||||
}
|
||||
upstream_bytes += chunk.len() as u64;
|
||||
let frame = StreamFrame {
|
||||
frame_type: StreamFrameType::Data,
|
||||
@@ -217,4 +238,103 @@ mod tests {
|
||||
"telemetry frame should include a measured ttfb"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn direct_execution_frame_stream_emits_telemetry_before_first_data_frame() {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("listener should bind");
|
||||
let addr = listener.local_addr().expect("local addr should resolve");
|
||||
let server = tokio::spawn(async move {
|
||||
let app = Router::new().route(
|
||||
"/stream",
|
||||
post(|| async {
|
||||
let body_stream = stream! {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
yield Ok::<Bytes, Infallible>(Bytes::from_static(b"data: hello\n\n"));
|
||||
};
|
||||
(
|
||||
[(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/event-stream"),
|
||||
)],
|
||||
Body::from_stream(body_stream),
|
||||
)
|
||||
}),
|
||||
);
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.expect("server should start");
|
||||
});
|
||||
|
||||
let runtime = DirectSyncExecutionRuntime::new();
|
||||
let execution = runtime
|
||||
.execute_stream(ExecutionPlan {
|
||||
request_id: "req-telemetry-order".to_string(),
|
||||
candidate_id: Some("cand-telemetry-order".to_string()),
|
||||
provider_name: Some("OpenAI".to_string()),
|
||||
provider_id: "provider-1".to_string(),
|
||||
endpoint_id: "endpoint-1".to_string(),
|
||||
key_id: "key-1".to_string(),
|
||||
method: "POST".to_string(),
|
||||
url: format!("http://{addr}/stream"),
|
||||
headers: BTreeMap::new(),
|
||||
content_type: None,
|
||||
content_encoding: None,
|
||||
body: RequestBody {
|
||||
json_body: None,
|
||||
body_bytes_b64: None,
|
||||
body_ref: None,
|
||||
},
|
||||
stream: true,
|
||||
client_api_format: "openai:chat".to_string(),
|
||||
provider_api_format: "openai:chat".to_string(),
|
||||
model_name: Some("gpt-5".into()),
|
||||
proxy: None,
|
||||
tls_profile: None,
|
||||
timeouts: Some(ExecutionTimeouts {
|
||||
connect_ms: Some(5_000),
|
||||
total_ms: Some(5_000),
|
||||
..ExecutionTimeouts::default()
|
||||
}),
|
||||
})
|
||||
.await
|
||||
.expect("stream execution should succeed");
|
||||
|
||||
let frames = build_direct_execution_frame_stream(execution)
|
||||
.map(|item| item.expect("frame should encode"))
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|bytes| String::from_utf8(bytes.to_vec()).expect("frame should be utf8"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
server.abort();
|
||||
|
||||
let frame_types = frames
|
||||
.iter()
|
||||
.map(|line| {
|
||||
serde_json::from_str::<Value>(line)
|
||||
.expect("frame should parse")
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let first_data_idx = frame_types
|
||||
.iter()
|
||||
.position(|kind| kind == "data")
|
||||
.expect("data frame should exist");
|
||||
let first_telemetry_idx = frame_types
|
||||
.iter()
|
||||
.position(|kind| kind == "telemetry")
|
||||
.expect("telemetry frame should exist");
|
||||
|
||||
assert!(
|
||||
first_telemetry_idx < first_data_idx,
|
||||
"first telemetry frame should be emitted before the first data frame"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ pub(crate) async fn execute_execution_runtime_sync(
|
||||
let plan_request_id_for_log = short_request_id(plan_request_id);
|
||||
let plan_candidate_id = plan.candidate_id.as_deref();
|
||||
let candidate_started_unix_secs = current_request_candidate_unix_ms();
|
||||
state
|
||||
.usage_runtime
|
||||
.record_pending(state.data.as_ref(), &plan, report_context.as_ref())
|
||||
.await;
|
||||
#[cfg(not(test))]
|
||||
let result = {
|
||||
match DirectSyncExecutionRuntime::new()
|
||||
@@ -276,10 +280,6 @@ pub(crate) async fn execute_execution_runtime_sync(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
state
|
||||
.usage_runtime
|
||||
.record_pending(state.data.as_ref(), &plan, report_context.as_ref())
|
||||
.await;
|
||||
let terminal_unix_secs = current_request_candidate_unix_ms();
|
||||
record_local_request_candidate_status(
|
||||
state,
|
||||
|
||||
Reference in New Issue
Block a user