feat(gateway): interrupt 支持 scope(当前/全部),默认维持现状(协同 A2)
- 原状:interrupt{sessionId} 取消在飞回合**并清空 pending** ⇒ 客户端「打断模式」连发会被静默丢弃
- 修法:新增 scope 三态(与项目既有「省略=保留现状」口径一致)
· 省略 或 "all" = 现状语义(取消当前 + 清 pending)
· "current" = **只取消当前回合、保留 pending**(pending 继续按编排器规则续跑)
· 非法值 → **定向回错误**(只回发送者,不广播),显式报错而非静默当默认
- 下行 session_interrupted 的 dropped 如实反映**真的被丢弃的条数**(scope=current 时通常为 0)
- 复核:gateway 包 build/vet/测试绿(新增 interrupt_scope_* 两组用例);后端 :10942 全程未停
This commit is contained in:
1 parent
decf59c1b4
commit
5494c31474
5 files changed
+700
-60
No files matched your search
@@ -0,0 +1,94 @@
|
||||
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:只取消在飞回合,**保留**待处理队列。它不递增
|
||||
// 代次,因此被取消回合的结算(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)。
|
||||
func (o *Orchestrator) interruptLocked(sessionID string, st *sessionState, scope InterruptScope) int {
|
||||
if st == nil || st.mode == ModeIdle {
|
||||
return 0
|
||||
}
|
||||
if st.cancel != nil {
|
||||
st.cancel()
|
||||
}
|
||||
if scope == InterruptScopeCurrent {
|
||||
// 只取消在途回合:不碰 pending、不递增代次。mode 如实报告为
|
||||
// 当时的忙碌通道(回合仍在收尾、队列还在续跑),而不是 idle。
|
||||
o.emitState(sessionID, EventSessionInterrupt, map[string]any{"sessionId": sessionID, "mode": st.mode, "dropped": 0})
|
||||
return 0
|
||||
}
|
||||
dropped := len(st.pending)
|
||||
st.pending = nil
|
||||
st.gen++
|
||||
st.mode = ModeIdle
|
||||
st.cancel = nil
|
||||
o.emitState(sessionID, EventSessionInterrupt, map[string]any{"sessionId": sessionID, "mode": ModeIdle, "dropped": dropped})
|
||||
return dropped
|
||||
}
|
||||
Reference in new issue
Block a user