c2402947a9
build / build (push) Successful in 1m26s
上游 task_dead_letters 没有解决标记,purge/DELETE 会销毁"失败过"的唯一证据。
治理侧新增解除记录(迁移 073):绑定实时行逐字节指纹、重放引用、源侧观测摘要与
操作人;行被改过、重放后又出现新死信、源侧未结清都不命中,封存照旧阻断。
指纹先压紧空白再摘要,避免 PG to_jsonb::text 与 json.Marshal 的空白差异。
试过:go test ./internal/{librarydeadletter,libraryagent,httpapi,releaseadapter/v071,
platform/postgres},make verify 15 项全绿。
150 lines
6.4 KiB
Go
150 lines
6.4 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
)
|
|
|
|
// 死信解除只写治理库:上游 task_dead_letters 一个字节都不动。为什么这样:
|
|
// 上游没有"已解决"标记,purge/DELETE 会销毁失败证据;治理侧保留审计并绑定
|
|
// 实时行指纹,行变了就不会再命中,阻断自动恢复。
|
|
|
|
// RecordDeadLetterRelease 幂等写入一条解除记录。同实例同指纹重复提交返回已存记录,
|
|
// 但内容不一致(重放引用/观测摘要不同)必须报冲突,不能悄悄覆盖审计。
|
|
func (s *Store) RecordDeadLetterRelease(ctx context.Context, release governance.LibraryDeadLetterRelease) (governance.LibraryDeadLetterRelease, bool, error) {
|
|
if release.ID == "" || release.Instance == "" || release.Fingerprint == "" || release.Actor == "" {
|
|
return governance.LibraryDeadLetterRelease{}, false, errors.New("dead letter release requires identity, fingerprint and actor")
|
|
}
|
|
observation := release.Observation
|
|
if len(observation) == 0 {
|
|
observation = json.RawMessage(`{}`)
|
|
}
|
|
var id string
|
|
err := s.db.QueryRow(ctx, `
|
|
INSERT INTO library_dead_letter_releases (id, instance, tenant_id, kb_id, dead_letter_id, fingerprint,
|
|
task_type, scope, scope_id, related_id, replay_queue, replay_task_id, replayed_at, replay_evidence,
|
|
observation_digest, observation, note, actor, created_at)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19)
|
|
ON CONFLICT (instance, fingerprint) DO NOTHING
|
|
RETURNING id`,
|
|
release.ID, release.Instance, release.TenantID, release.KnowledgeBaseID, release.DeadLetterID,
|
|
release.Fingerprint, release.TaskType, release.Scope, release.ScopeID, release.RelatedID,
|
|
release.ReplayQueue, release.ReplayTaskID, release.ReplayedAt.UTC(), release.ReplayEvidence,
|
|
release.ObservationDigest, observation, release.Note, release.Actor, release.CreatedAt.UTC()).Scan(&id)
|
|
if err == nil {
|
|
stored, loadErr := s.DeadLetterRelease(ctx, id)
|
|
return stored, false, loadErr
|
|
}
|
|
if !errors.Is(err, pgx.ErrNoRows) {
|
|
return governance.LibraryDeadLetterRelease{}, false, fmt.Errorf("record dead letter release: %w", err)
|
|
}
|
|
existing, loadErr := s.deadLetterReleaseByFingerprint(ctx, release.Instance, release.Fingerprint)
|
|
if loadErr != nil {
|
|
return governance.LibraryDeadLetterRelease{}, false, loadErr
|
|
}
|
|
if existing.ObservationDigest != release.ObservationDigest || existing.ReplayTaskID != release.ReplayTaskID ||
|
|
!existing.ReplayedAt.Equal(release.ReplayedAt.UTC()) {
|
|
return existing, false, governance.ErrLibraryJobIdempotencyConflict
|
|
}
|
|
return existing, true, nil
|
|
}
|
|
|
|
func (s *Store) deadLetterReleaseByFingerprint(ctx context.Context, instance, fingerprint string) (governance.LibraryDeadLetterRelease, error) {
|
|
var id string
|
|
if err := s.db.QueryRow(ctx, `SELECT id FROM library_dead_letter_releases WHERE instance=$1 AND fingerprint=$2`,
|
|
instance, fingerprint).Scan(&id); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return governance.LibraryDeadLetterRelease{}, errors.New("dead letter release not found")
|
|
}
|
|
return governance.LibraryDeadLetterRelease{}, fmt.Errorf("load dead letter release: %w", err)
|
|
}
|
|
return s.DeadLetterRelease(ctx, id)
|
|
}
|
|
|
|
func (s *Store) DeadLetterRelease(ctx context.Context, id string) (governance.LibraryDeadLetterRelease, error) {
|
|
var release governance.LibraryDeadLetterRelease
|
|
var observation []byte
|
|
err := s.db.QueryRow(ctx, `
|
|
SELECT id, instance, tenant_id, kb_id, dead_letter_id, fingerprint, task_type, scope, scope_id, related_id,
|
|
replay_queue, replay_task_id, replayed_at, replay_evidence, observation_digest, observation, note, actor, created_at
|
|
FROM library_dead_letter_releases WHERE id=$1`, id).Scan(&release.ID, &release.Instance, &release.TenantID,
|
|
&release.KnowledgeBaseID, &release.DeadLetterID, &release.Fingerprint, &release.TaskType, &release.Scope,
|
|
&release.ScopeID, &release.RelatedID, &release.ReplayQueue, &release.ReplayTaskID, &release.ReplayedAt,
|
|
&release.ReplayEvidence, &release.ObservationDigest, &observation, &release.Note, &release.Actor, &release.CreatedAt)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return governance.LibraryDeadLetterRelease{}, err
|
|
}
|
|
return governance.LibraryDeadLetterRelease{}, fmt.Errorf("load dead letter release: %w", err)
|
|
}
|
|
release.Observation = json.RawMessage(observation)
|
|
return release, nil
|
|
}
|
|
|
|
// DeadLetterReleases 按实例倒序列出解除记录(审计视图)。
|
|
func (s *Store) DeadLetterReleases(ctx context.Context, instance string, limit int) ([]governance.LibraryDeadLetterRelease, error) {
|
|
if instance == "" {
|
|
return nil, errors.New("dead letter releases require an instance")
|
|
}
|
|
if limit <= 0 || limit > 200 {
|
|
limit = 50
|
|
}
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id FROM library_dead_letter_releases WHERE instance=$1 ORDER BY created_at DESC LIMIT $2`, instance, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list dead letter releases: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
ids := []string{}
|
|
for rows.Next() {
|
|
var id string
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, fmt.Errorf("list dead letter releases: %w", err)
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
if rows.Err() != nil {
|
|
return nil, fmt.Errorf("list dead letter releases: %w", rows.Err())
|
|
}
|
|
result := make([]governance.LibraryDeadLetterRelease, 0, len(ids))
|
|
for _, id := range ids {
|
|
release, loadErr := s.DeadLetterRelease(ctx, id)
|
|
if loadErr != nil {
|
|
return nil, loadErr
|
|
}
|
|
result = append(result, release)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// DeadLetterReleaseFingerprints 是封存执行侧需要的唯一投影:只给指纹,不给证据。
|
|
// worker 把它原样传给 agent,CLI 再对实时行逐字节复核。
|
|
func (s *Store) DeadLetterReleaseFingerprints(ctx context.Context, instance string) ([]string, error) {
|
|
if instance == "" {
|
|
return nil, errors.New("dead letter release fingerprints require an instance")
|
|
}
|
|
rows, err := s.db.Query(ctx, `SELECT fingerprint FROM library_dead_letter_releases WHERE instance=$1 ORDER BY created_at`, instance)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list dead letter release fingerprints: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
result := []string{}
|
|
for rows.Next() {
|
|
var fingerprint string
|
|
if err := rows.Scan(&fingerprint); err != nil {
|
|
return nil, fmt.Errorf("list dead letter release fingerprints: %w", err)
|
|
}
|
|
result = append(result, fingerprint)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
var _ = time.Time{}
|