Skip to content

Commit 1e15209

Browse files
authored
Fix dies_at attribute (#57)
* Fix dies_at * Updated to dies_at and concurrency * Updated to limit concurrency
1 parent 396c95d commit 1e15209

6 files changed

Lines changed: 34 additions & 37 deletions

File tree

pgqueue/manager/exec.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"os"
8+
"runtime/debug"
79
"strings"
810
"sync"
911
"time"
@@ -150,7 +152,7 @@ func (exec *exec) RunQueueTask(ctx context.Context, task *schema.Task, result ch
150152
exec.RLock()
151153
defer exec.RUnlock()
152154

153-
if task.DiesAt.IsZero() {
155+
if task.DiesAt == nil {
154156
result <- &Result{Queue: task.Queue, Task: task, Error: httpresponse.ErrBadRequest.With("missing task deadline")}
155157
return
156158
}
@@ -165,7 +167,7 @@ func (exec *exec) RunQueueTask(ctx context.Context, task *schema.Task, result ch
165167
}
166168

167169
// Run the task function with the provided payload and deadline
168-
child, cancel := context.WithDeadline(ctx, task.DiesAt.UTC())
170+
child, cancel := context.WithDeadline(ctx, (*task.DiesAt).UTC())
169171
exec.wg.Go(func() {
170172
defer cancel()
171173

@@ -196,6 +198,7 @@ func run(ctx context.Context, fn schema.TaskFunc, payload json.RawMessage) (resp
196198
default:
197199
err = fmt.Errorf("panic: %v", value)
198200
}
201+
fmt.Fprintf(os.Stderr, "RunQueueTask panic: %v\n%s\n", err, debug.Stack())
199202
resp = types.Ptr(Result{Error: err})
200203
}
201204
}()

pgqueue/manager/exec_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func TestRunQueueTaskUsesTaskTTLDeadline(t *testing.T) {
9999
exec := NewExec(nil)
100100
results := make(chan *Result, 1)
101101
diesAt := time.Now().Add(2 * time.Second)
102-
task := &schema.Task{Id: 42, Queue: "queue_deadline", DiesAt: diesAt}
102+
task := &schema.Task{Id: 42, Queue: "queue_deadline", DiesAt: &diesAt}
103103

104104
require.NoError(t, exec.RegisterTask(task.Queue, func(ctx context.Context, _ json.RawMessage) (any, error) {
105105
deadline, ok := ctx.Deadline()

pgqueue/manager/run.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ func (manager *Manager) Run(ctx context.Context, log *slog.Logger) error {
8282
defer func() { endSpan(err) }()
8383

8484
// Process as many tasks as we have capacity for, until there are
85-
// no more tasks or an error occurs.
85+
// no more tasks or an error occurs. Cap at GOMAXPROCS per round so
86+
// we don't starve the event loop when concurrency=0 (unlimited).
8687
processed := false
8788
for i := 0; i < runtime.GOMAXPROCS(0); i++ {
8889
// Get next task for the queue
@@ -107,7 +108,7 @@ func (manager *Manager) Run(ctx context.Context, log *slog.Logger) error {
107108
processed = true
108109
}
109110

110-
// Return success
111+
// Return success — more tasks may still be waiting.
111112
return true, nil
112113
}
113114

@@ -176,6 +177,8 @@ func (manager *Manager) Run(ctx context.Context, log *slog.Logger) error {
176177
} else {
177178
log.InfoContext(resultCtx, "RunQueueTask result", "queue", result.Queue, "task", result.Task.Id, "status", status, "result", result)
178179
}
180+
// A slot just freed up — immediately try to pick up another task.
181+
queueTimer.Reset(0)
179182
continue
180183
case result != nil && result.Ticker != "":
181184
resultCtx := ctx

pgqueue/manager/task_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@ func TestCreateTask(t *testing.T) {
5050
assert.Equal(t, queue.Queue, task.Queue)
5151
assert.JSONEq(t, string(payload), string(task.Payload))
5252
assert.NotZero(t, task.Id)
53-
assert.False(t, task.DiesAt.IsZero())
53+
assert.Nil(t, task.DiesAt) // dies_at is only set when a worker locks the task
5454
}

pgqueue/schema/objects.sql

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,13 @@ CREATE TABLE IF NOT EXISTS ${"schema"}."task" (
4343
"delayed_at" TIMESTAMPTZ,
4444
"started_at" TIMESTAMPTZ,
4545
"finished_at" TIMESTAMPTZ,
46-
"dies_at" TIMESTAMPTZ NOT NULL,
46+
"dies_at" TIMESTAMPTZ,
4747
"retries" INTEGER NOT NULL CHECK ("retries" >= 0),
4848
"initial_retries" INTEGER NOT NULL CHECK ("initial_retries" >= 0),
4949
PRIMARY KEY ("id"),
5050
FOREIGN KEY ("queue") REFERENCES ${"schema"}."queue" ("queue") ON DELETE CASCADE
5151
) PARTITION BY RANGE (id);
5252

53-
UPDATE ${"schema"}."task" t
54-
SET "dies_at" = COALESCE(t."finished_at", t."delayed_at", t."created_at", NOW()) + q."ttl"
55-
FROM ${"schema"}."queue" q
56-
WHERE t."queue" = q."queue"
57-
AND t."dies_at" IS NULL;
58-
59-
ALTER TABLE ${"schema"}."task"
60-
ALTER COLUMN "dies_at" SET NOT NULL;
61-
6253
-- pgqueue.queue_status_index
6354
-- Covers 'new' and 'retry' states
6455
CREATE INDEX IF NOT EXISTS "task_queue_status_idx"
@@ -97,7 +88,7 @@ CREATE OR REPLACE FUNCTION ${"schema"}.queue_task_status(
9788
) RETURNS ${"schema"}.task_status AS $$
9889
SELECT CASE
9990
WHEN started_at IS NOT NULL AND finished_at IS NOT NULL THEN 'done'::${"schema"}.task_status
100-
WHEN dies_at < NOW() THEN 'expired'::${"schema"}.task_status
91+
WHEN started_at IS NOT NULL AND finished_at IS NULL AND dies_at IS NOT NULL AND dies_at < NOW() THEN 'expired'::${"schema"}.task_status
10192
WHEN started_at IS NULL AND finished_at IS NULL AND retries = 0 THEN 'failed'::${"schema"}.task_status
10293
WHEN started_at IS NULL AND finished_at IS NULL AND retries = initial_retries
10394
THEN 'new'::${"schema"}.task_status
@@ -111,30 +102,26 @@ $$ LANGUAGE SQL STABLE;
111102
CREATE OR REPLACE FUNCTION ${"schema"}.queue_insert(q TEXT, p JSONB, delayed_at TIMESTAMPTZ) RETURNS BIGINT AS $$
112103
DECLARE
113104
v_retries INTEGER;
114-
v_dies_at TIMESTAMPTZ;
115105
v_id BIGINT;
116106
BEGIN
117107
-- Get queue defaults, raise if queue doesn't exist
118108
SELECT
119-
"retries", CASE
120-
WHEN delayed_at IS NULL OR delayed_at < NOW() THEN NOW() + "ttl"
121-
ELSE delayed_at + "ttl"
122-
END
109+
"retries"
123110
INTO STRICT
124-
v_retries, v_dies_at
111+
v_retries
125112
FROM
126113
${"schema"}."queue"
127114
WHERE
128115
"queue" = q;
129116

130117
-- Insert the task
131-
INSERT INTO ${"schema"}."task" ("queue", "payload", "delayed_at", "retries", "initial_retries", "dies_at")
118+
INSERT INTO ${"schema"}."task" ("queue", "payload", "delayed_at", "retries", "initial_retries")
132119
VALUES (
133120
q, p, CASE
134121
WHEN delayed_at IS NULL THEN NULL
135122
WHEN delayed_at < NOW() THEN NOW()
136123
ELSE delayed_at
137-
END, v_retries, v_retries, v_dies_at
124+
END, v_retries, v_retries
138125
)
139126
RETURNING "id" INTO v_id;
140127

@@ -163,13 +150,10 @@ CREATE OR REPLACE TRIGGER queue_insert_trigger
163150

164151
-- pgqueue.queue_lock_func
165152
CREATE OR REPLACE FUNCTION ${"schema"}.queue_lock(q TEXT[], w TEXT) RETURNS BIGINT AS $$
166-
UPDATE ${"schema"}."task" SET
167-
"started_at" = NOW(),
168-
"worker" = w,
169-
"result" = 'null'
170-
WHERE "id" = (
153+
WITH selected AS (
171154
SELECT
172-
t."id"
155+
t."id",
156+
queue."ttl"
173157
FROM
174158
${"schema"}."task" t
175159
JOIN
@@ -195,8 +179,6 @@ WHERE "id" = (
195179
(CARDINALITY(q) = 0 OR t."queue" = ANY(q))
196180
AND
197181
(t."started_at" IS NULL AND t."finished_at" IS NULL)
198-
AND
199-
t."dies_at" > NOW()
200182
AND
201183
(t."delayed_at" IS NULL OR t."delayed_at" <= NOW())
202184
AND
@@ -207,14 +189,22 @@ WHERE "id" = (
207189
t."created_at"
208190
FOR UPDATE OF t SKIP LOCKED LIMIT 1
209191
)
210-
RETURNING "id";
192+
UPDATE ${"schema"}."task" SET
193+
"started_at" = NOW(),
194+
"worker" = w,
195+
"result" = 'null',
196+
"dies_at" = NOW() + selected."ttl"
197+
FROM selected
198+
WHERE ${"schema"}."task"."id" = selected."id"
199+
RETURNING ${"schema"}."task"."id";
211200
$$ LANGUAGE SQL;
212201

213202
-- pgqueue.queue_unlock_func
214203
CREATE OR REPLACE FUNCTION ${"schema"}.queue_unlock(tid BIGINT, r JSONB) RETURNS BIGINT AS $$
215204
UPDATE ${"schema"}."task" SET
216205
"finished_at" = NOW(),
217-
"result" = r
206+
"result" = r,
207+
"dies_at" = NULL
218208
WHERE
219209
"id" = tid
220210
AND
@@ -246,7 +236,8 @@ CREATE OR REPLACE FUNCTION ${"schema"}.queue_fail(tid BIGINT, r JSONB) RETURNS B
246236
"result" = r,
247237
"started_at" = NULL,
248238
"finished_at" = NULL,
249-
"delayed_at" = ${"schema"}.queue_backoff(tid)
239+
"delayed_at" = ${"schema"}.queue_backoff(tid),
240+
"dies_at" = NULL
250241
WHERE
251242
"id" = tid
252243
AND

pgqueue/schema/task.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type Task struct {
4242
CreatedAt *time.Time `json:"created_at,omitempty"`
4343
StartedAt *time.Time `json:"started_at,omitempty"`
4444
FinishedAt *time.Time `json:"finished_at,omitempty"`
45-
DiesAt time.Time `json:"dies_at,omitempty"`
45+
DiesAt *time.Time `json:"dies_at,omitempty"`
4646
Retries *uint64 `json:"retries,omitempty"`
4747
}
4848

0 commit comments

Comments
 (0)