Files
EvoBGP/internal/repository/module_snapshot.go
T
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <[email protected]>
2026-11-02 17:23:59 +07:00

286 lines
9.1 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
"hash/fnv"
"strconv"
"strings"
"time"
"evobgp/internal/store"
"github.com/jackc/pgx/v5"
)
// ListModuleSnapshotSourceRows returns persisted rows for one source key.
func (p *Postgres) ListModuleSnapshotSourceRows(ctx context.Context, tenantID, moduleID, source string) ([]store.PrefixRow, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
rows, err := p.pool.Query(ctx, `
SELECT prefix, community_id::text, source
FROM module_prefix_snapshot_row
WHERE tenant_id = $1::uuid AND module_id = $2::uuid AND source = $3
ORDER BY ord`, tenantID, moduleID, source)
if err != nil {
return nil, fmt.Errorf("repository: read snapshot source rows: %w", err)
}
defer rows.Close()
out, err := readModuleSnapshotRows(rows)
if err != nil {
return nil, err
}
return out, nil
}
func (p *Postgres) GetModulePrefixSnapshot(ctx context.Context, tenantID, moduleID string) (*store.ModulePrefixSnapshot, bool, error) {
tx, err := p.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly})
if err != nil {
return nil, false, fmt.Errorf("repository: begin snapshot read: %w", err)
}
defer rollbackSnapshotTx(tx)
snap, ok, err := readModuleSnapshotTx(ctx, tx, tenantID, moduleID)
if err != nil {
return nil, false, err
}
if err := tx.Commit(ctx); err != nil {
return nil, false, fmt.Errorf("repository: commit snapshot read: %w", err)
}
return snap, ok, nil
}
func readModuleSnapshotTx(ctx context.Context, tx pgx.Tx, tenantID, moduleID string) (*store.ModulePrefixSnapshot, bool, error) {
var inputHash string
var collectedAt time.Time
var version int64
err := tx.QueryRow(ctx, `
SELECT input_hash, collected_at, snapshot_version
FROM module_prefix_snapshot
WHERE tenant_id = $1 AND module_id = $2`,
tenantID, moduleID).Scan(&inputHash, &collectedAt, &version)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, false, nil
}
return nil, false, err
}
rows, qerr := tx.Query(ctx, `
SELECT prefix, community_id::text, source
FROM module_prefix_snapshot_row
WHERE tenant_id = $1::uuid AND module_id = $2::uuid
ORDER BY ord`, tenantID, moduleID)
if qerr != nil {
return nil, false, fmt.Errorf("repository: read snapshot rows: %w", qerr)
}
defer rows.Close()
prefixes, err := readModuleSnapshotRows(rows)
if err != nil {
return nil, false, err
}
return &store.ModulePrefixSnapshot{
InputHash: inputHash,
CollectedAt: collectedAt.UTC(),
Version: version,
Prefixes: prefixes,
}, true, nil
}
type moduleSnapshotRows interface {
Next() bool
Scan(...any) error
Err() error
}
func readModuleSnapshotRows(rows moduleSnapshotRows) ([]store.PrefixRow, error) {
var prefixes []store.PrefixRow
for rows.Next() {
var pr store.PrefixRow
var comm *string
if err := rows.Scan(&pr.Prefix, &comm, &pr.Source); err != nil {
return nil, fmt.Errorf("repository: scan snapshot row: %w", err)
}
pr.CommunityID = comm
prefixes = append(prefixes, pr)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("repository: iterate snapshot rows: %w", err)
}
return prefixes, nil
}
func (p *Postgres) SetModulePrefixSnapshot(ctx context.Context, tenantID, moduleID, inputHash string, prefixes []store.PrefixRow) error {
if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(moduleID) == "" || strings.TrimSpace(inputHash) == "" {
return store.ErrInvalidInput
}
return p.UpdateModulePrefixSnapshot(ctx, tenantID, moduleID, func(*store.ModulePrefixSnapshot) (*store.ModulePrefixSnapshot, error) {
return &store.ModulePrefixSnapshot{InputHash: inputHash, Prefixes: prefixes}, nil
})
}
func rollbackSnapshotTx(tx pgx.Tx) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = tx.Rollback(ctx)
}
// UpdateModulePrefixSnapshot serializes read, mutation and write on the same transaction connection.
func (p *Postgres) UpdateModulePrefixSnapshot(ctx context.Context, tenantID, moduleID string, update store.ModuleSnapshotUpdate) error {
if update == nil || strings.TrimSpace(tenantID) == "" || strings.TrimSpace(moduleID) == "" {
return store.ErrInvalidInput
}
ctx, cancel, timeoutInstalled := store.SnapshotWriteBudget(ctx)
defer cancel()
tx, err := p.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("repository: begin snapshot update: %w", err)
}
defer rollbackSnapshotTx(tx)
if err := setSnapshotStatementTimeout(ctx, tx, timeoutInstalled); err != nil {
return err
}
if _, err = tx.Exec(ctx, `SELECT pg_advisory_xact_lock($1::bigint)`, moduleSnapshotAdvisoryKey(tenantID, moduleID)); err != nil {
return fmt.Errorf("repository: lock snapshot transaction: %w", err)
}
current, _, err := readModuleSnapshotTx(ctx, tx, tenantID, moduleID)
if err != nil {
return err
}
next, err := update(current)
if err != nil {
return err
}
if next != nil && strings.TrimSpace(next.InputHash) == "" {
return store.ErrInvalidInput
}
if err := ctx.Err(); err != nil {
return err
}
if next == nil {
if _, err := tx.Exec(ctx, `DELETE FROM module_prefix_snapshot WHERE tenant_id=$1::uuid AND module_id=$2::uuid`, tenantID, moduleID); err != nil {
return fmt.Errorf("repository: delete module snapshot: %w", err)
}
} else if !store.SameSnapshotContent(current, next) {
if err := writeModuleSnapshotTx(ctx, tx, tenantID, moduleID, next); err != nil {
return err
}
}
hash := ""
if next != nil {
hash = next.InputHash
}
tag, err := tx.Exec(ctx, `UPDATE module SET input_hash=$3 WHERE tenant_id=$1::uuid AND id=$2::uuid AND deleted_at IS NULL`, tenantID, moduleID, hash)
if err != nil {
return fmt.Errorf("repository: update module input hash: %w", err)
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("repository: commit module snapshot: %w", err)
}
return nil
}
func writeModuleSnapshotTx(ctx context.Context, tx pgx.Tx, tenantID, moduleID string, next *store.ModulePrefixSnapshot) error {
_, err := tx.Exec(ctx, `
INSERT INTO module_prefix_snapshot (tenant_id, module_id, input_hash, collected_at)
VALUES ($1::uuid, $2::uuid, $3, now())
ON CONFLICT (tenant_id, module_id) DO UPDATE SET
input_hash = EXCLUDED.input_hash,
collected_at = EXCLUDED.collected_at`,
tenantID, moduleID, next.InputHash)
if err != nil {
return err
}
if _, err := tx.Exec(ctx, `
DELETE FROM module_prefix_snapshot_row
WHERE tenant_id = $1::uuid AND module_id = $2::uuid`, tenantID, moduleID); err != nil {
return err
}
if len(next.Prefixes) > 0 {
_, err = tx.CopyFrom(
ctx,
pgx.Identifier{"module_prefix_snapshot_row"},
[]string{"tenant_id", "module_id", "ord", "prefix", "community_id", "source"},
pgx.CopyFromSlice(len(next.Prefixes), func(i int) ([]any, error) {
pr := next.Prefixes[i]
var comm any
if pr.CommunityID != nil && strings.TrimSpace(*pr.CommunityID) != "" {
comm = strings.TrimSpace(*pr.CommunityID)
}
src := pr.Source
if strings.TrimSpace(src) == "" {
src = "render"
}
return []any{tenantID, moduleID, i, strings.TrimSpace(pr.Prefix), comm, src}, nil
}),
)
if err != nil {
return err
}
}
return nil
}
func (p *Postgres) DeleteModulePrefixSnapshot(ctx context.Context, tenantID, moduleID string) error {
return p.UpdateModulePrefixSnapshot(ctx, tenantID, moduleID, func(*store.ModulePrefixSnapshot) (*store.ModulePrefixSnapshot, error) {
return nil, nil
})
}
func (p *Postgres) SetModuleInputHash(ctx context.Context, tenantID, moduleID, hash string) error {
ctx, cancel, timeoutInstalled := store.SnapshotWriteBudget(ctx)
defer cancel()
tx, err := p.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("repository: begin module hash update: %w", err)
}
defer rollbackSnapshotTx(tx)
if err := setSnapshotStatementTimeout(ctx, tx, timeoutInstalled); err != nil {
return err
}
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock($1::bigint)`, moduleSnapshotAdvisoryKey(tenantID, moduleID)); err != nil {
return fmt.Errorf("repository: lock module hash update: %w", err)
}
tag, err := tx.Exec(ctx, `
UPDATE module SET input_hash = $3
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL`,
moduleID, tenantID, hash)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return tx.Commit(ctx)
}
// setSnapshotStatementTimeout bounds a lock when the caller did not set a deadline.
func setSnapshotStatementTimeout(ctx context.Context, tx pgx.Tx, installed bool) error {
if !installed {
return nil
}
ms := store.SnapshotStatementTimeout(ctx).Milliseconds()
if ms < 1 {
ms = 1
}
if _, err := tx.Exec(ctx, `SELECT set_config('statement_timeout', $1, true)`, strconv.FormatInt(ms, 10)); err != nil {
return fmt.Errorf("repository: set snapshot statement timeout: %w", err)
}
return nil
}
func moduleSnapshotAdvisoryKey(tenantID, moduleID string) int64 {
h := fnv.New64a()
_, _ = h.Write([]byte("evobgp-module-snapshot-v1\x00"))
_, _ = h.Write([]byte(strings.TrimSpace(tenantID)))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(strings.TrimSpace(moduleID)))
return int64(h.Sum64())
}