3643e9bf82
build / build (push) Successful in 1m20s
删掉会自己开入口、只看服务健康的 FinalizeAfterOperatorRecovery。 新的 RecoverWindow 绑定原作业/目标指纹/持久化预备份,在维护关闭下 执行恢复与逐类校验,通过才恢复服务与入口;失败继续维护。 预备份身份与内容风险在第一笔写入前一起落盘;后台任务必须静默后才停写者; 写入者发布端口即拒绝开窗口;维护占用改为按实例互斥(000072)。
116 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|