a7d643635a
build / build (push) Successful in 1m27s
R1 维护/发布守卫硬编码 j.id=$1:维护类入口的 $1 是 scope/instance,作业号在 $2, 导致 update/release/ingress 六个入口正常路径全部匹配不到作业。改为按被更新行自己的 owner_job_id 生成条件,并同时要求该代次在 library_attempts 里仍活跃。 R2 认领只改 jobs.attempt_epoch 不改 attempts.epoch,第二次接管即错位;唯一索引 (job_id) 与"每次接管新 attempt 且历史不可变"矛盾。改为同作业多代次:锁作业行与 operation 行、同 owner 幂等续租、活跃未过期者让路、过期/本作业旧代次标 abandoned、 新代次 epoch=max+1,并清同 operation 其它作业行租约。迟到 write/renew/finish 一律 ErrLibraryEpochStale(finish 补了"只能在活跃态进来一次")。 R3 续作不再新建 -resume 作业:PrepareLibraryJobResume 让原业务作业回 pending 并显式 结束上一代,维护窗口/journal/预备份/写集合仍按原作业 id 归属,代次由 worker 起手递增。 R4 执行域准入改为注册 handler 前读事实:目标实况(KB/租户/schema/索引)、旧 release 路由绑定的 KB、未释放维护窗口持有者;未知或重叠即拒绝执行(不做维护动作、不停既有服务)。 同时自纠 5 项设计偏离:current 次序键取意图创建时间(旧意图迟到事实被拒)、首次锚点 改事务 FOR UPDATE + 回读且 NULL≠0、版本事件冲突只做加法且 xmax=0 才算新事实、 恢复作为新 kind=recover 意图推进 current、隔离测试加测试库护栏并清理残留 schema。 测试:真实 PG(kg-pgtest-writeset,一用例一 schema)新增 8 条集成用例覆盖上述正负例; go test ./... 全绿(50 包)、go vet/build rc=0、web tsc + vitest 485 通过、make verify 16 项通过。
66 lines
2.7 KiB
Go
66 lines
2.7 KiB
Go
// Package pgtest 给真实 PostgreSQL 集成用例提供"一个用例一个 schema"的隔离入口。
|
||
//
|
||
// 为什么要单独成包:postgres 与 cmd/worker 的集成用例都要连真库,隔离逻辑只该有一份;
|
||
// 复制一份到另一个包里,迟早只改一边(AGENTS.md 硬约束 1:同一含义的代码只准有一份)。
|
||
package pgtest
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"net/url"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/jackc/pgx/v5/pgxpool"
|
||
)
|
||
|
||
// IsolatedDatabaseURL 为当前用例建一个独立 schema 并返回指向它的连接串;
|
||
// 用例结束自动 DROP。基地址为空时返回空串(调用方据此 Skip)。
|
||
func IsolatedDatabaseURL(t *testing.T, databaseURL string) string {
|
||
t.Helper()
|
||
if databaseURL == "" {
|
||
return ""
|
||
}
|
||
parsed, err := url.Parse(databaseURL)
|
||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||
t.Fatalf("TEST_DATABASE_URL must be a PostgreSQL URL: %v", err)
|
||
}
|
||
// 护栏:这些用例会在目标库里建/删 schema。指到业务库上就会在业务库里反复
|
||
// 建表删表(哪怕只删自己的 schema,也是在业务库上动 DDL)。所以库名必须明确
|
||
// 是一次性的(含 test),否则要求显式声明,绝不"顺便"跑在业务库上。
|
||
if databaseName := strings.TrimPrefix(parsed.Path, "/"); !strings.Contains(strings.ToLower(databaseName), "test") &&
|
||
os.Getenv("KG_ALLOW_NON_TEST_DATABASE_URL") != "1" {
|
||
t.Fatalf("TEST_DATABASE_URL 指向的库 %q 看起来不是一次性测试库:拒绝在其中建/删 schema"+
|
||
"(确属一次性库请设 KG_ALLOW_NON_TEST_DATABASE_URL=1)", databaseName)
|
||
}
|
||
digest := sha256.Sum256([]byte(t.Name()))
|
||
schema := "kgtest_" + hex.EncodeToString(digest[:8])
|
||
ctx := context.Background()
|
||
admin, err := pgxpool.New(ctx, databaseURL)
|
||
if err != nil {
|
||
t.Fatalf("connect integration database: %v", err)
|
||
}
|
||
defer admin.Close()
|
||
// 上一次被 kill 的运行可能留下同名 schema(cleanup 没跑到):先清掉自己的,
|
||
// 否则紧接着的 CREATE 会撞名,整包用例连"跑起来"都做不到。只清本用例自己的名字。
|
||
if _, err := admin.Exec(ctx, "DROP SCHEMA IF EXISTS "+schema+" CASCADE"); err != nil {
|
||
t.Fatalf("drop stale isolated schema: %v", err)
|
||
}
|
||
if _, err := admin.Exec(ctx, "CREATE SCHEMA "+schema); err != nil {
|
||
t.Fatalf("create isolated integration schema: %v", err)
|
||
}
|
||
t.Cleanup(func() {
|
||
cleanup, err := pgxpool.New(context.Background(), databaseURL)
|
||
if err == nil {
|
||
_, _ = cleanup.Exec(context.Background(), "DROP SCHEMA IF EXISTS "+schema+" CASCADE")
|
||
cleanup.Close()
|
||
}
|
||
})
|
||
query := parsed.Query()
|
||
query.Set("search_path", schema)
|
||
parsed.RawQuery = query.Encode()
|
||
return parsed.String()
|
||
}
|