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 项通过。
105 lines
4.4 KiB
Go
105 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"knowledge-governance/internal/appconfig"
|
|
"knowledge-governance/internal/jobs"
|
|
"knowledge-governance/internal/libraryadmission"
|
|
postgresstore "knowledge-governance/internal/platform/postgres"
|
|
v071 "knowledge-governance/internal/releaseadapter/v071"
|
|
)
|
|
|
|
// verifyLibraryExecutionDomain 在注册整库发布 handler 之前核对"这个进程能不能独占目标"。
|
|
//
|
|
// 为什么不在 API 里做:真正的执行者是 worker,能写目标的是它。准入放在执行者起手,
|
|
// 才不会被"另一个进程的配置"绕过去。
|
|
//
|
|
// 判定全部基于**读出来的事实**(目标实况 / 旧的 release 路由绑定 / 未释放的维护窗口),
|
|
// 任何一项读不到都按未知处理并拒绝;这里不停止、不修改任何既有服务(停旧 worker 是
|
|
// 运维决定),只决定"本进程是否注册整库发布执行器"。
|
|
func verifyLibraryExecutionDomain(ctx context.Context, config appconfig.Config, store *postgresstore.Store) error {
|
|
if strings.TrimSpace(config.LibraryProductionDatabaseURL) == "" {
|
|
return errors.New("整库目标未配置(LIBRARY_PRODUCTION_DATABASE_URL):不注册整库发布执行器")
|
|
}
|
|
instance := config.LibraryTargetInstance
|
|
if instance == "" {
|
|
return errors.New("目标实例身份未配置(LIBRARY_TARGET_INSTANCE):无法证明执行域唯一")
|
|
}
|
|
// 1) 目标实况:连上去读,而不是回声配置。
|
|
live, liveErr := v071.ProbeTargetIdentity(ctx, config.LibraryProductionDatabaseURL,
|
|
config.LibraryProductionTenantID, config.LibraryProductionInternalKB)
|
|
facts := libraryadmission.Facts{LiveErr: liveErr}
|
|
if liveErr == nil {
|
|
facts.Live = libraryadmission.Target{
|
|
DatabaseEndpoint: config.LibraryProductionDatabaseURL, Index: config.LibraryProductionIndex,
|
|
Bucket: config.LibraryProductionS3Bucket, KnowledgeBaseID: config.LibraryProductionInternalKB,
|
|
TenantID: config.LibraryProductionTenantID, SchemaVersion: live.SchemaVersion,
|
|
Instance: instance,
|
|
}
|
|
facts.LiveDirty = live.Dirty
|
|
if live.IndexName != "" {
|
|
facts.Live.Index = live.IndexName
|
|
}
|
|
}
|
|
// 2) 旧 release 路由写哪个知识库:production_kb_bindings 是它自己的持久事实。
|
|
if internalKB, externalKB, _, bindErr := store.LoadProductionKBBindings(ctx); bindErr != nil {
|
|
facts.LegacyErr = bindErr
|
|
} else {
|
|
// 旧路由把"测试库"发布到 external;两个方向都算重叠。
|
|
targetKB := strings.TrimSpace(config.LibraryProductionInternalKB)
|
|
for _, candidate := range []string{internalKB, externalKB} {
|
|
if candidate != "" && candidate == targetKB {
|
|
facts.LegacyReleaseKB = candidate
|
|
break
|
|
}
|
|
}
|
|
if facts.LegacyReleaseKB == "" && externalKB != "" {
|
|
facts.LegacyReleaseKB = externalKB
|
|
}
|
|
}
|
|
// 3) 未释放的维护窗口:谁此刻持有这个目标。
|
|
held, heldErr := store.HeldInstanceMaintenance(ctx)
|
|
if heldErr != nil {
|
|
facts.HeldErr = heldErr
|
|
} else {
|
|
for _, row := range held {
|
|
if row.ReleasedAt != nil {
|
|
continue
|
|
}
|
|
facts.HeldInstance, facts.HeldJob = row.Instance, row.OwnerJobID
|
|
break
|
|
}
|
|
}
|
|
configured := libraryadmission.Target{
|
|
DatabaseEndpoint: config.LibraryProductionDatabaseURL, Index: config.LibraryProductionIndex,
|
|
Bucket: config.LibraryProductionS3Bucket, KnowledgeBaseID: config.LibraryProductionInternalKB,
|
|
TenantID: config.LibraryProductionTenantID, SchemaVersion: 0, // 由目标实况回答;配置不再另声明一份容易过期的版本
|
|
Instance: instance,
|
|
}
|
|
if err := libraryadmission.Verify(configured, facts); err != nil {
|
|
return err
|
|
}
|
|
log.Printf("library execution domain admitted: instance=%s kb=%s schema=%d index=%s",
|
|
instance, configured.KnowledgeBaseID, facts.Live.SchemaVersion, configured.Index)
|
|
return nil
|
|
}
|
|
|
|
// admittanceGuarded 把一个 handler 包成"准入未通过就拒绝执行"。
|
|
//
|
|
// 用错误返回而不是不注册:不注册会让作业以 no-handler 进死信、看不出原因;
|
|
// 这里让作业**明确失败**并带上准入原因,页面能直接读到,且任何维护动作都没发生。
|
|
func admittanceGuarded(admissionErr error, handler func(context.Context, jobs.Job) error) func(context.Context, jobs.Job) error {
|
|
if admissionErr == nil {
|
|
return handler
|
|
}
|
|
reason := admissionErr.Error()
|
|
return func(context.Context, jobs.Job) error {
|
|
return fmt.Errorf("整库发布执行域准入未通过,未做任何维护动作:%s", reason)
|
|
}
|
|
}
|