宿主:并发拒单的两个身份改成回合身份

- Thread Manager 的 `accept_turn` 在并发冲突时回**已有的 `turn_id`**(调用方的 `clientTurnId`),不再回进程内的占用 token
- `DirectTurnReservation::accept` 的拒单载荷改成 `existing`=已在跑那一轮的 `clientTurnId`、`incoming`=本次请求的 `clientTurnId`;同一轮重发时两者相等,"同一轮消息仍在处理中"那条文案才走得到
- 补 `accept_conflict_reports_client_turn_ids_not_reservation_tokens`(同一轮 / 另一轮两条分支都钉住)与 `accept_conflict_returns_the_existing_turn_id`(manager 侧只回回合身份)
- 占用对象自己的 token 保持 UUID 不变(`complete_direct_thread_turn_if_reserved` 靠它配对),改的只有错误载荷
This commit is contained in:
2026-09-24 16:08:39 +08:00
parent 9ecb084b68
commit bd78a91e55
2 changed files with 74 additions and 4 deletions
@@ -200,8 +200,10 @@ impl DirectThreadManager {
/// 接单:同一个临界区里拒绝并发、登记占用、追加逻辑回合开始事件。
///
/// 返回 `Err(existing_token)` 表示这个 thread 已经有一条没收口的回合——此时不动队列,
/// 由调用方把它投影成接单拒绝。
/// 返回 `Err(existing_turn_id)` 表示这个 thread 已经有一条没收口的回合——此时不动队列,
/// 由调用方把它投影成接单拒绝。回的是**回合身份**(调用方接单时给的 `turn_id`)而不是占用
/// `token`:占用 token 只活在这个进程里,界面拿它匹配不了自己发出的那一轮,也没法判断
/// "撞的是同一轮还是另一轮"。
fn accept_turn(
&mut self,
thread_id: &str,
@@ -213,7 +215,7 @@ impl DirectThreadManager {
{
let thread = self.threads.entry(thread_id.to_string()).or_default();
if let Some(active) = thread.active_turn.as_ref() {
return Err(active.token.clone());
return Err(active.turn_id.clone());
}
thread.active_turn = Some(ActiveDirectTurn {
token: token.to_string(),
@@ -562,6 +564,9 @@ pub(crate) fn append_direct_thread_event(
}
/// 接单:拒绝并发 + 登记占用 + 发逻辑回合开始事件(见 [`DirectThreadManager::accept_turn`])。
/// `Err` 是这一轮**已有的回合身份**(`turn_id`,也就是调用方的 `clientTurnId`),不是占用 token:
/// 调用方拿它投影成 `TurnAlreadyRunning` 的两个身份字段,界面按"撞的是同一轮还是另一轮"决定要
/// 不要动当前回合。
pub(crate) fn accept_direct_thread_turn(
thread_id: &str,
token: &str,
@@ -1072,4 +1077,20 @@ mod tests {
);
assert!(snapshot_of(&manager, thread_id).is_none());
}
/// 并发接单回给调用方的是**回合身份**(`turn_id`),不是占用 `token`:token 只活在这个进程
/// 里,界面拿它匹配不了自己发出的那一轮。
#[test]
fn accept_conflict_returns_the_existing_turn_id() {
let mut manager = DirectThreadManager::with_limits(100, 100_000);
manager
.accept_turn("thread-1", "token-1", "turn-1", None, FIXED_AT_MS)
.expect("accept");
let conflict = manager
.accept_turn("thread-1", "token-2", "turn-2", None, FIXED_AT_MS)
.err();
assert_eq!(conflict.as_deref(), Some("turn-1"));
}
}
@@ -32,6 +32,10 @@ impl DirectTurnReservation {
/// 失败表示这个 thread 已经有一条没收口的回合(并发接单),此时不改队列、不发事件。
/// `client_turn_id` 是给界面看的回合身份(首页快照与进度回填按它匹配),与占用身份 `token`
/// 是两件事:前者来自调用方,后者只活在这个进程里。
///
/// 拒单载荷里的两个身份都取**回合身份**:`existing` 是已在跑的那一轮的 `clientTurnId`
/// (Thread Manager 回的就是它),`incoming` 是本次请求的 `clientTurnId`。同一轮重发时两者
/// 相等,界面才走得到"同一轮消息仍在处理中"那条文案。
pub(crate) fn accept(
thread_id: &str,
client_turn_id: &str,
@@ -47,7 +51,7 @@ impl DirectTurnReservation {
)
.map_err(|existing| DirectTurnError::TurnAlreadyRunning {
existing_invocation_id: existing,
incoming_invocation_id: token.clone(),
incoming_invocation_id: client_turn_id.to_string(),
})?;
Ok(Self {
thread_id: thread_id.to_string(),
@@ -152,6 +156,51 @@ mod tests {
drop(reservation);
}
/// 拒单载荷里的两个身份都是**回合身份**:撞的是同一轮时两者相等,界面才走得到"同一轮消息仍在
/// 处理中";撞的是另一轮时两者不等,界面才敢提示"另一条回合在运行"。占用 token 只活在本进程,
/// 一旦漏进载荷,这两个分支就都判不出来(UUID 永远不等于界面的 `clientTurnId`)。
#[test]
fn accept_conflict_reports_client_turn_ids_not_reservation_tokens() {
let thread = unique_thread("same-turn-conflict");
let reservation =
DirectTurnReservation::accept(&thread, "turn-1", Some("u-1")).expect("accept");
let same_turn = match DirectTurnReservation::accept(&thread, "turn-1", Some("u-1")) {
Ok(_) => panic!("同一 thread 的第二条回合必须被拒"),
Err(error) => error,
};
let DirectTurnError::TurnAlreadyRunning {
existing_invocation_id,
incoming_invocation_id,
} = &same_turn
else {
panic!("expected a concurrency rejection, got {same_turn:?}");
};
assert_eq!(existing_invocation_id, "turn-1");
assert_eq!(incoming_invocation_id, "turn-1");
assert!(
same_turn.to_string().contains("同一轮消息仍在处理中"),
"{same_turn}"
);
let other_turn = match DirectTurnReservation::accept(&thread, "turn-2", Some("u-2")) {
Ok(_) => panic!("同一 thread 的第二条回合必须被拒"),
Err(error) => error,
};
assert!(matches!(
&other_turn,
DirectTurnError::TurnAlreadyRunning {
existing_invocation_id,
incoming_invocation_id,
} if existing_invocation_id == "turn-1" && incoming_invocation_id == "turn-2"
));
assert!(
other_turn.to_string().contains("另一条 Direct 客户端回合"),
"{other_turn}"
);
drop(reservation);
}
#[test]
fn drop_without_a_terminal_writes_a_host_dropped_terminal() {
let thread = unique_thread("drop");