5e1482e349
build / build (push) Successful in 1m40s
根因:生产可恢复性只有 PRODUCTION_BACKUP_RESTORE_VERIFIED 这个手填 env 开关, 页面没有任何入口去真跑一次恢复;CLI(librarydrill)又不是 worker 能复用的实现, 所以"已验证"既证明不了跑过、也证明不了跑的是当前目标。 改动: - 共享实现 internal/libraryverify(维护隔离→捕获→基线校验→canary→恢复→复核→关窗口), cmd/librarydrill 改为薄封装,CLI 与 worker 只允许一份执行体。 - verify 成为独立业务作业:kind + VerifyStages(capture/verify/canary/restore/finalize), 与 sync/restore/baseline 共用同一把写互斥;迁移 79 加 kind 约束、重建部分唯一索引、 新建 library_restore_verifications(按 target_id 绑定,换库/索引/桶旧证据自动失效, 同 target+scope+job 幂等)。 - worker:admitLibraryJob + runOwnedLibraryAttempt 之后调用共享实现,阶段实时写 library_job_stages,结束后写真实终态;通过写 passed 证据,失败写 failed+原因(不伪成功); 中断接管按同一作业 id 收掉维护窗口。 - API:POST/GET /api/v1/library/verifications(管理员;闸口关闭/维护中拒绝;返回完整 整库目标、最近证据、失败原因),OpenAPI 同步。 - 闸口:同步/恢复/续作的证据判断改为读当前目标证据;历史 env 仅在证据缺失时兜底, 证据存在时以证据为准。 - 页面:生产保护与恢复面板加「恢复验证」块(完整 KB/索引/桶、状态、最近证据、失败原因、 管理员按钮、5s 轮询),作业类型补 verify 文案。 测试:make verify 16/16 全绿(go 1387 例 + web 454 例 + 契约/迁移/密钥/依赖锁定); 新增用例覆盖证据四条件、闸口证据优先、页面无证据/失败/通过三种呈现与管理员可见性。
70 lines
2.6 KiB
Go
70 lines
2.6 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"knowledge-governance/internal/governance"
|
|
"knowledge-governance/internal/jobs"
|
|
"time"
|
|
)
|
|
|
|
func (s *Store) CreateLibraryDispatch(ctx context.Context, job governance.LibraryJob, stages []string, handoff bool, now time.Time) (governance.LibraryJob, bool, error) {
|
|
tx, err := s.db.Begin(ctx)
|
|
if err != nil {
|
|
return governance.LibraryJob{}, false, err
|
|
}
|
|
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
|
|
created, reused, err := NewStore(tx).WithLibraryTarget(s.libraryTargetID).createLibraryDispatch(ctx, job, stages, handoff, now)
|
|
if err != nil {
|
|
return governance.LibraryJob{}, false, err
|
|
}
|
|
if err = tx.Commit(ctx); err != nil {
|
|
return governance.LibraryJob{}, false, err
|
|
}
|
|
return created, reused, nil
|
|
}
|
|
func (s *Store) createLibraryDispatch(ctx context.Context, job governance.LibraryJob, stages []string, handoff bool, now time.Time) (governance.LibraryJob, bool, error) {
|
|
created, reused, err := s.CreateLibraryJob(ctx, job, stages, now)
|
|
if err != nil {
|
|
return created, reused, err
|
|
}
|
|
if reused && (created.State != governance.LibraryJobPending || created.QueueJobID != "") {
|
|
return created, true, nil
|
|
}
|
|
kind := "library." + string(created.Kind)
|
|
if created.Kind != governance.LibraryJobSync && created.Kind != governance.LibraryJobRestore &&
|
|
created.Kind != governance.LibraryJobBaseline && created.Kind != governance.LibraryJobVerify {
|
|
return created, reused, errors.New("unsupported library dispatch kind")
|
|
}
|
|
queueID, kind, raw, err := governance.LibraryQueueRequest(handoff, created.ID, created.Scope, created.TargetSnapshotID, kind)
|
|
if err != nil {
|
|
return created, reused, err
|
|
}
|
|
var payload map[string]string
|
|
if err = json.Unmarshal(raw, &payload); err != nil {
|
|
return created, reused, err
|
|
}
|
|
queued, err := s.Enqueue(ctx, jobs.EnqueueRequest{ID: queueID, Type: kind, Payload: raw, AvailableAt: now, MaxAttempts: 1, IdempotencyKey: kind + ".queue:" + created.ID}, now)
|
|
if err != nil {
|
|
return created, reused, err
|
|
}
|
|
if queued == nil || queued.ID != queueID || queued.Type != kind {
|
|
return created, reused, errors.New("queue identity conflict")
|
|
}
|
|
var actual map[string]string
|
|
if json.Unmarshal(queued.Payload, &actual) != nil || len(actual) != len(payload) {
|
|
return created, reused, errors.New("queue payload mismatch")
|
|
}
|
|
for key, value := range payload {
|
|
if actual[key] != value {
|
|
return created, reused, errors.New("queue payload mismatch")
|
|
}
|
|
}
|
|
if err := s.AttachLibraryQueueJob(ctx, created.ID, queued.ID, now); err != nil {
|
|
return created, reused, err
|
|
}
|
|
created.QueueJobID = queued.ID
|
|
return created, reused, nil
|
|
}
|