Skip to content

feat(v2): coordinate scheduled conversations through shared execution - #217

Merged
EricWvi merged 35 commits into
ora-space:mainfrom
wanglongan587:feat/785-schedule-orchestration
Oct 31, 2026
Merged

EricWvi merged 35 commits into
ora-space:mainfrom
wanglongan587:feat/785-schedule-orchestration

Conversation

@wanglongan587

@wanglongan587 wanglongan587 commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

调度编排与对话执行适配:周期扫描 Host、ConversationAdapter、立即运行与取消 HTTP 入口,接入统一 server.Options.Execution / Catalog。

#214 记录/时间 → #215 最终状态/查询
                         ├→ #217 调度/操作
                         └→ #221 历史/结果
                              → #224 页面消费

本轮在已同步 #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 预算。
  • HTTP 操作错误与退出顺序:配置与运行记录已存在但目标模型/资源返回 execution.ErrNotFound 时返回 409 execution_target_unavailable 并附 params.occurrenceId,500 internal 补齐 slog.Error 服务端日志;main.go 收到退出信号后先取消扫描 Host 并停止 ticker,再执行 HTTP Shutdown。

本 PR 依赖与审查范围: 依赖 #215;审查自身增量可用 git diff 1380b6eb bf4a771b -- v2-server。

Evidence

  • 本次 head bf4a771b48211af205c232c64349829a6802fb38。CI:https://github.com/ora-space/cloud/actions/runs/41164811270 (server / frontend / e2e 全绿)。
  • 真实 PostgreSQL go test -race -count=1 . ./internal/scheduler ./internal/server 及 Linux go vet / go build 通过。
  • 下图为当前五份 PR 组合在本地预览环境的消费页面截图(截图提交于独立分支 screenshots/schedule-review @ 7b34f765,不进入产品 PR)。

运行历史与选中态预览

Merge Danger

Door: two-way

Blast Radius: v2

v2,不涉及 specs。

Refs ora-space/circus#785

@wanglongan587 wanglongan587 changed the title feat(v2): coordinate schedules through model execution interface feat(v2): coordinate scheduled conversations through shared execution Oct 30, 2026
@wanglongan587
wanglongan587 requested a review from ObsisMc October 31, 2026 02:05

@ObsisMc ObsisMc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 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 不补跑,游标从当前时刻之后开始;计划锚点不随跳过或手动运行移动。
  • 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,没有在本地跑测试套件。

非阻塞建议

  1. (建议合并前改)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 的其他错误也全部看不到。建议先拆开逐个过滤再记录。
  2. 自动触发可能一直停在 recorded。Tick 在 RecordOccurrence 之前就推进了游标(scheduler.go:188)。记录成功后,如果 Dispatch 在 begin 之前失败(GetOccurrence 或 begin 遇到数据库瞬时错误,或 30s scope 预算刚好用完),这条记录就留在 recorded:它是 released=true,ActiveOccurrences 不会再取到它,只有下次重启 Recover 才改成 skipped/server_restarted。历史里会一直显示"已记录",和"要么接收、要么明确跳过"不一致。建议 dispatch 前失败时记一个明确的 skipped 原因,或者让下一次 Tick 收尾这类记录。
  3. scheduleActionError(schedule_actions.go:121)默认分支直接写 500,没有像其他 handler 那样经 internal() 记录原因,数据库错误在服务端不留痕。
  4. StartModel 对无效模型可能返回 execution.ErrNotFound:这时记录已经是 skipped/execution_target_unavailable,HTTP 却回 404 not_found 并去掉 params.occurrenceId,和 README 里"已保存记录的错误响应带 occurrenceId"不一致,前端会以为任务不存在。
  5. Tick 先 Reconcile 再派发,共用一个 30s 预算;End 等确认可能吃掉大部分预算,使 Start 拿到 DeadlineExceeded,下一次 Tick 再转成 execution_not_started。跳过原因其实是本地预算用完。可以给 Dispatch 单独的预算。
  6. main.go 里 srv.Shutdown 只给 5s,而 Host 的 ctx 在 Shutdown 之后才取消;正在进行的 Manual 最长 30s,会让 Shutdown 超时退出码为 1。可以先取消 Host 再 Shutdown。
  7. runtime_integration_test.go 依赖的 internal/executionruntime 在本 PR head 上不存在(main 上已有),带 execution_integration tag 目前编译不过;rebase 到 main 后即可。

结论:没有阻塞问题,可以合并;第 1、2 条建议合并前处理,是否等改完由 owner 决定。

@ObsisMc ObsisMc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 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 ObsisMc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 head bf4a771b48211af205c232c64349829a6802fb38。相对上次 Approve 的 158cca9c,新增的 a9d544f0 和 bf4a771b 两个提交逐条处理了我上次的建议,我都核对过:

  1. 错误吞日志:reportableScanError 递归拆开 errors.Join,逐个叶子过滤 Unavailable/NotFound/OperationInProgress,fmt.Errorf("dispatch %s: %w", id, errors.Join(...)) 这种带前缀的组合错误也会展开并保留前缀。单条 NotFound 不会再让同批的数据库或派发错误不打日志。
  2. 自动触发停在 recorded:abandonScheduledRecord 只处理 kind=scheduled 且仍是 recorded 的记录,走 begin → not_accepted,复用 #215 的状态检查(其他占位、配置变更、暂停都照常判定),已经越过 begin 的记录不会被动到。它在 Dispatch 失败后立即用独立的 5s 上下文执行,Tick 开头的 RecoverableOccurrences 也会兜底。手动的 recorded 留给同键重试。Tick 持有 scope 锁,不会和 Dispatch 并发。
  3. 预算:prepare、查询、Reconcile 和每次 Dispatch 各有独立的 30s 预算,Reconcile 不再挤掉 Start 的时间。
  4. 错误码:execution.ErrNotFound 在记录已存在时返回 409 execution_target_unavailable,并带上 occurrenceId;500 都会 slog.Error。契约里 409 是泛化描述,不需要改。新测试覆盖了这条路径。
  5. 关停:退出时先 cancelSchedules(),再进入 5s 的 srv.Shutdown,HTTP 排空不再被 30s 的手动操作卡住。

CI 全绿。

剩下一点不阻塞:已绑定 Ref 但 ReadAttempt 一直返回 NotFound 的记录,仍会被当作"预期错误"过滤,永久占位时没有日志。演示期可以接受,以后如果真遇到任务一直 previous_unfinished,可以考虑对这种情况单独打一条日志。

结论:可以合并,合并由 owner 决定。

@ObsisMc ObsisMc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 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 ObsisMc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 head 73012b2a(上次 Approve 在 abdf7a49)。

  • b4cfc1a3 合并 main 3a30b0b8(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。

非阻塞:

  1. 这个分支只在 t.Kind == "scheduled" 时走到,HTTP 手动运行是 manual,不会经过这里;Tick 在调用前也已自己解析规则并把错误计入 scan failures。所以新映射实际起的是兜底作用,比如规则在 Tick 和记录之间被改成无效值。
  2. 直接 return ErrIneligible 会丢掉原始解析错误。可以考虑 fmt.Errorf("%w: %v", ErrIneligible, e),排查时能看到是哪条规则、什么原因。

@EricWvi
EricWvi merged commit 117d16d into ora-space:main Oct 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants