This commit is contained in:
2026-06-20 19:16:44 +08:00
parent db3d030169
commit dbb87823e8
26 changed files with 676 additions and 115 deletions
+38 -13
View File
@@ -22,9 +22,13 @@ import (
type Repository interface {
Enqueue(ctx context.Context, typ JobType, payload json.RawMessage, scheduledAt time.Time, maxAttempts int32) (*Job, error)
Lease(ctx context.Context, leasedBy string) (*Job, error)
Complete(ctx context.Context, id uuid.UUID) error
FailWithRetry(ctx context.Context, id uuid.UUID, scheduledAt time.Time, lastErr string) error
DeadLetter(ctx context.Context, id uuid.UUID, lastErr string) error
// Complete, FailWithRetry and DeadLetter take the leasing worker id and apply
// a fencing predicate (id + leased_by + status='running'); if the lease has
// been lost (e.g. the reaper re-leased the job) they return ErrLeaseLost so
// the caller drops the write instead of clobbering the new run's state.
Complete(ctx context.Context, id uuid.UUID, leasedBy string) error
FailWithRetry(ctx context.Context, id uuid.UUID, leasedBy string, scheduledAt time.Time, lastErr string) error
DeadLetter(ctx context.Context, id uuid.UUID, leasedBy string, lastErr string) error
Get(ctx context.Context, id uuid.UUID) (*Job, error)
ReapStuckRunningJobs(ctx context.Context, leasedBefore time.Time) ([]uuid.UUID, error)
PurgeCompleted(ctx context.Context, completedBefore time.Time) (int64, error)
@@ -90,11 +94,20 @@ func (r *PgRepository) Lease(ctx context.Context, leasedBy string) (*Job, error)
return rowToJob(row), nil
}
// Complete marks a job as completed and clears its lease/error fields.
func (r *PgRepository) Complete(ctx context.Context, id uuid.UUID) error {
if err := r.q.CompleteJob(ctx, toPgUUID(id)); err != nil {
// Complete marks a job as completed and clears its lease/error fields. The
// update is fenced on the leasing worker id and status='running'; if no row
// matches (lease lost to the reaper) it returns ErrLeaseLost as a no-op signal.
func (r *PgRepository) Complete(ctx context.Context, id uuid.UUID, leasedBy string) error {
n, err := r.q.CompleteJob(ctx, jobssqlc.CompleteJobParams{
ID: toPgUUID(id),
LeasedBy: &leasedBy,
})
if err != nil {
return errs.Wrap(err, errs.CodeInternal, "complete job")
}
if n == 0 {
return ErrLeaseLost
}
return nil
}
@@ -102,25 +115,37 @@ func (r *PgRepository) Complete(ctx context.Context, id uuid.UUID) error {
// the last error. Attempts is preserved (it was incremented on lease) so the
// worker can compare against MaxAttempts to decide whether to retry or
// dead-letter on the next failure.
func (r *PgRepository) FailWithRetry(ctx context.Context, id uuid.UUID, scheduledAt time.Time, lastErr string) error {
if err := r.q.FailJobWithRetry(ctx, jobssqlc.FailJobWithRetryParams{
func (r *PgRepository) FailWithRetry(ctx context.Context, id uuid.UUID, leasedBy string, scheduledAt time.Time, lastErr string) error {
n, err := r.q.FailJobWithRetry(ctx, jobssqlc.FailJobWithRetryParams{
ID: toPgUUID(id),
ScheduledAt: pgtype.Timestamptz{Time: scheduledAt, Valid: true},
LastError: &lastErr,
}); err != nil {
LeasedBy: &leasedBy,
})
if err != nil {
return errs.Wrap(err, errs.CodeInternal, "fail job with retry")
}
if n == 0 {
return ErrLeaseLost
}
return nil
}
// DeadLetter terminally marks a job as dead and stores the last error.
func (r *PgRepository) DeadLetter(ctx context.Context, id uuid.UUID, lastErr string) error {
if err := r.q.DeadLetterJob(ctx, jobssqlc.DeadLetterJobParams{
// DeadLetter terminally marks a job as dead and stores the last error. Fenced
// on the leasing worker id and status='running'; returns ErrLeaseLost on a
// 0-row match so a stale worker does not overwrite a re-leased run.
func (r *PgRepository) DeadLetter(ctx context.Context, id uuid.UUID, leasedBy string, lastErr string) error {
n, err := r.q.DeadLetterJob(ctx, jobssqlc.DeadLetterJobParams{
ID: toPgUUID(id),
LastError: &lastErr,
}); err != nil {
LeasedBy: &leasedBy,
})
if err != nil {
return errs.Wrap(err, errs.CodeInternal, "dead-letter job")
}
if n == 0 {
return ErrLeaseLost
}
return nil
}