mochengqian opened a new pull request, #1477:
URL: https://github.com/apache/dubbo-admin/pull/1477
# Support version history and rollback for traffic rules (#1473)
Closes #1473.
设计文档:`docs/design/issue-1473-rule-version-history.md`
落地复盘:`docs/design/issue-1473-remediation-plan.md`
联调证据:`docs/design/issue-1473-smoke-checklist.md` + 截图/HTTP 凭证(评论区附)
---
## 1. 这个 PR 解决了什么
目前 Dubbo Admin 对四类流量规则(条件路由 / 标签路由 / 动态配置 /
亲和路由)只支持"覆盖式保存",一旦误改无法回滚、上游推送也没有审计记录。本 PR 为前三类(governor 管理的规则)落地一套不可变发布账本 +
一键回滚体验,与 Apollo / Nacos 的成熟控制台经验对齐,**不引入审批工作流和 GitOps**(PDF 调研里的方案乙/丁/戊明确延后)。
### 用户能看到的能力
- 在每条规则详情页的 History 抽屉里查看历史版本,看到来源(ADMIN / UPSTREAM / ROLLBACK /
BOOTSTRAP)、操作人、时间、变更理由。
- 点 "对比当前",用 Monaco diff 看任意旧版本与当前版本的差异。
- 点 "回滚",填写 reason,旧规则被作为一次新发布写回注册中心,旧版本永不被改写。
- 多人同时编辑时,后保存的人收到 409 + Reload 通知,而不是静默覆盖。
- 默认保留每条规则最近 5 个版本,可通过 `versioning.maxVersionsPerRule` 调整。
---
## 2. 工作原理(简版)
```
UI 发布 ┐
├─► ResourceManager → governor → 注册中心写入 ──┐
ZK 推送 ┘ │
▼
EventBus.ResourceChangedEvent
│
▼
RuleVersionSubscriber(每种规则一个实例)
│
┌── 命中 admin hint ───────┤
│ (TTL 30s) │
▼ ▼
source=ADMIN/ROLLBACK source=UPSTREAM
│ │ author 从
event.Context()
└────────► InsertVersion ◄─┘ 的
source-registry 读出
│
▼
rule_version (immutable) +
rule_version_meta (current)
│
▼
按 maxVersionsPerRule trim-on-insert
```
- **唯一录写点**是事件总线上的 subscriber;UI 发布与上游推送都汇聚到这里,避免双写。
- ADMIN 与 UPSTREAM 的区分通过一个 in-process、按 `(kind, key, content_hash)` 索引的 TTL
提示表完成。
- **回滚 = 用旧 snapshot 重新发布**:经过 governor → 注册中心 → 回声事件,再生成一条
`source=ROLLBACK` 新行,`rolled_back_from_id` 指向源版本。旧版本不被修改。
- `version_no` 在 trim 后不重用,永远单调递增(用户看到的版本号始终有序)。
- 2s 合并窗口收敛上游连续推送,避免噪声。
---
## 3. 范围
### 本 PR 覆盖
- 后端:`pkg/core/versioning` 新包(types / store memory+gorm / service / hint
registry / subscriber / component)、`pkg/config/versioning` 配置项、bootstrap 扫描入册、4
个新 REST 端点 × 3 种规则、PUT/POST/DELETE 增加 `expectedVersionId` 乐观锁。
- 前端:`ui-vue3/src/views/traffic/_shared/` 4 件套(RuleHistoryDrawer /
RuleHistoryPanel / RuleDiffEditor / ruleVersion.ts),三个规则页面接入历史 panel、Monaco
diff、回滚 modal,409 弹窗用 `notification` 持久显示并带 Reload 按钮。
- 配置:`versioning.enabled` / `.maxVersionsPerRule` / `.coalesceWindowMs` /
`.adminHintTTLSec` / `.rollbackWaitTimeoutMs`。`enabled=false` 时所有版本端点返回 503
FEATURE_DISABLED。
- 测试:23 个新单测(normalize / hint TTL / 内存与 GORM store / 合并 / subscriber 来源识别 /
rollback 各种路径 / 配置默认值与校验 / zk 删除 nil-guard)+ 一个端到端 rollback drill。
### 不在本 PR 的内容(已记为 follow-up)
- AffinityRoute 接入(亲和路由当前绕过 governor 直接写 store,需要先给它接上 governor 路径)。
- Nacos rule subscriber(当前只 ZK 一种实现)。
- 编辑/发布两阶段 + 审批流(PDF 方案乙)。
- 软删除模式(保留被 trim 掉的版本用于审计)。
---
## 4. 锁定的设计决策(设计文档 §12)
| # | 选择 | 理由 |
|---|---|---|
| 1 | Rollback reason 必填 | 审计链最关键的一环,前端校验 + 后端 400 兜底 |
| 2 | 上游合并窗口 2s | 收敛 ZK 推送噪声,对 UI 发布无感 |
| 3 | AffinityRoute 延后 | 不在 `governor.RuleResourceKinds` 中,独立设计成本不在本 PR 范围 |
| 4 | Monaco diff | 复用现有编辑器依赖 |
| 5 | trim 硬删 | 匹配"默认 5 个"的产品语义;表小、查询快 |
---
## 5. API 变化
### 新增端点(每种规则各一组)
```
GET /api/v1/{condition-rule|tag-rule|configurator}/:ruleName/versions
GET /api/v1/{...}/:ruleName/versions/:versionId
GET /api/v1/{...}/:ruleName/versions/:versionId/diff?against=current|<id>
POST /api/v1/{...}/:ruleName/versions/:versionId/rollback
body: { "reason": "<required>", "expectedVersionId": <optional int64>
}
```
### 既有端点的扩展(向后兼容)
`PUT/POST/DELETE /api/v1/{...}/:ruleName` 接受可选的 `?expectedVersionId=<id>`。
不传 → 行为完全不变。传 → 与服务端 `current_version` 不匹配时返回:
```http
HTTP/1.1 409 Conflict
Content-Type: application/json
{"code":"VERSION_CONFLICT","currentVersionId":5,"message":"rule version
conflict"}
```
说明:`expectedVersionId` 是 v1 的弱 CAS / best-effort 冲突检测。它在进入既有
ResourceManager/governor 发布路径前检查本地 ledger 的 current pointer;真正的版本行仍由 EventBus
subscriber 统一记录,避免 admin path 和 registry echo path 双写。
### 关闭功能时
`versioning.enabled=false` → 所有新端点返回 `503 + {"code":"FEATURE_DISABLED"}`;既有
CRUD 路径完全不受影响。
---
## 6. 数据库
两张新表,跑在已有 GORM 连接(MySQL / Postgres)上,通过 `AutoMigrate` 创建。`store.type=memory`
时自动用内存实现,不需要额外配置。
```sql
CREATE TABLE rule_version (
id, rule_kind, mesh, resource_key, rule_name,
version_no, -- 单调,trim 后不重用
content_hash, -- sha256(canonical spec json)
spec_json,
source, -- ADMIN | UPSTREAM | ROLLBACK | BOOTSTRAP
operation, -- CREATE | UPDATE | DELETE
author, reason,
rolled_back_from_id,
created_at,
UNIQUE (rule_kind, resource_key, version_no),
INDEX (rule_kind, resource_key, created_at DESC),
INDEX (rule_kind, content_hash)
);
CREATE TABLE rule_version_meta (
rule_kind, resource_key, -- PK
current_version, -- nullable,被删除时为 NULL
last_version_no, -- 单调
updated_at
);
```
升级路径:启动时扫描所有现存规则,给每条插入一条 `source=BOOTSTRAP` 的基线版本(幂等)。下游用户无感升级。
---
## 7. 提交结构(请按 commit 顺序审)
```
0541218 chore(store): add ListResources + align gorm/memory empty-index
semantics
cec939e fix(discovery): nil-guard zk rule delete + emit registry context on
events
ce66043 feat(versioning): backend immutable release ledger for traffic rules
69a3f0b feat(versioning): UI history drawer, diff, and rollback for rule
pages
bd01211 test(versioning): end-to-end rollback drill
9368874 fix(versioning): close gaps surfaced by the smoke drill
dcf428f fix(traffic): preserve priority/force/configVersion on rule edit
forms
cf9e43a chore(planning): record smoke evidence + ignore local planning
artifacts
```
前两个 commit 是为支撑版本功能做的横向调整,作用面小但需要单独审:
1. **`chore(store): ListResources + 空 index 语义对齐`** — 给 `ResourceStore` 接口新增
`ListResources()`,bootstrap 扫描使用。同时把 `gorm_store.getKeysByIndexes([])`
的语义从"返回全部"改成"返回空",与 memory store 对齐。所有现存调用方都传至少一个 index,因此对生产无影响;测试在两个后端都明确这一契约。
2. **`fix(discovery): zk 删除路径 nil 防御 + 在事件上下文里携带 registry 类型`** — 真实 bug
顺手修;同一文件把 `source-registry: zookeeper` 写入事件 Context,下游版本订阅者据此区分上游来源。这是版本功能正确识别
`system:zookeeper` 的前提。
后六个 commit 都是版本功能本体;其中 `9368874`、`dcf428f` 是 §9.4 联调演练暴露的真实 bug 的修复(详见下文
§10)。
---
## 8. 验证
### 自动测试
```bash
go test ./pkg/core/versioning/... \
./pkg/config/versioning/... \
./pkg/console/handler/... \
./pkg/console/service/... \
./pkg/store/... \
./pkg/core/discovery/subscriber/... \
./pkg/core/manager/...
```
全绿;`go vet ./pkg/...` 无新增告警。
端到端 rollback drill:`pkg/core/versioning/e2e_rollback_drill_test.go` 串联
bootstrap → admin edit → upstream push → rollback → 批量插入到溢出 → 审计链断言(来源序列、author
非空、`created_at` 单调)+ trim 后 `version_no` 不重用断言(`[]int64{10,9,8,7,6}`)。
### 前端构建
```bash
cd ui-vue3 && npm install --legacy-peer-deps && npm run build
```
通过。`npm run type-check` 仍报 264 个错误,全部为**仓库历史 TypeScript
债**(home/index.vue、AuthUtil.ts 缺类型声明、GrafanaPage 找不到等),与本 PR 无关;用 stash 切到
develop 跑同样命令得到 265 个错误,差值都是 PR 改动减少的。
### 本地联调
按 `docs/design/issue-1473-smoke-checklist.md` 走完 §1–§9,全部勾选;产物(HTTP
凭证、JSON、UI 截图)见评论区。
---
## 9. 升级与回滚
- **升级**:直接 deploy 即可。后端启动时 `AutoMigrate` 创建两张表并扫描存量规则生成 BOOTSTRAP 基线,整个过程
idempotent。客户端 / 注册中心都不需要协同改动。
- **关闭**:把 `versioning.enabled` 改为 `false` 重启,CRUD 路径完全不受影响;新端点返回 503
提示功能未启用。表不会被自动清理(保留以备后续重启再次启用)。
- **完全回退**:如果发现严重问题,可单独 revert `feat(versioning)`、`feat(versioning) UI` 两个
commit;前两个底层 commit(store / discovery)保留也无害。
---
## 10. 复盘 — 联调发现并修复的 3 个真实问题
`§9.4` 端到端演练不是走过场,跑出了 3 个单测没覆盖到的活路 bug,本 PR 一并修复:
1. **重复的 UPSTREAM 行**:刚记完 BOOTSTRAP,ZK watcher 立刻同步来一条内容完全相同的 UPDATE
事件,造成版本链路里出现重复行。修复:版本 store 在插入时按内容 hash 和可见状态去重;DELETE 与非 DELETE 转换不互相折叠,避免
DELETE `{}` 后重新 CREATE 空规则被误跳过。
2. **空 reason 回滚返回 200 UnknownError**:service 层抛
`bizerror.InvalidArgument`,handler 的 default 分支把所有 bizerror 都映射成
200/UnknownError。修复:handler 增加 `InvalidArgument → 400` 映射,新增针对性单测。
3. **409 通知会自动消失**:默认 4.5s 后消失,用户可能错过 Reload 按钮。修复:`notification.warning({
duration: 0, ... })`。
另外 `dcf428f` 修了一个非版本功能的回归:rollback 流程要求"回滚后刷新 UI 看到旧值",这一步把"编辑表单丢了
`priority` / `force` / `configVersion` 三个字段"这个**预存在已久**的回归暴露了出来。本 PR
不是该回归的责任来源,但既然演练流程依赖它能正常工作,顺手补齐 GET/PUT 双向回路。
---
## 11. 给审阅者的建议路径
- 时间充裕:按 commit 顺序看完整 8 个 commit。
- 时间有限(30 分钟):
1. 读设计文档 §3-§6(数据模型 / 捕获路径 / API)。
2. `pkg/core/versioning/e2e_rollback_drill_test.go`(一份测试看完整生命周期)。
3. `pkg/core/versioning/subscriber.go`(dedup 与 hint 逻辑)。
4. `pkg/console/handler/rule_version.go`(错误映射)。
5. 评论区截图。
## 12. Test plan checklist for reviewers
- [ ] CI 全绿
- [ ] Pull branch, `go test ./pkg/...` 本地通过
- [ ] `cd ui-vue3 && npm run build` 通过
- [ ] 设计文档 §3、§4 读完
- [ ] e2e drill 测试读完
- [ ] 浏览过本地 smoke 截图
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]