@pgpm/jobs 0.4.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.
Files changed (115) hide show
  1. package/LICENSE +22 -0
  2. package/Makefile +6 -0
  3. package/README.md +5 -0
  4. package/__tests__/__snapshots__/jobs.test.ts.snap +16 -0
  5. package/__tests__/jobs.test.ts +139 -0
  6. package/deploy/schemas/app_jobs/helpers/json_build_object_apply.sql +28 -0
  7. package/deploy/schemas/app_jobs/procedures/add_job.sql +65 -0
  8. package/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql +66 -0
  9. package/deploy/schemas/app_jobs/procedures/complete_job.sql +32 -0
  10. package/deploy/schemas/app_jobs/procedures/complete_jobs.sql +19 -0
  11. package/deploy/schemas/app_jobs/procedures/do_notify.sql +16 -0
  12. package/deploy/schemas/app_jobs/procedures/fail_job.sql +41 -0
  13. package/deploy/schemas/app_jobs/procedures/get_job.sql +77 -0
  14. package/deploy/schemas/app_jobs/procedures/get_scheduled_job.sql +46 -0
  15. package/deploy/schemas/app_jobs/procedures/permanently_fail_jobs.sql +24 -0
  16. package/deploy/schemas/app_jobs/procedures/release_jobs.sql +34 -0
  17. package/deploy/schemas/app_jobs/procedures/release_scheduled_jobs.sql +26 -0
  18. package/deploy/schemas/app_jobs/procedures/reschedule_jobs.sql +26 -0
  19. package/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql +67 -0
  20. package/deploy/schemas/app_jobs/schema.sql +7 -0
  21. package/deploy/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql +12 -0
  22. package/deploy/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql +8 -0
  23. package/deploy/schemas/app_jobs/tables/job_queues/table.sql +12 -0
  24. package/deploy/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql +12 -0
  25. package/deploy/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql +8 -0
  26. package/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +8 -0
  27. package/deploy/schemas/app_jobs/tables/jobs/table.sql +26 -0
  28. package/deploy/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql +42 -0
  29. package/deploy/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql +32 -0
  30. package/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +13 -0
  31. package/deploy/schemas/app_jobs/tables/jobs/triggers/timestamps.sql +20 -0
  32. package/deploy/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql +12 -0
  33. package/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql +8 -0
  34. package/deploy/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql +8 -0
  35. package/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql +26 -0
  36. package/deploy/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql +12 -0
  37. package/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql +50 -0
  38. package/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql +26 -0
  39. package/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql +26 -0
  40. package/deploy/schemas/app_jobs/triggers/tg_update_timestamps.sql +21 -0
  41. package/jest.config.js +15 -0
  42. package/launchql-jobs.control +8 -0
  43. package/launchql.plan +38 -0
  44. package/package.json +29 -0
  45. package/revert/schemas/app_jobs/helpers/json_build_object_apply.sql +7 -0
  46. package/revert/schemas/app_jobs/procedures/add_job.sql +7 -0
  47. package/revert/schemas/app_jobs/procedures/add_scheduled_job.sql +7 -0
  48. package/revert/schemas/app_jobs/procedures/complete_job.sql +7 -0
  49. package/revert/schemas/app_jobs/procedures/complete_jobs.sql +7 -0
  50. package/revert/schemas/app_jobs/procedures/do_notify.sql +7 -0
  51. package/revert/schemas/app_jobs/procedures/fail_job.sql +7 -0
  52. package/revert/schemas/app_jobs/procedures/get_job.sql +7 -0
  53. package/revert/schemas/app_jobs/procedures/get_scheduled_job.sql +7 -0
  54. package/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql +7 -0
  55. package/revert/schemas/app_jobs/procedures/release_jobs.sql +7 -0
  56. package/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql +7 -0
  57. package/revert/schemas/app_jobs/procedures/reschedule_jobs.sql +7 -0
  58. package/revert/schemas/app_jobs/procedures/run_scheduled_job.sql +7 -0
  59. package/revert/schemas/app_jobs/schema.sql +7 -0
  60. package/revert/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql +7 -0
  61. package/revert/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql +7 -0
  62. package/revert/schemas/app_jobs/tables/job_queues/table.sql +7 -0
  63. package/revert/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql +7 -0
  64. package/revert/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql +7 -0
  65. package/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +7 -0
  66. package/revert/schemas/app_jobs/tables/jobs/table.sql +7 -0
  67. package/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql +7 -0
  68. package/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql +7 -0
  69. package/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +5 -0
  70. package/revert/schemas/app_jobs/tables/jobs/triggers/timestamps.sql +9 -0
  71. package/revert/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql +7 -0
  72. package/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql +7 -0
  73. package/revert/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql +7 -0
  74. package/revert/schemas/app_jobs/tables/scheduled_jobs/table.sql +7 -0
  75. package/revert/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql +8 -0
  76. package/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql +7 -0
  77. package/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql +7 -0
  78. package/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql +5 -0
  79. package/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql +7 -0
  80. package/sql/launchql-jobs--0.4.6.sql +658 -0
  81. package/verify/schemas/app_jobs/helpers/json_build_object_apply.sql +7 -0
  82. package/verify/schemas/app_jobs/procedures/add_job.sql +7 -0
  83. package/verify/schemas/app_jobs/procedures/add_scheduled_job.sql +7 -0
  84. package/verify/schemas/app_jobs/procedures/complete_job.sql +7 -0
  85. package/verify/schemas/app_jobs/procedures/complete_jobs.sql +7 -0
  86. package/verify/schemas/app_jobs/procedures/do_notify.sql +7 -0
  87. package/verify/schemas/app_jobs/procedures/fail_job.sql +7 -0
  88. package/verify/schemas/app_jobs/procedures/get_job.sql +7 -0
  89. package/verify/schemas/app_jobs/procedures/get_scheduled_job.sql +7 -0
  90. package/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql +7 -0
  91. package/verify/schemas/app_jobs/procedures/release_jobs.sql +7 -0
  92. package/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql +7 -0
  93. package/verify/schemas/app_jobs/procedures/reschedule_jobs.sql +7 -0
  94. package/verify/schemas/app_jobs/procedures/run_scheduled_job.sql +7 -0
  95. package/verify/schemas/app_jobs/schema.sql +7 -0
  96. package/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql +10 -0
  97. package/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql +7 -0
  98. package/verify/schemas/app_jobs/tables/job_queues/table.sql +7 -0
  99. package/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql +10 -0
  100. package/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql +7 -0
  101. package/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +7 -0
  102. package/verify/schemas/app_jobs/tables/jobs/table.sql +7 -0
  103. package/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql +10 -0
  104. package/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql +10 -0
  105. package/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +6 -0
  106. package/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql +16 -0
  107. package/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql +10 -0
  108. package/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql +7 -0
  109. package/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql +7 -0
  110. package/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql +7 -0
  111. package/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql +8 -0
  112. package/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql +7 -0
  113. package/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql +7 -0
  114. package/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql +6 -0
  115. package/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql +7 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Interweb, Inc.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/Makefile ADDED
