package postgres import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "github.com/jackc/pgx/v5" "knowledge-governance/internal/jobs" "knowledge-governance/internal/versionhistory" "regexp" "strings" "time" ) func (s *Store) EnqueueVersionHistorySync(ctx context.Context, repo, actor, target string, now time.Time) (*jobs.Job, error) { if strings.Count(repo, "/") != 1 || strings.TrimSpace(repo) != repo || len(repo) > 200 || actor == "" || len(actor) > 256 || target != "" && !regexp.MustCompile(`^[a-f0-9]{40}$`).MatchString(target) { return nil, errors.New("invalid version synchronization request") } tx, e := s.db.Begin(ctx) if e != nil { return nil, e } defer tx.Rollback(context.Background()) if _, e = tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1,0))`, "version-history:"+repo); e != nil { return nil, e } // Retire exhausted expired owners. Terminal jobs are history, not a perpetual // idempotency key: a later refresh must be allowed to execute again. if _, e = tx.Exec(ctx, `UPDATE jobs SET state='failed',lease_owner=NULL,lease_until=NULL,updated_at=$3 WHERE type=$1 AND payload->>'repo_key'=$2 AND state='running' AND lease_until=max_attempts`, versionhistory.JobType, repo, now); e != nil { return nil, e } var active string e = tx.QueryRow(ctx, `SELECT id FROM jobs WHERE type=$1 AND payload->>'repo_key'=$2 AND state IN ('pending','running','retry') ORDER BY created_at DESC,id DESC LIMIT 1`, versionhistory.JobType, repo).Scan(&active) if e != nil && !errors.Is(e, pgx.ErrNoRows) { return nil, e } if active != "" { job, e := scanJob(tx.QueryRow(ctx, `SELECT id,type,payload,state,priority,available_at,lease_owner,lease_until,attempt,max_attempts,last_error,idempotency_key,created_at,updated_at FROM jobs WHERE id=$1`, active)) if e != nil { return nil, e } if e = tx.Commit(ctx); e != nil { return nil, e } return job, nil } var random [16]byte if _, e = rand.Read(random[:]); e != nil { return nil, e } id := "vh-" + hex.EncodeToString(random[:]) payload, _ := json.Marshal(map[string]string{"repo_key": repo, "triggered_by": actor, "target_sha": target}) job, e := NewStore(tx).Enqueue(ctx, jobs.EnqueueRequest{ID: id, Type: versionhistory.JobType, Payload: payload, AvailableAt: now, MaxAttempts: 3, IdempotencyKey: "version-history:" + repo + ":" + id}, now) if e != nil { return nil, e } if e = tx.Commit(ctx); e != nil { return nil, e } return job, nil }