Files
knowledge-governance/internal/platform/postgres/library_maintenance_integration_test.go
T
姚章浩 3643e9bf82
build / build (push) Successful in 1m20s
fix(library): 维护收尾必须按预备份恢复并校验
删掉会自己开入口、只看服务健康的 FinalizeAfterOperatorRecovery。
新的 RecoverWindow 绑定原作业/目标指纹/持久化预备份,在维护关闭下
执行恢复与逐类校验,通过才恢复服务与入口;失败继续维护。
预备份身份与内容风险在第一笔写入前一起落盘;后台任务必须静默后才停写者;
写入者发布端口即拒绝开窗口;维护占用改为按实例互斥(000072)。
2026-10-29 00:50:32 +08:00

116 lines
4.4 KiB
Go

package postgres
import (
"context"
"errors"
"os"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// 维护占用的生命周期必须只有一条规则:
// - 正在持有的窗口只允许原作业重入,别的作业一律拒绝;
// - 已经释放的窗口必须能被新作业接管,否则"第一次同步之后谁也再进不去"。
func TestLibraryMaintenanceOwnershipAcrossRelease(t *testing.T) {
databaseURL := isolatedIntegrationDatabaseURL(t, os.Getenv("TEST_DATABASE_URL"))
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer pool.Close()
if err := ApplyMigrations(ctx, pool); err != nil {
t.Fatalf("migrate: %v", err)
}
store := NewStore(pool)
now := time.Now().UTC()
first, err := store.AcquireLibraryMaintenance(ctx, LibraryMaintenance{
Scope: "internal", OwnerJobID: "job-1", TargetIdentity: "instance=kgmaint",
Level: "instance", TargetFingerprint: "fp", Services: []string{"app"},
IngressState: "unknown", State: "acquired", OriginalServiceState: "app=running",
Note: "test",
}, now)
if err != nil {
t.Fatalf("first acquire: %v", err)
}
if first.Level != "instance" || len(first.Services) != 1 || first.Services[0] != "app" {
t.Fatalf("acquired row lost identity: %+v", first)
}
// 同一作业重入(接管)必须成功。
if _, err := store.AcquireLibraryMaintenance(ctx, LibraryMaintenance{
Scope: "internal", OwnerJobID: "job-1", State: "applying", Level: "instance",
}, now.Add(time.Second)); err != nil {
t.Fatalf("same owner re-entry rejected: %v", err)
}
// 另一个作业在持有期间必须被拒。
if _, err := store.AcquireLibraryMaintenance(ctx, LibraryMaintenance{
Scope: "internal", OwnerJobID: "job-2", State: "acquired", Level: "instance",
}, now.Add(time.Second)); !errors.Is(err, ErrMaintenanceHeld) {
t.Fatalf("second owner during hold: err=%v", err)
}
if err := store.ReleaseLibraryMaintenance(ctx, "internal", "job-1", now.Add(2*time.Second)); err != nil {
t.Fatalf("release: %v", err)
}
second, err := store.AcquireLibraryMaintenance(ctx, LibraryMaintenance{
Scope: "internal", OwnerJobID: "job-2", TargetIdentity: "instance=kgmaint",
Level: "instance", TargetFingerprint: "fp2", Services: []string{"app"},
IngressState: "unknown", State: "acquired", Note: "second",
}, now.Add(3*time.Second))
if err != nil {
t.Fatalf("acquire after release: %v", err)
}
if second.OwnerJobID != "job-2" || second.ReleasedAt != nil || second.TargetFingerprint != "fp2" {
t.Fatalf("released window not re-acquired cleanly: %+v", second)
}
}
// 实例级占用必须跨 scope 互斥:internal 持有期间 external 也进不来。
// (旧实现按 scope 建行,两个 scope 各持一个锁,会同时停/起同一个实例。)
func TestInstanceMaintenanceExcludesAcrossScopes(t *testing.T) {
databaseURL := isolatedIntegrationDatabaseURL(t, os.Getenv("TEST_DATABASE_URL"))
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatalf("connect: %v", err)
}
defer pool.Close()
if err := ApplyMigrations(ctx, pool); err != nil {
t.Fatalf("migrate: %v", err)
}
store := NewStore(pool)
now := time.Now().UTC()
base := LibraryInstanceMaintenance{Instance: "kgmaint-test", OwnerJobID: "job-internal",
OwnerScope: "internal", TargetFingerprint: "fp", Level: "instance", Services: []string{"app"},
State: "acquired", Note: "test"}
if _, err := store.AcquireInstanceMaintenance(ctx, base, now); err != nil {
t.Fatalf("first acquire: %v", err)
}
external := base
external.OwnerJobID = "job-external"
external.OwnerScope = "external"
if _, err := store.AcquireInstanceMaintenance(ctx, external, now.Add(time.Second)); !errors.Is(err, ErrMaintenanceHeld) {
t.Fatalf("external scope entered while internal held the instance: err=%v", err)
}
if err := store.ReleaseInstanceMaintenance(ctx, "kgmaint-test", "job-internal", now.Add(2*time.Second)); err != nil {
t.Fatalf("release: %v", err)
}
if _, err := store.AcquireInstanceMaintenance(ctx, external, now.Add(3*time.Second)); err != nil {
t.Fatalf("external scope could not take over the released instance: %v", err)
}
held, err := store.HeldInstanceMaintenance(ctx)
if err != nil || len(held) != 1 || held[0].OwnerScope != "external" {
t.Fatalf("held=%+v err=%v", held, err)
}
}