afe9affc2a
build / build (push) Successful in 1m27s
为什么:远端 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:✅,前端:✅)。只改空白,不改逻辑。
143 lines
4.8 KiB
Go
143 lines
4.8 KiB
Go
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
|
||
}
|