Repository navigation
feat(v2): coordinate scheduled conversations through shared execution - #217
Conversation
ObsisMc
left a comment
There was a problem hiding this comment.
审查 head 1252db8292f6c29793e6a3bf3712b7f66bf68820。只审本 PR 相对 #215(2ad52b8d)的增量:git diff 2ad52b8d 1252db82,21 个文件。
核对
- 增量都在
v2-server/下,都是代码、测试、OpenAPI 和长期文档,描述写了「v2,不涉及 specs」。 - owner 规则逐条对上了:
- 防重叠:自动和手动都经 #215 的配置行锁和部分唯一索引,
begin时再查一次。 - 明确 Busy 记 skipped,不等待、不重试。
- 超时、取消、Conflict 这类结果转
reconciling,只按原 Caller 用ReadAttempt核对,不会再次 Start。 - 只有 End 确认后才释放占位。
- Recover 不补跑,游标从当前时刻之后开始;计划锚点不随跳过或手动运行移动。
- 防重叠:自动和手动都经 #215 的配置行锁和部分唯一索引,
- HTTP:先用
GetScheduleConfig校验创建者归属,取消时再核对old.ScheduleID == id,隐藏资源统一 404。 - 并发:同一 occurrence 的操作由
enter()串行;Recover/Tick/Manual 共用 scope 锁;Host.ops的 Add/Wait 有锁保证先后。main.go先等 Host 结束再关库。 schedule-actions.openapi.json和 handler 的请求体、回执字段一致。- head 上 CI 全绿。本次做了静态审查和
go vet ./internal/scheduler,没有在本地跑测试套件。
非阻塞建议
- (建议合并前改)
Host.scan会吞掉同批的真实错误。Coordinator.Tick返回errors.Join(failures...)(scheduler.go:205),host.go:119再对合并后的错误做errors.Is(..., ErrNotFound / ErrOperationInProgress / ...)。errors.Is会遍历所有成员,只要有一条是 NotFound,同一次 Tick 里数据库写失败、dispatch 失败等错误都不打日志。比如某条已绑定 Ref 的记录ReadAttempt一直返回 NotFound(按文档会永久占位),这个任务之后每次都是previous_unfinished,同 scope 的其他错误也全部看不到。建议先拆开逐个过滤再记录。 - 自动触发可能一直停在
recorded。Tick在RecordOccurrence之前就推进了游标(scheduler.go:188)。记录成功后,如果Dispatch在begin之前失败(GetOccurrence或begin遇到数据库瞬时错误,或 30s scope 预算刚好用完),这条记录就留在recorded:它是released=true,ActiveOccurrences不会再取到它,只有下次重启 Recover 才改成skipped/server_restarted。历史里会一直显示"已记录",和"要么接收、要么明确跳过"不一致。建议 dispatch 前失败时记一个明确的 skipped 原因,或者让下一次 Tick 收尾这类记录。 scheduleActionError(schedule_actions.go:121)默认分支直接写 500,没有像其他 handler 那样经internal()记录原因,数据库错误在服务端不留痕。StartModel对无效模型可能返回execution.ErrNotFound:这时记录已经是skipped/execution_target_unavailable,HTTP 却回404 not_found并去掉params.occurrenceId,和 README 里"已保存记录的错误响应带 occurrenceId"不一致,前端会以为任务不存在。- Tick 先 Reconcile 再派发,共用一个 30s 预算;
End等确认可能吃掉大部分预算,使 Start 拿到DeadlineExceeded,下一次 Tick 再转成execution_not_started。跳过原因其实是本地预算用完。可以给 Dispatch 单独的预算。 main.go里srv.Shutdown只给 5s,而 Host 的 ctx 在 Shutdown 之后才取消;正在进行的 Manual 最长 30s,会让 Shutdown 超时退出码为 1。可以先取消 Host 再 Shutdown。runtime_integration_test.go依赖的internal/executionruntime在本 PR head 上不存在(main 上已有),带execution_integrationtag 目前编译不过;rebase 到 main 后即可。
结论:没有阻塞问题,可以合并;第 1、2 条建议合并前处理,是否等改完由 owner 决定。
ObsisMc
left a comment
There was a problem hiding this comment.
复审 head 158cca9c4ddd2f69166a976a3a18eabda48bdc4b。相对上次 Approve 的 1252db82 只多了一个 merge 提交(合进 #215 的新 head,其中带着 main 的 cloud#237)。我在本地重做了合并:冲突只在 v2-server/README.md 和 v2-server/main.go,其余文件和自动合并一致。
main.go 的解法:
server.Options改用 main 装配的executionruntime.Service作为Execution/Catalog,同时带上Projects;NewScheduleCoordinator和NewHost沿用同一份 options,没有另造 provider。- defer 后进先出:先停 Host、等扫描和已进入的 HTTP 操作,再
runtime.Close,最后关库,顺序正确;runtime.Close未确认时不关库的逻辑保留了。 fixture-check分支在 Host 创建之前返回,不受影响。
README 对应地删掉了"main 当前仍注入不可用 Stub,不会真正执行"的说法,改成由公共 Service 驱动实际派发。CI 全绿,可合并性 CLEAN。
要提醒的一点:这次合并之后,定时扫描和"立即运行"会真正调用公共执行服务,不再被 Stub 挡住。所以上次的第 1、2 条建议(errors.Join 吞日志、自动触发记录后在 begin 前失败会停在 recorded)从"假设场景"变成了真实运行路径上会遇到的问题,更建议合并前处理。另外 <-schedules.Ready() 要等首轮扫描(每个 scope 先做 Recover,预算各 30s)完成才开始监听 HTTP,scope 多时启动会变慢,演示规模下可以接受。
结论:没有阻塞问题,可以合并;上述两条建议是否等改完由 owner 决定。
ObsisMc
left a comment
There was a problem hiding this comment.
复审 head bf4a771b48211af205c232c64349829a6802fb38。相对上次 Approve 的 158cca9c,新增的 a9d544f0 和 bf4a771b 两个提交逐条处理了我上次的建议,我都核对过:
- 错误吞日志:
reportableScanError递归拆开errors.Join,逐个叶子过滤 Unavailable/NotFound/OperationInProgress,fmt.Errorf("dispatch %s: %w", id, errors.Join(...))这种带前缀的组合错误也会展开并保留前缀。单条 NotFound 不会再让同批的数据库或派发错误不打日志。 - 自动触发停在
recorded:abandonScheduledRecord只处理kind=scheduled且仍是recorded的记录,走begin→not_accepted,复用 #215 的状态检查(其他占位、配置变更、暂停都照常判定),已经越过begin的记录不会被动到。它在 Dispatch 失败后立即用独立的 5s 上下文执行,Tick 开头的RecoverableOccurrences也会兜底。手动的recorded留给同键重试。Tick 持有 scope 锁,不会和 Dispatch 并发。 - 预算:prepare、查询、Reconcile 和每次 Dispatch 各有独立的 30s 预算,Reconcile 不再挤掉 Start 的时间。
- 错误码:
execution.ErrNotFound在记录已存在时返回409 execution_target_unavailable,并带上occurrenceId;500 都会slog.Error。契约里 409 是泛化描述,不需要改。新测试覆盖了这条路径。 - 关停:退出时先
cancelSchedules(),再进入 5s 的srv.Shutdown,HTTP 排空不再被 30s 的手动操作卡住。
CI 全绿。
剩下一点不阻塞:已绑定 Ref 但 ReadAttempt 一直返回 NotFound 的记录,仍会被当作"预期错误"过滤,永久占位时没有日志。演示期可以接受,以后如果真遇到任务一直 previous_unfinished,可以考虑对这种情况单独打一条日志。
结论:可以合并,合并由 owner 决定。
ObsisMc
left a comment
There was a problem hiding this comment.
复审 head abdf7a49(上次 Approve 在 bf4a771b)。
- 合并 main
3460f078:我在本地用bf4a771b重新合并了一次 main,结果与2d2ff769树完全一致,没有手工改动;相对 main 只剩本 PR 的调度/适配增量和下面的收尾。 26e6a56d:按 owner 在 #784 的要求删掉了schema.sql里三条草稿状态的UPDATE;occurrence-states.md写明了创建者被移出租户后持续占位的已知限制,以及cancelling经recover后依赖公共执行重启结算interrupted的前提;README 的描述同步改了。abdf7a49:重开测试改为先写入当前合法状态,再验证身份、快照和占位不变,并拒绝旧枚举,覆盖到位。
CI 全绿,没有阻塞问题,Approve。
非阻塞提醒:本 PR 和 cloud#221 都改了 v2-server/README.md 里定时任务配置那一段,后合的那个会冲突。合并时以本 PR 的措辞为准(已接入执行入口),cloud#221 里"不提供模拟运行、删除或重试入口"一句在本 PR 合入后已经过时。上次提的"已绑定 Ref 一直 NotFound 时持续占位且无日志"仍是演示期可接受的非阻塞项。
ObsisMc
left a comment
There was a problem hiding this comment.
复审 head 73012b2a(上次 Approve 在 abdf7a49)。
b4cfc1a3合并 main3a30b0b8(cloud#221):我在本地用abdf7a49重新合并了一次,只有v2-server/README.md冲突;README 取本 PR 的版本后,其余文件和实际合并结果完全一致。README 的解法正确:去掉了 #221 那段过时的"不提供模拟运行、删除或重试入口",保留本 PR 的执行操作说明,#221 的只读历史/结果段落没受影响。05d76fba:RecordOccurrence在 scheduled 分支里遇到规则 JSON 解析失败或schedule.Next报错时,改为返回ErrIneligible,HTTP 会落到400 invalid_execution_request,不再是 500。新加的 PostgreSQL 回归覆盖了字段类型错误、零间隔、无效时区三种情况,并断言不会写入记录。73012b2a:#221 带进来的历史测试还依赖已删除的草稿升级,现改为写入当前合法状态,重开两次后断言身份/快照/引用/占位不变,和 #784 的要求一致。
CI 全绿,没有阻塞问题,Approve。
非阻塞:
- 这个分支只在
t.Kind == "scheduled"时走到,HTTP 手动运行是manual,不会经过这里;Tick 在调用前也已自己解析规则并把错误计入 scan failures。所以新映射实际起的是兜底作用,比如规则在 Tick 和记录之间被改成无效值。 - 直接
return ErrIneligible会丢掉原始解析错误。可以考虑fmt.Errorf("%w: %v", ErrIneligible, e),排查时能看到是哪条规则、什么原因。
Summary
调度编排与对话执行适配:周期扫描
Host、ConversationAdapter、立即运行与取消 HTTP 入口,接入统一server.Options.Execution/Catalog。本轮在已同步 #214/#215 复合游标与
main(b3b875d7)的基础上,补充收口调度扫描与操作边界:Host.scan递归拆开errors.Join(含带上下文包装的子聚合错误),仅过滤无害叶错误(ErrNotFound/ErrUnavailable/ErrOperationInProgress),确保同批真实数据库或派发失败不被吞日志;prepare与Tick预算分离。Coordinator.Tick改用RecoverableOccurrences,当scheduled记录在begin前因瞬时错误或预算耗尽未进入执行时,立即或在下一次Tick收尾为skipped/execution_not_started(保留手动recorded记录供同一requestId幂等重试);Reconcile与每次Dispatch使用独立 30s 预算。execution.ErrNotFound时返回409 execution_target_unavailable并附params.occurrenceId,500 internal补齐slog.Error服务端日志;main.go收到退出信号后先取消扫描Host并停止ticker,再执行 HTTPShutdown。本 PR 依赖与审查范围: 依赖 #215;审查自身增量可用
git diff 1380b6eb bf4a771b -- v2-server。Evidence
bf4a771b48211af205c232c64349829a6802fb38。CI:https://github.com/ora-space/cloud/actions/runs/41164811270 (server/frontend/e2e全绿)。go test -race -count=1 . ./internal/scheduler ./internal/server及 Linuxgo vet/go build通过。screenshots/schedule-review@7b34f765,不进入产品 PR)。Merge Danger
Door: two-way
Blast Radius: v2
v2,不涉及 specs。
Refs ora-space/circus#785