Files
knowledge-governance/internal/platform/postgres/pgtest/isolated.go
T
姚章浩 a7d643635a
build / build (push) Successful in 1m27s
fix(library): 守卫按行取作业身份、同作业多代次、原作业续作与执行域准入
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 项通过。
2026-10-30 17:20:40 +08:00

66 lines
2.7 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 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()
}