feat: add scoped workflow instance queries and SDK pagination

This commit is contained in:
2026-09-18 08:34:45 +08:00
parent 17a8e0126c
commit 082898e705
33 changed files with 1598 additions and 18 deletions

View File

@@ -0,0 +1,114 @@
# 流程实例筛选与分页
状态:已实现HTTP和独立Go SDK。查询返回有限元数据,不聚合业务输入输出。单个实例的完整视图和步骤详情见[实例接口](workflows.md),历史激活、轮次与信号见[历史查询](workflow-history.md)。
## 接口与权限
`GET /v1/workflows`,要求显式 `workflows:read`。命名空间取自认证凭据,不接受查询参数覆盖;返回的每个实例必须满足其**启动时固定定义版本的全部队列权限**,包含尚未执行的分支、后续步骤和POLL队列。只拥有某个已生成任务的权限不代表可以读整个流程。
没有权限覆盖的实例在数据库中被过滤。指定定义/版本但当前范围没有匹配项,返回空列表;该结果不能用于断言资源不存在。只有SIGNAL/TIMER且不引用队列的版本仍需要 `workflows:read`,队列集合为空。不额外要求 `tasks:read` 或定义发布权限。
| 参数 | 语义 |
| --- | --- |
| `definition_id` | 可选,32位小写十六进制定义ID;不传则查询命名空间内可读实例 |
| `version` | 可选,必须同时指定definition_id;1–64位发布版本名,首位字母/数字,后续可含`_ . -` |
| `state` | 可选:RUNNING、WAITING、CANCELLING、NEEDS_REVIEW、SUCCEEDED、FAILED、CANCELLED |
| `paused` | 可选,只接受`true`或`false`;与state独立,false不等于不筛选 |
| `business_key` | 可选,非空且最多191个Unicode字符,无NUL;精确匹配,保留大小写、换行和尾部空格,URL必须正确编码 |
| `created_from` | 创建时间下界,包含;RFC3339,最多微秒精度 |
| `created_before` | 创建时间上界,不包含;与下界同时提供时必须晚于下界 |
| `limit` | 默认20,1–100 |
| `cursor` | 上一页next_cursor,最多1024字节 |
时间规范化到UTC,UTC年份2000–9998;拒绝微秒以下精度。拒绝未知、重复、空值参数和无效URL编码。业务键空字符串在SDK中表示不筛选;HTTP显式`business_key=`被拒绝,首版不提供“只查无业务键”条件。
请求示意:
```http
GET /v1/workflows?definition_id=11111111111111111111111111111111&version=v1&state=WAITING&paused=false&business_key=object-001%20&limit=20
Authorization: Bearer <private-token>
```
响应信封使用 `request_id` 与 `data`:
```json
{
"request_id": "example-request",
"data": {
"items": [{
"id": "22222222222222222222222222222222",
"namespace_id": "33333333333333333333333333333333",
"definition_id": "11111111111111111111111111111111",
"definition_version_id": "44444444444444444444444444444444",
"version": "v1",
"state": "WAITING",
"revision": "2",
"paused": false,
"control_revision": "1",
"business_key": "object-001 ",
"deadline_at": "2026-09-18T01:00:00Z",
"created_at": "2026-09-18T00:00:00Z",
"updated_at": "2026-09-18T00:00:00Z"
}]
}
}
```
摘要另可包含 `cancel_requested_at`、`stop_kind`、`stop_requested_at`、`completed_at`。版本数值使用十进制字符串;state和paused可以独立变化。没有 `input`、`output`、定义正文或策略快照,读取正文使用 `GetWorkflow`。空列表为 `items: []`;没有更多结果时省略 `next_cursor`。
## 翻页一致性
按 `created_at DESC, id DESC` 排序,两个字段共同定位,时间相同也不会因只按时间定位而漏项。每页最多返回100条,数据库读取最多limit+1个候选用于判断下一页。队列范围在LIMIT前筛选,不全量加载后在Go中裁剪。
游标绑定命名空间、全部筛选和规范化队列权限范围,不绑定页长。后续页可调整limit,不能删掉筛选重试;不同权限范围必须重新从第一页查询。队列权限顺序变化不影响游标。游标不授予权限,每页仍重新认证并核验版本归属和原定义。
分页不提供跨页数据库快照。期间新建的较新实例不会出现在已有游标后,状态或paused变化可能令实例进入或离开筛选。需要长期同步应另外设计有提交水位的事件消费,不能拿列表游标代替结果订阅确认。
## SDK
```go
paused := false
query := orbit.ListWorkflowsRequest{
DefinitionID: definitionID,
Version: "v1",
State: "WAITING",
Paused: &paused,
BusinessKey: "object-001 ",
CreatedFrom: &from,
CreatedBefore: &before,
Limit: 20,
}
for {
page, err := client.ListWorkflows(ctx, query)
if err != nil {
return err // 保留筛选;不能降级成全量查询
}
for _, item := range page.Items {
fmt.Println(item.ID, item.State, item.ControlRevision)
}
if page.NextCursor == "" {
break
}
query.Cursor = page.NextCursor
}
```
这是函数内片段,完整初始化、发布、创建、分页和取消清理见[可运行示例](../examples/workflow-query.md)。SDK采用独立 `WorkflowSummary`,不引用服务端模型。
## 存储与错误边界
迁移000024增加 `weave_version_scopes`,保存每个不可变版本的完整去重队列集合和原内容摘要。发布版本与范围索引同事务,升级从原DSL节点结构回填,输入/输出常量中的同名queue_id不计入范围。模型与SQL字段均有中文注释,**没有外键或级联约束**。
索引只协助SQL过滤。返回前仍严格验证原定义及其摘要、队列集合、固定版本/定义/门控归属和冻结策略;数据损坏时整页失败,不返回部分已读数据,也不把缺失范围解释为无需队列权限。范围中错误增加的队列可能使合法记录被隐藏,应通过数据库运维修复损坏,不能以放宽权限绕过。列表不验证实例的完整输入/输出正文;这些数据由单实例详情与运行器按固定契约核验。
八种定义/业务键/状态组合使用固定复合索引;version与paused作为附加筛选。授权过滤稀疏或paused选择性低时,扫描行数可能高于返回行数,limit不是扫描上限。P6须评估队列权限分布、数据保留量、索引写放大和查询超时预算,当前回归不构成百万租户容量结论。
| 情况 | 返回 |
| --- | --- |
| 参数、游标、筛选或权限指纹不匹配 | 400 / INVALID_ARGUMENT |
| 无workflows:read | 403 / PERMISSION_DENIED |
| 当前筛选无可读记录 | 200 / 空items |
| 关联或范围损坏 | 拒绝返回该页;可能为404或内部错误,保留原请求定位数据,禁止自动删条件重试 |
| 数据库暂不可用 | 保留原筛选及游标按有限退避重试,不解释为无匹配记录 |
本迁移按维护窗口执行:先停止旧版本的流程发布写入,完成范围回填,再启动新版服务开放发布。回填后仍由旧节点发布会生成缺少范围索引的版本,新查询会拒绝返回,不能靠重试修复;不支持新旧发布写节点无协调并行升级。旧服务没有此GET集合接口,不支持时SDK返回服务端错误,不静默降级。验证记录见[本阶段报告](../reports/phase-5-queries.md)。