@pgpm/database-jobs 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Makefile +1 -1
- package/deploy/schemas/app_jobs/procedures/add_job.sql +38 -34
- package/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql +31 -21
- package/deploy/schemas/app_jobs/procedures/force_unlock_workers.sql +20 -0
- package/deploy/schemas/app_jobs/procedures/get_job.sql +27 -51
- package/deploy/schemas/app_jobs/procedures/remove_job.sql +34 -0
- package/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql +2 -0
- package/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +11 -2
- package/deploy/schemas/app_jobs/tables/jobs/table.sql +9 -5
- package/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +15 -5
- package/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql +7 -5
- package/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql +1 -1
- package/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql +2 -2
- package/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql +2 -2
- package/package.json +5 -4
- package/pgpm-database-jobs.control +2 -2
- package/pgpm.plan +4 -2
- package/revert/schemas/app_jobs/procedures/force_unlock_workers.sql +7 -0
- package/revert/schemas/app_jobs/procedures/remove_job.sql +7 -0
- package/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +3 -1
- package/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +3 -2
- package/sql/{pgpm-database-jobs--0.15.3.sql → pgpm-database-jobs--0.22.0.sql} +201 -124
- package/verify/schemas/app_jobs/procedures/force_unlock_workers.sql +7 -0
- package/verify/schemas/app_jobs/procedures/remove_job.sql +7 -0
- package/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +2 -1
- package/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +1 -2
package/Makefile
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
-- requires: schemas/app_jobs/schema
|
|
3
3
|
-- requires: schemas/app_jobs/tables/jobs/table
|
|
4
4
|
-- requires: schemas/app_jobs/tables/job_queues/table
|
|
5
|
+
-- requires: pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id
|
|
6
|
+
-- requires: pgpm-jwt-claims:schemas/jwt_public/procedures/current_user_id
|
|
5
7
|
|
|
6
8
|
BEGIN;
|
|
7
9
|
CREATE FUNCTION app_jobs.add_job (
|
|
8
|
-
db_id uuid,
|
|
9
10
|
identifier text,
|
|
10
11
|
payload json DEFAULT '{}' ::json,
|
|
11
12
|
job_key text DEFAULT NULL,
|
|
@@ -18,14 +19,18 @@ CREATE FUNCTION app_jobs.add_job (
|
|
|
18
19
|
AS $$
|
|
19
20
|
DECLARE
|
|
20
21
|
v_job app_jobs.jobs;
|
|
22
|
+
v_database_id uuid;
|
|
23
|
+
v_actor_id uuid;
|
|
21
24
|
BEGIN
|
|
22
|
-
--
|
|
23
|
-
|
|
25
|
+
-- Read context from JWT claims
|
|
26
|
+
v_database_id := jwt_private.current_database_id();
|
|
27
|
+
v_actor_id := jwt_public.current_user_id();
|
|
24
28
|
|
|
25
29
|
IF job_key IS NOT NULL THEN
|
|
26
|
-
-- Upsert job
|
|
30
|
+
-- Upsert job
|
|
27
31
|
INSERT INTO app_jobs.jobs (
|
|
28
32
|
database_id,
|
|
33
|
+
actor_id,
|
|
29
34
|
task_identifier,
|
|
30
35
|
payload,
|
|
31
36
|
queue_name,
|
|
@@ -34,52 +39,51 @@ BEGIN
|
|
|
34
39
|
key,
|
|
35
40
|
priority
|
|
36
41
|
) VALUES (
|
|
37
|
-
|
|
42
|
+
v_database_id,
|
|
43
|
+
v_actor_id,
|
|
38
44
|
identifier,
|
|
39
|
-
coalesce(payload,
|
|
40
|
-
'{}'::json),
|
|
45
|
+
coalesce(payload, '{}'::json),
|
|
41
46
|
queue_name,
|
|
42
47
|
coalesce(run_at, now()),
|
|
43
48
|
coalesce(max_attempts, 25),
|
|
44
49
|
job_key,
|
|
45
50
|
coalesce(priority, 0)
|
|
46
|
-
)
|
|
47
|
-
ON CONFLICT (key)
|
|
48
|
-
DO UPDATE SET
|
|
51
|
+
)
|
|
52
|
+
ON CONFLICT (key)
|
|
53
|
+
DO UPDATE SET
|
|
49
54
|
task_identifier = EXCLUDED.task_identifier,
|
|
50
55
|
payload = EXCLUDED.payload,
|
|
51
56
|
queue_name = EXCLUDED.queue_name,
|
|
52
57
|
max_attempts = EXCLUDED.max_attempts,
|
|
53
58
|
run_at = EXCLUDED.run_at,
|
|
54
|
-
priority = EXCLUDED.priority,
|
|
55
|
-
-- always reset error/retry state
|
|
56
|
-
attempts = 0, last_error = NULL
|
|
57
|
-
WHERE
|
|
58
|
-
jobs.locked_at IS NULL
|
|
59
|
-
RETURNING
|
|
60
|
-
* INTO v_job;
|
|
59
|
+
priority = EXCLUDED.priority,
|
|
60
|
+
-- always reset error/retry state
|
|
61
|
+
attempts = 0, last_error = NULL
|
|
62
|
+
WHERE
|
|
63
|
+
jobs.locked_at IS NULL
|
|
64
|
+
RETURNING
|
|
65
|
+
* INTO v_job;
|
|
61
66
|
|
|
62
|
-
-- If upsert succeeded (insert or update), return early
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
END IF;
|
|
67
|
+
-- If upsert succeeded (insert or update), return early
|
|
68
|
+
IF NOT (v_job IS NULL) THEN
|
|
69
|
+
RETURN v_job;
|
|
70
|
+
END IF;
|
|
67
71
|
|
|
68
|
-
-- Upsert failed -> there must be an existing job that is locked. Remove
|
|
69
|
-
-- existing key to allow a new one to be inserted, and prevent any
|
|
72
|
+
-- Upsert failed -> there must be an existing job that is locked. Remove
|
|
73
|
+
-- existing key to allow a new one to be inserted, and prevent any
|
|
70
74
|
-- subsequent retries by bumping attempts to the max allowed.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
KEY = job_key;
|
|
75
|
+
UPDATE
|
|
76
|
+
app_jobs.jobs
|
|
77
|
+
SET
|
|
78
|
+
key = NULL,
|
|
79
|
+
attempts = jobs.max_attempts
|
|
80
|
+
WHERE
|
|
81
|
+
key = job_key;
|
|
79
82
|
END IF;
|
|
80
83
|
|
|
81
84
|
INSERT INTO app_jobs.jobs (
|
|
82
85
|
database_id,
|
|
86
|
+
actor_id,
|
|
83
87
|
task_identifier,
|
|
84
88
|
payload,
|
|
85
89
|
queue_name,
|
|
@@ -87,7 +91,8 @@ BEGIN
|
|
|
87
91
|
max_attempts,
|
|
88
92
|
priority
|
|
89
93
|
) VALUES (
|
|
90
|
-
|
|
94
|
+
v_database_id,
|
|
95
|
+
v_actor_id,
|
|
91
96
|
identifier,
|
|
92
97
|
payload,
|
|
93
98
|
queue_name,
|
|
@@ -103,4 +108,3 @@ $$
|
|
|
103
108
|
LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER;
|
|
104
109
|
|
|
105
110
|
COMMIT;
|
|
106
|
-
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
-- requires: schemas/app_jobs/schema
|
|
4
4
|
-- requires: schemas/app_jobs/tables/scheduled_jobs/table
|
|
5
|
+
-- requires: pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id
|
|
6
|
+
-- requires: pgpm-jwt-claims:schemas/jwt_public/procedures/current_user_id
|
|
5
7
|
|
|
6
8
|
BEGIN;
|
|
7
9
|
|
|
8
10
|
CREATE FUNCTION app_jobs.add_scheduled_job(
|
|
9
|
-
db_id uuid,
|
|
10
11
|
identifier text,
|
|
11
12
|
payload json DEFAULT '{}'::json,
|
|
12
13
|
schedule_info json DEFAULT '{}'::json,
|
|
@@ -19,12 +20,18 @@ CREATE FUNCTION app_jobs.add_scheduled_job(
|
|
|
19
20
|
AS $$
|
|
20
21
|
DECLARE
|
|
21
22
|
v_job app_jobs.scheduled_jobs;
|
|
23
|
+
v_database_id uuid;
|
|
24
|
+
v_actor_id uuid;
|
|
22
25
|
BEGIN
|
|
26
|
+
v_database_id := jwt_private.current_database_id();
|
|
27
|
+
v_actor_id := jwt_public.current_user_id();
|
|
28
|
+
|
|
23
29
|
IF job_key IS NOT NULL THEN
|
|
24
30
|
|
|
25
|
-
-- Upsert job
|
|
31
|
+
-- Upsert job
|
|
26
32
|
INSERT INTO app_jobs.scheduled_jobs (
|
|
27
33
|
database_id,
|
|
34
|
+
actor_id,
|
|
28
35
|
task_identifier,
|
|
29
36
|
payload,
|
|
30
37
|
queue_name,
|
|
@@ -33,7 +40,8 @@ BEGIN
|
|
|
33
40
|
key,
|
|
34
41
|
priority
|
|
35
42
|
) VALUES (
|
|
36
|
-
|
|
43
|
+
v_database_id,
|
|
44
|
+
v_actor_id,
|
|
37
45
|
identifier,
|
|
38
46
|
coalesce(payload, '{}'::json),
|
|
39
47
|
queue_name,
|
|
@@ -41,37 +49,38 @@ BEGIN
|
|
|
41
49
|
coalesce(max_attempts, 25),
|
|
42
50
|
job_key,
|
|
43
51
|
coalesce(priority, 0)
|
|
44
|
-
)
|
|
45
|
-
ON CONFLICT (key)
|
|
46
|
-
DO UPDATE SET
|
|
52
|
+
)
|
|
53
|
+
ON CONFLICT (key)
|
|
54
|
+
DO UPDATE SET
|
|
47
55
|
task_identifier = EXCLUDED.task_identifier,
|
|
48
56
|
payload = EXCLUDED.payload,
|
|
49
57
|
queue_name = EXCLUDED.queue_name,
|
|
50
58
|
max_attempts = EXCLUDED.max_attempts,
|
|
51
59
|
schedule_info = EXCLUDED.schedule_info,
|
|
52
60
|
priority = EXCLUDED.priority
|
|
53
|
-
WHERE
|
|
54
|
-
scheduled_jobs.locked_at IS NULL
|
|
55
|
-
RETURNING
|
|
56
|
-
* INTO v_job;
|
|
61
|
+
WHERE
|
|
62
|
+
scheduled_jobs.locked_at IS NULL
|
|
63
|
+
RETURNING
|
|
64
|
+
* INTO v_job;
|
|
65
|
+
|
|
66
|
+
-- If upsert succeeded (insert or update), return early
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
IF
|
|
61
|
-
RETURN v_job;
|
|
62
|
-
END IF;
|
|
68
|
+
IF NOT (v_job IS NULL) THEN
|
|
69
|
+
RETURN v_job;
|
|
70
|
+
END IF;
|
|
63
71
|
|
|
64
|
-
-- Upsert failed -> there must be an existing scheduled job that is locked. Remove
|
|
72
|
+
-- Upsert failed -> there must be an existing scheduled job that is locked. Remove
|
|
65
73
|
-- and allow a new one to be inserted
|
|
66
74
|
|
|
67
|
-
DELETE FROM
|
|
68
|
-
app_jobs.scheduled_jobs
|
|
69
|
-
WHERE
|
|
70
|
-
KEY = job_key;
|
|
75
|
+
DELETE FROM
|
|
76
|
+
app_jobs.scheduled_jobs
|
|
77
|
+
WHERE
|
|
78
|
+
KEY = job_key;
|
|
71
79
|
END IF;
|
|
72
80
|
|
|
73
81
|
INSERT INTO app_jobs.scheduled_jobs (
|
|
74
82
|
database_id,
|
|
83
|
+
actor_id,
|
|
75
84
|
task_identifier,
|
|
76
85
|
payload,
|
|
77
86
|
queue_name,
|
|
@@ -79,7 +88,8 @@ BEGIN
|
|
|
79
88
|
max_attempts,
|
|
80
89
|
priority
|
|
81
90
|
) VALUES (
|
|
82
|
-
|
|
91
|
+
v_database_id,
|
|
92
|
+
v_actor_id,
|
|
83
93
|
identifier,
|
|
84
94
|
payload,
|
|
85
95
|
queue_name,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Deploy schemas/app_jobs/procedures/force_unlock_workers to pg
|
|
2
|
+
-- requires: schemas/app_jobs/schema
|
|
3
|
+
-- requires: schemas/app_jobs/tables/jobs/table
|
|
4
|
+
-- requires: schemas/app_jobs/tables/job_queues/table
|
|
5
|
+
|
|
6
|
+
BEGIN;
|
|
7
|
+
CREATE FUNCTION app_jobs.force_unlock_workers (worker_ids text[])
|
|
8
|
+
RETURNS void
|
|
9
|
+
LANGUAGE sql
|
|
10
|
+
VOLATILE
|
|
11
|
+
AS $$
|
|
12
|
+
UPDATE app_jobs.jobs
|
|
13
|
+
SET locked_at = NULL, locked_by = NULL
|
|
14
|
+
WHERE locked_by = ANY (worker_ids);
|
|
15
|
+
|
|
16
|
+
UPDATE app_jobs.job_queues
|
|
17
|
+
SET locked_at = NULL, locked_by = NULL
|
|
18
|
+
WHERE locked_by = ANY (worker_ids);
|
|
19
|
+
$$;
|
|
20
|
+
COMMIT;
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
-- requires: schemas/app_jobs/tables/jobs/table
|
|
5
5
|
|
|
6
6
|
BEGIN;
|
|
7
|
-
CREATE FUNCTION app_jobs.get_job (
|
|
7
|
+
CREATE FUNCTION app_jobs.get_job (
|
|
8
|
+
worker_id text,
|
|
9
|
+
task_identifiers text[] DEFAULT NULL,
|
|
10
|
+
job_expiry interval DEFAULT '4 hours'
|
|
11
|
+
)
|
|
8
12
|
RETURNS app_jobs.jobs
|
|
9
13
|
LANGUAGE plpgsql
|
|
10
14
|
AS $$
|
|
@@ -14,79 +18,51 @@ DECLARE
|
|
|
14
18
|
v_row app_jobs.jobs;
|
|
15
19
|
v_now timestamptz = now();
|
|
16
20
|
BEGIN
|
|
17
|
-
|
|
18
21
|
IF worker_id IS NULL THEN
|
|
19
|
-
RAISE
|
|
22
|
+
RAISE EXCEPTION 'INVALID_WORKER_ID';
|
|
20
23
|
END IF;
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
jobs.
|
|
27
|
-
|
|
28
|
-
FROM
|
|
29
|
-
app_jobs.jobs
|
|
30
|
-
WHERE (jobs.locked_at IS NULL
|
|
31
|
-
OR jobs.locked_at < (v_now - job_expiry))
|
|
25
|
+
SELECT jobs.queue_name, jobs.id
|
|
26
|
+
INTO v_queue_name, v_job_id
|
|
27
|
+
FROM app_jobs.jobs
|
|
28
|
+
WHERE is_available = true
|
|
29
|
+
AND (jobs.locked_at IS NULL
|
|
30
|
+
OR jobs.locked_at < (v_now - job_expiry))
|
|
32
31
|
AND (jobs.queue_name IS NULL
|
|
33
|
-
OR
|
|
34
|
-
SELECT
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
AND (job_queues.locked_at IS NULL
|
|
41
|
-
OR job_queues.locked_at < (v_now - job_expiry))
|
|
42
|
-
FOR UPDATE
|
|
43
|
-
SKIP LOCKED))
|
|
32
|
+
OR jobs.queue_name IN (
|
|
33
|
+
SELECT jq.queue_name
|
|
34
|
+
FROM app_jobs.job_queues jq
|
|
35
|
+
WHERE (jq.locked_at IS NULL
|
|
36
|
+
OR jq.locked_at < (v_now - job_expiry))
|
|
37
|
+
FOR UPDATE SKIP LOCKED
|
|
38
|
+
))
|
|
44
39
|
AND run_at <= v_now
|
|
45
40
|
AND attempts < max_attempts
|
|
46
41
|
AND (task_identifiers IS NULL
|
|
47
42
|
OR task_identifier = ANY (task_identifiers))
|
|
48
|
-
ORDER BY
|
|
49
|
-
priority ASC,
|
|
50
|
-
run_at ASC,
|
|
51
|
-
id ASC
|
|
43
|
+
ORDER BY priority ASC, run_at ASC, id ASC
|
|
52
44
|
LIMIT 1
|
|
53
|
-
FOR UPDATE
|
|
54
|
-
SKIP LOCKED;
|
|
55
|
-
|
|
56
|
-
--
|
|
45
|
+
FOR UPDATE SKIP LOCKED;
|
|
57
46
|
|
|
58
47
|
IF v_job_id IS NULL THEN
|
|
59
48
|
RETURN NULL;
|
|
60
49
|
END IF;
|
|
61
50
|
|
|
62
|
-
--
|
|
63
|
-
|
|
64
51
|
IF v_queue_name IS NOT NULL THEN
|
|
65
|
-
UPDATE
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
locked_by = worker_id,
|
|
69
|
-
locked_at = v_now
|
|
70
|
-
WHERE
|
|
71
|
-
job_queues.queue_name = v_queue_name;
|
|
52
|
+
UPDATE app_jobs.job_queues
|
|
53
|
+
SET locked_by = worker_id, locked_at = v_now
|
|
54
|
+
WHERE job_queues.queue_name = v_queue_name;
|
|
72
55
|
END IF;
|
|
73
56
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
UPDATE
|
|
77
|
-
app_jobs.jobs
|
|
57
|
+
UPDATE app_jobs.jobs
|
|
78
58
|
SET
|
|
79
59
|
attempts = attempts + 1,
|
|
80
60
|
locked_by = worker_id,
|
|
81
61
|
locked_at = v_now
|
|
82
|
-
WHERE
|
|
83
|
-
|
|
84
|
-
RETURNING
|
|
85
|
-
* INTO v_row;
|
|
62
|
+
WHERE id = v_job_id
|
|
63
|
+
RETURNING * INTO v_row;
|
|
86
64
|
|
|
87
|
-
--
|
|
88
65
|
RETURN v_row;
|
|
89
66
|
END;
|
|
90
67
|
$$;
|
|
91
68
|
COMMIT;
|
|
92
|
-
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- Deploy schemas/app_jobs/procedures/remove_job to pg
|
|
2
|
+
-- requires: schemas/app_jobs/schema
|
|
3
|
+
-- requires: schemas/app_jobs/tables/jobs/table
|
|
4
|
+
|
|
5
|
+
BEGIN;
|
|
6
|
+
CREATE FUNCTION app_jobs.remove_job (job_key text)
|
|
7
|
+
RETURNS app_jobs.jobs
|
|
8
|
+
LANGUAGE plpgsql
|
|
9
|
+
STRICT
|
|
10
|
+
AS $$
|
|
11
|
+
DECLARE
|
|
12
|
+
v_job app_jobs.jobs;
|
|
13
|
+
BEGIN
|
|
14
|
+
DELETE FROM app_jobs.jobs
|
|
15
|
+
WHERE key = job_key
|
|
16
|
+
AND (locked_at IS NULL
|
|
17
|
+
OR locked_at < NOW() - interval '4 hours')
|
|
18
|
+
RETURNING * INTO v_job;
|
|
19
|
+
|
|
20
|
+
IF NOT (v_job IS NULL) THEN
|
|
21
|
+
RETURN v_job;
|
|
22
|
+
END IF;
|
|
23
|
+
|
|
24
|
+
UPDATE app_jobs.jobs
|
|
25
|
+
SET
|
|
26
|
+
key = NULL,
|
|
27
|
+
attempts = jobs.max_attempts
|
|
28
|
+
WHERE key = job_key
|
|
29
|
+
RETURNING * INTO v_job;
|
|
30
|
+
|
|
31
|
+
RETURN v_job;
|
|
32
|
+
END;
|
|
33
|
+
$$;
|
|
34
|
+
COMMIT;
|
|
@@ -41,6 +41,7 @@ BEGIN
|
|
|
41
41
|
-- insert new job
|
|
42
42
|
INSERT INTO app_jobs.jobs (
|
|
43
43
|
database_id,
|
|
44
|
+
actor_id,
|
|
44
45
|
queue_name,
|
|
45
46
|
task_identifier,
|
|
46
47
|
payload,
|
|
@@ -49,6 +50,7 @@ BEGIN
|
|
|
49
50
|
key
|
|
50
51
|
) SELECT
|
|
51
52
|
database_id,
|
|
53
|
+
actor_id,
|
|
52
54
|
queue_name,
|
|
53
55
|
task_identifier,
|
|
54
56
|
payload,
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
-- requires: schemas/app_jobs/tables/jobs/table
|
|
4
4
|
|
|
5
5
|
BEGIN;
|
|
6
|
-
CREATE INDEX priority_run_at_id_idx ON app_jobs.jobs (priority, run_at, id);
|
|
7
|
-
COMMIT;
|
|
8
6
|
|
|
7
|
+
CREATE INDEX jobs_main_index
|
|
8
|
+
ON app_jobs.jobs USING btree (priority, run_at)
|
|
9
|
+
INCLUDE (id, queue_name)
|
|
10
|
+
WHERE (is_available = true);
|
|
11
|
+
|
|
12
|
+
CREATE INDEX jobs_no_queue_index
|
|
13
|
+
ON app_jobs.jobs USING btree (priority, run_at)
|
|
14
|
+
INCLUDE (id)
|
|
15
|
+
WHERE (is_available = true AND queue_name IS NULL);
|
|
16
|
+
|
|
17
|
+
COMMIT;
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
BEGIN;
|
|
5
5
|
CREATE TABLE app_jobs.jobs (
|
|
6
6
|
id bigserial PRIMARY KEY,
|
|
7
|
-
database_id uuid
|
|
8
|
-
|
|
7
|
+
database_id uuid,
|
|
8
|
+
actor_id uuid,
|
|
9
|
+
queue_name text DEFAULT NULL,
|
|
9
10
|
task_identifier text NOT NULL,
|
|
10
11
|
payload json DEFAULT '{}' ::json NOT NULL,
|
|
11
12
|
priority integer DEFAULT 0 NOT NULL,
|
|
@@ -16,17 +17,19 @@ CREATE TABLE app_jobs.jobs (
|
|
|
16
17
|
last_error text,
|
|
17
18
|
locked_at timestamptz,
|
|
18
19
|
locked_by text,
|
|
20
|
+
is_available boolean GENERATED ALWAYS AS ((locked_at IS NULL) AND (attempts < max_attempts)) STORED NOT NULL,
|
|
19
21
|
CHECK (length(key) < 513),
|
|
20
22
|
CHECK (length(task_identifier) < 127),
|
|
21
|
-
CHECK (max_attempts
|
|
23
|
+
CHECK (max_attempts >= 1),
|
|
22
24
|
CHECK (length(queue_name) < 127),
|
|
23
25
|
CHECK (length(locked_by) > 3),
|
|
24
26
|
UNIQUE (key)
|
|
25
27
|
);
|
|
26
28
|
|
|
27
|
-
COMMENT ON TABLE app_jobs.jobs IS 'Background job queue
|
|
29
|
+
COMMENT ON TABLE app_jobs.jobs IS 'Background job queue: each row is a pending or in-progress task, optionally scoped to a database';
|
|
28
30
|
COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier';
|
|
29
|
-
COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to
|
|
31
|
+
COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to (nullable for system-level jobs without tenant context)';
|
|
32
|
+
COMMENT ON COLUMN app_jobs.jobs.actor_id IS 'User who triggered this job, read from JWT claims at enqueue time';
|
|
30
33
|
COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control';
|
|
31
34
|
COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)';
|
|
32
35
|
COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler';
|
|
@@ -38,6 +41,7 @@ COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; preve
|
|
|
38
41
|
COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt';
|
|
39
42
|
COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing';
|
|
40
43
|
COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock';
|
|
44
|
+
COMMENT ON COLUMN app_jobs.jobs.is_available IS 'Generated column: true when job is unlocked and has remaining attempts';
|
|
41
45
|
|
|
42
46
|
COMMIT;
|
|
43
47
|
|
|
@@ -5,9 +5,19 @@
|
|
|
5
5
|
-- requires: schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count
|
|
6
6
|
|
|
7
7
|
BEGIN;
|
|
8
|
-
CREATE
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
CREATE FUNCTION app_jobs.tg_jobs__after_insert ()
|
|
9
|
+
RETURNS TRIGGER
|
|
10
|
+
AS $$
|
|
11
|
+
BEGIN
|
|
12
|
+
PERFORM
|
|
13
|
+
pg_notify('jobs:insert', '');
|
|
14
|
+
RETURN NULL;
|
|
15
|
+
END;
|
|
16
|
+
$$
|
|
17
|
+
LANGUAGE plpgsql;
|
|
13
18
|
|
|
19
|
+
CREATE TRIGGER _900_after_insert
|
|
20
|
+
AFTER INSERT ON app_jobs.jobs
|
|
21
|
+
FOR EACH STATEMENT
|
|
22
|
+
EXECUTE PROCEDURE app_jobs.tg_jobs__after_insert ();
|
|
23
|
+
COMMIT;
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
BEGIN;
|
|
5
5
|
CREATE TABLE app_jobs.scheduled_jobs (
|
|
6
6
|
id bigserial PRIMARY KEY,
|
|
7
|
-
database_id uuid
|
|
8
|
-
|
|
7
|
+
database_id uuid,
|
|
8
|
+
actor_id uuid,
|
|
9
|
+
queue_name text DEFAULT NULL,
|
|
9
10
|
task_identifier text NOT NULL,
|
|
10
11
|
payload json DEFAULT '{}' ::json NOT NULL,
|
|
11
12
|
priority integer DEFAULT 0 NOT NULL,
|
|
@@ -18,15 +19,16 @@ CREATE TABLE app_jobs.scheduled_jobs (
|
|
|
18
19
|
last_scheduled_id bigint,
|
|
19
20
|
CHECK (length(key) < 513),
|
|
20
21
|
CHECK (length(task_identifier) < 127),
|
|
21
|
-
CHECK (max_attempts
|
|
22
|
+
CHECK (max_attempts >= 1),
|
|
22
23
|
CHECK (length(queue_name) < 127),
|
|
23
24
|
CHECK (length(locked_by) > 3),
|
|
24
25
|
UNIQUE (key)
|
|
25
26
|
);
|
|
26
27
|
|
|
27
|
-
COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions
|
|
28
|
+
COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions: each row spawns jobs on a schedule, optionally scoped to a database';
|
|
28
29
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier';
|
|
29
|
-
COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to
|
|
30
|
+
COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to (nullable for system-level schedules without tenant context)';
|
|
31
|
+
COMMENT ON COLUMN app_jobs.scheduled_jobs.actor_id IS 'User who created this scheduled job, read from JWT claims at creation time';
|
|
30
32
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into';
|
|
31
33
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs';
|
|
32
34
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job';
|
|
@@ -34,7 +34,7 @@ BEGIN
|
|
|
34
34
|
END IF;
|
|
35
35
|
END LOOP;
|
|
36
36
|
PERFORM
|
|
37
|
-
app_jobs.add_job (
|
|
37
|
+
app_jobs.add_job (fn, app_jobs.json_build_object_apply (args));
|
|
38
38
|
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
|
|
39
39
|
RETURN NEW;
|
|
40
40
|
END IF;
|
|
@@ -8,12 +8,12 @@ CREATE FUNCTION app_jobs.tg_add_job_with_row ()
|
|
|
8
8
|
BEGIN
|
|
9
9
|
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
|
|
10
10
|
PERFORM
|
|
11
|
-
app_jobs.add_job (
|
|
11
|
+
app_jobs.add_job (TG_ARGV[0], to_json(NEW));
|
|
12
12
|
RETURN NEW;
|
|
13
13
|
END IF;
|
|
14
14
|
IF (TG_OP = 'DELETE') THEN
|
|
15
15
|
PERFORM
|
|
16
|
-
app_jobs.add_job (
|
|
16
|
+
app_jobs.add_job (TG_ARGV[0], to_json(OLD));
|
|
17
17
|
RETURN OLD;
|
|
18
18
|
END IF;
|
|
19
19
|
END;
|
|
@@ -9,12 +9,12 @@ CREATE FUNCTION app_jobs.tg_add_job_with_row_id ()
|
|
|
9
9
|
BEGIN
|
|
10
10
|
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
|
|
11
11
|
PERFORM
|
|
12
|
-
app_jobs.add_job (
|
|
12
|
+
app_jobs.add_job (tg_argv[0], json_build_object('id', NEW.id));
|
|
13
13
|
RETURN NEW;
|
|
14
14
|
END IF;
|
|
15
15
|
IF (TG_OP = 'DELETE') THEN
|
|
16
16
|
PERFORM
|
|
17
|
-
app_jobs.add_job (
|
|
17
|
+
app_jobs.add_job (tg_argv[0], json_build_object('id', OLD.id));
|
|
18
18
|
RETURN OLD;
|
|
19
19
|
END IF;
|
|
20
20
|
END;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpm/database-jobs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Database-specific job handling and queue management",
|
|
5
5
|
"author": "Dan Lynch <pyramation@gmail.com>",
|
|
6
6
|
"contributors": [
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
"test:watch": "jest --watch"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"pgpm": "^4.2
|
|
24
|
+
"pgpm": "^4.23.2"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@pgpm/
|
|
27
|
+
"@pgpm/jwt-claims": "0.22.0",
|
|
28
|
+
"@pgpm/verify": "0.22.0"
|
|
28
29
|
},
|
|
29
30
|
"repository": {
|
|
30
31
|
"type": "git",
|
|
@@ -34,5 +35,5 @@
|
|
|
34
35
|
"bugs": {
|
|
35
36
|
"url": "https://github.com/constructive-io/pgpm-modules/issues"
|
|
36
37
|
},
|
|
37
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "96ae0195a8b3a3dd056808c344f0e3c31a199e5e"
|
|
38
39
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pgpm-database-jobs extension
|
|
2
2
|
comment = 'pgpm-database-jobs extension'
|
|
3
|
-
default_version = '0.
|
|
3
|
+
default_version = '0.22.0'
|
|
4
4
|
module_pathname = '$libdir/pgpm-database-jobs'
|
|
5
|
-
requires = 'plpgsql,pgcrypto,pgpm-verify'
|
|
5
|
+
requires = 'plpgsql,pgcrypto,pgpm-verify,pgpm-jwt-claims'
|
|
6
6
|
relocatable = false
|
|
7
7
|
superuser = false
|
|
8
8
|
|
package/pgpm.plan
CHANGED
|
@@ -34,5 +34,7 @@ schemas/app_jobs/procedures/get_job [schemas/app_jobs/schema schemas/app_jobs/ta
|
|
|
34
34
|
schemas/app_jobs/procedures/fail_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm <pgpm@5b0c196eeb62> # add schemas/app_jobs/procedures/fail_job
|
|
35
35
|
schemas/app_jobs/procedures/complete_jobs [schemas/app_jobs/schema schemas/app_jobs/tables/job_queues/table schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm <pgpm@5b0c196eeb62> # add schemas/app_jobs/procedures/complete_jobs
|
|
36
36
|
schemas/app_jobs/procedures/complete_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm <pgpm@5b0c196eeb62> # add schemas/app_jobs/procedures/complete_job
|
|
37
|
-
schemas/app_jobs/procedures/add_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table] 2025-08-26T23:57:41Z pgpm <pgpm@5b0c196eeb62> # add schemas/app_jobs/procedures/add_scheduled_job
|
|
38
|
-
schemas/app_jobs/procedures/add_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm <pgpm@5b0c196eeb62> # add schemas/app_jobs/procedures/add_job
|
|
37
|
+
schemas/app_jobs/procedures/add_scheduled_job [schemas/app_jobs/schema schemas/app_jobs/tables/scheduled_jobs/table pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id] 2025-08-26T23:57:41Z pgpm <pgpm@5b0c196eeb62> # add schemas/app_jobs/procedures/add_scheduled_job
|
|
38
|
+
schemas/app_jobs/procedures/add_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table pgpm-jwt-claims:schemas/jwt_private/procedures/current_database_id pgpm-jwt-claims:schemas/jwt_public/procedures/current_user_id] 2025-08-26T23:57:41Z pgpm <pgpm@5b0c196eeb62> # add schemas/app_jobs/procedures/add_job
|
|
39
|
+
schemas/app_jobs/procedures/remove_job [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table] 2025-08-26T23:57:41Z pgpm <pgpm@5b0c196eeb62> # add schemas/app_jobs/procedures/remove_job
|
|
40
|
+
schemas/app_jobs/procedures/force_unlock_workers [schemas/app_jobs/schema schemas/app_jobs/tables/jobs/table schemas/app_jobs/tables/job_queues/table] 2025-08-26T23:57:41Z pgpm <pgpm@5b0c196eeb62> # add schemas/app_jobs/procedures/force_unlock_workers
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
-- Revert schemas/app_jobs/tables/jobs/triggers/notify_worker from pg
|
|
2
2
|
BEGIN;
|
|
3
|
-
DROP TRIGGER _900_notify_worker ON app_jobs.jobs;
|
|
3
|
+
DROP TRIGGER IF EXISTS _900_notify_worker ON app_jobs.jobs;
|
|
4
|
+
DROP TRIGGER IF EXISTS _900_after_insert ON app_jobs.jobs;
|
|
5
|
+
DROP FUNCTION IF EXISTS app_jobs.tg_jobs__after_insert;
|
|
4
6
|
COMMIT;
|
|
5
|
-
|
|
@@ -23,12 +23,12 @@ CREATE FUNCTION app_jobs.tg_add_job_with_row_id() RETURNS trigger AS $EOFCODE$
|
|
|
23
23
|
BEGIN
|
|
24
24
|
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
|
|
25
25
|
PERFORM
|
|
26
|
-
app_jobs.add_job (
|
|
26
|
+
app_jobs.add_job (tg_argv[0], json_build_object('id', NEW.id));
|
|
27
27
|
RETURN NEW;
|
|
28
28
|
END IF;
|
|
29
29
|
IF (TG_OP = 'DELETE') THEN
|
|
30
30
|
PERFORM
|
|
31
|
-
app_jobs.add_job (
|
|
31
|
+
app_jobs.add_job (tg_argv[0], json_build_object('id', OLD.id));
|
|
32
32
|
RETURN OLD;
|
|
33
33
|
END IF;
|
|
34
34
|
END;
|
|
@@ -40,12 +40,12 @@ CREATE FUNCTION app_jobs.tg_add_job_with_row() RETURNS trigger AS $EOFCODE$
|
|
|
40
40
|
BEGIN
|
|
41
41
|
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
|
|
42
42
|
PERFORM
|
|
43
|
-
app_jobs.add_job (
|
|
43
|
+
app_jobs.add_job (TG_ARGV[0], to_json(NEW));
|
|
44
44
|
RETURN NEW;
|
|
45
45
|
END IF;
|
|
46
46
|
IF (TG_OP = 'DELETE') THEN
|
|
47
47
|
PERFORM
|
|
48
|
-
app_jobs.add_job (
|
|
48
|
+
app_jobs.add_job (TG_ARGV[0], to_json(OLD));
|
|
49
49
|
RETURN OLD;
|
|
50
50
|
END IF;
|
|
51
51
|
END;
|
|
@@ -102,7 +102,7 @@ BEGIN
|
|
|
102
102
|
END IF;
|
|
103
103
|
END LOOP;
|
|
104
104
|
PERFORM
|
|
105
|
-
app_jobs.add_job (
|
|
105
|
+
app_jobs.add_job (fn, app_jobs.json_build_object_apply (args));
|
|
106
106
|
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
|
|
107
107
|
RETURN NEW;
|
|
108
108
|
END IF;
|
|
@@ -114,8 +114,9 @@ $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
|
|
|
114
114
|
|
|
115
115
|
CREATE TABLE app_jobs.scheduled_jobs (
|
|
116
116
|
id bigserial PRIMARY KEY,
|
|
117
|
-
database_id uuid
|
|
118
|
-
|
|
117
|
+
database_id uuid,
|
|
118
|
+
actor_id uuid,
|
|
119
|
+
queue_name text DEFAULT NULL,
|
|
119
120
|
task_identifier text NOT NULL,
|
|
120
121
|
payload pg_catalog.json DEFAULT '{}'::json NOT NULL,
|
|
121
122
|
priority int DEFAULT 0 NOT NULL,
|
|
@@ -128,25 +129,40 @@ CREATE TABLE app_jobs.scheduled_jobs (
|
|
|
128
129
|
last_scheduled_id bigint,
|
|
129
130
|
CHECK (length(key) < 513),
|
|
130
131
|
CHECK (length(task_identifier) < 127),
|
|
131
|
-
CHECK (max_attempts
|
|
132
|
+
CHECK (max_attempts >= 1),
|
|
132
133
|
CHECK (length(queue_name) < 127),
|
|
133
134
|
CHECK (length(locked_by) > 3),
|
|
134
135
|
UNIQUE (key)
|
|
135
136
|
);
|
|
136
137
|
|
|
137
|
-
COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions
|
|
138
|
+
COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions: each row spawns jobs on a schedule, optionally scoped to a database';
|
|
139
|
+
|
|
138
140
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier';
|
|
139
|
-
|
|
141
|
+
|
|
142
|
+
COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to (nullable for system-level schedules without tenant context)';
|
|
143
|
+
|
|
144
|
+
COMMENT ON COLUMN app_jobs.scheduled_jobs.actor_id IS 'User who created this scheduled job, read from JWT claims at creation time';
|
|
145
|
+
|
|
140
146
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into';
|
|
147
|
+
|
|
141
148
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs';
|
|
149
|
+
|
|
142
150
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job';
|
|
151
|
+
|
|
143
152
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.priority IS 'Priority assigned to spawned jobs (lower = higher priority)';
|
|
153
|
+
|
|
144
154
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.max_attempts IS 'Max retry attempts for spawned jobs';
|
|
155
|
+
|
|
145
156
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.key IS 'Optional unique deduplication key';
|
|
157
|
+
|
|
146
158
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_at IS 'Timestamp when the scheduler locked this record for processing';
|
|
159
|
+
|
|
147
160
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_by IS 'Identifier of the scheduler worker holding the lock';
|
|
161
|
+
|
|
148
162
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.schedule_info IS 'JSON schedule configuration (e.g. cron expression, interval)';
|
|
163
|
+
|
|
149
164
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled IS 'Timestamp when a job was last spawned from this schedule';
|
|
165
|
+
|
|
150
166
|
COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled_id IS 'ID of the last job spawned from this schedule';
|
|
151
167
|
|
|
152
168
|
CREATE FUNCTION app_jobs.do_notify() RETURNS trigger AS $EOFCODE$
|
|
@@ -171,8 +187,9 @@ GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.scheduled_jobs TO administrator
|
|
|
171
187
|
|
|
172
188
|
CREATE TABLE app_jobs.jobs (
|
|
173
189
|
id bigserial PRIMARY KEY,
|
|
174
|
-
database_id uuid
|
|
175
|
-
|
|
190
|
+
database_id uuid,
|
|
191
|
+
actor_id uuid,
|
|
192
|
+
queue_name text DEFAULT NULL,
|
|
176
193
|
task_identifier text NOT NULL,
|
|
177
194
|
payload pg_catalog.json DEFAULT '{}'::json NOT NULL,
|
|
178
195
|
priority int DEFAULT 0 NOT NULL,
|
|
@@ -183,29 +200,48 @@ CREATE TABLE app_jobs.jobs (
|
|
|
183
200
|
last_error text,
|
|
184
201
|
locked_at timestamptz,
|
|
185
202
|
locked_by text,
|
|
203
|
+
is_available boolean GENERATED ALWAYS AS (locked_at IS NULL
|
|
204
|
+
AND attempts < max_attempts) STORED NOT NULL,
|
|
186
205
|
CHECK (length(key) < 513),
|
|
187
206
|
CHECK (length(task_identifier) < 127),
|
|
188
|
-
CHECK (max_attempts
|
|
207
|
+
CHECK (max_attempts >= 1),
|
|
189
208
|
CHECK (length(queue_name) < 127),
|
|
190
209
|
CHECK (length(locked_by) > 3),
|
|
191
210
|
UNIQUE (key)
|
|
192
211
|
);
|
|
193
212
|
|
|
194
|
-
COMMENT ON TABLE app_jobs.jobs IS 'Background job queue
|
|
213
|
+
COMMENT ON TABLE app_jobs.jobs IS 'Background job queue: each row is a pending or in-progress task, optionally scoped to a database';
|
|
214
|
+
|
|
195
215
|
COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier';
|
|
196
|
-
|
|
216
|
+
|
|
217
|
+
COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to (nullable for system-level jobs without tenant context)';
|
|
218
|
+
|
|
219
|
+
COMMENT ON COLUMN app_jobs.jobs.actor_id IS 'User who triggered this job, read from JWT claims at enqueue time';
|
|
220
|
+
|
|
197
221
|
COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control';
|
|
222
|
+
|
|
198
223
|
COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)';
|
|
224
|
+
|
|
199
225
|
COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler';
|
|
226
|
+
|
|
200
227
|
COMMENT ON COLUMN app_jobs.jobs.priority IS 'Execution priority; lower numbers run first (default 0)';
|
|
228
|
+
|
|
201
229
|
COMMENT ON COLUMN app_jobs.jobs.run_at IS 'Earliest time this job should be executed; used for delayed/scheduled execution';
|
|
230
|
+
|
|
202
231
|
COMMENT ON COLUMN app_jobs.jobs.attempts IS 'Number of times this job has been attempted so far';
|
|
232
|
+
|
|
203
233
|
COMMENT ON COLUMN app_jobs.jobs.max_attempts IS 'Maximum retry attempts before the job is considered permanently failed';
|
|
234
|
+
|
|
204
235
|
COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; prevents duplicate jobs with the same key';
|
|
236
|
+
|
|
205
237
|
COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt';
|
|
238
|
+
|
|
206
239
|
COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing';
|
|
240
|
+
|
|
207
241
|
COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock';
|
|
208
242
|
|
|
243
|
+
COMMENT ON COLUMN app_jobs.jobs.is_available IS 'Generated column: true when job is unlocked and has remaining attempts';
|
|
244
|
+
|
|
209
245
|
ALTER TABLE app_jobs.jobs
|
|
210
246
|
ADD COLUMN created_at timestamptz;
|
|
211
247
|
|
|
@@ -250,11 +286,19 @@ CREATE TRIGGER _500_increase_job_queue_count_on_update
|
|
|
250
286
|
AND new.queue_name IS NOT NULL)
|
|
251
287
|
EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count();
|
|
252
288
|
|
|
253
|
-
CREATE
|
|
289
|
+
CREATE FUNCTION app_jobs.tg_jobs__after_insert() RETURNS trigger AS $EOFCODE$
|
|
290
|
+
BEGIN
|
|
291
|
+
PERFORM
|
|
292
|
+
pg_notify('jobs:insert', '');
|
|
293
|
+
RETURN NULL;
|
|
294
|
+
END;
|
|
295
|
+
$EOFCODE$ LANGUAGE plpgsql;
|
|
296
|
+
|
|
297
|
+
CREATE TRIGGER _900_after_insert
|
|
254
298
|
AFTER INSERT
|
|
255
299
|
ON app_jobs.jobs
|
|
256
|
-
FOR EACH
|
|
257
|
-
EXECUTE PROCEDURE app_jobs.
|
|
300
|
+
FOR EACH STATEMENT
|
|
301
|
+
EXECUTE PROCEDURE app_jobs.tg_jobs__after_insert();
|
|
258
302
|
|
|
259
303
|
CREATE FUNCTION app_jobs.tg_decrease_job_queue_count() RETURNS trigger AS $EOFCODE$
|
|
260
304
|
DECLARE
|
|
@@ -292,7 +336,10 @@ CREATE TRIGGER decrease_job_queue_count_on_update
|
|
|
292
336
|
AND old.queue_name IS NOT NULL)
|
|
293
337
|
EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count();
|
|
294
338
|
|
|
295
|
-
CREATE INDEX
|
|
339
|
+
CREATE INDEX jobs_main_index ON app_jobs.jobs (priority, run_at) INCLUDE (id, queue_name) WHERE is_available = true;
|
|
340
|
+
|
|
341
|
+
CREATE INDEX jobs_no_queue_index ON app_jobs.jobs (priority, run_at) INCLUDE (id) WHERE is_available = true
|
|
342
|
+
AND queue_name IS NULL;
|
|
296
343
|
|
|
297
344
|
CREATE INDEX jobs_locked_by_idx ON app_jobs.jobs (locked_by);
|
|
298
345
|
|
|
@@ -306,9 +353,13 @@ CREATE TABLE app_jobs.job_queues (
|
|
|
306
353
|
);
|
|
307
354
|
|
|
308
355
|
COMMENT ON TABLE app_jobs.job_queues IS 'Queue metadata: tracks job counts and locking state for each named queue';
|
|
356
|
+
|
|
309
357
|
COMMENT ON COLUMN app_jobs.job_queues.queue_name IS 'Unique name identifying this queue';
|
|
358
|
+
|
|
310
359
|
COMMENT ON COLUMN app_jobs.job_queues.job_count IS 'Number of pending jobs in this queue';
|
|
360
|
+
|
|
311
361
|
COMMENT ON COLUMN app_jobs.job_queues.locked_at IS 'Timestamp when this queue was locked for batch processing';
|
|
362
|
+
|
|
312
363
|
COMMENT ON COLUMN app_jobs.job_queues.locked_by IS 'Identifier of the worker that currently holds the queue lock';
|
|
313
364
|
|
|
314
365
|
CREATE INDEX job_queues_locked_by_idx ON app_jobs.job_queues (locked_by);
|
|
@@ -350,6 +401,7 @@ BEGIN
|
|
|
350
401
|
-- insert new job
|
|
351
402
|
INSERT INTO app_jobs.jobs (
|
|
352
403
|
database_id,
|
|
404
|
+
actor_id,
|
|
353
405
|
queue_name,
|
|
354
406
|
task_identifier,
|
|
355
407
|
payload,
|
|
@@ -358,6 +410,7 @@ BEGIN
|
|
|
358
410
|
key
|
|
359
411
|
) SELECT
|
|
360
412
|
database_id,
|
|
413
|
+
actor_id,
|
|
361
414
|
queue_name,
|
|
362
415
|
task_identifier,
|
|
363
416
|
payload,
|
|
@@ -510,77 +563,50 @@ DECLARE
|
|
|
510
563
|
v_row app_jobs.jobs;
|
|
511
564
|
v_now timestamptz = now();
|
|
512
565
|
BEGIN
|
|
513
|
-
|
|
514
566
|
IF worker_id IS NULL THEN
|
|
515
|
-
RAISE
|
|
567
|
+
RAISE EXCEPTION 'INVALID_WORKER_ID';
|
|
516
568
|
END IF;
|
|
517
569
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
jobs.
|
|
523
|
-
|
|
524
|
-
FROM
|
|
525
|
-
app_jobs.jobs
|
|
526
|
-
WHERE (jobs.locked_at IS NULL
|
|
527
|
-
OR jobs.locked_at < (v_now - job_expiry))
|
|
570
|
+
SELECT jobs.queue_name, jobs.id
|
|
571
|
+
INTO v_queue_name, v_job_id
|
|
572
|
+
FROM app_jobs.jobs
|
|
573
|
+
WHERE is_available = true
|
|
574
|
+
AND (jobs.locked_at IS NULL
|
|
575
|
+
OR jobs.locked_at < (v_now - job_expiry))
|
|
528
576
|
AND (jobs.queue_name IS NULL
|
|
529
|
-
OR
|
|
530
|
-
SELECT
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
AND (job_queues.locked_at IS NULL
|
|
537
|
-
OR job_queues.locked_at < (v_now - job_expiry))
|
|
538
|
-
FOR UPDATE
|
|
539
|
-
SKIP LOCKED))
|
|
577
|
+
OR jobs.queue_name IN (
|
|
578
|
+
SELECT jq.queue_name
|
|
579
|
+
FROM app_jobs.job_queues jq
|
|
580
|
+
WHERE (jq.locked_at IS NULL
|
|
581
|
+
OR jq.locked_at < (v_now - job_expiry))
|
|
582
|
+
FOR UPDATE SKIP LOCKED
|
|
583
|
+
))
|
|
540
584
|
AND run_at <= v_now
|
|
541
585
|
AND attempts < max_attempts
|
|
542
586
|
AND (task_identifiers IS NULL
|
|
543
587
|
OR task_identifier = ANY (task_identifiers))
|
|
544
|
-
ORDER BY
|
|
545
|
-
priority ASC,
|
|
546
|
-
run_at ASC,
|
|
547
|
-
id ASC
|
|
588
|
+
ORDER BY priority ASC, run_at ASC, id ASC
|
|
548
589
|
LIMIT 1
|
|
549
|
-
FOR UPDATE
|
|
550
|
-
SKIP LOCKED;
|
|
551
|
-
|
|
552
|
-
--
|
|
590
|
+
FOR UPDATE SKIP LOCKED;
|
|
553
591
|
|
|
554
592
|
IF v_job_id IS NULL THEN
|
|
555
593
|
RETURN NULL;
|
|
556
594
|
END IF;
|
|
557
595
|
|
|
558
|
-
--
|
|
559
|
-
|
|
560
596
|
IF v_queue_name IS NOT NULL THEN
|
|
561
|
-
UPDATE
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
locked_by = worker_id,
|
|
565
|
-
locked_at = v_now
|
|
566
|
-
WHERE
|
|
567
|
-
job_queues.queue_name = v_queue_name;
|
|
597
|
+
UPDATE app_jobs.job_queues
|
|
598
|
+
SET locked_by = worker_id, locked_at = v_now
|
|
599
|
+
WHERE job_queues.queue_name = v_queue_name;
|
|
568
600
|
END IF;
|
|
569
601
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
UPDATE
|
|
573
|
-
app_jobs.jobs
|
|
602
|
+
UPDATE app_jobs.jobs
|
|
574
603
|
SET
|
|
575
604
|
attempts = attempts + 1,
|
|
576
605
|
locked_by = worker_id,
|
|
577
606
|
locked_at = v_now
|
|
578
|
-
WHERE
|
|
579
|
-
|
|
580
|
-
RETURNING
|
|
581
|
-
* INTO v_row;
|
|
607
|
+
WHERE id = v_job_id
|
|
608
|
+
RETURNING * INTO v_row;
|
|
582
609
|
|
|
583
|
-
--
|
|
584
610
|
RETURN v_row;
|
|
585
611
|
END;
|
|
586
612
|
$EOFCODE$;
|
|
@@ -646,15 +672,21 @@ BEGIN
|
|
|
646
672
|
END;
|
|
647
673
|
$EOFCODE$;
|
|
648
674
|
|
|
649
|
-
CREATE FUNCTION app_jobs.add_scheduled_job(
|
|
675
|
+
CREATE FUNCTION app_jobs.add_scheduled_job(identifier text, payload pg_catalog.json DEFAULT '{}'::json, schedule_info pg_catalog.json DEFAULT '{}'::json, job_key text DEFAULT NULL, queue_name text DEFAULT NULL, max_attempts int DEFAULT 25, priority int DEFAULT 0) RETURNS app_jobs.scheduled_jobs AS $EOFCODE$
|
|
650
676
|
DECLARE
|
|
651
677
|
v_job app_jobs.scheduled_jobs;
|
|
678
|
+
v_database_id uuid;
|
|
679
|
+
v_actor_id uuid;
|
|
652
680
|
BEGIN
|
|
681
|
+
v_database_id := jwt_private.current_database_id();
|
|
682
|
+
v_actor_id := jwt_public.current_user_id();
|
|
683
|
+
|
|
653
684
|
IF job_key IS NOT NULL THEN
|
|
654
685
|
|
|
655
|
-
-- Upsert job
|
|
686
|
+
-- Upsert job
|
|
656
687
|
INSERT INTO app_jobs.scheduled_jobs (
|
|
657
688
|
database_id,
|
|
689
|
+
actor_id,
|
|
658
690
|
task_identifier,
|
|
659
691
|
payload,
|
|
660
692
|
queue_name,
|
|
@@ -663,7 +695,8 @@ BEGIN
|
|
|
663
695
|
key,
|
|
664
696
|
priority
|
|
665
697
|
) VALUES (
|
|
666
|
-
|
|
698
|
+
v_database_id,
|
|
699
|
+
v_actor_id,
|
|
667
700
|
identifier,
|
|
668
701
|
coalesce(payload, '{}'::json),
|
|
669
702
|
queue_name,
|
|
@@ -671,37 +704,38 @@ BEGIN
|
|
|
671
704
|
coalesce(max_attempts, 25),
|
|
672
705
|
job_key,
|
|
673
706
|
coalesce(priority, 0)
|
|
674
|
-
)
|
|
675
|
-
ON CONFLICT (key)
|
|
676
|
-
DO UPDATE SET
|
|
707
|
+
)
|
|
708
|
+
ON CONFLICT (key)
|
|
709
|
+
DO UPDATE SET
|
|
677
710
|
task_identifier = EXCLUDED.task_identifier,
|
|
678
711
|
payload = EXCLUDED.payload,
|
|
679
712
|
queue_name = EXCLUDED.queue_name,
|
|
680
713
|
max_attempts = EXCLUDED.max_attempts,
|
|
681
714
|
schedule_info = EXCLUDED.schedule_info,
|
|
682
715
|
priority = EXCLUDED.priority
|
|
683
|
-
WHERE
|
|
684
|
-
scheduled_jobs.locked_at IS NULL
|
|
685
|
-
RETURNING
|
|
686
|
-
* INTO v_job;
|
|
687
|
-
|
|
688
|
-
-- If upsert succeeded (insert or update), return early
|
|
689
|
-
|
|
690
|
-
IF NOT (v_job IS NULL) THEN
|
|
691
|
-
RETURN v_job;
|
|
692
|
-
END IF;
|
|
693
|
-
|
|
694
|
-
-- Upsert failed -> there must be an existing scheduled job that is locked. Remove
|
|
716
|
+
WHERE
|
|
717
|
+
scheduled_jobs.locked_at IS NULL
|
|
718
|
+
RETURNING
|
|
719
|
+
* INTO v_job;
|
|
720
|
+
|
|
721
|
+
-- If upsert succeeded (insert or update), return early
|
|
722
|
+
|
|
723
|
+
IF NOT (v_job IS NULL) THEN
|
|
724
|
+
RETURN v_job;
|
|
725
|
+
END IF;
|
|
726
|
+
|
|
727
|
+
-- Upsert failed -> there must be an existing scheduled job that is locked. Remove
|
|
695
728
|
-- and allow a new one to be inserted
|
|
696
729
|
|
|
697
|
-
DELETE FROM
|
|
698
|
-
app_jobs.scheduled_jobs
|
|
699
|
-
WHERE
|
|
700
|
-
KEY = job_key;
|
|
730
|
+
DELETE FROM
|
|
731
|
+
app_jobs.scheduled_jobs
|
|
732
|
+
WHERE
|
|
733
|
+
KEY = job_key;
|
|
701
734
|
END IF;
|
|
702
735
|
|
|
703
736
|
INSERT INTO app_jobs.scheduled_jobs (
|
|
704
737
|
database_id,
|
|
738
|
+
actor_id,
|
|
705
739
|
task_identifier,
|
|
706
740
|
payload,
|
|
707
741
|
queue_name,
|
|
@@ -709,7 +743,8 @@ BEGIN
|
|
|
709
743
|
max_attempts,
|
|
710
744
|
priority
|
|
711
745
|
) VALUES (
|
|
712
|
-
|
|
746
|
+
v_database_id,
|
|
747
|
+
v_actor_id,
|
|
713
748
|
identifier,
|
|
714
749
|
payload,
|
|
715
750
|
queue_name,
|
|
@@ -721,14 +756,21 @@ BEGIN
|
|
|
721
756
|
END;
|
|
722
757
|
$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
|
|
723
758
|
|
|
724
|
-
CREATE FUNCTION app_jobs.add_job(
|
|
759
|
+
CREATE FUNCTION app_jobs.add_job(identifier text, payload pg_catalog.json DEFAULT '{}'::json, job_key text DEFAULT NULL, queue_name text DEFAULT NULL, run_at timestamptz DEFAULT now(), max_attempts int DEFAULT 25, priority int DEFAULT 0) RETURNS app_jobs.jobs AS $EOFCODE$
|
|
725
760
|
DECLARE
|
|
726
761
|
v_job app_jobs.jobs;
|
|
762
|
+
v_database_id uuid;
|
|
763
|
+
v_actor_id uuid;
|
|
727
764
|
BEGIN
|
|
765
|
+
-- Read context from JWT claims
|
|
766
|
+
v_database_id := jwt_private.current_database_id();
|
|
767
|
+
v_actor_id := jwt_public.current_user_id();
|
|
768
|
+
|
|
728
769
|
IF job_key IS NOT NULL THEN
|
|
729
|
-
-- Upsert job
|
|
770
|
+
-- Upsert job
|
|
730
771
|
INSERT INTO app_jobs.jobs (
|
|
731
772
|
database_id,
|
|
773
|
+
actor_id,
|
|
732
774
|
task_identifier,
|
|
733
775
|
payload,
|
|
734
776
|
queue_name,
|
|
@@ -737,52 +779,51 @@ BEGIN
|
|
|
737
779
|
key,
|
|
738
780
|
priority
|
|
739
781
|
) VALUES (
|
|
740
|
-
|
|
782
|
+
v_database_id,
|
|
783
|
+
v_actor_id,
|
|
741
784
|
identifier,
|
|
742
|
-
coalesce(payload,
|
|
743
|
-
'{}'::json),
|
|
785
|
+
coalesce(payload, '{}'::json),
|
|
744
786
|
queue_name,
|
|
745
787
|
coalesce(run_at, now()),
|
|
746
788
|
coalesce(max_attempts, 25),
|
|
747
789
|
job_key,
|
|
748
790
|
coalesce(priority, 0)
|
|
749
|
-
)
|
|
750
|
-
ON CONFLICT (key)
|
|
751
|
-
DO UPDATE SET
|
|
791
|
+
)
|
|
792
|
+
ON CONFLICT (key)
|
|
793
|
+
DO UPDATE SET
|
|
752
794
|
task_identifier = EXCLUDED.task_identifier,
|
|
753
795
|
payload = EXCLUDED.payload,
|
|
754
796
|
queue_name = EXCLUDED.queue_name,
|
|
755
797
|
max_attempts = EXCLUDED.max_attempts,
|
|
756
798
|
run_at = EXCLUDED.run_at,
|
|
757
|
-
priority = EXCLUDED.priority,
|
|
758
|
-
-- always reset error/retry state
|
|
759
|
-
attempts = 0, last_error = NULL
|
|
760
|
-
WHERE
|
|
761
|
-
jobs.locked_at IS NULL
|
|
762
|
-
RETURNING
|
|
763
|
-
* INTO v_job;
|
|
764
|
-
|
|
765
|
-
-- If upsert succeeded (insert or update), return early
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
END IF;
|
|
770
|
-
|
|
771
|
-
-- Upsert failed -> there must be an existing job that is locked. Remove
|
|
772
|
-
-- existing key to allow a new one to be inserted, and prevent any
|
|
773
|
-
-- subsequent retries by bumping attempts to the max allowed.
|
|
799
|
+
priority = EXCLUDED.priority,
|
|
800
|
+
-- always reset error/retry state
|
|
801
|
+
attempts = 0, last_error = NULL
|
|
802
|
+
WHERE
|
|
803
|
+
jobs.locked_at IS NULL
|
|
804
|
+
RETURNING
|
|
805
|
+
* INTO v_job;
|
|
806
|
+
|
|
807
|
+
-- If upsert succeeded (insert or update), return early
|
|
808
|
+
IF NOT (v_job IS NULL) THEN
|
|
809
|
+
RETURN v_job;
|
|
810
|
+
END IF;
|
|
774
811
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
812
|
+
-- Upsert failed -> there must be an existing job that is locked. Remove
|
|
813
|
+
-- existing key to allow a new one to be inserted, and prevent any
|
|
814
|
+
-- subsequent retries by bumping attempts to the max allowed.
|
|
815
|
+
UPDATE
|
|
816
|
+
app_jobs.jobs
|
|
817
|
+
SET
|
|
818
|
+
key = NULL,
|
|
819
|
+
attempts = jobs.max_attempts
|
|
820
|
+
WHERE
|
|
821
|
+
key = job_key;
|
|
782
822
|
END IF;
|
|
783
823
|
|
|
784
824
|
INSERT INTO app_jobs.jobs (
|
|
785
825
|
database_id,
|
|
826
|
+
actor_id,
|
|
786
827
|
task_identifier,
|
|
787
828
|
payload,
|
|
788
829
|
queue_name,
|
|
@@ -790,7 +831,8 @@ BEGIN
|
|
|
790
831
|
max_attempts,
|
|
791
832
|
priority
|
|
792
833
|
) VALUES (
|
|
793
|
-
|
|
834
|
+
v_database_id,
|
|
835
|
+
v_actor_id,
|
|
794
836
|
identifier,
|
|
795
837
|
payload,
|
|
796
838
|
queue_name,
|
|
@@ -803,3 +845,38 @@ BEGIN
|
|
|
803
845
|
RETURN v_job;
|
|
804
846
|
END;
|
|
805
847
|
$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
|
|
848
|
+
|
|
849
|
+
CREATE FUNCTION app_jobs.remove_job(job_key text) RETURNS app_jobs.jobs LANGUAGE plpgsql STRICT AS $EOFCODE$
|
|
850
|
+
DECLARE
|
|
851
|
+
v_job app_jobs.jobs;
|
|
852
|
+
BEGIN
|
|
853
|
+
DELETE FROM app_jobs.jobs
|
|
854
|
+
WHERE key = job_key
|
|
855
|
+
AND (locked_at IS NULL
|
|
856
|
+
OR locked_at < NOW() - interval '4 hours')
|
|
857
|
+
RETURNING * INTO v_job;
|
|
858
|
+
|
|
859
|
+
IF NOT (v_job IS NULL) THEN
|
|
860
|
+
RETURN v_job;
|
|
861
|
+
END IF;
|
|
862
|
+
|
|
863
|
+
UPDATE app_jobs.jobs
|
|
864
|
+
SET
|
|
865
|
+
key = NULL,
|
|
866
|
+
attempts = jobs.max_attempts
|
|
867
|
+
WHERE key = job_key
|
|
868
|
+
RETURNING * INTO v_job;
|
|
869
|
+
|
|
870
|
+
RETURN v_job;
|
|
871
|
+
END;
|
|
872
|
+
$EOFCODE$;
|
|
873
|
+
|
|
874
|
+
CREATE FUNCTION app_jobs.force_unlock_workers(worker_ids text[]) RETURNS void LANGUAGE sql VOLATILE AS $EOFCODE$
|
|
875
|
+
UPDATE app_jobs.jobs
|
|
876
|
+
SET locked_at = NULL, locked_by = NULL
|
|
877
|
+
WHERE locked_by = ANY (worker_ids);
|
|
878
|
+
|
|
879
|
+
UPDATE app_jobs.job_queues
|
|
880
|
+
SET locked_at = NULL, locked_by = NULL
|
|
881
|
+
WHERE locked_by = ANY (worker_ids);
|
|
882
|
+
$EOFCODE$;
|