feat: add generic queue and subscription retirement

This commit is contained in:
2026-09-27 14:39:49 +08:00
parent d54bdd9847
commit c9e877c3f7
53 changed files with 1013 additions and 88 deletions

View File

@@ -278,3 +278,10 @@ Event.WorkflowID为只读STORED生成列,从原不可变事件派生流程归
### 当前 Token 保管(000033)
`orbit_credentials.secret_key_id` 指向独立文件中的主密钥;`secret_ciphertext` 保存 AES-256-GCM 的 nonce 与认证密文,两字段同时为空或非空。原文不进入持久化模型、日志和审计。既有摘要保留用于认证,旧记录通过管理员导入当前原文补存,不改变身份、版本、有效期或过渡截止。撤销清空密文,轮换原子替换当前摘要与密文;无外键。
## 000034:通用资源退役
- `pulse_queues.retired_at`:UTC 永久退役时间,NULL 表示未退役;退役必须 paused=true。
- `orbit_subscriptions.retired_at`:UTC 永久退役时间,NULL 表示仍占活动名额。
- `orbit_subscriptions.retired_sequence`:退役事务锁定的命名空间结果流提交水位,NULL 表示未退役;退役后 enabled=false、pending_count=0。
- `ix_subscription_active(namespace_id,retired_at)` 用于活动名额统计。无外键、不删除历史、不回收名称;审计与幂等回执和退役状态同事务。

View File

