Files
knowledge-governance/internal/platform/postgres/library_protection.go
T
姚章浩 afe9affc2a
build / build (push) Successful in 1m27s
style: gofmt 新增的三个文件
为什么:远端 CI 的格式门在推送后判红(run 258,job build,format=failure),
本地 `make verify` 那次跑的是"拉取前"的文件集,漏掉了随后新增/改写的三个文件。
失败项:internal/httpapi/library_intent_test.go、internal/jobs/lease_loss_test.go、
internal/platform/postgres/library_protection.go。

怎么验:`gofmt -l internal/ cmd/` 输出为空;`python3 scripts/check_format.py` rc=0
(Go:✅,前端:✅)。只改空白,不改逻辑。
2026-10-30 16:16:45 +08:00

143 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package postgres
import (
"context"
"fmt"
)
// LibraryProtection 回答"这些快照现在为什么不能删"。
//
// 关键点(F10):保护依据必须来自**全量依赖**,不能来自"最近 100 个版本 / 20 个作业"的
// 展示窗口。历史上一份需要恢复的旧预备份、一个 failed 但仍持有维护锁的作业,都完全
// 可能掉出任何窗口——按窗口判断就会把它显示成"可删除候选"。
//
// 任何一条查询失败都返回错误:调用方必须显示"保护未知"并拒绝生成可删候选,
// 不允许因为"读不到"就当成"没被引用"。
func (s *Store) LibraryProtection(ctx context.Context) (map[string][]string, error) {
protected := map[string][]string{}
add := func(snapshotID, reason string) {
if snapshotID == "" {
return
}
for _, existing := range protected[snapshotID] {
if existing == reason {
return
}
}
protected[snapshotID] = append(protected[snapshotID], reason)
}
// 1) 当前生效事实(指针):每个 scope 一条,与历史长度无关。
rows, err := s.db.Query(ctx, `SELECT snapshot_id FROM library_current_versions`)
if err != nil {
return nil, fmt.Errorf("读取当前生效版本失败:%w", err)
}
for rows.Next() {
var snapshot string
if err := rows.Scan(&snapshot); err != nil {
rows.Close()
return nil, fmt.Errorf("扫描当前生效版本失败:%w", err)
}
add(snapshot, "生产生效版本")
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取当前生效版本失败:%w", err)
}
// 2) 全部生产预备份:任何一份都是某个窗口唯一的可回滚基线,没有窗口限制。
rows, err = s.db.Query(ctx, `SELECT DISTINCT snapshot_id FROM library_versions WHERE kind='production_prebackup'`)
if err != nil {
return nil, fmt.Errorf("读取生产预备份失败:%w", err)
}
for rows.Next() {
var snapshot string
if err := rows.Scan(&snapshot); err != nil {
rows.Close()
return nil, fmt.Errorf("扫描生产预备份失败:%w", err)
}
add(snapshot, "生产预备份")
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取生产预备份失败:%w", err)
}
// 3) 每个 scope 最近一次测试封存(最新可发布版本),精确取而不是窗口里挑。
rows, err = s.db.Query(ctx, `
SELECT DISTINCT ON (scope) scope, snapshot_id FROM library_versions
WHERE kind='test_sealed' ORDER BY scope, created_at DESC`)
if err != nil {
return nil, fmt.Errorf("读取最近测试封存失败:%w", err)
}
for rows.Next() {
var scope, snapshot string
if err := rows.Scan(&scope, &snapshot); err != nil {
rows.Close()
return nil, fmt.Errorf("扫描最近测试封存失败:%w", err)
}
add(snapshot, "最近测试封存("+scope+")")
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取最近测试封存失败:%w", err)
}
// 4) 未释放的维护窗口:窗口持有者的预备份与目标快照都要留住——
// 否则"删除候选"会把正在等待恢复的那一份清掉。
var owners []string
rows, err = s.db.Query(ctx, `
SELECT owner_job_id FROM library_maintenance WHERE released_at IS NULL
UNION
SELECT owner_job_id FROM library_instance_maintenance WHERE released_at IS NULL`)
if err != nil {
return nil, fmt.Errorf("读取未释放维护窗口失败:%w", err)
}
for rows.Next() {
var owner string
if err := rows.Scan(&owner); err != nil {
rows.Close()
return nil, fmt.Errorf("扫描未释放维护窗口失败:%w", err)
}
owners = append(owners, owner)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取未释放维护窗口失败:%w", err)
}
// 5) 在飞作业与"失败但仍持有窗口"的作业。
rows, err = s.db.Query(ctx, `
SELECT coalesce(snapshot_id,''), coalesce(target_snapshot_id,'')
FROM library_jobs WHERE state IN ('pending','running')`)
if err != nil {
return nil, fmt.Errorf("读取在飞作业失败:%w", err)
}
for rows.Next() {
var snapshot, target string
if err := rows.Scan(&snapshot, &target); err != nil {
rows.Close()
return nil, fmt.Errorf("扫描在飞作业失败:%w", err)
}
add(snapshot, "在飞作业引用")
add(target, "在飞作业引用")
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取在飞作业失败:%w", err)
}
for _, owner := range owners {
var snapshot, target string
err := s.db.QueryRow(ctx, `
SELECT coalesce(snapshot_id,''), coalesce(target_snapshot_id,'') FROM library_jobs WHERE id=$1`, owner).
Scan(&snapshot, &target)
if err != nil {
return nil, fmt.Errorf("读取维护窗口持有作业 %s 失败:%w", owner, err)
}
add(snapshot, "未释放维护窗口的预备份")
add(target, "未释放维护窗口的目标快照")
}
return protected, nil
}