Files
DenozordecandCursor 37f28dfcfd
CI / changes (push) Successful in 5s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 57s
CI / web (push) Successful in 55s
CI / go (push) Successful in 1m14s
CI / bird2 (push) Successful in 14s
CI / release (push) Successful in 4m7s
chore(tsconfig): remove baseUrl from TypeScript configuration files
Removed the "baseUrl" property from tsconfig.base.json, apps/web/tsconfig.json, and packages/ui/tsconfig.json to streamline path resolution. Updated check-openapi-gen.sh to use 'sh' instead of 'bash' for improved compatibility and adjusted the script's error handling.

Co-authored-by: Cursor <[email protected]>
2026-07-31 12:44:31 +07:00

131 lines
3.2 KiB
Go

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 func() { _ = 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()
}