@@ -0,0 +1,6 @@
1
+ EXTENSION = launchql-jobs
2
+ DATA = sql/launchql-jobs--0.4.6.sql
3
+
4
+ PG_CONFIG = pg_config
5
+ PGXS := $(shell $(PG_CONFIG) --pgxs)
6
+ include $(PGXS)
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @pgpm/jobs
2
+
3
+ Core job system for background task processing in PostgreSQL.
4
+
5
+ Provides job queue management, task scheduling, and background processing capabilities for PostgreSQL applications.
@@ -0,0 +1,16 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`scheduled jobs schedule jobs 1`] = `
4
+ {
5
+ "attempts": null,
6
+ "id": null,
7
+ "key": null,
8
+ "last_error": null,
9
+ "locked_at": null,
10
+ "locked_by": null,
11
+ "max_attempts": null,
12
+ "payload": null,
13
+ "priority": null,
14
+ "task_identifier": null,
15
+ }
16
+ `;
@@ -0,0 +1,139 @@
1
+ import { getConnections, PgTestClient } from 'pgsql-test';
2
+
3
+ let pg: PgTestClient;
4
+ let teardown: () => Promise<void>;
5
+
6
+ const objs: Record<string, any> = {};
7
+
8
+ describe('scheduled jobs', () => {
9
+ beforeAll(async () => {
10
+ ({ pg, teardown } = await getConnections());
11
+ });
12
+
13
+ beforeEach(async () => {
14
+ await pg.beforeEach();
15
+ });
16
+
17
+ afterEach(async () => {
18
+ await pg.afterEach();
19
+ });
20
+
21
+ afterAll(async () => {
22
+ await teardown();
23
+ });
24
+
25
+ it('schedule jobs by cron', async () => {
26
+ const result = await pg.one(
27
+ `INSERT INTO app_jobs.scheduled_jobs (task_identifier, schedule_info)
28
+ VALUES ($1, $2)
29
+ RETURNING *`,
30
+ [
31
+ 'my_job',
32
+ {
33
+ hour: Array.from({ length: 23 }, (_: unknown, i: number) => i),
34
+ minute: [0, 15, 30, 45],
35
+ dayOfWeek: Array.from({ length: 6 }, (_: unknown, i: number) => i),
36
+ },
37
+ ]
38
+ );
39
+ objs.scheduled1 = result;
40
+ });
41
+
42
+ it('schedule jobs by rule', async () => {
43
+ const start = new Date(Date.now() + 10000);
44
+ const end = new Date(start.getTime() + 180000);
45
+
46
+ const result = await pg.one(
47
+ `INSERT INTO app_jobs.scheduled_jobs (task_identifier, payload, schedule_info)
48
+ VALUES ($1, $2, $3)
49
+ RETURNING *`,
50
+ [
51
+ 'my_job',
52
+ { just: 'run it' },
53
+ { start, end, rule: '*/1 * * * *' }
54
+ ]
55
+ );
56
+ objs.scheduled2 = result;
57
+ });
58
+
59
+ it('schedule jobs', async () => {
60
+ const [result] = await pg.any(
61
+ `SELECT * FROM app_jobs.run_scheduled_job($1)`,
62
+ [objs.scheduled2.id]
63
+ );
64
+
65
+ const { queue_name, run_at, created_at, updated_at, ...obj } = result;
66
+ expect(obj).toMatchSnapshot();
67
+ });
68
+
69
+ it('schedule jobs with keys', async () => {
70
+ const start = new Date(Date.now() + 10000);
71
+ const end = new Date(start.getTime() + 180000);
72
+
73
+ const [result] = await pg.any(
74
+ `SELECT * FROM app_jobs.add_scheduled_job(
75
+ identifier := $1::text,
76
+ payload := $2::json,
77
+ schedule_info := $3::json,
78
+ job_key := $4::text,
79
+ queue_name := $5::text,
80
+ max_attempts := $6::integer,
81
+ priority := $7::integer
82
+ )`,
83
+ [
84
+ 'my_job',
85
+ { just: 'run it' },
86
+ { start, end, rule: '*/1 * * * *' },
87
+ 'new_key',
88
+ null,
89
+ 25,
90
+ 0
91
+ ]
92
+ );
93
+
94
+ const {
95
+ queue_name,
96
+ run_at,
97
+ created_at,
98
+ updated_at,
99
+ schedule_info: sch,
100
+ start: s1,
101
+ end: d1,
102
+ ...obj
103
+ } = result;
104
+
105
+ const [result2] = await pg.any(
106
+ `SELECT * FROM app_jobs.add_scheduled_job(
107
+ identifier := $1,
108
+ payload := $2,
109
+ schedule_info := $3,
110
+ job_key := $4,
111
+ queue_name := $5,
112
+ max_attempts := $6,
113
+ priority := $7
114
+ )`,
115
+ [
116
+ 'my_job',
117
+ { just: 'run it' },
118
+ { start, end, rule: '*/1 * * * *' },
119
+ 'new_key',
120
+ null,
121
+ 25,
122
+ 0
123
+ ]
124
+ );
125
+
126
+ const {
127
+ queue_name: qn,
128
+ created_at: ca,
129
+ updated_at: ua,
130
+ schedule_info: sch2,
131
+ start: s,
132
+ end: e,
133
+ ...obj2
134
+ } = result2;
135
+
136
+ console.log('First insert:', obj);
137
+ console.log('Duplicate insert (job_key conflict):', obj2);
138
+ });
139
+ });
@@ -0,0 +1,28 @@
1
+ -- Deploy schemas/app_jobs/helpers/json_build_object_apply to pg
2
+ -- requires: schemas/app_jobs/schema
3
+
4
+ BEGIN;
5
+ CREATE FUNCTION app_jobs.json_build_object_apply (arguments text[])
6
+ RETURNS json
7
+ AS $$
8
+ DECLARE
9
+ arg text;
10
+ _sql text;
11
+ _res json;
12
+ args text[];
13
+ BEGIN
14
+ _sql = 'SELECT json_build_object(';
15
+ FOR arg IN
16
+ SELECT
17
+ unnest(arguments)
18
+ LOOP
19
+ args = array_append(args, format('''%s''', arg));
20
+ END LOOP;
21
+ _sql = _sql || format('%s);', array_to_string(args, ','));
22
+ EXECUTE _sql INTO _res;
23
+ RETURN _res;
24
+ END;
25
+ $$
26
+ LANGUAGE 'plpgsql';
27
+ COMMIT;
28
+
@@ -0,0 +1,65 @@
1
+ -- Deploy schemas/app_jobs/procedures/add_job 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.add_job (
8
+ identifier text,
9
+ payload json DEFAULT '{}' ::json,
10
+ job_key text DEFAULT NULL,
11
+ queue_name text DEFAULT NULL,
12
+ run_at timestamptz DEFAULT now(),
13
+ max_attempts integer DEFAULT 25,
14
+ priority integer DEFAULT 0
15
+ )
16
+ RETURNS app_jobs.jobs
17
+ AS $$
18
+ DECLARE
19
+ v_job app_jobs.jobs;
20
+ BEGIN
21
+ IF job_key IS NOT NULL THEN
22
+ -- Upsert job
23
+ INSERT INTO app_jobs.jobs (task_identifier, payload, queue_name, run_at, max_attempts, KEY, priority)
24
+ VALUES (identifier, coalesce(payload, '{}'::json), queue_name, coalesce(run_at, now()), coalesce(max_attempts, 25), job_key, coalesce(priority, 0))
25
+ ON CONFLICT (KEY)
26
+ DO UPDATE SET
27
+ task_identifier = excluded.task_identifier, payload = excluded.payload, queue_name = excluded.queue_name, max_attempts = excluded.max_attempts, run_at = excluded.run_at, priority = excluded.priority,
28
+ -- always reset error/retry state
29
+ attempts = 0, last_error = NULL
30
+ WHERE
31
+ jobs.locked_at IS NULL
32
+ RETURNING
33
+ * INTO v_job;
34
+
35
+ -- If upsert succeeded (insert or update), return early
36
+
37
+ IF NOT (v_job IS NULL) THEN
38
+ RETURN v_job;
39
+ END IF;
40
+
41
+ -- Upsert failed -> there must be an existing job that is locked. Remove
42
+ -- existing key to allow a new one to be inserted, and prevent any
43
+ -- subsequent retries by bumping attempts to the max allowed.
44
+
45
+ UPDATE
46
+ app_jobs.jobs
47
+ SET
48
+ KEY = NULL,
49
+ attempts = jobs.max_attempts
50
+ WHERE
51
+ KEY = job_key;
52
+ END IF;
53
+
54
+ INSERT INTO app_jobs.jobs (task_identifier, payload, queue_name, run_at, max_attempts, priority)
55
+ VALUES (identifier, payload, queue_name, run_at, max_attempts, priority)
56
+ RETURNING
57
+ * INTO v_job;
58
+ RETURN v_job;
59
+ END;
60
+ $$
61
+ LANGUAGE 'plpgsql'
62
+ VOLATILE
63
+ SECURITY DEFINER;
64
+ COMMIT;
65
+
@@ -0,0 +1,66 @@
1
+ -- Deploy schemas/app_jobs/procedures/add_scheduled_job to pg
2
+
3
+ -- requires: schemas/app_jobs/schema
4
+ -- requires: schemas/app_jobs/tables/scheduled_jobs/table
5
+
6
+ BEGIN;
7
+
8
+ CREATE FUNCTION app_jobs.add_scheduled_job(
9
+ identifier text,
10
+ payload json DEFAULT '{}'::json,
11
+ schedule_info json DEFAULT '{}'::json,
12
+ job_key text DEFAULT NULL,
13
+ queue_name text DEFAULT NULL,
14
+ max_attempts integer DEFAULT 25,
15
+ priority integer DEFAULT 0
16
+ )
17
+ RETURNS app_jobs.scheduled_jobs
18
+ AS $$
19
+ DECLARE
20
+ v_job app_jobs.scheduled_jobs;
21
+ BEGIN
22
+ IF job_key IS NOT NULL THEN
23
+
24
+ -- Upsert job
25
+ INSERT INTO app_jobs.scheduled_jobs (task_identifier, payload, queue_name, schedule_info, max_attempts, KEY, priority)
26
+ VALUES (identifier, coalesce(payload, '{}'::json), queue_name, schedule_info, coalesce(max_attempts, 25), job_key, coalesce(priority, 0))
27
+ ON CONFLICT (KEY)
28
+ DO UPDATE SET
29
+ task_identifier = excluded.task_identifier,
30
+ payload = excluded.payload,
31
+ queue_name = excluded.queue_name,
32
+ max_attempts = excluded.max_attempts,
33
+ schedule_info = excluded.schedule_info,
34
+ priority = excluded.priority
35
+ WHERE
36
+ scheduled_jobs.locked_at IS NULL
37
+ RETURNING
38
+ * INTO v_job;
39
+
40
+ -- If upsert succeeded (insert or update), return early
41
+
42
+ IF NOT (v_job IS NULL) THEN
43
+ RETURN v_job;
44
+ END IF;
45
+
46
+ -- Upsert failed -> there must be an existing scheduled job that is locked. Remove
47
+ -- and allow a new one to be inserted
48
+
49
+ DELETE FROM
50
+ app_jobs.scheduled_jobs
51
+ WHERE
52
+ KEY = job_key;
53
+ END IF;
54
+
55
+ INSERT INTO app_jobs.scheduled_jobs (task_identifier, payload, queue_name, schedule_info, max_attempts, priority)
56
+ VALUES (identifier, payload, queue_name, schedule_info, max_attempts, priority)
57
+ RETURNING
58
+ * INTO v_job;
59
+ RETURN v_job;
60
+ END;
61
+ $$
62
+ LANGUAGE 'plpgsql'
63
+ VOLATILE
64
+ SECURITY DEFINER;
65
+ COMMIT;
66
+
@@ -0,0 +1,32 @@
1
+ -- Deploy schemas/app_jobs/procedures/complete_job 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.complete_job (worker_id text, job_id bigint)
8
+ RETURNS app_jobs.jobs
9
+ LANGUAGE plpgsql
10
+ AS $$
11
+ DECLARE
12
+ v_row app_jobs.jobs;
13
+ BEGIN
14
+ DELETE FROM app_jobs.jobs
15
+ WHERE id = job_id
16
+ RETURNING
17
+ * INTO v_row;
18
+ IF v_row.queue_name IS NOT NULL THEN
19
+ UPDATE
20
+ app_jobs.job_queues
21
+ SET
22
+ locked_by = NULL,
23
+ locked_at = NULL
24
+ WHERE
25
+ queue_name = v_row.queue_name
26
+ AND locked_by = worker_id;
27
+ END IF;
28
+ RETURN v_row;
29
+ END;
30
+ $$;
31
+ COMMIT;
32
+
@@ -0,0 +1,19 @@
1
+ -- Deploy schemas/app_jobs/procedures/complete_jobs to pg
2
+ -- requires: schemas/app_jobs/schema
3
+ -- requires: schemas/app_jobs/tables/job_queues/table
4
+ -- requires: schemas/app_jobs/tables/jobs/table
5
+
6
+ BEGIN;
7
+ CREATE FUNCTION app_jobs.complete_jobs (job_ids bigint[])
8
+ RETURNS SETOF app_jobs.jobs
9
+ LANGUAGE sql
10
+ AS $$
11
+ DELETE FROM app_jobs.jobs
12
+ WHERE id = ANY (job_ids)
13
+ AND (locked_by IS NULL
14
+ OR locked_at < NOW() - interval '4 hours')
15
+ RETURNING
16
+ *;
17
+ $$;
18
+ COMMIT;
19
+
@@ -0,0 +1,16 @@
1
+ -- Deploy schemas/app_jobs/procedures/do_notify to pg
2
+ -- requires: schemas/app_jobs/schema
3
+
4
+ BEGIN;
5
+ CREATE FUNCTION app_jobs.do_notify ()
6
+ RETURNS TRIGGER
7
+ AS $$
8
+ BEGIN
9
+ PERFORM
10
+ pg_notify(TG_ARGV[0], '');
11
+ RETURN NEW;
12
+ END;
13
+ $$
14
+ LANGUAGE plpgsql;
15
+ COMMIT;
16
+
@@ -0,0 +1,41 @@
1
+ -- Deploy schemas/app_jobs/procedures/fail_job 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.fail_job (worker_id text, job_id bigint, error_message text)
8
+ RETURNS app_jobs.jobs
9
+ LANGUAGE plpgsql
10
+ STRICT
11
+ AS $$
12
+ DECLARE
13
+ v_row app_jobs.jobs;
14
+ BEGIN
15
+ UPDATE
16
+ app_jobs.jobs
17
+ SET
18
+ last_error = error_message,
19
+ run_at = greatest (now(), run_at) + (exp(least (attempts, 10))::text || ' seconds')::interval,
20
+ locked_by = NULL,
21
+ locked_at = NULL
22
+ WHERE
23
+ id = job_id
24
+ AND locked_by = worker_id
25
+ RETURNING
26
+ * INTO v_row;
27
+ IF v_row.queue_name IS NOT NULL THEN
28
+ UPDATE
29
+ app_jobs.job_queues
30
+ SET
31
+ locked_by = NULL,
32
+ locked_at = NULL
33
+ WHERE
34
+ queue_name = v_row.queue_name
35
+ AND locked_by = worker_id;
36
+ END IF;
37
+ RETURN v_row;
38
+ END;
39
+ $$;
40
+ COMMIT;
41
+
@@ -0,0 +1,77 @@
1
+ -- Deploy schemas/app_jobs/procedures/get_job to pg
2
+ -- requires: schemas/app_jobs/schema
3
+ -- requires: schemas/app_jobs/tables/job_queues/table
4
+ -- requires: schemas/app_jobs/tables/jobs/table
5
+
6
+ BEGIN;
7
+ CREATE FUNCTION app_jobs.get_job (worker_id text, task_identifiers text[] DEFAULT NULL, job_expiry interval DEFAULT '4 hours')
8
+ RETURNS app_jobs.jobs
9
+ LANGUAGE plpgsql
10
+ AS $$
11
+ DECLARE
12
+ v_job_id bigint;
13
+ v_queue_name text;
14
+ v_row app_jobs.jobs;
15
+ v_now timestamptz = now();
16
+ BEGIN
17
+ IF worker_id IS NULL THEN
18
+ RAISE exception 'INVALID_WORKER_ID';
19
+ END IF;
20
+ SELECT
21
+ jobs.queue_name,
22
+ jobs.id INTO v_queue_name,
23
+ v_job_id
24
+ FROM
25
+ app_jobs.jobs
26
+ WHERE (jobs.locked_at IS NULL
27
+ OR jobs.locked_at < (v_now - job_expiry))
28
+ AND (jobs.queue_name IS NULL
29
+ OR EXISTS (
30
+ SELECT
31
+ 1
32
+ FROM
33
+ app_jobs.job_queues
34
+ WHERE
35
+ job_queues.queue_name = jobs.queue_name
36
+ AND (job_queues.locked_at IS NULL
37
+ OR job_queues.locked_at < (v_now - job_expiry))
38
+ FOR UPDATE
39
+ SKIP LOCKED))
40
+ AND run_at <= v_now
41
+ AND attempts < max_attempts
42
+ AND (task_identifiers IS NULL
43
+ OR task_identifier = ANY (task_identifiers))
44
+ ORDER BY
45
+ priority ASC,
46
+ run_at ASC,
47
+ id ASC
48
+ LIMIT 1
49
+ FOR UPDATE
50
+ SKIP LOCKED;
51
+ IF v_job_id IS NULL THEN
52
+ RETURN NULL;
53
+ END IF;
54
+ IF v_queue_name IS NOT NULL THEN
55
+ UPDATE
56
+ app_jobs.job_queues
57
+ SET
58
+ locked_by = worker_id,
59
+ locked_at = v_now
60
+ WHERE
61
+ job_queues.queue_name = v_queue_name;
62
+ END IF;
63
+ UPDATE
64
+ app_jobs.jobs
65
+ SET
66
+ attempts = attempts + 1,
67
+ locked_by = worker_id,
68
+ locked_at = v_now
69
+ WHERE
70
+ id = v_job_id
71
+ RETURNING
72
+ * INTO v_row;
73
+ RETURN v_row;
74
+ END;
75
+ $$;
76
+ COMMIT;
77
+
@@ -0,0 +1,46 @@
1
+ -- Deploy schemas/app_jobs/procedures/get_scheduled_job to pg
2
+ -- requires: schemas/app_jobs/schema
3
+ -- requires: schemas/app_jobs/tables/scheduled_jobs/table
4
+
5
+ BEGIN;
6
+ CREATE FUNCTION app_jobs.get_scheduled_job (worker_id text, task_identifiers text[] DEFAULT NULL)
7
+ RETURNS app_jobs.scheduled_jobs
8
+ LANGUAGE plpgsql
9
+ AS $$
10
+ DECLARE
11
+ v_job_id bigint;
12
+ v_row app_jobs.scheduled_jobs;
13
+ BEGIN
14
+ IF worker_id IS NULL THEN
15
+ RAISE exception 'INVALID_WORKER_ID';
16
+ END IF;
17
+ SELECT
18
+ scheduled_jobs.id INTO v_job_id
19
+ FROM
20
+ app_jobs.scheduled_jobs
21
+ WHERE (scheduled_jobs.locked_at IS NULL)
22
+ AND (task_identifiers IS NULL
23
+ OR task_identifier = ANY (task_identifiers))
24
+ ORDER BY
25
+ priority ASC,
26
+ id ASC
27
+ LIMIT 1
28
+ FOR UPDATE
29
+ SKIP LOCKED;
30
+ IF v_job_id IS NULL THEN
31
+ RETURN NULL;
32
+ END IF;
33
+ UPDATE
34
+ app_jobs.scheduled_jobs
35
+ SET
36
+ locked_by = worker_id,
37
+ locked_at = NOW()
38
+ WHERE
39
+ id = v_job_id
40
+ RETURNING
41
+ * INTO v_row;
42
+ RETURN v_row;
43
+ END;
44
+ $$;
45
+ COMMIT;
46
+
@@ -0,0 +1,24 @@
1
+ -- Deploy schemas/app_jobs/procedures/permanently_fail_jobs to pg
2
+ -- requires: schemas/app_jobs/schema
3
+ -- requires: schemas/app_jobs/tables/job_queues/table
4
+ -- requires: schemas/app_jobs/tables/jobs/table
5
+
6
+ BEGIN;
7
+ CREATE FUNCTION app_jobs.permanently_fail_jobs (job_ids bigint[], error_message text DEFAULT NULL)
8
+ RETURNS SETOF app_jobs.jobs
9
+ LANGUAGE sql
10
+ AS $$
11
+ UPDATE
12
+ app_jobs.jobs
13
+ SET
14
+ last_error = coalesce(error_message, 'Manually marked as failed'),
15
+ attempts = max_attempts
16
+ WHERE
17
+ id = ANY (job_ids)
18
+ AND (locked_by IS NULL
19
+ OR locked_at < NOW() - interval '4 hours')
20
+ RETURNING
21
+ *;
22
+ $$;
23
+ COMMIT;
24
+
@@ -0,0 +1,34 @@
1
+ -- Deploy schemas/app_jobs/procedures/release_jobs 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.release_jobs (worker_id text)
8
+ RETURNS void
9
+ AS $$
10
+ DECLARE
11
+ BEGIN
12
+ -- clear the job
13
+ UPDATE
14
+ app_jobs.jobs
15
+ SET
16
+ locked_at = NULL,
17
+ locked_by = NULL,
18
+ attempts = GREATEST (attempts - 1, 0)
19
+ WHERE
20
+ locked_by = worker_id;
21
+ -- clear the queue
22
+ UPDATE
23
+ app_jobs.job_queues
24
+ SET
25
+ locked_at = NULL,
26
+ locked_by = NULL
27
+ WHERE
28
+ locked_by = worker_id;
29
+ END;
30
+ $$
31
+ LANGUAGE 'plpgsql'
32
+ VOLATILE;
33
+ COMMIT;
34
+
@@ -0,0 +1,26 @@
1
+ -- Deploy schemas/app_jobs/procedures/release_scheduled_jobs to pg
2
+ -- requires: schemas/app_jobs/schema
3
+ -- requires: schemas/app_jobs/tables/scheduled_jobs/table
4
+
5
+ BEGIN;
6
+ CREATE FUNCTION app_jobs.release_scheduled_jobs (worker_id text, ids bigint[] DEFAULT NULL)
7
+ RETURNS void
8
+ AS $$
9
+ DECLARE
10
+ BEGIN
11
+ -- clear the scheduled job
12
+ UPDATE
13
+ app_jobs.scheduled_jobs s
14
+ SET
15
+ locked_at = NULL,
16
+ locked_by = NULL
17
+ WHERE
18
+ locked_by = worker_id
19
+ AND (ids IS NULL
20
+ OR s.id = ANY (ids));
21
+ END;
22
+ $$
23
+ LANGUAGE 'plpgsql'
24
+ VOLATILE;
25
+ COMMIT;
26
+