@pgpm/database-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 +19 -0
  5. package/__tests__/jobs.test.ts +138 -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 +103 -0
  8. package/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql +97 -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 +92 -0
  14. package/deploy/schemas/app_jobs/procedures/get_scheduled_job.sql +61 -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 +78 -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 +27 -0
  28. package/deploy/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql +45 -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 +27 -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 +27 -0
  40. package/deploy/schemas/app_jobs/triggers/tg_update_timestamps.sql +21 -0
  41. package/jest.config.js +15 -0
  42. package/launchql-database-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-database-jobs--0.4.6.sql +769 -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-database-jobs
2
+ DATA = sql/launchql-database-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/database-jobs
2
+
3
+ Database-specific job handling and queue management.
4
+
5
+ Extends the core jobs system with database-specific functionality for managing background tasks and job processing workflows.
@@ -0,0 +1,19 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`scheduled jobs schedule jobs 1`] = `
4
+ {
5
+ "attempts": 0,
6
+ "database_id": "5b720132-17d5-424d-9bcb-ee7b17c13d43",
7
+ "id": "1",
8
+ "key": null,
9
+ "last_error": null,
10
+ "locked_at": null,
11
+ "locked_by": null,
12
+ "max_attempts": 25,
13
+ "payload": {
14
+ "just": "run it",
15
+ },
16
+ "priority": 0,
17
+ "task_identifier": "my_job",
18
+ }
19
+ `;
@@ -0,0 +1,138 @@
1
+ import { getConnections, PgTestClient } from 'pgsql-test';
2
+
3
+ let pg: PgTestClient;
4
+ let teardown: () => Promise<void>;
5
+
6
+ const database_id = '5b720132-17d5-424d-9bcb-ee7b17c13d43';
7
+ const objs: Record<string, any> = {};
8
+
9
+ describe('scheduled jobs', () => {
10
+ beforeAll(async () => {
11
+ ({ pg, teardown } = await getConnections());
12
+ });
13
+
14
+ afterAll(async () => {
15
+ await teardown();
16
+ });
17
+
18
+ it('schedule jobs by cron', async () => {
19
+ const result = await pg.one(
20
+ `INSERT INTO app_jobs.scheduled_jobs (database_id, task_identifier, schedule_info)
21
+ VALUES ($1, $2, $3)
22
+ RETURNING *`,
23
+ [
24
+ database_id,
25
+ 'my_job',
26
+ {
27
+ hour: Array.from({ length: 23 }, (_, i) => i),
28
+ minute: [0, 15, 30, 45],
29
+ dayOfWeek: Array.from({ length: 6 }, (_, i) => i)
30
+ }
31
+ ]
32
+ );
33
+ objs.scheduled1 = result;
34
+ });
35
+
36
+ it('schedule jobs by rule', async () => {
37
+ const start = new Date(Date.now() + 10000); // 10s from now
38
+ const end = new Date(start.getTime() + 180000); // +3min
39
+
40
+ const result = await pg.one(
41
+ `INSERT INTO app_jobs.scheduled_jobs (database_id, task_identifier, payload, schedule_info)
42
+ VALUES ($1, $2, $3, $4)
43
+ RETURNING *`,
44
+ [
45
+ database_id,
46
+ 'my_job',
47
+ { just: 'run it' },
48
+ { start, end, rule: '*/1 * * * *' }
49
+ ]
50
+ );
51
+ objs.scheduled2 = result;
52
+ });
53
+
54
+ it('schedule jobs', async () => {
55
+ const [result] = await pg.any(
56
+ `SELECT * FROM app_jobs.run_scheduled_job($1)`,
57
+ [objs.scheduled2.id]
58
+ );
59
+
60
+ const { queue_name, run_at, created_at, updated_at, ...obj } = result;
61
+ expect(obj).toMatchSnapshot();
62
+ });
63
+
64
+ it('schedule jobs with keys', async () => {
65
+ const start = new Date(Date.now() + 10000); // 10s
66
+ const end = new Date(start.getTime() + 180000); // +3min
67
+
68
+ const [result] = await pg.any(
69
+ `SELECT * FROM app_jobs.add_scheduled_job(
70
+ db_id := $1::uuid,
71
+ identifier := $2::text,
72
+ payload := $3::json,
73
+ schedule_info := $4::json,
74
+ job_key := $5::text,
75
+ queue_name := $6::text,
76
+ max_attempts := $7::integer,
77
+ priority := $8::integer
78
+ )`,
79
+ [
80
+ database_id,
81
+ 'my_job',
82
+ { just: 'run it' },
83
+ { start, end, rule: '*/1 * * * *' },
84
+ 'new_key',
85
+ null,
86
+ 25,
87
+ 0
88
+ ]
89
+ );
90
+
91
+ const {
92
+ queue_name,
93
+ run_at,
94
+ created_at,
95
+ updated_at,
96
+ schedule_info: sch,
97
+ start: s1,
98
+ end: d1,
99
+ ...obj
100
+ } = result;
101
+
102
+ const [result2] = await pg.any(
103
+ `SELECT * FROM app_jobs.add_scheduled_job(
104
+ db_id := $1,
105
+ identifier := $2,
106
+ payload := $3,
107
+ schedule_info := $4,
108
+ job_key := $5,
109
+ queue_name := $6,
110
+ max_attempts := $7,
111
+ priority := $8
112
+ )`,
113
+ [
114
+ database_id,
115
+ 'my_job',
116
+ { just: 'run it' },
117
+ { start, end, rule: '*/1 * * * *' },
118
+ 'new_key',
119
+ null,
120
+ 25,
121
+ 0
122
+ ]
123
+ );
124
+
125
+ const {
126
+ queue_name: qn,
127
+ created_at: ca,
128
+ updated_at: ua,
129
+ schedule_info: sch2,
130
+ start: s,
131
+ end: e,
132
+ ...obj2
133
+ } = result2;
134
+
135
+ console.log('First insert:', obj);
136
+ console.log('Duplicate insert (job_key conflict):', obj2);
137
+ });
138
+ });
@@ -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,103 @@
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
+ db_id uuid,
9
+ identifier text,
10
+ payload json DEFAULT '{}' ::json,
11
+ job_key text DEFAULT NULL,
12
+ queue_name text DEFAULT NULL,
13
+ run_at timestamptz DEFAULT now(),
14
+ max_attempts integer DEFAULT 25,
15
+ priority integer DEFAULT 0
16
+ )
17
+ RETURNS app_jobs.jobs
18
+ AS $$
19
+ DECLARE
20
+ v_job app_jobs.jobs;
21
+ BEGIN
22
+ IF job_key IS NOT NULL THEN
23
+ -- Upsert job
24
+ INSERT INTO app_jobs.jobs (
25
+ database_id,
26
+ task_identifier,
27
+ payload,
28
+ queue_name,
29
+ run_at,
30
+ max_attempts,
31
+ key,
32
+ priority
33
+ ) VALUES (
34
+ db_id,
35
+ identifier,
36
+ coalesce(payload,
37
+ '{}'::json),
38
+ queue_name,
39
+ coalesce(run_at, now()),
40
+ coalesce(max_attempts, 25),
41
+ job_key,
42
+ coalesce(priority, 0)
43
+ )
44
+ ON CONFLICT (key)
45
+ DO UPDATE SET
46
+ task_identifier = EXCLUDED.task_identifier,
47
+ payload = EXCLUDED.payload,
48
+ queue_name = EXCLUDED.queue_name,
49
+ max_attempts = EXCLUDED.max_attempts,
50
+ run_at = EXCLUDED.run_at,
51
+ priority = EXCLUDED.priority,
52
+ -- always reset error/retry state
53
+ attempts = 0, last_error = NULL
54
+ WHERE
55
+ jobs.locked_at IS NULL
56
+ RETURNING
57
+ * INTO v_job;
58
+
59
+ -- If upsert succeeded (insert or update), return early
60
+
61
+ IF NOT (v_job IS NULL) THEN
62
+ RETURN v_job;
63
+ END IF;
64
+
65
+ -- Upsert failed -> there must be an existing job that is locked. Remove
66
+ -- existing key to allow a new one to be inserted, and prevent any
67
+ -- subsequent retries by bumping attempts to the max allowed.
68
+
69
+ UPDATE
70
+ app_jobs.jobs
71
+ SET
72
+ KEY = NULL,
73
+ attempts = jobs.max_attempts
74
+ WHERE
75
+ KEY = job_key;
76
+ END IF;
77
+
78
+ INSERT INTO app_jobs.jobs (
79
+ database_id,
80
+ task_identifier,
81
+ payload,
82
+ queue_name,
83
+ run_at,
84
+ max_attempts,
85
+ priority
86
+ ) VALUES (
87
+ db_id,
88
+ identifier,
89
+ payload,
90
+ queue_name,
91
+ run_at,
92
+ max_attempts,
93
+ priority
94
+ )
95
+ RETURNING * INTO v_job;
96
+
97
+ RETURN v_job;
98
+ END;
99
+ $$
100
+ LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER;
101
+
102
+ COMMIT;
103
+
@@ -0,0 +1,97 @@
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
+ db_id uuid,
10
+ identifier text,
11
+ payload json DEFAULT '{}'::json,
12
+ schedule_info json DEFAULT '{}'::json,
13
+ job_key text DEFAULT NULL,
14
+ queue_name text DEFAULT NULL,
15
+ max_attempts integer DEFAULT 25,
16
+ priority integer DEFAULT 0
17
+ )
18
+ RETURNS app_jobs.scheduled_jobs
19
+ AS $$
20
+ DECLARE
21
+ v_job app_jobs.scheduled_jobs;
22
+ BEGIN
23
+ IF job_key IS NOT NULL THEN
24
+
25
+ -- Upsert job
26
+ INSERT INTO app_jobs.scheduled_jobs (
27
+ database_id,
28
+ task_identifier,
29
+ payload,
30
+ queue_name,
31
+ schedule_info,
32
+ max_attempts,
33
+ key,
34
+ priority
35
+ ) VALUES (
36
+ db_id,
37
+ identifier,
38
+ coalesce(payload, '{}'::json),
39
+ queue_name,
40
+ schedule_info,
41
+ coalesce(max_attempts, 25),
42
+ job_key,
43
+ coalesce(priority, 0)
44
+ )
45
+ ON CONFLICT (key)
46
+ DO UPDATE SET
47
+ task_identifier = EXCLUDED.task_identifier,
48
+ payload = EXCLUDED.payload,
49
+ queue_name = EXCLUDED.queue_name,
50
+ max_attempts = EXCLUDED.max_attempts,
51
+ schedule_info = EXCLUDED.schedule_info,
52
+ priority = EXCLUDED.priority
53
+ WHERE
54
+ scheduled_jobs.locked_at IS NULL
55
+ RETURNING
56
+ * INTO v_job;
57
+
58
+ -- If upsert succeeded (insert or update), return early
59
+
60
+ IF NOT (v_job IS NULL) THEN
61
+ RETURN v_job;
62
+ END IF;
63
+
64
+ -- Upsert failed -> there must be an existing scheduled job that is locked. Remove
65
+ -- and allow a new one to be inserted
66
+
67
+ DELETE FROM
68
+ app_jobs.scheduled_jobs
69
+ WHERE
70
+ KEY = job_key;
71
+ END IF;
72
+
73
+ INSERT INTO app_jobs.scheduled_jobs (
74
+ database_id,
75
+ task_identifier,
76
+ payload,
77
+ queue_name,
78
+ schedule_info,
79
+ max_attempts,
80
+ priority
81
+ ) VALUES (
82
+ db_id,
83
+ identifier,
84
+ payload,
85
+ queue_name,
86
+ schedule_info,
87
+ max_attempts,
88
+ priority
89
+ ) RETURNING * INTO v_job;
90
+ RETURN v_job;
91
+ END;
92
+ $$
93
+ LANGUAGE 'plpgsql'
94
+ VOLATILE
95
+ SECURITY DEFINER;
96
+ COMMIT;
97
+
@@ -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,92 @@
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
+
18
+ IF worker_id IS NULL THEN
19
+ RAISE exception 'INVALID_WORKER_ID';
20
+ END IF;
21
+
22
+ --
23
+
24
+ SELECT
25
+ jobs.queue_name,
26
+ jobs.id INTO v_queue_name,
27
+ v_job_id
28
+ FROM
29
+ app_jobs.jobs
30
+ WHERE (jobs.locked_at IS NULL
31
+ OR jobs.locked_at < (v_now - job_expiry))
32
+ AND (jobs.queue_name IS NULL
33
+ OR EXISTS (
34
+ SELECT
35
+ 1
36
+ FROM
37
+ app_jobs.job_queues
38
+ WHERE
39
+ job_queues.queue_name = jobs.queue_name
40
+ AND (job_queues.locked_at IS NULL
41
+ OR job_queues.locked_at < (v_now - job_expiry))
42
+ FOR UPDATE
43
+ SKIP LOCKED))
44
+ AND run_at <= v_now
45
+ AND attempts < max_attempts
46
+ AND (task_identifiers IS NULL
47
+ OR task_identifier = ANY (task_identifiers))
48
+ ORDER BY
49
+ priority ASC,
50
+ run_at ASC,
51
+ id ASC
52
+ LIMIT 1
53
+ FOR UPDATE
54
+ SKIP LOCKED;
55
+
56
+ --
57
+
58
+ IF v_job_id IS NULL THEN
59
+ RETURN NULL;
60
+ END IF;
61
+
62
+ --
63
+
64
+ IF v_queue_name IS NOT NULL THEN
65
+ UPDATE
66
+ app_jobs.job_queues
67
+ SET
68
+ locked_by = worker_id,
69
+ locked_at = v_now
70
+ WHERE
71
+ job_queues.queue_name = v_queue_name;
72
+ END IF;
73
+
74
+ --
75
+
76
+ UPDATE
77
+ app_jobs.jobs
78
+ SET
79
+ attempts = attempts + 1,
80
+ locked_by = worker_id,
81
+ locked_at = v_now
82
+ WHERE
83
+ id = v_job_id
84
+ RETURNING
85
+ * INTO v_row;
86
+
87
+ --
88
+ RETURN v_row;
89
+ END;
90
+ $$;
91
+ COMMIT;
92
+
@@ -0,0 +1,61 @@
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
+
15
+ --
16
+
17
+ IF worker_id IS NULL THEN
18
+ RAISE exception 'INVALID_WORKER_ID';
19
+ END IF;
20
+
21
+ --
22
+
23
+ SELECT
24
+ scheduled_jobs.id INTO v_job_id
25
+ FROM
26
+ app_jobs.scheduled_jobs
27
+ WHERE (scheduled_jobs.locked_at IS NULL)
28
+ AND (task_identifiers IS NULL
29
+ OR task_identifier = ANY (task_identifiers))
30
+ ORDER BY
31
+ priority ASC,
32
+ id ASC
33
+ LIMIT 1
34
+ FOR UPDATE
35
+ SKIP LOCKED;
36
+
37
+ --
38
+
39
+ IF v_job_id IS NULL THEN
40
+ RETURN NULL;
41
+ END IF;
42
+
43
+ --
44
+
45
+ UPDATE
46
+ app_jobs.scheduled_jobs
47
+ SET
48
+ locked_by = worker_id,
49
+ locked_at = NOW()
50
+ WHERE
51
+ id = v_job_id
52
+ RETURNING
53
+ * INTO v_row;
54
+
55
+ --
56
+
57
+ RETURN v_row;
58
+ END;
59
+ $$;
60
+ COMMIT;
61
+