Files
knowledge-governance/internal/platform/postgres/library_restore_dispatch.go
T
姚章浩 6209072e43 fix(library): 跨SHA兼容需受信证明默认拒绝
原实现把 ProtocolEpoch 相等当作构建兼容证明:任何 40 位小写 hex SHA 加一个非空
display 都会被记成 compatible_upgrade 并允许激活,换一个 SHA 即可绕开同 SHA 的
display 绑定,构建身份仍是声明式。

收口为精确绑定运行完整 SHA:同 SHA 必须 display 相符,不同 SHA 一律失败关闭,
错误明确写"cross-sha compatible activation is disabled without a verifiable
protocol build membership proof"。不新建签名基础设施,也不引入第二份 allowlist;
使用点与激活点调用同一函数,行为一致。台账不重写、旧激活行保留(不匹配即 held)。
2026-11-02 21:16:45 +08:00

622 lines
30 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 postgres
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"knowledge-governance/internal/governance"
"knowledge-governance/internal/libraryrelease"
)
// DS01:恢复标记派发的持久准入。
//
// 关键点不是"多了一张表",而是发送许可的产生方式:
// - Prepare 只建立 prepared(不返回许可);
// - Acquire 在**独立短事务**里按 job→operation→activation→active attempt→effect
// 的顺序取锁,校验当前 epoch/owner/lease(数据库时钟)、原作业/operation 锚点、
// 维护窗口、恢复投递身份与不可变绑定后做 prepared→dispatching 的 CAS,只有
// COMMIT 明确成功才返回一次性许可;
// - Inspect 纯读,永不返回许可;
// - 表本身拒绝状态回退、绑定变化、删行与激活撤销回退(迁移里的触发器)。
//
// 外层 WithLibraryExecution 的事务只 rollback、从不 commit,所以这里绝不能用它:
// 在那种事务里写 dispatching,发出请求后账会被回滚成"未派发"。
var ErrRestoreDispatchNotActivated = libraryrelease.ErrRestoreDispatchUnavailable
// 台账行的 protocol_version 仍是 1(派发协议身份,迁移 78 的 CHECK 约束);证据文档
// 升到 v2 只改变"激活输入要证明什么",不改表结构。两者不是同一个数字。
const restoreDispatchActivationLedgerVersion = 1
// ActivateRestoreDispatch 消费一份**可核验的**激活证据文件(协议 v2)。
//
// 证据只声明目标身份、维护作业、journal 文件名/摘要与 writer 集合;实际事实由
// verifyRestoreDispatchEvidence 直接读维护 journal、治理库维护行与现场构建/指纹
// 核出(见 library_restore_dispatch_activation.go)。没有配置受信 journal 根时
// 返回 ErrRestoreDispatchActivationDisabled,绝不退回"只验字符串就开门"。
func (s *Store) ActivateRestoreDispatch(ctx context.Context, evidencePath, evidenceDigest,
targetIdentity, targetFingerprint string, now time.Time) error {
evidence, err := libraryrelease.LoadRestoreDispatchActivationEvidence(evidencePath, evidenceDigest,
now.UTC(), libraryrelease.RestoreDispatchActivationMaxAge)
if err != nil {
return err
}
if evidence.TargetIdentity != targetIdentity {
return fmt.Errorf("restore dispatch activation evidence is for target %q, not %q", evidence.TargetIdentity, targetIdentity)
}
verified, err := s.verifyRestoreDispatchEvidence(ctx, evidence, targetFingerprint, now.UTC())
if err != nil {
return err
}
record, err := json.Marshal(restoreDispatchActivationRecord{Request: evidence, Verified: verified})
if err != nil {
return err
}
if _, err := s.db.Exec(ctx, `
INSERT INTO library_index_restore_dispatch_activation
(target_identity, protocol_version, target_fingerprint, target_instance, governance_database,
protocol_build, activated_by, drain_evidence_digest, evidence, attested_at, activated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11)
ON CONFLICT (target_identity) DO NOTHING`,
evidence.TargetIdentity, restoreDispatchActivationLedgerVersion, evidence.TargetFingerprint, evidence.TargetInstance,
evidence.GovernanceDatabase, evidence.ProtocolBuild, evidence.AttestedBy,
verified.MaintenanceJournalSHA256, string(record), evidence.AttestedAt.UTC(), now.UTC()); err != nil {
return fmt.Errorf("activate restore dispatch: %w", err)
}
return s.requireRestoreDispatchActivation(ctx, targetIdentity)
}
func (s *Store) RevokeRestoreDispatch(ctx context.Context, targetIdentity string, now time.Time) error {
tag, err := s.db.Exec(ctx, `
UPDATE library_index_restore_dispatch_activation SET revoked_at=$2
WHERE target_identity=$1 AND revoked_at IS NULL`, targetIdentity, now.UTC())
if err != nil {
return fmt.Errorf("revoke restore dispatch: %w", err)
}
if tag.RowsAffected() != 1 {
return errors.New("restore dispatch activation is not active for this target")
}
return nil
}
func (s *Store) requireRestoreDispatchActivation(ctx context.Context, targetIdentity string) error {
var one int
err := s.db.QueryRow(ctx, `
SELECT 1 FROM library_index_restore_dispatch_activation
WHERE target_identity=$1 AND protocol_version=1 AND revoked_at IS NULL`, targetIdentity).Scan(&one)
if errors.Is(err, pgx.ErrNoRows) {
return fmt.Errorf("%w(目标 %s 没有激活登记)", ErrRestoreDispatchNotActivated, targetIdentity)
}
if err != nil {
return fmt.Errorf("read restore dispatch activation: %w", err)
}
return nil
}
// lockRestoreDispatchActivation 在**同一个 CAS 事务**里锁激活行。撤销是同一行的
// UPDATE:撤销先完成 ⇒ 这里看到 revoked_at;CAS 先取锁 ⇒ 撤销必须等 CAS 提交,
// 之后按"已派发"的窄边界处理。两者只能有一个线性化顺序。
//
// 这里同时是**使用点**的权威域/构建身份重核:激活行里的 verified 事实必须与当前
// 连接的 PG 权威域(集群 system_identifier + 库 OID + primary)一致;运行构建必须
// 有完整 SHA,且与激活登记的 SHA 精确相同(跨 SHA 兼容默认关闭,epoch 相等不算
// 证明);恢复目标指纹必须等于本次观察值。同名库、副本、
// unknown 构建或换目标都在这里失败关闭,而不是只在激活那一刻检查。
func lockRestoreDispatchActivation(ctx context.Context, tx pgx.Tx, targetIdentity, targetFingerprint string) error {
var revoked *time.Time
var evidenceRaw, rowFingerprint string
err := tx.QueryRow(ctx, `
SELECT revoked_at, evidence::text, target_fingerprint FROM library_index_restore_dispatch_activation
WHERE target_identity=$1 AND protocol_version=1 FOR UPDATE`, targetIdentity).Scan(&revoked, &evidenceRaw, &rowFingerprint)
if errors.Is(err, pgx.ErrNoRows) {
return fmt.Errorf("%w(目标 %s 没有激活登记)", ErrRestoreDispatchNotActivated, targetIdentity)
}
if err != nil {
return fmt.Errorf("lock restore dispatch activation: %w", err)
}
if revoked != nil {
return fmt.Errorf("%w(目标 %s 的激活已于 %s 撤销)", ErrRestoreDispatchNotActivated, targetIdentity, revoked.UTC().Format(time.RFC3339))
}
var record restoreDispatchActivationRecord
if err := json.Unmarshal([]byte(evidenceRaw), &record); err != nil {
return fmt.Errorf("parse restore dispatch activation record: %w", err)
}
verified := record.Verified
// 激活行的 target_fingerprint 列是**维护窗口**指纹;binding 带的是本次恢复对
// 目标做的只读观察值。两层不能相互顶替,否则正常恢复会被窗口指纹误拒。
if rowFingerprint != verified.TargetFingerprint {
return fmt.Errorf("%w(目标 %s 的激活行指纹 %s 与登记证据 %s 不一致)",
ErrRestoreDispatchNotActivated, targetIdentity, rowFingerprint, verified.TargetFingerprint)
}
if verified.RestoreTargetFingerprint != targetFingerprint {
return fmt.Errorf("%w(目标 %s 的激活登记恢复目标指纹 %s,本次恢复目标指纹 %s)",
ErrRestoreDispatchNotActivated, targetIdentity, verified.RestoreTargetFingerprint, targetFingerprint)
}
authority, primary, _, err := observedGovernanceAuthority(ctx, tx)
if err != nil {
return err
}
if verified.GovernanceAuthority.SystemIdentifier == "" || verified.GovernanceAuthority.DatabaseOID == "" {
return fmt.Errorf("%w(目标 %s 的激活登记没有治理权威域身份:拒绝仅凭库名启用)",
ErrRestoreDispatchNotActivated, targetIdentity)
}
if authority != verified.GovernanceAuthority {
return fmt.Errorf("%w(目标 %s 的激活登记属于治理域 %s/%s,当前是 %s/%s)",
ErrRestoreDispatchNotActivated, targetIdentity, verified.GovernanceAuthority.SystemIdentifier,
verified.GovernanceAuthority.DatabaseOID, authority.SystemIdentifier, authority.DatabaseOID)
}
if !primary {
return fmt.Errorf("%w(治理库当前处于恢复/副本角色,拒绝授予派发许可)", ErrRestoreDispatchNotActivated)
}
buildEvidence := libraryrelease.RestoreDispatchActivationEvidence{
ProtocolEpoch: verified.ProtocolEpoch,
ProtocolBuild: verified.ProtocolBuild,
ProtocolBuildSHA: verified.ProtocolBuildSHA,
}
if _, _, _, err := verifyRestoreDispatchBuild(buildEvidence); err != nil {
return fmt.Errorf("%w(目标 %s:%v)", ErrRestoreDispatchNotActivated, targetIdentity, err)
}
return nil
}
func restoreDispatchBindingDigest(b libraryrelease.RestoreMarkerBinding) (string, error) {
return b.Digest()
}
func (s *Store) InspectRestoreMarker(ctx context.Context, b libraryrelease.RestoreMarkerBinding) (libraryrelease.RestoreDispatchStatus, bool, error) {
digest, err := restoreDispatchBindingDigest(b)
if err != nil {
return libraryrelease.RestoreDispatchStatus{}, false, err
}
var state, stored string
err = s.db.QueryRow(ctx, `
SELECT state, binding_digest FROM library_index_restore_dispatch
WHERE target_identity=$1 AND operation_id=$2 AND window_job_id=$3 AND plan_digest=$4
AND index_name=$5 AND document_id=$6 AND phase=$7`,
b.Key.TargetIdentity, b.Key.OperationID, b.Key.WindowJobID, b.Key.PlanDigest,
b.Key.Index, b.Key.DocumentID, b.Key.Phase).Scan(&state, &stored)
if errors.Is(err, pgx.ErrNoRows) {
return libraryrelease.RestoreDispatchStatus{}, false, nil
}
if err != nil {
return libraryrelease.RestoreDispatchStatus{}, false, fmt.Errorf("inspect restore dispatch: %w", err)
}
if stored != digest {
return libraryrelease.RestoreDispatchStatus{State: state, Held: true, BindingConflict: true,
Reason: "restore dispatch binding differs from the immutable plan"}, true, nil
}
switch state {
case libraryrelease.RestoreDispatchPrepared:
return libraryrelease.RestoreDispatchStatus{State: state}, true, nil
case libraryrelease.RestoreDispatchDispatching:
return libraryrelease.RestoreDispatchStatus{State: state, Held: true,
Reason: "dispatch outcome unknown: no further create is permitted"}, true, nil
case libraryrelease.RestoreDispatchMarkerConfirmed:
return libraryrelease.RestoreDispatchStatus{State: state, Held: true,
Reason: "marker already dispatched; only the original confirmation path may continue"}, true, nil
default:
return libraryrelease.RestoreDispatchStatus{State: state, Held: true,
Reason: "unrecognised dispatch state"}, true, nil
}
}
// UnresolvedRestoreDispatches 返回原 window 的权威台账全集(跨 plan 摘要)。
// 前置与收尾都查同一份记录:只遍历当前 plan 的文档列表会漏掉计划外/别名 effect。
func (s *Store) UnresolvedRestoreDispatches(ctx context.Context, targetIdentity, operationID, windowJobID string) ([]libraryrelease.RestoreDispatchRecord, error) {
rows, err := s.db.Query(ctx, `
SELECT plan_digest, index_name, document_id, state FROM library_index_restore_dispatch
WHERE target_identity=$1 AND operation_id=$2 AND window_job_id=$3
ORDER BY plan_digest, index_name, document_id`, targetIdentity, operationID, windowJobID)
if err != nil {
return nil, fmt.Errorf("read restore dispatch window: %w", err)
}
defer rows.Close()
var records []libraryrelease.RestoreDispatchRecord
for rows.Next() {
var record libraryrelease.RestoreDispatchRecord
if err := rows.Scan(&record.PlanDigest, &record.Index, &record.DocumentID, &record.State); err != nil {
return nil, err
}
records = append(records, record)
}
return records, rows.Err()
}
// rejectRestoreDispatchPlanAlias 拒绝同一逻辑 effect 用另一个 plan 摘要另起一行。
// key 里虽然有 plan_digest,但唯一约束允许"同 target/operation/window/index/doc/phase、
// 不同 plan"共存;那正是"换计划别名重开证明"的旁路。
func rejectRestoreDispatchPlanAlias(ctx context.Context, tx pgx.Tx, b libraryrelease.RestoreMarkerBinding) error {
var other string
err := tx.QueryRow(ctx, `
SELECT plan_digest FROM library_index_restore_dispatch
WHERE target_identity=$1 AND operation_id=$2 AND window_job_id=$3
AND index_name=$4 AND document_id=$5 AND phase=$6 AND plan_digest<>$7
LIMIT 1 FOR UPDATE`,
b.Key.TargetIdentity, b.Key.OperationID, b.Key.WindowJobID,
b.Key.Index, b.Key.DocumentID, b.Key.Phase, b.Key.PlanDigest).Scan(&other)
if errors.Is(err, pgx.ErrNoRows) {
return nil
}
if err != nil {
return fmt.Errorf("read restore dispatch plan alias: %w", err)
}
return fmt.Errorf("%w: effect already belongs to another plan digest", libraryrelease.ErrRestoreDispatchHeld)
}
// PrepareRestoreMarker 只建立 prepared:没有发送权,也不把已有 dispatching 抬回 prepared。
func (s *Store) PrepareRestoreMarker(ctx context.Context, b libraryrelease.RestoreMarkerBinding) (libraryrelease.RestoreDispatchStatus, error) {
digest, err := restoreDispatchBindingDigest(b)
if err != nil {
return libraryrelease.RestoreDispatchStatus{}, err
}
if err := s.requireRestoreDispatchActivation(ctx, b.Key.TargetIdentity); err != nil {
return libraryrelease.RestoreDispatchStatus{}, err
}
tx, err := s.db.Begin(ctx)
if err != nil {
return libraryrelease.RestoreDispatchStatus{}, fmt.Errorf("prepare restore dispatch: %w", err)
}
defer func() { _ = tx.Rollback(ctx) }()
// 与 BeginLibraryAttempt 同序:先作业行、再 operation 行。这样"准备"不会与代次切换
// 交错成一份属于旧 identity 的台账。
if err := lockRestoreDispatchIdentity(ctx, tx, b); err != nil {
return libraryrelease.RestoreDispatchStatus{}, err
}
if err := lockRestoreDispatchActivation(ctx, tx, b.Key.TargetIdentity, b.TargetFingerprint); err != nil {
return libraryrelease.RestoreDispatchStatus{}, err
}
if err := rejectRestoreDispatchPlanAlias(ctx, tx, b); err != nil {
return libraryrelease.RestoreDispatchStatus{}, err
}
if _, err := tx.Exec(ctx, `
INSERT INTO library_index_restore_dispatch
(target_identity, operation_id, window_job_id, plan_digest, index_name, document_id, phase, binding_digest, state)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'prepared')
ON CONFLICT ON CONSTRAINT library_index_restore_dispatch_identity DO NOTHING`,
b.Key.TargetIdentity, b.Key.OperationID, b.Key.WindowJobID, b.Key.PlanDigest,
b.Key.Index, b.Key.DocumentID, b.Key.Phase, digest); err != nil {
return libraryrelease.RestoreDispatchStatus{}, fmt.Errorf("record prepared restore dispatch: %w", err)
}
status, found, err := inspectRestoreDispatchTx(ctx, tx, b)
if err != nil {
return libraryrelease.RestoreDispatchStatus{}, err
}
if !found {
return libraryrelease.RestoreDispatchStatus{}, errors.New("prepared restore dispatch was not durable")
}
if status.Held {
return status, fmt.Errorf("%w: %s", libraryrelease.ErrRestoreDispatchHeld, status.Reason)
}
if err := tx.Commit(ctx); err != nil {
return libraryrelease.RestoreDispatchStatus{}, fmt.Errorf("commit prepared restore dispatch: %w", err)
}
return status, nil
}
// acquireRestoreMarker 的 beforeCommit 是提交结果不明这一档的测试接缝:钩子返回错误时
// 事务回滚,调用方不得发送。生产路径传 nil。
func (s *Store) acquireRestoreMarker(ctx context.Context, b libraryrelease.RestoreMarkerBinding, beforeCommit func() error) (*libraryrelease.RestoreSendPermit, libraryrelease.RestoreDispatchStatus, error) {
digest, err := restoreDispatchBindingDigest(b)
if err != nil {
return nil, libraryrelease.RestoreDispatchStatus{}, err
}
tx, err := s.db.Begin(ctx)
if err != nil {
return nil, libraryrelease.RestoreDispatchStatus{}, fmt.Errorf("dispatch restore marker: %w", err)
}
defer func() { _ = tx.Rollback(ctx) }()
if err := lockRestoreDispatchIdentity(ctx, tx, b); err != nil {
return nil, libraryrelease.RestoreDispatchStatus{}, err
}
// 撤销与 CAS 在同一事务线性化:这里拿激活行锁,撤销的 UPDATE 必须排队。
if err := lockRestoreDispatchActivation(ctx, tx, b.Key.TargetIdentity, b.TargetFingerprint); err != nil {
return nil, libraryrelease.RestoreDispatchStatus{}, err
}
// 当前代次必须属于这个原 window 作业,且租户/epoch/租约都仍有效。lease 用
// clock_timestamp():等锁可能跨越租约到期,事务起始的 now() 不是判据。
var epoch int64
var owner, attemptState, attemptJob string
var leaseUntil *time.Time
err = tx.QueryRow(ctx, `
SELECT epoch, owner, state, job_id, lease_until FROM library_attempts
WHERE operation_id=$1 AND state IN ('claimed','running') FOR UPDATE`, b.Key.OperationID).Scan(
&epoch, &owner, &attemptState, &attemptJob, &leaseUntil)
if errors.Is(err, pgx.ErrNoRows) {
return nil, libraryrelease.RestoreDispatchStatus{}, fmt.Errorf("%w: no active attempt for operation %s",
governance.ErrLibraryEpochStale, b.Key.OperationID)
}
if err != nil {
return nil, libraryrelease.RestoreDispatchStatus{}, fmt.Errorf("read active attempt: %w", err)
}
if attemptJob != b.Key.WindowJobID {
return nil, libraryrelease.RestoreDispatchStatus{}, fmt.Errorf("%w: active attempt belongs to job %s, not the original window %s",
governance.ErrLibraryEpochStale, attemptJob, b.Key.WindowJobID)
}
var leaseLive bool
if err := tx.QueryRow(ctx,
`SELECT COALESCE($1::timestamptz > clock_timestamp(), false)`, leaseUntil).Scan(&leaseLive); err != nil {
return nil, libraryrelease.RestoreDispatchStatus{}, fmt.Errorf("read attempt lease: %w", err)
}
if owner != b.Owner || epoch != b.Epoch || !leaseLive {
return nil, libraryrelease.RestoreDispatchStatus{State: libraryrelease.RestoreDispatchPrepared, Held: true,
Reason: fmt.Sprintf("attempt %s#%d is not the live authorized owner", b.Owner, b.Epoch)},
fmt.Errorf("%w: attempt %s#%d is not current", governance.ErrLibraryEpochStale, b.Owner, b.Epoch)
}
if err := rejectRestoreDispatchPlanAlias(ctx, tx, b); err != nil {
return nil, libraryrelease.RestoreDispatchStatus{}, err
}
var id int64
var state, stored string
err = tx.QueryRow(ctx, `
SELECT id, state, binding_digest FROM library_index_restore_dispatch
WHERE target_identity=$1 AND operation_id=$2 AND window_job_id=$3 AND plan_digest=$4
AND index_name=$5 AND document_id=$6 AND phase=$7 FOR UPDATE`,
b.Key.TargetIdentity, b.Key.OperationID, b.Key.WindowJobID, b.Key.PlanDigest,
b.Key.Index, b.Key.DocumentID, b.Key.Phase).Scan(&id, &state, &stored)
if errors.Is(err, pgx.ErrNoRows) {
return nil, libraryrelease.RestoreDispatchStatus{}, errors.New("no prepared restore dispatch record: refusing to send")
}
if err != nil {
return nil, libraryrelease.RestoreDispatchStatus{}, fmt.Errorf("lock restore dispatch: %w", err)
}
if stored != digest {
return nil, libraryrelease.RestoreDispatchStatus{State: state, Held: true, BindingConflict: true,
Reason: "restore dispatch binding differs from the immutable plan"}, libraryrelease.ErrRestoreDispatchHeld
}
if state != libraryrelease.RestoreDispatchPrepared {
return nil, libraryrelease.RestoreDispatchStatus{State: state, Held: true,
Reason: "dispatch already claimed; no second send is permitted"}, libraryrelease.ErrRestoreDispatchHeld
}
// effect 行锁是本次 CAS 前最后一个可能阻塞的锁。job/attempt/恢复投递的租约可能
// 正好在等它的时候过期;前面那几次检查发生在拿锁之前,不能充当 CAS 点的判据。
// 这些行锁都已被本事务持有,这里只重读、不改变锁序。
if err := recheckRestoreDispatchLeases(ctx, tx, b); err != nil {
var held *restoreDispatchLeaseExpired
if errors.As(err, &held) {
return nil, libraryrelease.RestoreDispatchStatus{State: libraryrelease.RestoreDispatchPrepared, Held: true,
Reason: held.Error()}, fmt.Errorf("%w: %s", governance.ErrLibraryEpochStale, held.Error())
}
return nil, libraryrelease.RestoreDispatchStatus{}, err
}
var raw [16]byte
if _, err := rand.Read(raw[:]); err != nil {
return nil, libraryrelease.RestoreDispatchStatus{}, err
}
nonce := hex.EncodeToString(raw[:])
tag, err := tx.Exec(ctx, `
UPDATE library_index_restore_dispatch
SET state='dispatching', dispatch_owner=$2, dispatch_epoch=$3, dispatch_nonce=$4,
request_digest=$5, dispatch_committed_at=clock_timestamp(), updated_at=clock_timestamp()
WHERE id=$1 AND state='prepared'`, id, b.Owner, b.Epoch, nonce, b.RequestDigest)
if err != nil {
return nil, libraryrelease.RestoreDispatchStatus{}, fmt.Errorf("claim restore dispatch: %w", err)
}
if tag.RowsAffected() != 1 {
return nil, libraryrelease.RestoreDispatchStatus{State: state, Held: true,
Reason: "dispatch CAS lost"}, libraryrelease.ErrRestoreDispatchHeld
}
if beforeCommit != nil {
if err := beforeCommit(); err != nil {
// 提交结果不明:本调用不发送,也不把状态当成 prepared 重试。
return nil, libraryrelease.RestoreDispatchStatus{State: libraryrelease.RestoreDispatchDispatching, Held: true,
Reason: "commit outcome unknown"}, fmt.Errorf("commit restore dispatch: %w", err)
}
}
if err := tx.Commit(ctx); err != nil {
return nil, libraryrelease.RestoreDispatchStatus{State: libraryrelease.RestoreDispatchDispatching, Held: true,
Reason: "commit outcome unknown"}, fmt.Errorf("commit restore dispatch: %w", err)
}
return libraryrelease.IssueRestoreSendPermit(b.Key, digest, nonce),
libraryrelease.RestoreDispatchStatus{State: libraryrelease.RestoreDispatchDispatching}, nil
}
func (s *Store) AcquireRestoreMarker(ctx context.Context, b libraryrelease.RestoreMarkerBinding) (*libraryrelease.RestoreSendPermit, libraryrelease.RestoreDispatchStatus, error) {
return s.acquireRestoreMarker(ctx, b, nil)
}
// ConfirmRestoreMarker 只记录真实回执(不再发送)。状态仍是 dispatching 时才允许;
// 回执本身来自本次受控发送或同计划标记的实时核实,不由客户端布尔值决定。
func (s *Store) ConfirmRestoreMarker(ctx context.Context, b libraryrelease.RestoreMarkerBinding, receipt []byte) error {
if len(receipt) == 0 || !json.Valid(receipt) {
return errors.New("restore marker confirmation requires a real receipt")
}
digest, err := restoreDispatchBindingDigest(b)
if err != nil {
return err
}
tx, err := s.db.Begin(ctx)
if err != nil {
return fmt.Errorf("confirm restore marker: %w", err)
}
defer func() { _ = tx.Rollback(ctx) }()
if err := lockRestoreDispatchIdentity(ctx, tx, b); err != nil {
return err
}
if err := lockRestoreDispatchActivation(ctx, tx, b.Key.TargetIdentity, b.TargetFingerprint); err != nil {
return err
}
var epoch int64
var owner, attemptJob string
var leaseUntil *time.Time
err = tx.QueryRow(ctx, `
SELECT epoch, owner, job_id, lease_until FROM library_attempts
WHERE operation_id=$1 AND state IN ('claimed','running') FOR UPDATE`, b.Key.OperationID).Scan(&epoch, &owner, &attemptJob, &leaseUntil)
if err != nil {
return fmt.Errorf("confirm restore marker attempt: %w", err)
}
var leaseLive bool
if err := tx.QueryRow(ctx,
`SELECT COALESCE($1::timestamptz > clock_timestamp(), false)`, leaseUntil).Scan(&leaseLive); err != nil {
return fmt.Errorf("confirm restore marker lease: %w", err)
}
if attemptJob != b.Key.WindowJobID || owner != b.Owner || epoch != b.Epoch || !leaseLive {
return fmt.Errorf("%w: confirmation by a stale owner", governance.ErrLibraryEpochStale)
}
// 与 Acquire 同一个顺序:确认前的最后一把锁也要先拿到,再按数据库实时时钟重核
// 租约。否则"确认"可以在等锁跨过到期之后仍落库,把一次过期 owner 的回执写成事实。
var effectID int64
if err := tx.QueryRow(ctx, `
SELECT id FROM library_index_restore_dispatch
WHERE target_identity=$1 AND operation_id=$2 AND window_job_id=$3 AND plan_digest=$4
AND index_name=$5 AND document_id=$6 AND phase=$7 FOR UPDATE`,
b.Key.TargetIdentity, b.Key.OperationID, b.Key.WindowJobID, b.Key.PlanDigest,
b.Key.Index, b.Key.DocumentID, b.Key.Phase).Scan(&effectID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return errors.New("restore marker confirmation did not match a dispatch record")
}
return fmt.Errorf("lock restore marker for confirmation: %w", err)
}
if err := recheckRestoreDispatchLeases(ctx, tx, b); err != nil {
return fmt.Errorf("%w: %s", governance.ErrLibraryEpochStale, err)
}
tag, err := tx.Exec(ctx, `
UPDATE library_index_restore_dispatch
SET state='marker_confirmed', marker_receipt=$1::jsonb, confirmed_at=clock_timestamp(), updated_at=clock_timestamp()
WHERE target_identity=$2 AND operation_id=$3 AND window_job_id=$4 AND plan_digest=$5
AND index_name=$6 AND document_id=$7 AND phase=$8
AND state='dispatching' AND binding_digest=$9`,
string(receipt), b.Key.TargetIdentity, b.Key.OperationID, b.Key.WindowJobID, b.Key.PlanDigest,
b.Key.Index, b.Key.DocumentID, b.Key.Phase, digest)
if err != nil {
return fmt.Errorf("confirm restore marker: %w", err)
}
if tag.RowsAffected() != 1 {
return errors.New("restore marker confirmation did not match a dispatching record")
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit restore marker confirmation: %w", err)
}
return nil
}
// lockRestoreDispatchIdentity 以 job→operation 锁序把"这份许可是不是仍属于原作业"
// 钉死在原持久锚点上:目标/scope/来源快照、写集摘要(若已登记)、维护窗口与实例
// 指纹、恢复投递的 queue/operation 绑定都必须与不可变绑定一致。
func lockRestoreDispatchIdentity(ctx context.Context, tx pgx.Tx, b libraryrelease.RestoreMarkerBinding) error {
var operationID, scope, jobSource string
var leaseOwner string
var jobEpoch int64
var jobLease *time.Time
err := tx.QueryRow(ctx, `
SELECT operation_id, scope, target_snapshot_id, lease_owner, attempt_epoch, lease_until
FROM library_jobs WHERE id=$1 FOR UPDATE`, b.Key.WindowJobID).Scan(
&operationID, &scope, &jobSource, &leaseOwner, &jobEpoch, &jobLease)
if errors.Is(err, pgx.ErrNoRows) {
return fmt.Errorf("window job %s not found", b.Key.WindowJobID)
}
if err != nil {
return fmt.Errorf("lock window job: %w", err)
}
if operationID != b.Key.OperationID {
return errors.New("window job belongs to another operation")
}
if scope != b.Scope {
return fmt.Errorf("window job scope %s does not match binding %s", scope, b.Scope)
}
if jobSource == "" || jobSource != b.SourceSnapshot {
return fmt.Errorf("window job source snapshot %q does not match binding %q", jobSource, b.SourceSnapshot)
}
if leaseOwner != b.Owner || jobEpoch != b.Epoch || jobLease == nil {
return fmt.Errorf("%w: window job lease is not held by attempt %s#%d", governance.ErrLibraryEpochStale, b.Owner, b.Epoch)
}
var jobLeaseLive bool
if err := tx.QueryRow(ctx, `SELECT COALESCE($1::timestamptz > clock_timestamp(), false)`, jobLease).Scan(&jobLeaseLive); err != nil {
return fmt.Errorf("read window job lease: %w", err)
}
if !jobLeaseLive {
return fmt.Errorf("%w: window job lease expired while waiting for the dispatch CAS", governance.ErrLibraryEpochStale)
}
var opScope, opKind, opSource, requestDigest, writesetDigest, firstBackup string
err = tx.QueryRow(ctx, `
SELECT scope, kind, target_snapshot_id, request_digest, COALESCE(writeset_digest,''), COALESCE(first_backup_snapshot_id,'')
FROM library_operations WHERE id=$1 FOR UPDATE`, b.Key.OperationID).Scan(
&opScope, &opKind, &opSource, &requestDigest, &writesetDigest, &firstBackup)
if errors.Is(err, pgx.ErrNoRows) {
return fmt.Errorf("operation %s not found", b.Key.OperationID)
}
if err != nil {
return fmt.Errorf("lock operation: %w", err)
}
if opScope != b.Scope {
return fmt.Errorf("operation scope %s does not match binding %s", opScope, b.Scope)
}
if opSource == "" || opSource != b.SourceSnapshot {
return fmt.Errorf("operation source snapshot %q does not match binding %q", opSource, b.SourceSnapshot)
}
if requestDigest == "" {
return errors.New("operation has no request digest anchor")
}
if writesetDigest != "" && writesetDigest != b.Key.PlanDigest {
return fmt.Errorf("operation plan digest %s does not match binding %s", writesetDigest, b.Key.PlanDigest)
}
if firstBackup != "" && firstBackup != b.BackupSnapshot {
return fmt.Errorf("operation backup snapshot %s does not match binding %s", firstBackup, b.BackupSnapshot)
}
_ = opKind
if b.TargetInstance != "" {
var ownerJob, ownerScope string
var released *time.Time
var state string
err = tx.QueryRow(ctx, `
SELECT owner_job_id, owner_scope, state, released_at
FROM library_instance_maintenance WHERE instance=$1 FOR UPDATE`, b.TargetInstance).Scan(
&ownerJob, &ownerScope, &state, &released)
if errors.Is(err, pgx.ErrNoRows) {
return fmt.Errorf("target instance %s has no maintenance window", b.TargetInstance)
}
if err != nil {
return fmt.Errorf("lock instance maintenance: %w", err)
}
if released != nil || state == "released" {
return errors.New("target instance maintenance window is already released")
}
if ownerJob != b.Key.WindowJobID || ownerScope != b.Scope {
return fmt.Errorf("maintenance window belongs to %s (scope=%s), not window %s", ownerJob, ownerScope, b.Key.WindowJobID)
}
}
if b.RecoveryJobID != "" {
if err := lockAndVerifyRecoveryDelivery(ctx, tx, b); err != nil {
return err
}
}
return nil
}
func inspectRestoreDispatchTx(ctx context.Context, tx pgx.Tx, b libraryrelease.RestoreMarkerBinding) (libraryrelease.RestoreDispatchStatus, bool, error) {
digest, err := restoreDispatchBindingDigest(b)
if err != nil {
return libraryrelease.RestoreDispatchStatus{}, false, err
}
var state, stored string
err = tx.QueryRow(ctx, `
SELECT state, binding_digest FROM library_index_restore_dispatch
WHERE target_identity=$1 AND operation_id=$2 AND window_job_id=$3 AND plan_digest=$4
AND index_name=$5 AND document_id=$6 AND phase=$7`,
b.Key.TargetIdentity, b.Key.OperationID, b.Key.WindowJobID, b.Key.PlanDigest,
b.Key.Index, b.Key.DocumentID, b.Key.Phase).Scan(&state, &stored)
if errors.Is(err, pgx.ErrNoRows) {
return libraryrelease.RestoreDispatchStatus{}, false, nil
}
if err != nil {
return libraryrelease.RestoreDispatchStatus{}, false, fmt.Errorf("inspect restore dispatch: %w", err)
}
if stored != digest {
return libraryrelease.RestoreDispatchStatus{State: state, Held: true,
Reason: "restore dispatch binding differs from the immutable plan"}, true, nil
}
return libraryrelease.RestoreDispatchStatus{State: state,
Held: state != libraryrelease.RestoreDispatchPrepared}, true, nil
}