Files
Cyrene_v2/internal/services/gateway/session_interrupt.go
T
AskaEth 89e13cf53d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway,loop): 忙时并行回合基础管线(I1′ 有条件放宽)
- turnId 由编排器生成、loop Run 复用(硬前提:否则 turn_begin 与流帧各铸一个,客户端两级分桶立即失配)
- StateSink 增带 turnId 的变体(未接线时退化为现签名,不丢状态帧)
- 上行 chat.parallel(WS chat 帧 + REST 三入口同形;省略 = 逐字不变)
- 单会话额度默认 2 / 硬上限 3 / <2 关闭;额度满或未声明 ⇒ 落回既有排队语义
- I1′:checkpoint 归属、历史配对、忙态计数三处从会话级单例升为路级
- session_idle 收紧为「所有在途路收尾且 pending 空」(仅并行时生效;单路逐字不变)
- 预留接口(形状定死、无实现无接线):tool/legfile.go 的 FileClaimer / ErrFileBusy / WithLegID
- 顺手修一个真 bug:并行路 ctx 派生自提交请求作用域,REST 同步入口一写响应就把它连坐杀死(改 context.WithoutCancel 派生)
- 23 个新用例(含 -race -count=3);变异 6 条(turnId 复用 / idle 时点 / 并行升级 / checkpoint 键 / 编排器交 id / pending_ack reason)
- 契约变更点 10 条待监工升 v0.65(本人未改 Docs/**)
2026-10-05 09:18:50 +08:00

112 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
// 本文件归属协同项 A2 的打断口径:`interrupt {sessionId, scope}` 决定一次
// 用户打断清到哪一层。它从 session_orchestrator.go 拆出,使编排器本体只留
// 消息状态机(Submit/pending/续跑),打断的选择与语义在这里回答 —— 也避免
// 编排器主文件越过「单文件 ≤500 行」的硬约束。
//
// 上游清单:`Cyrene-For-Android/devdocs/02-前后端协同事项.md`(A2.scope)。
// 客户端有「追加队列」与「打断」两种模式,若打断一律连队列一起清,用户选
// 「打断」时连发的多条消息会被静默丢弃,只回一个 dropped 数字。
// InterruptScope 选择一次 interrupt 清到哪一层(协同项 A2)。
type InterruptScope string
const (
// InterruptScopeAll 取消全部在途路并清空待处理队列。它是省略 scope 时的
// 默认口径,与历史行为逐字一致。
InterruptScopeAll InterruptScope = "all"
// InterruptScopeCurrent 只取消在途路(多路时为**每一路**,见下),**保留**
// 待处理队列:队列里的消息按编排器正常的续跑规则继续执行,一条都不丢。
InterruptScopeCurrent InterruptScope = "current"
)
// Interrupt 取消**全部**在途路、清空待处理队列并把会话回到空闲(design/15 §3,
// 「停下/算了/换个话题」路径)。它是忙碌会话不必排空队列就能复位的唯一方式;
// 运行中 goroutine 的完成变成失效路,不续跑任何东西。它返回被丢弃的
// 待处理回合数。
//
// 它把会话立即报成空闲并立即接受下一条消息(design/15 §3 的「立即
// 处理新消息」),但被取消的回合仍要收尾:下一个回合的启动被 session_turn_fence.go
// 的围栏挡在那次收尾之后,因此它绝不会读到被放弃回合那份仍可恢复的旧快照。
func (o *Orchestrator) Interrupt(sessionID string) int {
return o.InterruptScope(sessionID, InterruptScopeAll)
}
// InterruptScope 按 scope 中断一个会话(协同项 A2),返回真的被丢弃的
// 待处理回合数:
//
// - InterruptScopeAll:取消全部在途路、清空待处理队列并把会话回到空闲
// —— 省略 scope 时的历史语义,逐字不变;
// - InterruptScopeCurrent:只取消在途路(多路时每一路都取消,因为省略
// turnId 的 scope 就是「作用于每一路」——design/15 §2.5.4),**保留**待处理
// 队列。它不解除路的登记,因此被取消回合的结算(completeTurn)仍会按正常
// 续跑规则弹出队列里的下一条消息、带上会话尾部上下文继续跑(design/15
// §2.2),返回 0 —— 真的没有消息被丢弃。
//
// 空字符串与省略同义(都是现状语义),使老客户端只发 {sessionId} 的
// 行为完全不变。
func (o *Orchestrator) InterruptScope(sessionID string, scope InterruptScope) int {
if scope == "" {
scope = InterruptScopeAll
}
sessionID = o.normalize(sessionID)
o.mu.Lock()
defer o.mu.Unlock()
return o.interruptLocked(sessionID, o.states[sessionID], scope)
}
// InterruptAll 取消每一个在飞会话并清空所有待处理队列。它在网关关闭时使用,
// 使没有后台回合比 hub 活得更久。它返回被丢弃的待处理回合总数。
func (o *Orchestrator) InterruptAll() int {
o.mu.Lock()
total := 0
for id, st := range o.states {
total += o.interruptLocked(id, st, InterruptScopeAll)
}
o.mu.Unlock()
return total
}
// interruptLocked 在状态锁下执行一次中断。scope 决定待处理队列的去留
// (见 InterruptScope):All 清队列、使全部在途路失效并把会话回到空闲;
// Current 只取消在途路,队列与路登记原样留下,使被取消回合的结算仍会续跑队列。
// 它返回真的被丢弃的待处理回合数(Current 恒为 0)。
//
// 多路口径(design/15 §2.5.4):无论哪个 scope,被取消的都是**当前全部在途路**
// —— 省略 turnId 的 scope 就是「作用于每一路」的意思(精确停某一路是后续切片的
// `interrupt.turnId`,本切片不做)。解除在途登记的方式与旧口径的 `st.gen++` 等价:
// 把路从 st.legs 里摘掉,被取消路的迟到结算因此找不到自己的路、成为空操作。
func (o *Orchestrator) interruptLocked(sessionID string, st *sessionState, scope InterruptScope) int {
if st == nil || st.mode == ModeIdle {
return 0
}
canceled := o.cancelLegsLocked(st)
if scope == InterruptScopeCurrent {
// 只取消在途路:不碰 pending、不解除路的登记(它们仍在收尾)。mode 如实
// 报告为当时的忙碌通道(回合仍在收尾、队列还在续跑),而不是 idle。
o.emitState(sessionID, EventSessionInterrupt, interruptPayload(sessionID, st.mode, 0, canceled, len(st.legs)))
return 0
}
dropped := len(st.pending)
st.pending = nil
st.legs = nil
st.mode = ModeIdle
o.emitState(sessionID, EventSessionInterrupt, interruptPayload(sessionID, ModeIdle, dropped, canceled, 0))
return dropped
}
// interruptPayload 组装中断帧的载荷(design/15 §2.6):既有键(sessionId / mode /
// dropped)一个不改,新增两个**计数**键 —— cancelledTurns(本次真的取消了几路)
// 与 activeTurns(清点之后还在途的路,scope=all 恒为 0)。两者都不含内容,
// 因此该帧仍在「状态机信号」白名单内。
func interruptPayload(sessionID string, mode Mode, dropped, cancelledTurns, activeTurns int) map[string]any {
return map[string]any{
"sessionId": sessionID,
"mode": mode,
"dropped": dropped,
"cancelledTurns": cancelledTurns,
"activeTurns": activeTurns,
}
}