feat(jobs): add durable PG queue reclaim, slog, and richer metrics
JSON slog в ключевых пакетах; Prometheus path_group, job_audit_depth, upstream breaker; job_audit ClaimQueued/ReclaimStaleRunning + Adopt loop для HA после рестарта. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// ClaimedJob is a durable job_audit row claimed for in-process execution (SKIP LOCKED).
|
||||
type ClaimedJob struct {
|
||||
ID string
|
||||
TenantID string
|
||||
Kind string
|
||||
IdempotencyKey *string
|
||||
ModuleID *string
|
||||
Meta map[string]any
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// ReclaimStaleRunning resets orphaned running jobs older than staleAfter back to queued.
|
||||
func (w *JobAuditWriter) ReclaimStaleRunning(ctx context.Context, staleAfter time.Duration) (int64, error) {
|
||||
if w == nil || w.pool == nil {
|
||||
return 0, nil
|
||||
}
|
||||
if staleAfter <= 0 {
|
||||
staleAfter = 15 * time.Minute
|
||||
}
|
||||
tag, err := w.pool.Exec(ctx, `
|
||||
UPDATE job_audit
|
||||
SET status = 'queued', started_at = NULL, error_message = NULL
|
||||
WHERE status = 'running'
|
||||
AND started_at IS NOT NULL
|
||||
AND started_at < now() - $1::interval`,
|
||||
staleAfter.String())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return tag.RowsAffected(), nil
|
||||
}
|
||||
|
||||
// ClaimQueued claims up to limit queued job_audit rows via FOR UPDATE SKIP LOCKED and marks them running.
|
||||
// Only rows older than grace are claimed so the originating process can own fresh enqueues.
|
||||
func (w *JobAuditWriter) ClaimQueued(ctx context.Context, limit int, grace time.Duration) ([]ClaimedJob, error) {
|
||||
if w == nil || w.pool == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 8
|
||||
}
|
||||
if grace <= 0 {
|
||||
grace = 30 * time.Second
|
||||
}
|
||||
tx, err := w.pool.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
rows, err := tx.Query(ctx, `
|
||||
WITH cte AS (
|
||||
SELECT id
|
||||
FROM job_audit
|
||||
WHERE status = 'queued'
|
||||
AND created_at < now() - $2::interval
|
||||
ORDER BY created_at ASC
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT $1
|
||||
)
|
||||
UPDATE job_audit j
|
||||
SET status = 'running', started_at = COALESCE(j.started_at, now())
|
||||
FROM cte
|
||||
WHERE j.id = cte.id
|
||||
RETURNING j.id::text, j.tenant_id::text, j.kind, j.idempotency_key, j.module_id::text, j.meta_json, j.created_at`,
|
||||
limit, grace.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []ClaimedJob
|
||||
for rows.Next() {
|
||||
var c ClaimedJob
|
||||
var idem, mod *string
|
||||
var metaBytes []byte
|
||||
if err := rows.Scan(&c.ID, &c.TenantID, &c.Kind, &idem, &mod, &metaBytes, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.IdempotencyKey = idem
|
||||
if mod != nil && *mod != "" {
|
||||
c.ModuleID = mod
|
||||
}
|
||||
meta := map[string]any{}
|
||||
if len(metaBytes) > 0 {
|
||||
_ = json.Unmarshal(metaBytes, &meta)
|
||||
}
|
||||
c.Meta = meta
|
||||
out = append(out, c)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CountByStatus returns job_audit row counts grouped by status (best-effort metrics).
|
||||
func (w *JobAuditWriter) CountByStatus(ctx context.Context) (map[string]int64, error) {
|
||||
out := map[string]int64{}
|
||||
if w == nil || w.pool == nil {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := w.pool.Query(ctx, `SELECT status, count(*) FROM job_audit GROUP BY status`)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var status string
|
||||
var n int64
|
||||
if err := rows.Scan(&status, &n); err != nil {
|
||||
return out, err
|
||||
}
|
||||
out[status] = n
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user