mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 11:19:50 +08:00
refactor(ws): 拆分 LogicalTurn 与 ProviderAttempt,结算改表驱动
评审第 5 条:一个 ResponsesWebSocketTurn 同时代表 logical turn 和 provider attempt,finalize() 又用 outcome.cancelled() 一个布尔驱动 billing、candidate 状态和供应商效果,于是 provider 终态已经到达、只是最后一跳写客户端失败时, 供应商事实会被 Cancelled 覆盖掉。 - ResponsesWebSocketTurn → ResponsesProviderAttempt, ActiveResponsesWebSocketTurn → ActiveProviderAttempt:类型名字明确它只代表 一次上游执行,logical turn 由 C1 落地的 LogicalTurn 承担。 - 新增 settlement.rs:AttemptProviderOutcome × AttemptClientDelivery 两个正交 事实,classify_attempt_settlement 一张表推出 status_code / billing / candidate 状态 / candidate 错误分类 / 供应商效果 / 是否提交 execution report。 - attempt 观察到 provider 终态即记录 provider_outcome。结算信号 ResponsesWebSocketTurnOutcome 只回答「为什么现在结算」:ProviderTerminal 与 Failure 对 provider 是权威的,Cancelled 只描述客户端/连接层面的停止,不再 覆盖已观察到的 provider 事实。 - candidate 状态与 candidate 错误分类分开输出:现状存在 「missing_terminal=true 而记账层判 Success」的组合(report kind 不要求观察到 终态事件时),会写出 status=Success + error_type=stream_missing_terminal_event, 这个组合必须原样保留。 classify_responses_websocket_turn_effect 的判定表原样搬入 settlement.rs,分支 和顺序均未改动,两个既有不变量测试随之迁移。 行为等价。结算表当前口径与拆分前完全一致:客户端投递失败仍与「供应商声明取消」 落在同一侧(作废账单、candidate 记 Cancelled、只释放 lease、不提交 execution report),即使 provider 终态已经到达——这一行由 settlement_table_row_client_delivery_failure_currently_voids_a_reached_terminal 锁住现状,修正它是下一步独立的行为修正。 新增 15 个测试:outcome → 双事实映射表逐行(含 stream_timeout 只在 504 失败一族 成立、provider 终态即使 504 也不投射流式超时)、结算表逐行、投递失败时 forced_error 必须为 None、已观察终态不被 Cancelled 覆盖、以及跨整张表的 「每个分支都释放 pool key lease」「作废账单一律不提交 report」不变量。
This commit is contained in:
@@ -11,7 +11,7 @@ use tokio::time::timeout;
|
||||
|
||||
use super::state::BoundResponsesConnection;
|
||||
use super::turn::{
|
||||
spawn_responses_websocket_turn_finalization, ResponsesWebSocketTurn,
|
||||
spawn_responses_websocket_turn_finalization, ResponsesProviderAttempt,
|
||||
ResponsesWebSocketTurnOutcome,
|
||||
};
|
||||
use crate::handlers::proxy::websocket::session::{
|
||||
@@ -37,13 +37,13 @@ macro_rules! warn {
|
||||
/// would otherwise be discarded with its usage row left `Pending`, its
|
||||
/// candidate row left `Streaming`, and its distributed pool key lease leaked
|
||||
/// until the lease expires. Mirrors the HTTP path's `DirectPassthroughFinalizer`.
|
||||
pub(super) struct ActiveResponsesWebSocketTurn {
|
||||
turn: Option<ResponsesWebSocketTurn>,
|
||||
pub(super) struct ActiveProviderAttempt {
|
||||
turn: Option<ResponsesProviderAttempt>,
|
||||
state: AppState,
|
||||
}
|
||||
|
||||
impl ActiveResponsesWebSocketTurn {
|
||||
pub(super) fn new(state: &AppState, turn: ResponsesWebSocketTurn) -> Self {
|
||||
impl ActiveProviderAttempt {
|
||||
pub(super) fn new(state: &AppState, turn: ResponsesProviderAttempt) -> Self {
|
||||
Self {
|
||||
turn: Some(turn),
|
||||
state: state.clone(),
|
||||
@@ -51,15 +51,15 @@ impl ActiveResponsesWebSocketTurn {
|
||||
}
|
||||
|
||||
/// Hands the turn back to a caller that will finalize it explicitly.
|
||||
pub(super) fn disarm(mut self) -> ResponsesWebSocketTurn {
|
||||
pub(super) fn disarm(mut self) -> ResponsesProviderAttempt {
|
||||
self.turn
|
||||
.take()
|
||||
.expect("an armed active turn always holds its turn")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ActiveResponsesWebSocketTurn {
|
||||
type Target = ResponsesWebSocketTurn;
|
||||
impl std::ops::Deref for ActiveProviderAttempt {
|
||||
type Target = ResponsesProviderAttempt;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.turn
|
||||
@@ -68,7 +68,7 @@ impl std::ops::Deref for ActiveResponsesWebSocketTurn {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for ActiveResponsesWebSocketTurn {
|
||||
impl std::ops::DerefMut for ActiveProviderAttempt {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.turn
|
||||
.as_mut()
|
||||
@@ -76,7 +76,7 @@ impl std::ops::DerefMut for ActiveResponsesWebSocketTurn {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ActiveResponsesWebSocketTurn {
|
||||
impl Drop for ActiveProviderAttempt {
|
||||
fn drop(&mut self) {
|
||||
let Some(turn) = self.turn.take() else {
|
||||
return;
|
||||
@@ -120,7 +120,7 @@ pub(super) async fn finalize_active_turn(
|
||||
pub(super) async fn queue_turn_finalization(
|
||||
bound: &mut BoundResponsesConnection,
|
||||
state: &AppState,
|
||||
turn: ResponsesWebSocketTurn,
|
||||
turn: ResponsesProviderAttempt,
|
||||
outcome: ResponsesWebSocketTurnOutcome,
|
||||
) {
|
||||
await_pending_adapter_observation(bound).await;
|
||||
@@ -161,7 +161,7 @@ pub(super) async fn await_pending_adapter_observation(bound: &mut BoundResponses
|
||||
|
||||
pub(super) async fn finalize_unbound_turn(
|
||||
state: AppState,
|
||||
turn: ResponsesWebSocketTurn,
|
||||
turn: ResponsesProviderAttempt,
|
||||
outcome: ResponsesWebSocketTurnOutcome,
|
||||
) -> JoinHandle<()> {
|
||||
spawn_responses_websocket_turn_finalization(state, turn, outcome).await
|
||||
|
||||
Reference in New Issue
Block a user