@@ -312,3 +312,11 @@ A04派发已提交而消息尚未到达Worker的精确进程故障已通过[专
- [x] 新建/换证/撤销密文与摘要同步,原有效期和过渡策略保持;历史凭据导入校验当前摘要。
- [x] 前向迁移 000033、中文模型注释、无外键;本地 Compose 持久主密钥与丢失保护。
- [x] 标准回归、完整 MySQL/SDK/进程恢复、控制台端到端和浏览器验证通过;本地数据库升级为 33,原身份保持。见[验收报告](reports/credential-token-vault.md)。历史凭据仅有摘要时仍需导入当前原文,不会自动重置。
## 2026-09-27:通用资源退役与接入选择
范围:不含接入方名称或业务判断。迁移 000034、队列/订阅退役 API 与 SDK、活动订阅名额提示、管理端退役入口、按命名空间记忆队列、未补存 Token 文案、可运行示例。
保护:未完成/未知任务、执行许可、活动计划和流程、未退役订阅阻止队列退役;积压、未确认和耗尽交付阻止订阅退役。退役不删历史或审计,不修改凭据。
进度:实现已完成,真实 MySQL、HTTP/SDK、控制台、历史升级与完整回归验收中;最终结果见后续报告。

View File

@@ -69,3 +69,5 @@ P1 业务接口已遵守下述约定,后续接口继续沿用:
- [周期归档与作业管理](archive-scheduler.md)
- [控制台应用接入与 Token 管理](console-applications.md)
- [通用资源退役](resource-retirement.md):订阅结清、活动名额释放、队列依赖保护、SDK 与控制台操作。

View File

@@ -145,3 +145,7 @@ unfinished=0 rate_per_second=1 worker_drained=true
创建命令省略新字段时,幂等摘要保留旧字段集合。旧创建回执不含新字段,SDK 解码为零,只表示历史快照缺少信息,实时配置另行 GetQueue。配置接口是新能力;自动 SDK 已能处理 RATE_LIMITED,进行降额管理前应先升级 Worker。
本阶段验证见 [资源控制报告](../reports/phase-3-queue-resources.md)。[应用级跨命名空间配额](application-resources.md) 已实现并同时生效。[资源键许可](keyed-concurrency.md) 与 [优先级/节点公平派发](priority-and-fairness.md) 已实现;可靠事件积压限制及跨节点公平负载和容量验收继续推进。
## 永久退役
`POST /v1/queues/{id}:retire`,需要 `queues:manage`、原幂等键、`expected_revision` 和 `reason`。完整前置条件、错误和 SDK 用法见[通用资源退役](resource-retirement.md)。返回原 QueueResult,新增可空 `queue.retired_at`;暂停仍允许受理,退役禁止新工作。历史读取保持可用。

View File

@@ -0,0 +1,85 @@
# 通用资源退役
适用于长期服务、临时环境和自动化回归。平台不根据应用名称、队列名称或测试前缀自动清理。退役是不可恢复的显式管理动作,保留资源 ID、名称、任务、交付、幂等回执与审计;不是硬删除,也不自动归档或缩小数据库。
## 接口与权限
| 操作 | HTTP | 权限 | Go SDK |
| --- | --- | --- | --- |
| 退役订阅 | `POST /v1/subscriptions/{id}:retire` | `events:manage`,原凭据所有权和队列授权 | `RetireSubscription` |
| 退役队列 | `POST /v1/queues/{id}:retire` | `queues:manage` 和队列授权 | `RetireQueue` |
单管理员通过已登录的控制台管理授权范围;普通 SDK 不能操作其他凭据的订阅。请求需要 Bearer 身份及 `Idempotency-Key`,不接受查询参数,正文:
```json
{"expected_revision":"3","reason":"临时资源已结清,结束使用"}
```
成功 HTTP 200,保持现有 `request_id + data/error` 信封。data 分别为 `{subscription,deduplicated}`、`{queue,deduplicated}`。查询及列表增加可选 `retired_at`;订阅同时增加十进制字符串 `retired_sequence`。旧记录不需要回填。
同键同内容返回原回执,不因状态已经退役而失败;同键不同内容为 `IDEMPOTENCY_CONFLICT`。无明确回执时必须保存并重用原目标、请求和键,不自动生成新键。新命令须携带最新配置 revision。
## 订阅的结清边界
退役事务锁定命名空间结果流的已提交水位,再锁订阅。必须同时满足:
- pending_count=0,实际不存在 PENDING、LEASED 或 EXPIRED 交付。
- 截止水位之前,没有该队列和类型过滤范围内尚未物化的结果;缺失来源或历史保留缺口也拒绝退役。
成功后 enabled=false,记录退役水位并推进 cursor 至该水位,释放一个活动名额。此后新产生的事件不再属于该订阅。管理员应先停止旧生产者并排空消费者;平台无法判断调用方将来是否还会提交任务。
暂停依然占活动名额;每命名空间最多 64 个未退役订阅。已退役记录仍可列表、读取和查交付历史,不能恢复、重置起点或更换原名称。不存在通过忽略未消费事件强制释放名额的接口。
## 队列的依赖保护
队列退役按应用额度、操作身份、队列持锁,与提交、执行和流程推进串行。下列情况返回 `RESOURCE_BUSY` 并说明原因:
- 有未完成或待核查任务、在途执行尝试或未释放执行许可。
- 有启用的定时计划。先暂停计划,已产生任务仍须结清。
- 有固定版本引用该队列的未终结流程,包括暂停、等待、尚未执行到该队列的分支。
- 有未退役订阅,包括暂停订阅。
成功后 paused=true,禁止新任务、新订阅、Worker 注册、计划创建/发布/恢复以及使用该队列启动新流程。已发布流程定义和暂停计划作为历史保留,不能绕过退役恢复新工作。任务与流程历史查询继续可用,原已完成命令的冻结回执不重写。队列名称不回收,重新使用需创建新 ID 和名称。
## SDK 与自动化收尾
使用 `git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.2`;旧 v0.1.1 消费/提交协议继续兼容,但不包含新方法。当前仓库内可直接从 `sdk/go` 运行示例。
```go
sub, err := client.GetSubscription(ctx, subscriptionID)
if err != nil { return err }
// 实际服务应把 key 和请求持久保存,未知回执重放原内容。
retire := orbit.RetireResourceRequest{
ExpectedRevision: sub.Revision,
Reason: "临时资源已经结清",
}
_, err = client.RetireSubscription(ctx, subscriptionID, stableOperationKey, retire)
if err != nil { return err } // RESOURCE_BUSY 时先排空/核查,不能跳过结果。
queue, err := client.GetQueue(ctx, queueID)
if err != nil { return err }
_, err = client.RetireQueue(ctx, queueID, stableQueueOperationKey,
orbit.RetireResourceRequest{ExpectedRevision: queue.Revision, Reason: "停止使用队列"})
return err
```
建议顺序:停止生产者和计划 → 结清执行与流程 → 消费并确认所有订阅结果 → 退役订阅 → 退役队列。失败的测试可能保留待核查任务,收尾应报告资源 ID 与阻塞原因,不能把测试失败变成丢弃结果的理由。
完整可运行示例:[resource-lifecycle](../../sdk/go/examples/resource-lifecycle/README.md)。示例只操作本次新建空资源;进程异常终止仍可能留下资源,需要按输出 ID 核查,不能无限创建来绕过名额。
## 错误和管理页面
| HTTP / code | 含义 |
| --- | --- |
| 409 `RESOURCE_BUSY` | 尚未结清,message 给出依赖或积压原因;状态不变 |
| 409 `RESOURCE_RETIRED` | 新命令试图复用已退役资源 |
| 409 `STATE_CONFLICT` | 配置版本不匹配 |
| 409 `IDEMPOTENCY_CONFLICT` | 原键对应不同内容 |
| 410 `EVENT_HISTORY_EXPIRED` | 订阅存在无法验证的历史缺口 |
| 401 / 403 / 404 | 保持现有身份、权限和资源隔离语义 |
控制台“结果订阅”详情提供退役入口;“应用接入”提供所选队列退役和活动订阅名额。多队列时明确选择,浏览器只记忆命名空间对应的队列 ID;暂停/退役状态清晰展示。复制配置不会静默换成另一张凭据有权限的队列。
## 部署与保留
先停止旧写节点,再执行迁移 000034,再启动新版本;不要混跑不认识退役状态的旧服务端。现有资源默认未退役,不自动处理任何应用或测试数据。新库基线仍为零外键。历史保留、归档导出继续按现有配置执行;本次退役不引入自动删除机制。

View File

@@ -14,10 +14,11 @@
| GET /v1/subscriptions?queue_id=... | events:read | ListSubscriptions |
| GET /v1/subscriptions/{id} | events:read | GetSubscription |
| POST /v1/subscriptions/{id}:configure | events:manage | ConfigureSubscription |
| POST /v1/subscriptions/{id}:retire | events:manage | RetireSubscription |
| GET /v1/subscriptions/{id}/deliveries | events:read | ListEventDeliveries |
| GET /v1/subscriptions/{id}/deliveries/{delivery_id} | events:read | GetEventDelivery |
| POST /v1/subscriptions/{id}/deliveries/{delivery_id}:retry | events:manage | RetryEventDelivery |
| GET /v1/events/connect(WebSocket Upgrade) | events:consume | 连接 SDK 待交付,可按下文协议接入 |
| GET /v1/events/connect(WebSocket Upgrade) | events:consume | ConnectEvents / NewEventConsumer |
HTTP 使用统一 request_id + data/error 信封,响应带 X-Request-ID 和 Cache-Control: no-store。管理请求 8 秒服务端超时、JSON 最大 512 KiB。未知字段、大小写别名、重复 JSON 键、未知或重复查询参数均被拒绝。计数/版本以规范十进制字符串传输,不能带正号、前导零或小数。
@@ -76,7 +77,7 @@ POST /v1/subscriptions,Content-Type: application/json,Idempotency-Key: 调
类型集合按固定顺序规范化。cursor_sequence 表示已经扫描并建立交付记录的位置,**不是确认水位**。pending_count 包含等待、租约中和次数耗尽记录。配置 revision 不随物化/ACK 用量变化。
每命名空间最多 64 个订阅。from_sequence 不能超过已提交流头,也不能早于已清理的保留边界。现存日志从迁移 000013 启用,不补造此前的任务结果。
每命名空间最多 64 个未退役订阅;暂停仍占名额,成功退役后释放名额。from_sequence 不能超过已提交流头,也不能早于已清理的保留边界。现存日志从迁移 000013 启用,不补造此前的任务结果。
## 3. 查询和控制订阅
@@ -291,7 +292,7 @@ snapshot 还可包含业务 result 和 current_attempt_id;result 用原始 JSO
| NOT_FOUND | 404;订阅/交付不在凭据与父资源范围;连接内发生时关闭 |
| STATE_CONFLICT | 409;版本、状态或累计预算不允许操作 |
| IDEMPOTENCY_CONFLICT | 409;同键内容不同 |
| SUBSCRIPTION_LIMIT | 409;命名空间订阅数达到 64 |
| SUBSCRIPTION_LIMIT | 409;命名空间未退役订阅数达到 64 |
| EVENT_HISTORY_EXPIRED | 410;WSS 领取发现历史缺口则报错并关闭,不静默跳到最新位置 |
| DELIVERY_LEASE_LOST | WSS ACK 证明已替换或无效;连接保留,等待新交付或核对历史 |
| PROTOCOL_REQUIRED | 升级前 426,必须指定 orbit.events.v1 |
@@ -325,3 +326,7 @@ return err
[完整管理示例](../../examples/subscriptions/main.go) 的启动、输入输出、失败和保留规则见 [场景文档](../examples/subscriptions.md)。它演示管理与幂等,不启动结果消费者。正式 [事件连接 SDK 与自动消费器](../sdk/event-consumer.md) 已提供接收、ACK、心跳、重连与有界收尾;[完整协议示例](../examples/event-protocol.md) 验证跨连接重报原确认。[独立 MySQL Inbox 示例](../examples/result-inbox.md) 已实现并验证消费者进程退出后的去重。
机器可读消息契约见 [events.schema.json](../../protocol/eventsv1/events.schema.json),使用 Draft 2020-12,clientFrame/serverFrame 分方向约束。运行时额外拒绝重复 JSON 键、超范围 uint64 及非规范 base64url。Schema 不代替授权、状态机、租约及事务校验。
## 退役与资源名额
见[通用资源退役](resource-retirement.md)。退役保留订阅和交付历史,不支持重新启用;通过 `retired_at`、`retired_sequence` 区分暂停与退役。未确认、过期或尚未物化的匹配结果都会阻止退役。

View File

@@ -89,3 +89,9 @@ export ORBIT_CONSOLE_QUEUE_ID='queue_xxx'
切换采用完整导航,隔离旧请求和详情;有未保存策略、未确认操作或打开的详情时需先处理完。URL 只保存非敏感 ID。无效范围显示错误,不降级读取默认应用。总览仍是最近查询窗口的统计,不是全库累计。
只有部署指定管理员可以切换应用;其他账号只展示自己的应用和授权队列。后台停用应用、撤销绑定凭据或退出账号后,原页面范围不能继续授予权限。[范围接口及授权规则](api/console-scope.md)。
## 资源退役与明确接入目标
应用接入页按命名空间记忆所选队列;多队列且没有记忆时要求明确选择,不根据名称猜测业务用途。下拉框显示暂停/退役状态,已退役队列不能导出成新接入配置;凭据无该队列权限时直接提示,不自动换目标。
结果订阅详情和应用接入页分别提供订阅、队列退役;每次提交需要原因和当前版本,网络不明确时保留原键重报。活动订阅名额包含暂停订阅,退役后释放,历史仍可查询;阻塞原因明确显示。未补存当前 Token 时,预览入口与过渡提示都要求先补存,避免误认为已有可复制的新配置。

View File

@@ -0,0 +1,22 @@
# 通用资源退役与接入体验验收
日期:2026-09-27。范围:平台、Go SDK、控制台及通用示例;不包含接入方业务处理器、支付渠道或业务数据变更。
## 实现
迁移 000034 增加队列/订阅退役时间及订阅提交水位、活动索引和约束,无外键。退役、不可变审计、原操作回执同事务;新工作不能复用退役队列,已完成流程的原启动回执仍可重放。订阅结清释放名额,暂停仍占名额,不删除或伪造确认历史。
控制台增加明确队列选择/记忆、暂停/退役状态和名额统计,提供带原因和版本的退役入口。凭据未补存时统一提示补存,不能静默换队列复制。没有修改任何现有应用的 Token、权限或过渡期。
## 检查进度
- 单元/race、vet、build、Markdown 链接已通过首轮;最终完整结果待回归结束后记录。
- 新增真实 MySQL 8 项场景通过:未物化/租约/耗尽交付保护、64 名额释放且历史保留、任务/订阅依赖、计划恢复阻止、等待流程保护、完成流程原回执和历史、真实锁竞争、审计失败回滚。
- 控制台真实 HTTP 验收通过:跨应用管理员范围、CSRF、退役幂等、名额变化、历史读取。
- 初轮完整 MySQL 回归发现两处旧升级夹具未计入新约束/索引,已修正:旧结构还原时移除 000034 新字段与约束;原索引对比排除新加活动索引。没有放宽原索引、数据和零外键断言。完整回归重跑中。
## 明确边界
不提供强制忽略结果或批量硬删除。历史未确认/未知效果须先核查;没有消费者时需要恢复消费并确认积压。资源名称不回收。归档仍按已有配置导出并保留源记录,退役不缩减物理存储。
新服务端禁止与忽略退役状态的旧写节点混跑。旧 SDK 既有 API 兼容;新的退役方法使用 v0.1.2。隔离回归不代替目标生产环境的容量、跨故障域或真实业务验收。

View File

@@ -151,7 +151,7 @@ Options.HTTPClient 可注入专用 HTTP 客户端。SDK 复制配置,不修改
业务项目需要 Go 1.27.1 或更高版本。在业务项目的 `go.mod` 所在目录安装当前接入版本:
```sh
go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.1
go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.2
```
代码中导入:
@@ -167,9 +167,9 @@ go mod tidy
go mod verify
```
SDK 位于仓库子目录,对应 Git 标签为 `sdk/go/v0.1.1`;`go get` 使用模块版本 `v0.1.1`。服务端对应仓库标签为 `v0.1.1`。业务项目固定版本并提交 `go.mod` 与 `go.sum`,无需本地源码、`replace` 或 Orbit 的 `go.work`。如果此前配置过本地替换,先执行 `go mod edit -dropreplace=git.yuntuo.hk/k2safe/orbit/sdk/go`。
SDK 位于仓库子目录,对应 Git 标签为 `sdk/go/v0.1.2`;`go get` 使用模块版本 `v0.1.2`。服务端对应仓库标签为 `v0.1.2`。业务项目固定版本并提交 `go.mod` 与 `go.sum`,无需本地源码、`replace` 或 Orbit 的 `go.work`。如果此前配置过本地替换,先执行 `go mod edit -dropreplace=git.yuntuo.hk/k2safe/orbit/sdk/go`。
这是开发与测试接入版本;生产部署仍按[交付清单](../release-checklist.md)完成目标环境验收。版本发布和校验规则见[发布说明](release.md),空缓存远端取包结果见[验证报告](../reports/v0.1.1-release.md)。
这是开发与测试接入版本;生产部署仍按[交付清单](../release-checklist.md)完成目标环境验收。版本发布和校验规则见[发布说明](release.md),空缓存远端取包结果见[验证报告](../reports/resource-retirement.md)。
## 6. 查询执行历史与证据

View File

@@ -29,19 +29,21 @@ sh scripts/test-sdk-isolation.sh
## 接入方式
当前接入版本为 `v0.1.1`,需要 Go 1.27.1 或更高版本。接入方应固定 SDK 版本,升级前先执行自己的接口和业务幂等回归:
当前接入版本为 `v0.1.2`,需要 Go 1.27.1 或更高版本。接入方应固定 SDK 版本,升级前先执行自己的接口和业务幂等回归:
```sh
go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.1
go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.2
# 在业务代码导入 SDK 并添加调用后:
go mod tidy
go mod verify
```
仓库主分支为 `main`。服务端标签 `v0.1.1` 和独立 SDK 标签 `sdk/go/v0.1.1` 指向同一发布提交;Go 子模块标签必须包含 `sdk/go/` 前缀,业务端的模块版本不带此前缀。已发布标签保持不可变,修复使用新的补丁版本。版本发布不替代目标环境验收,正式生产仍需完成[交付核对清单](../release-checklist.md)。
仓库主分支为 `main`。服务端标签 `v0.1.2` 和独立 SDK 标签 `sdk/go/v0.1.2` 指向同一发布提交;Go 子模块标签必须包含 `sdk/go/` 前缀,业务端的模块版本不带此前缀。已发布标签保持不可变,修复使用新的补丁版本。版本发布不替代目标环境验收,正式生产仍需完成[交付核对清单](../release-checklist.md)。
`v0.1.0` 已在仓库外的新业务 module、空模块缓存和 `GOWORK=off` 下通过公开远端下载、`go mod tidy`、编译、SDK HTTP smoke 和 `go mod verify`,解析版本精确为 `v0.1.0` 且不存在 `Replace`。发布提交、校验和与网络条件见[远端验证报告](../reports/phase-7-sdk-remote-release.md)。后续发布继续重复这一步,并按[兼容矩阵](compatibility.md)验证旧/新组合。
本地联调可以临时添加 `replace`,但不能提交到业务项目的发布分支。SDK 不负责 TLS 终止、凭据托管、对象存储或外部副作用核查;这些边界仍由平台接口手册和接入方运行手册定义。
Outbox 独立模块使用 `adapters/outbox/mysql/v0.1.1` 标签,安装与版本边界见[适配包手册](outbox-mysql.md)。本次目录整理、发布与远端取包验证见[v0.1.1 记录](../reports/v0.1.1-release.md)。
`v0.1.2` 发布队列/订阅退役与对应 SDK 方法,数据库升级到 000034;[本次报告](../reports/resource-retirement.md)列出隔离回归与远端安装结果。Outbox 不需要同步升级。