@pgpm/database-jobs 0.4.0 → 0.6.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.
@@ -6,6 +6,53 @@ GRANT USAGE ON SCHEMA app_jobs TO administrator;
6
6
  ALTER DEFAULT PRIVILEGES IN SCHEMA app_jobs
7
7
  GRANT EXECUTE ON FUNCTIONS TO administrator;
8
8
 
9
+ CREATE FUNCTION app_jobs.tg_update_timestamps() RETURNS trigger AS $EOFCODE$
10
+ BEGIN
11
+ IF TG_OP = 'INSERT' THEN
12
+ NEW.created_at = NOW();
13
+ NEW.updated_at = NOW();
14
+ ELSIF TG_OP = 'UPDATE' THEN
15
+ NEW.created_at = OLD.created_at;
16
+ NEW.updated_at = greatest (now(), OLD.updated_at + interval '1 millisecond');
17
+ END IF;
18
+ RETURN NEW;
19
+ END;
20
+ $EOFCODE$ LANGUAGE plpgsql;
21
+
22
+ CREATE FUNCTION app_jobs.tg_add_job_with_row_id() RETURNS trigger AS $EOFCODE$
23
+ BEGIN
24
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
25
+ PERFORM
26
+ app_jobs.add_job (jwt_private.current_database_id(), tg_argv[0], json_build_object('id', NEW.id));
27
+ RETURN NEW;
28
+ END IF;
29
+ IF (TG_OP = 'DELETE') THEN
30
+ PERFORM
31
+ app_jobs.add_job (jwt_private.current_database_id(), tg_argv[0], json_build_object('id', OLD.id));
32
+ RETURN OLD;
33
+ END IF;
34
+ END;
35
+ $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
36
+
37
+ COMMENT ON FUNCTION app_jobs.tg_add_job_with_row_id IS 'Useful shortcut to create a job on insert or update. Pass the task name as the trigger argument, and the record id will automatically be available on the JSON payload.';
38
+
39
+ CREATE FUNCTION app_jobs.tg_add_job_with_row() RETURNS trigger AS $EOFCODE$
40
+ BEGIN
41
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
42
+ PERFORM
43
+ app_jobs.add_job (jwt_private.current_database_id(), TG_ARGV[0], to_json(NEW));
44
+ RETURN NEW;
45
+ END IF;
46
+ IF (TG_OP = 'DELETE') THEN
47
+ PERFORM
48
+ app_jobs.add_job (jwt_private.current_database_id(), TG_ARGV[0], to_json(OLD));
49
+ RETURN OLD;
50
+ END IF;
51
+ END;
52
+ $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
53
+
54
+ COMMENT ON FUNCTION app_jobs.tg_add_job_with_row IS 'Useful shortcut to create a job on insert or update. Pass the task name as the trigger argument, and the record data will automatically be available on the JSON payload.';
55
+
9
56
  CREATE FUNCTION app_jobs.json_build_object_apply(arguments text[]) RETURNS pg_catalog.json AS $EOFCODE$
10
57
  DECLARE
11
58
  arg text;
@@ -26,20 +73,59 @@ BEGIN
26
73
  END;
27
74
  $EOFCODE$ LANGUAGE plpgsql;
28
75
 
29
- CREATE TABLE app_jobs.jobs (
76
+ CREATE FUNCTION app_jobs.trigger_job_with_fields() RETURNS trigger AS $EOFCODE$
77
+ DECLARE
78
+ arg text;
79
+ fn text;
80
+ i int;
81
+ args text[];
82
+ BEGIN
83
+ FOR i IN
84
+ SELECT
85
+ *
86
+ FROM
87
+ generate_series(1, TG_NARGS) g (i)
88
+ LOOP
89
+ IF (i = 1) THEN
90
+ fn = TG_ARGV[i - 1];
91
+ ELSE
92
+ args = array_append(args, TG_ARGV[i - 1]);
93
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
94
+ EXECUTE format('SELECT ($1).%s::text', TG_ARGV[i - 1])
95
+ USING NEW INTO arg;
96
+ END IF;
97
+ IF (TG_OP = 'DELETE') THEN
98
+ EXECUTE format('SELECT ($1).%s::text', TG_ARGV[i - 1])
99
+ USING OLD INTO arg;
100
+ END IF;
101
+ args = array_append(args, arg);
102
+ END IF;
103
+ END LOOP;
104
+ PERFORM
105
+ app_jobs.add_job (jwt_private.current_database_id(), fn, app_jobs.json_build_object_apply (args));
106
+ IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
107
+ RETURN NEW;
108
+ END IF;
109
+ IF (TG_OP = 'DELETE') THEN
110
+ RETURN OLD;
111
+ END IF;
112
+ END;
113
+ $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
114
+
115
+ CREATE TABLE app_jobs.scheduled_jobs (
30
116
  id bigserial PRIMARY KEY,
31
117
  database_id uuid NOT NULL,
32
118
  queue_name text DEFAULT public.gen_random_uuid()::text,
33
119
  task_identifier text NOT NULL,
34
120
  payload pg_catalog.json DEFAULT '{}'::json NOT NULL,
35
121
  priority int DEFAULT 0 NOT NULL,
36
- run_at timestamptz DEFAULT now() NOT NULL,
37
- attempts int DEFAULT 0 NOT NULL,
38
122
  max_attempts int DEFAULT 25 NOT NULL,
39
123
  key text,
40
- last_error text,
41
124
  locked_at timestamptz,
42
125
  locked_by text,
126
+ schedule_info pg_catalog.json NOT NULL,
127
+ last_scheduled timestamptz,
128
+ last_scheduled_id bigint,
43
129
  CHECK (length(key) < 513),
44
130
  CHECK (length(task_identifier) < 127),
45
131
  CHECK (max_attempts > 0),
@@ -48,110 +134,40 @@ CREATE TABLE app_jobs.jobs (
48
134
  UNIQUE (key)
49
135
  );
50
136
 
51
- CREATE TABLE app_jobs.job_queues (
52
- queue_name text NOT NULL PRIMARY KEY,
53
- job_count int DEFAULT 0 NOT NULL,
54
- locked_at timestamptz,
55
- locked_by text
56
- );
57
-
58
- CREATE FUNCTION app_jobs.add_job(db_id uuid, 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$
59
- DECLARE
60
- v_job app_jobs.jobs;
137
+ CREATE FUNCTION app_jobs.do_notify() RETURNS trigger AS $EOFCODE$
61
138
  BEGIN
62
- IF job_key IS NOT NULL THEN
63
- -- Upsert job
64
- INSERT INTO app_jobs.jobs (
65
- database_id,
66
- task_identifier,
67
- payload,
68
- queue_name,
69
- run_at,
70
- max_attempts,
71
- key,
72
- priority
73
- ) VALUES (
74
- db_id,
75
- identifier,
76
- coalesce(payload,
77
- '{}'::json),
78
- queue_name,
79
- coalesce(run_at, now()),
80
- coalesce(max_attempts, 25),
81
- job_key,
82
- coalesce(priority, 0)
83
- )
84
- ON CONFLICT (key)
85
- DO UPDATE SET
86
- task_identifier = EXCLUDED.task_identifier,
87
- payload = EXCLUDED.payload,
88
- queue_name = EXCLUDED.queue_name,
89
- max_attempts = EXCLUDED.max_attempts,
90
- run_at = EXCLUDED.run_at,
91
- priority = EXCLUDED.priority,
92
- -- always reset error/retry state
93
- attempts = 0, last_error = NULL
94
- WHERE
95
- jobs.locked_at IS NULL
96
- RETURNING
97
- * INTO v_job;
98
-
99
- -- If upsert succeeded (insert or update), return early
100
-
101
- IF NOT (v_job IS NULL) THEN
102
- RETURN v_job;
103
- END IF;
139
+ PERFORM
140
+ pg_notify(TG_ARGV[0], '');
141
+ RETURN NEW;
142
+ END;
143
+ $EOFCODE$ LANGUAGE plpgsql;
104
144
 
105
- -- Upsert failed -> there must be an existing job that is locked. Remove
106
- -- existing key to allow a new one to be inserted, and prevent any
107
- -- subsequent retries by bumping attempts to the max allowed.
145
+ CREATE TRIGGER _900_notify_scheduled_job
146
+ AFTER INSERT
147
+ ON app_jobs.scheduled_jobs
148
+ FOR EACH ROW
149
+ EXECUTE PROCEDURE app_jobs.do_notify('scheduled_jobs:insert');
108
150
 
109
- UPDATE
110
- app_jobs.jobs
111
- SET
112
- KEY = NULL,
113
- attempts = jobs.max_attempts
114
- WHERE
115
- KEY = job_key;
116
- END IF;
151
+ CREATE INDEX scheduled_jobs_priority_id_idx ON app_jobs.scheduled_jobs (priority, id);
117
152
 
118
- INSERT INTO app_jobs.jobs (
119
- database_id,
120
- task_identifier,
121
- payload,
122
- queue_name,
123
- run_at,
124
- max_attempts,
125
- priority
126
- ) VALUES (
127
- db_id,
128
- identifier,
129
- payload,
130
- queue_name,
131
- run_at,
132
- max_attempts,
133
- priority
134
- )
135
- RETURNING * INTO v_job;
153
+ CREATE INDEX scheduled_jobs_locked_by_idx ON app_jobs.scheduled_jobs (locked_by);
136
154
 
137
- RETURN v_job;
138
- END;
139
- $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
155
+ GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.scheduled_jobs TO administrator;
140
156
 
141
- CREATE TABLE app_jobs.scheduled_jobs (
157
+ CREATE TABLE app_jobs.jobs (
142
158
  id bigserial PRIMARY KEY,
143
159
  database_id uuid NOT NULL,
144
160
  queue_name text DEFAULT public.gen_random_uuid()::text,
145
161
  task_identifier text NOT NULL,
146
162
  payload pg_catalog.json DEFAULT '{}'::json NOT NULL,
147
163
  priority int DEFAULT 0 NOT NULL,
164
+ run_at timestamptz DEFAULT now() NOT NULL,
165
+ attempts int DEFAULT 0 NOT NULL,
148
166
  max_attempts int DEFAULT 25 NOT NULL,
149
167
  key text,
168
+ last_error text,
150
169
  locked_at timestamptz,
151
170
  locked_by text,
152
- schedule_info pg_catalog.json NOT NULL,
153
- last_scheduled timestamptz,
154
- last_scheduled_id bigint,
155
171
  CHECK (length(key) < 513),
156
172
  CHECK (length(task_identifier) < 127),
157
173
  CHECK (max_attempts > 0),
@@ -160,366 +176,122 @@ CREATE TABLE app_jobs.scheduled_jobs (
160
176
  UNIQUE (key)
161
177
  );
162
178
 
163
- CREATE FUNCTION app_jobs.add_scheduled_job(db_id uuid, 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$
164
- DECLARE
165
- v_job app_jobs.scheduled_jobs;
166
- BEGIN
167
- IF job_key IS NOT NULL THEN
179
+ ALTER TABLE app_jobs.jobs
180
+ ADD COLUMN created_at timestamptz;
168
181
 
169
- -- Upsert job
170
- INSERT INTO app_jobs.scheduled_jobs (
171
- database_id,
172
- task_identifier,
173
- payload,
174
- queue_name,
175
- schedule_info,
176
- max_attempts,
177
- key,
178
- priority
179
- ) VALUES (
180
- db_id,
181
- identifier,
182
- coalesce(payload, '{}'::json),
183
- queue_name,
184
- schedule_info,
185
- coalesce(max_attempts, 25),
186
- job_key,
187
- coalesce(priority, 0)
188
- )
189
- ON CONFLICT (key)
190
- DO UPDATE SET
191
- task_identifier = EXCLUDED.task_identifier,
192
- payload = EXCLUDED.payload,
193
- queue_name = EXCLUDED.queue_name,
194
- max_attempts = EXCLUDED.max_attempts,
195
- schedule_info = EXCLUDED.schedule_info,
196
- priority = EXCLUDED.priority
197
- WHERE
198
- scheduled_jobs.locked_at IS NULL
199
- RETURNING
200
- * INTO v_job;
182
+ ALTER TABLE app_jobs.jobs
183
+ ALTER COLUMN created_at SET DEFAULT now();
201
184
 
202
- -- If upsert succeeded (insert or update), return early
203
-
204
- IF NOT (v_job IS NULL) THEN
205
- RETURN v_job;
206
- END IF;
185
+ ALTER TABLE app_jobs.jobs
186
+ ADD COLUMN updated_at timestamptz;
207
187
 
208
- -- Upsert failed -> there must be an existing scheduled job that is locked. Remove
209
- -- and allow a new one to be inserted
188
+ ALTER TABLE app_jobs.jobs
189
+ ALTER COLUMN updated_at SET DEFAULT now();
210
190
 
211
- DELETE FROM
212
- app_jobs.scheduled_jobs
213
- WHERE
214
- KEY = job_key;
215
- END IF;
216
-
217
- INSERT INTO app_jobs.scheduled_jobs (
218
- database_id,
219
- task_identifier,
220
- payload,
221
- queue_name,
222
- schedule_info,
223
- max_attempts,
224
- priority
225
- ) VALUES (
226
- db_id,
227
- identifier,
228
- payload,
229
- queue_name,
230
- schedule_info,
231
- max_attempts,
232
- priority
233
- ) RETURNING * INTO v_job;
234
- RETURN v_job;
235
- END;
236
- $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
191
+ CREATE TRIGGER _100_update_jobs_modtime_tg
192
+ BEFORE INSERT OR UPDATE
193
+ ON app_jobs.jobs
194
+ FOR EACH ROW
195
+ EXECUTE PROCEDURE app_jobs.tg_update_timestamps();
237
196
 
238
- CREATE FUNCTION app_jobs.complete_job(worker_id text, job_id bigint) RETURNS app_jobs.jobs LANGUAGE plpgsql AS $EOFCODE$
239
- DECLARE
240
- v_row app_jobs.jobs;
197
+ CREATE FUNCTION app_jobs.tg_increase_job_queue_count() RETURNS trigger AS $EOFCODE$
241
198
  BEGIN
242
- DELETE FROM app_jobs.jobs
243
- WHERE id = job_id
244
- RETURNING
245
- * INTO v_row;
246
- IF v_row.queue_name IS NOT NULL THEN
247
- UPDATE
248
- app_jobs.job_queues
249
- SET
250
- locked_by = NULL,
251
- locked_at = NULL
252
- WHERE
253
- queue_name = v_row.queue_name
254
- AND locked_by = worker_id;
255
- END IF;
256
- RETURN v_row;
199
+ INSERT INTO app_jobs.job_queues (queue_name, job_count)
200
+ VALUES (NEW.queue_name, 1)
201
+ ON CONFLICT (queue_name)
202
+ DO UPDATE SET
203
+ job_count = job_queues.job_count + 1;
204
+ RETURN NEW;
257
205
  END;
258
- $EOFCODE$;
206
+ $EOFCODE$ LANGUAGE plpgsql VOLATILE;
259
207
 
260
- CREATE FUNCTION app_jobs.complete_jobs(job_ids bigint[]) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$
261
- DELETE FROM app_jobs.jobs
262
- WHERE id = ANY (job_ids)
263
- AND (locked_by IS NULL
264
- OR locked_at < NOW() - interval '4 hours')
265
- RETURNING
266
- *;
267
- $EOFCODE$;
208
+ CREATE TRIGGER _500_increase_job_queue_count_on_insert
209
+ AFTER INSERT
210
+ ON app_jobs.jobs
211
+ FOR EACH ROW
212
+ WHEN (new.queue_name IS NOT NULL)
213
+ EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count();
268
214
 
269
- CREATE FUNCTION app_jobs.do_notify() RETURNS trigger AS $EOFCODE$
270
- BEGIN
271
- PERFORM
272
- pg_notify(TG_ARGV[0], '');
273
- RETURN NEW;
274
- END;
275
- $EOFCODE$ LANGUAGE plpgsql;
215
+ CREATE TRIGGER _500_increase_job_queue_count_on_update
216
+ AFTER UPDATE OF queue_name
217
+ ON app_jobs.jobs
218
+ FOR EACH ROW
219
+ WHEN (new.queue_name IS DISTINCT FROM old.queue_name
220
+ AND new.queue_name IS NOT NULL)
221
+ EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count();
276
222
 
277
- CREATE FUNCTION app_jobs.fail_job(worker_id text, job_id bigint, error_message text) RETURNS app_jobs.jobs LANGUAGE plpgsql STRICT AS $EOFCODE$
223
+ CREATE TRIGGER _900_notify_worker
224
+ AFTER INSERT
225
+ ON app_jobs.jobs
226
+ FOR EACH ROW
227
+ EXECUTE PROCEDURE app_jobs.do_notify('jobs:insert');
228
+
229
+ CREATE FUNCTION app_jobs.tg_decrease_job_queue_count() RETURNS trigger AS $EOFCODE$
278
230
  DECLARE
279
- v_row app_jobs.jobs;
231
+ v_new_job_count int;
280
232
  BEGIN
281
233
  UPDATE
282
- app_jobs.jobs
234
+ app_jobs.job_queues
283
235
  SET
284
- last_error = error_message,
285
- run_at = greatest (now(), run_at) + (exp(least (attempts, 10))::text || ' seconds')::interval,
286
- locked_by = NULL,
287
- locked_at = NULL
236
+ job_count = job_queues.job_count - 1
288
237
  WHERE
289
- id = job_id
290
- AND locked_by = worker_id
238
+ queue_name = OLD.queue_name
291
239
  RETURNING
292
- * INTO v_row;
293
- IF v_row.queue_name IS NOT NULL THEN
294
- UPDATE
295
- app_jobs.job_queues
296
- SET
297
- locked_by = NULL,
298
- locked_at = NULL
299
- WHERE
300
- queue_name = v_row.queue_name
301
- AND locked_by = worker_id;
240
+ job_count INTO v_new_job_count;
241
+ IF v_new_job_count <= 0 THEN
242
+ DELETE FROM app_jobs.job_queues
243
+ WHERE queue_name = OLD.queue_name
244
+ AND job_count <= 0;
302
245
  END IF;
303
- RETURN v_row;
246
+ RETURN OLD;
304
247
  END;
305
- $EOFCODE$;
306
-
307
- CREATE FUNCTION app_jobs.get_job(worker_id text, task_identifiers text[] DEFAULT NULL, job_expiry interval DEFAULT '4 hours') RETURNS app_jobs.jobs LANGUAGE plpgsql AS $EOFCODE$
308
- DECLARE
309
- v_job_id bigint;
310
- v_queue_name text;
311
- v_row app_jobs.jobs;
312
- v_now timestamptz = now();
313
- BEGIN
314
-
315
- IF worker_id IS NULL THEN
316
- RAISE exception 'INVALID_WORKER_ID';
317
- END IF;
318
-
319
- --
248
+ $EOFCODE$ LANGUAGE plpgsql VOLATILE;
320
249
 
321
- SELECT
322
- jobs.queue_name,
323
- jobs.id INTO v_queue_name,
324
- v_job_id
325
- FROM
326
- app_jobs.jobs
327
- WHERE (jobs.locked_at IS NULL
328
- OR jobs.locked_at < (v_now - job_expiry))
329
- AND (jobs.queue_name IS NULL
330
- OR EXISTS (
331
- SELECT
332
- 1
333
- FROM
334
- app_jobs.job_queues
335
- WHERE
336
- job_queues.queue_name = jobs.queue_name
337
- AND (job_queues.locked_at IS NULL
338
- OR job_queues.locked_at < (v_now - job_expiry))
339
- FOR UPDATE
340
- SKIP LOCKED))
341
- AND run_at <= v_now
342
- AND attempts < max_attempts
343
- AND (task_identifiers IS NULL
344
- OR task_identifier = ANY (task_identifiers))
345
- ORDER BY
346
- priority ASC,
347
- run_at ASC,
348
- id ASC
349
- LIMIT 1
350
- FOR UPDATE
351
- SKIP LOCKED;
250
+ CREATE TRIGGER decrease_job_queue_count_on_delete
251
+ AFTER DELETE
252
+ ON app_jobs.jobs
253
+ FOR EACH ROW
254
+ WHEN (old.queue_name IS NOT NULL)
255
+ EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count();
352
256
 
353
- --
257
+ CREATE TRIGGER decrease_job_queue_count_on_update
258
+ AFTER UPDATE OF queue_name
259
+ ON app_jobs.jobs
260
+ FOR EACH ROW
261
+ WHEN (new.queue_name IS DISTINCT FROM old.queue_name
262
+ AND old.queue_name IS NOT NULL)
263
+ EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count();
354
264
 
355
- IF v_job_id IS NULL THEN
356
- RETURN NULL;
357
- END IF;
265
+ CREATE INDEX priority_run_at_id_idx ON app_jobs.jobs (priority, run_at, id);
358
266
 
359
- --
267
+ CREATE INDEX jobs_locked_by_idx ON app_jobs.jobs (locked_by);
360
268
 
361
- IF v_queue_name IS NOT NULL THEN
362
- UPDATE
363
- app_jobs.job_queues
364
- SET
365
- locked_by = worker_id,
366
- locked_at = v_now
367
- WHERE
368
- job_queues.queue_name = v_queue_name;
369
- END IF;
269
+ GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.jobs TO administrator;
370
270
 
371
- --
271
+ CREATE TABLE app_jobs.job_queues (
272
+ queue_name text NOT NULL PRIMARY KEY,
273
+ job_count int DEFAULT 0 NOT NULL,
274
+ locked_at timestamptz,
275
+ locked_by text
276
+ );
372
277
 
373
- UPDATE
374
- app_jobs.jobs
375
- SET
376
- attempts = attempts + 1,
377
- locked_by = worker_id,
378
- locked_at = v_now
379
- WHERE
380
- id = v_job_id
381
- RETURNING
382
- * INTO v_row;
278
+ CREATE INDEX job_queues_locked_by_idx ON app_jobs.job_queues (locked_by);
383
279
 
384
- --
385
- RETURN v_row;
386
- END;
387
- $EOFCODE$;
280
+ GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.job_queues TO administrator;
388
281
 
389
- CREATE FUNCTION app_jobs.get_scheduled_job(worker_id text, task_identifiers text[] DEFAULT NULL) RETURNS app_jobs.scheduled_jobs LANGUAGE plpgsql AS $EOFCODE$
282
+ CREATE FUNCTION app_jobs.run_scheduled_job(id bigint, job_expiry interval DEFAULT '1 hours') RETURNS app_jobs.jobs AS $EOFCODE$
390
283
  DECLARE
391
- v_job_id bigint;
392
- v_row app_jobs.scheduled_jobs;
284
+ j app_jobs.jobs;
285
+ last_id bigint;
286
+ lkd_by text;
393
287
  BEGIN
394
-
395
- --
396
-
397
- IF worker_id IS NULL THEN
398
- RAISE exception 'INVALID_WORKER_ID';
399
- END IF;
400
-
401
- --
402
-
288
+ -- check last scheduled
403
289
  SELECT
404
- scheduled_jobs.id INTO v_job_id
290
+ last_scheduled_id
405
291
  FROM
406
- app_jobs.scheduled_jobs
407
- WHERE (scheduled_jobs.locked_at IS NULL)
408
- AND (task_identifiers IS NULL
409
- OR task_identifier = ANY (task_identifiers))
410
- ORDER BY
411
- priority ASC,
412
- id ASC
413
- LIMIT 1
414
- FOR UPDATE
415
- SKIP LOCKED;
416
-
417
- --
418
-
419
- IF v_job_id IS NULL THEN
420
- RETURN NULL;
421
- END IF;
422
-
423
- --
424
-
425
- UPDATE
426
- app_jobs.scheduled_jobs
427
- SET
428
- locked_by = worker_id,
429
- locked_at = NOW()
430
- WHERE
431
- id = v_job_id
432
- RETURNING
433
- * INTO v_row;
434
-
435
- --
436
-
437
- RETURN v_row;
438
- END;
439
- $EOFCODE$;
440
-
441
- CREATE FUNCTION app_jobs.permanently_fail_jobs(job_ids bigint[], error_message text DEFAULT NULL) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$
442
- UPDATE
443
- app_jobs.jobs
444
- SET
445
- last_error = coalesce(error_message, 'Manually marked as failed'),
446
- attempts = max_attempts
447
- WHERE
448
- id = ANY (job_ids)
449
- AND (locked_by IS NULL
450
- OR locked_at < NOW() - interval '4 hours')
451
- RETURNING
452
- *;
453
- $EOFCODE$;
454
-
455
- CREATE FUNCTION app_jobs.release_jobs(worker_id text) RETURNS void AS $EOFCODE$
456
- DECLARE
457
- BEGIN
458
- -- clear the job
459
- UPDATE
460
- app_jobs.jobs
461
- SET
462
- locked_at = NULL,
463
- locked_by = NULL,
464
- attempts = GREATEST (attempts - 1, 0)
465
- WHERE
466
- locked_by = worker_id;
467
- -- clear the queue
468
- UPDATE
469
- app_jobs.job_queues
470
- SET
471
- locked_at = NULL,
472
- locked_by = NULL
473
- WHERE
474
- locked_by = worker_id;
475
- END;
476
- $EOFCODE$ LANGUAGE plpgsql VOLATILE;
477
-
478
- CREATE FUNCTION app_jobs.release_scheduled_jobs(worker_id text, ids bigint[] DEFAULT NULL) RETURNS void AS $EOFCODE$
479
- DECLARE
480
- BEGIN
481
- -- clear the scheduled job
482
- UPDATE
483
- app_jobs.scheduled_jobs s
484
- SET
485
- locked_at = NULL,
486
- locked_by = NULL
487
- WHERE
488
- locked_by = worker_id
489
- AND (ids IS NULL
490
- OR s.id = ANY (ids));
491
- END;
492
- $EOFCODE$ LANGUAGE plpgsql VOLATILE;
493
-
494
- CREATE FUNCTION app_jobs.reschedule_jobs(job_ids bigint[], run_at timestamptz DEFAULT NULL, priority int DEFAULT NULL, attempts int DEFAULT NULL, max_attempts int DEFAULT NULL) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$
495
- UPDATE
496
- app_jobs.jobs
497
- SET
498
- run_at = coalesce(reschedule_jobs.run_at, jobs.run_at),
499
- priority = coalesce(reschedule_jobs.priority, jobs.priority),
500
- attempts = coalesce(reschedule_jobs.attempts, jobs.attempts),
501
- max_attempts = coalesce(reschedule_jobs.max_attempts, jobs.max_attempts)
502
- WHERE
503
- id = ANY (job_ids)
504
- AND (locked_by IS NULL
505
- OR locked_at < NOW() - interval '4 hours')
506
- RETURNING
507
- *;
508
- $EOFCODE$;
509
-
510
- CREATE FUNCTION app_jobs.run_scheduled_job(id bigint, job_expiry interval DEFAULT '1 hours') RETURNS app_jobs.jobs AS $EOFCODE$
511
- DECLARE
512
- j app_jobs.jobs;
513
- last_id bigint;
514
- lkd_by text;
515
- BEGIN
516
- -- check last scheduled
517
- SELECT
518
- last_scheduled_id
519
- FROM
520
- app_jobs.scheduled_jobs s
521
- WHERE
522
- s.id = run_scheduled_job.id INTO last_id;
292
+ app_jobs.scheduled_jobs s
293
+ WHERE
294
+ s.id = run_scheduled_job.id INTO last_id;
523
295
 
524
296
  -- if it's been scheduled check if it's been run
525
297
 
@@ -574,196 +346,424 @@ BEGIN
574
346
  END;
575
347
  $EOFCODE$ LANGUAGE plpgsql VOLATILE;
576
348
 
577
- GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.job_queues TO administrator;
578
-
579
- CREATE INDEX job_queues_locked_by_idx ON app_jobs.job_queues (locked_by);
580
-
581
- GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.jobs TO administrator;
582
-
583
- CREATE INDEX jobs_locked_by_idx ON app_jobs.jobs (locked_by);
584
-
585
- CREATE INDEX priority_run_at_id_idx ON app_jobs.jobs (priority, run_at, id);
349
+ CREATE FUNCTION app_jobs.reschedule_jobs(job_ids bigint[], run_at timestamptz DEFAULT NULL, priority int DEFAULT NULL, attempts int DEFAULT NULL, max_attempts int DEFAULT NULL) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$
350
+ UPDATE
351
+ app_jobs.jobs
352
+ SET
353
+ run_at = coalesce(reschedule_jobs.run_at, jobs.run_at),
354
+ priority = coalesce(reschedule_jobs.priority, jobs.priority),
355
+ attempts = coalesce(reschedule_jobs.attempts, jobs.attempts),
356
+ max_attempts = coalesce(reschedule_jobs.max_attempts, jobs.max_attempts)
357
+ WHERE
358
+ id = ANY (job_ids)
359
+ AND (locked_by IS NULL
360
+ OR locked_at < NOW() - interval '4 hours')
361
+ RETURNING
362
+ *;
363
+ $EOFCODE$;
586
364
 
587
- CREATE FUNCTION app_jobs.tg_decrease_job_queue_count() RETURNS trigger AS $EOFCODE$
365
+ CREATE FUNCTION app_jobs.release_scheduled_jobs(worker_id text, ids bigint[] DEFAULT NULL) RETURNS void AS $EOFCODE$
588
366
  DECLARE
589
- v_new_job_count int;
590
367
  BEGIN
368
+ -- clear the scheduled job
591
369
  UPDATE
592
- app_jobs.job_queues
370
+ app_jobs.scheduled_jobs s
593
371
  SET
594
- job_count = job_queues.job_count - 1
372
+ locked_at = NULL,
373
+ locked_by = NULL
595
374
  WHERE
596
- queue_name = OLD.queue_name
597
- RETURNING
598
- job_count INTO v_new_job_count;
599
- IF v_new_job_count <= 0 THEN
600
- DELETE FROM app_jobs.job_queues
601
- WHERE queue_name = OLD.queue_name
602
- AND job_count <= 0;
603
- END IF;
604
- RETURN OLD;
375
+ locked_by = worker_id
376
+ AND (ids IS NULL
377
+ OR s.id = ANY (ids));
605
378
  END;
606
379
  $EOFCODE$ LANGUAGE plpgsql VOLATILE;
607
380
 
608
- CREATE TRIGGER decrease_job_queue_count_on_delete
609
- AFTER DELETE
610
- ON app_jobs.jobs
611
- FOR EACH ROW
612
- WHEN (old.queue_name IS NOT NULL)
613
- EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count();
614
-
615
- CREATE TRIGGER decrease_job_queue_count_on_update
616
- AFTER UPDATE OF queue_name
617
- ON app_jobs.jobs
618
- FOR EACH ROW
619
- WHEN (new.queue_name IS DISTINCT FROM old.queue_name
620
- AND old.queue_name IS NOT NULL)
621
- EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count();
622
-
623
- CREATE FUNCTION app_jobs.tg_increase_job_queue_count() RETURNS trigger AS $EOFCODE$
381
+ CREATE FUNCTION app_jobs.release_jobs(worker_id text) RETURNS void AS $EOFCODE$
382
+ DECLARE
624
383
  BEGIN
625
- INSERT INTO app_jobs.job_queues (queue_name, job_count)
626
- VALUES (NEW.queue_name, 1)
627
- ON CONFLICT (queue_name)
628
- DO UPDATE SET
629
- job_count = job_queues.job_count + 1;
630
- RETURN NEW;
384
+ -- clear the job
385
+ UPDATE
386
+ app_jobs.jobs
387
+ SET
388
+ locked_at = NULL,
389
+ locked_by = NULL,
390
+ attempts = GREATEST (attempts - 1, 0)
391
+ WHERE
392
+ locked_by = worker_id;
393
+ -- clear the queue
394
+ UPDATE
395
+ app_jobs.job_queues
396
+ SET
397
+ locked_at = NULL,
398
+ locked_by = NULL
399
+ WHERE
400
+ locked_by = worker_id;
631
401
  END;
632
402
  $EOFCODE$ LANGUAGE plpgsql VOLATILE;
633
403
 
634
- CREATE TRIGGER _500_increase_job_queue_count_on_insert
635
- AFTER INSERT
636
- ON app_jobs.jobs
637
- FOR EACH ROW
638
- WHEN (new.queue_name IS NOT NULL)
639
- EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count();
404
+ CREATE FUNCTION app_jobs.permanently_fail_jobs(job_ids bigint[], error_message text DEFAULT NULL) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$
405
+ UPDATE
406
+ app_jobs.jobs
407
+ SET
408
+ last_error = coalesce(error_message, 'Manually marked as failed'),
409
+ attempts = max_attempts
410
+ WHERE
411
+ id = ANY (job_ids)
412
+ AND (locked_by IS NULL
413
+ OR locked_at < NOW() - interval '4 hours')
414
+ RETURNING
415
+ *;
416
+ $EOFCODE$;
640
417
 
641
- CREATE TRIGGER _500_increase_job_queue_count_on_update
642
- AFTER UPDATE OF queue_name
643
- ON app_jobs.jobs
644
- FOR EACH ROW
645
- WHEN (new.queue_name IS DISTINCT FROM old.queue_name
646
- AND new.queue_name IS NOT NULL)
647
- EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count();
418
+ CREATE FUNCTION app_jobs.get_scheduled_job(worker_id text, task_identifiers text[] DEFAULT NULL) RETURNS app_jobs.scheduled_jobs LANGUAGE plpgsql AS $EOFCODE$
419
+ DECLARE
420
+ v_job_id bigint;
421
+ v_row app_jobs.scheduled_jobs;
422
+ BEGIN
648
423
 
649
- CREATE TRIGGER _900_notify_worker
650
- AFTER INSERT
651
- ON app_jobs.jobs
652
- FOR EACH ROW
653
- EXECUTE PROCEDURE app_jobs.do_notify('jobs:insert');
424
+ --
654
425
 
655
- CREATE FUNCTION app_jobs.tg_update_timestamps() RETURNS trigger AS $EOFCODE$
656
- BEGIN
657
- IF TG_OP = 'INSERT' THEN
658
- NEW.created_at = NOW();
659
- NEW.updated_at = NOW();
660
- ELSIF TG_OP = 'UPDATE' THEN
661
- NEW.created_at = OLD.created_at;
662
- NEW.updated_at = greatest (now(), OLD.updated_at + interval '1 millisecond');
426
+ IF worker_id IS NULL THEN
427
+ RAISE exception 'INVALID_WORKER_ID';
663
428
  END IF;
664
- RETURN NEW;
665
- END;
666
- $EOFCODE$ LANGUAGE plpgsql;
667
429
 
668
- ALTER TABLE app_jobs.jobs
669
- ADD COLUMN created_at timestamptz;
430
+ --
670
431
 
671
- ALTER TABLE app_jobs.jobs
672
- ALTER COLUMN created_at SET DEFAULT now();
432
+ SELECT
433
+ scheduled_jobs.id INTO v_job_id
434
+ FROM
435
+ app_jobs.scheduled_jobs
436
+ WHERE (scheduled_jobs.locked_at IS NULL)
437
+ AND (task_identifiers IS NULL
438
+ OR task_identifier = ANY (task_identifiers))
439
+ ORDER BY
440
+ priority ASC,
441
+ id ASC
442
+ LIMIT 1
443
+ FOR UPDATE
444
+ SKIP LOCKED;
673
445
 
674
- ALTER TABLE app_jobs.jobs
675
- ADD COLUMN updated_at timestamptz;
676
-
677
- ALTER TABLE app_jobs.jobs
678
- ALTER COLUMN updated_at SET DEFAULT now();
446
+ --
679
447
 
680
- CREATE TRIGGER _100_update_jobs_modtime_tg
681
- BEFORE INSERT OR UPDATE
682
- ON app_jobs.jobs
683
- FOR EACH ROW
684
- EXECUTE PROCEDURE app_jobs.tg_update_timestamps();
448
+ IF v_job_id IS NULL THEN
449
+ RETURN NULL;
450
+ END IF;
685
451
 
686
- GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.scheduled_jobs TO administrator;
452
+ --
687
453
 
688
- CREATE INDEX scheduled_jobs_locked_by_idx ON app_jobs.scheduled_jobs (locked_by);
454
+ UPDATE
455
+ app_jobs.scheduled_jobs
456
+ SET
457
+ locked_by = worker_id,
458
+ locked_at = NOW()
459
+ WHERE
460
+ id = v_job_id
461
+ RETURNING
462
+ * INTO v_row;
689
463
 
690
- CREATE INDEX scheduled_jobs_priority_id_idx ON app_jobs.scheduled_jobs (priority, id);
464
+ --
691
465
 
692
- CREATE TRIGGER _900_notify_scheduled_job
693
- AFTER INSERT
694
- ON app_jobs.scheduled_jobs
695
- FOR EACH ROW
696
- EXECUTE PROCEDURE app_jobs.do_notify('scheduled_jobs:insert');
466
+ RETURN v_row;
467
+ END;
468
+ $EOFCODE$;
697
469
 
698
- CREATE FUNCTION app_jobs.trigger_job_with_fields() RETURNS trigger AS $EOFCODE$
470
+ CREATE FUNCTION app_jobs.get_job(worker_id text, task_identifiers text[] DEFAULT NULL, job_expiry interval DEFAULT '4 hours') RETURNS app_jobs.jobs LANGUAGE plpgsql AS $EOFCODE$
699
471
  DECLARE
700
- arg text;
701
- fn text;
702
- i int;
703
- args text[];
472
+ v_job_id bigint;
473
+ v_queue_name text;
474
+ v_row app_jobs.jobs;
475
+ v_now timestamptz = now();
704
476
  BEGIN
705
- FOR i IN
477
+
478
+ IF worker_id IS NULL THEN
479
+ RAISE exception 'INVALID_WORKER_ID';
480
+ END IF;
481
+
482
+ --
483
+
706
484
  SELECT
707
- *
485
+ jobs.queue_name,
486
+ jobs.id INTO v_queue_name,
487
+ v_job_id
708
488
  FROM
709
- generate_series(1, TG_NARGS) g (i)
710
- LOOP
711
- IF (i = 1) THEN
712
- fn = TG_ARGV[i - 1];
713
- ELSE
714
- args = array_append(args, TG_ARGV[i - 1]);
715
- IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
716
- EXECUTE format('SELECT ($1).%s::text', TG_ARGV[i - 1])
717
- USING NEW INTO arg;
718
- END IF;
719
- IF (TG_OP = 'DELETE') THEN
720
- EXECUTE format('SELECT ($1).%s::text', TG_ARGV[i - 1])
721
- USING OLD INTO arg;
722
- END IF;
723
- args = array_append(args, arg);
724
- END IF;
725
- END LOOP;
726
- PERFORM
727
- app_jobs.add_job (jwt_private.current_database_id(), fn, app_jobs.json_build_object_apply (args));
728
- IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
729
- RETURN NEW;
489
+ app_jobs.jobs
490
+ WHERE (jobs.locked_at IS NULL
491
+ OR jobs.locked_at < (v_now - job_expiry))
492
+ AND (jobs.queue_name IS NULL
493
+ OR EXISTS (
494
+ SELECT
495
+ 1
496
+ FROM
497
+ app_jobs.job_queues
498
+ WHERE
499
+ job_queues.queue_name = jobs.queue_name
500
+ AND (job_queues.locked_at IS NULL
501
+ OR job_queues.locked_at < (v_now - job_expiry))
502
+ FOR UPDATE
503
+ SKIP LOCKED))
504
+ AND run_at <= v_now
505
+ AND attempts < max_attempts
506
+ AND (task_identifiers IS NULL
507
+ OR task_identifier = ANY (task_identifiers))
508
+ ORDER BY
509
+ priority ASC,
510
+ run_at ASC,
511
+ id ASC
512
+ LIMIT 1
513
+ FOR UPDATE
514
+ SKIP LOCKED;
515
+
516
+ --
517
+
518
+ IF v_job_id IS NULL THEN
519
+ RETURN NULL;
730
520
  END IF;
731
- IF (TG_OP = 'DELETE') THEN
732
- RETURN OLD;
521
+
522
+ --
523
+
524
+ IF v_queue_name IS NOT NULL THEN
525
+ UPDATE
526
+ app_jobs.job_queues
527
+ SET
528
+ locked_by = worker_id,
529
+ locked_at = v_now
530
+ WHERE
531
+ job_queues.queue_name = v_queue_name;
733
532
  END IF;
533
+
534
+ --
535
+
536
+ UPDATE
537
+ app_jobs.jobs
538
+ SET
539
+ attempts = attempts + 1,
540
+ locked_by = worker_id,
541
+ locked_at = v_now
542
+ WHERE
543
+ id = v_job_id
544
+ RETURNING
545
+ * INTO v_row;
546
+
547
+ --
548
+ RETURN v_row;
734
549
  END;
735
- $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
550
+ $EOFCODE$;
736
551
 
737
- CREATE FUNCTION app_jobs.tg_add_job_with_row_id() RETURNS trigger AS $EOFCODE$
552
+ CREATE FUNCTION app_jobs.fail_job(worker_id text, job_id bigint, error_message text) RETURNS app_jobs.jobs LANGUAGE plpgsql STRICT AS $EOFCODE$
553
+ DECLARE
554
+ v_row app_jobs.jobs;
738
555
  BEGIN
739
- IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
740
- PERFORM
741
- app_jobs.add_job (jwt_private.current_database_id(), tg_argv[0], json_build_object('id', NEW.id));
742
- RETURN NEW;
743
- END IF;
744
- IF (TG_OP = 'DELETE') THEN
745
- PERFORM
746
- app_jobs.add_job (jwt_private.current_database_id(), tg_argv[0], json_build_object('id', OLD.id));
747
- RETURN OLD;
556
+ UPDATE
557
+ app_jobs.jobs
558
+ SET
559
+ last_error = error_message,
560
+ run_at = greatest (now(), run_at) + (exp(least (attempts, 10))::text || ' seconds')::interval,
561
+ locked_by = NULL,
562
+ locked_at = NULL
563
+ WHERE
564
+ id = job_id
565
+ AND locked_by = worker_id
566
+ RETURNING
567
+ * INTO v_row;
568
+ IF v_row.queue_name IS NOT NULL THEN
569
+ UPDATE
570
+ app_jobs.job_queues
571
+ SET
572
+ locked_by = NULL,
573
+ locked_at = NULL
574
+ WHERE
575
+ queue_name = v_row.queue_name
576
+ AND locked_by = worker_id;
748
577
  END IF;
578
+ RETURN v_row;
749
579
  END;
750
- $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
580
+ $EOFCODE$;
751
581
 
752
- COMMENT ON FUNCTION app_jobs.tg_add_job_with_row_id IS 'Useful shortcut to create a job on insert or update. Pass the task name as the trigger argument, and the record id will automatically be available on the JSON payload.';
582
+ CREATE FUNCTION app_jobs.complete_jobs(job_ids bigint[]) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$
583
+ DELETE FROM app_jobs.jobs
584
+ WHERE id = ANY (job_ids)
585
+ AND (locked_by IS NULL
586
+ OR locked_at < NOW() - interval '4 hours')
587
+ RETURNING
588
+ *;
589
+ $EOFCODE$;
753
590
 
754
- CREATE FUNCTION app_jobs.tg_add_job_with_row() RETURNS trigger AS $EOFCODE$
591
+ CREATE FUNCTION app_jobs.complete_job(worker_id text, job_id bigint) RETURNS app_jobs.jobs LANGUAGE plpgsql AS $EOFCODE$
592
+ DECLARE
593
+ v_row app_jobs.jobs;
755
594
  BEGIN
756
- IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
757
- PERFORM
758
- app_jobs.add_job (jwt_private.current_database_id(), TG_ARGV[0], to_json(NEW));
759
- RETURN NEW;
595
+ DELETE FROM app_jobs.jobs
596
+ WHERE id = job_id
597
+ RETURNING
598
+ * INTO v_row;
599
+ IF v_row.queue_name IS NOT NULL THEN
600
+ UPDATE
601
+ app_jobs.job_queues
602
+ SET
603
+ locked_by = NULL,
604
+ locked_at = NULL
605
+ WHERE
606
+ queue_name = v_row.queue_name
607
+ AND locked_by = worker_id;
760
608
  END IF;
761
- IF (TG_OP = 'DELETE') THEN
762
- PERFORM
763
- app_jobs.add_job (jwt_private.current_database_id(), TG_ARGV[0], to_json(OLD));
764
- RETURN OLD;
609
+ RETURN v_row;
610
+ END;
611
+ $EOFCODE$;
612
+
613
+ CREATE FUNCTION app_jobs.add_scheduled_job(db_id uuid, 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$
614
+ DECLARE
615
+ v_job app_jobs.scheduled_jobs;
616
+ BEGIN
617
+ IF job_key IS NOT NULL THEN
618
+
619
+ -- Upsert job
620
+ INSERT INTO app_jobs.scheduled_jobs (
621
+ database_id,
622
+ task_identifier,
623
+ payload,
624
+ queue_name,
625
+ schedule_info,
626
+ max_attempts,
627
+ key,
628
+ priority
629
+ ) VALUES (
630
+ db_id,
631
+ identifier,
632
+ coalesce(payload, '{}'::json),
633
+ queue_name,
634
+ schedule_info,
635
+ coalesce(max_attempts, 25),
636
+ job_key,
637
+ coalesce(priority, 0)
638
+ )
639
+ ON CONFLICT (key)
640
+ DO UPDATE SET
641
+ task_identifier = EXCLUDED.task_identifier,
642
+ payload = EXCLUDED.payload,
643
+ queue_name = EXCLUDED.queue_name,
644
+ max_attempts = EXCLUDED.max_attempts,
645
+ schedule_info = EXCLUDED.schedule_info,
646
+ priority = EXCLUDED.priority
647
+ WHERE
648
+ scheduled_jobs.locked_at IS NULL
649
+ RETURNING
650
+ * INTO v_job;
651
+
652
+ -- If upsert succeeded (insert or update), return early
653
+
654
+ IF NOT (v_job IS NULL) THEN
655
+ RETURN v_job;
656
+ END IF;
657
+
658
+ -- Upsert failed -> there must be an existing scheduled job that is locked. Remove
659
+ -- and allow a new one to be inserted
660
+
661
+ DELETE FROM
662
+ app_jobs.scheduled_jobs
663
+ WHERE
664
+ KEY = job_key;
765
665
  END IF;
666
+
667
+ INSERT INTO app_jobs.scheduled_jobs (
668
+ database_id,
669
+ task_identifier,
670
+ payload,
671
+ queue_name,
672
+ schedule_info,
673
+ max_attempts,
674
+ priority
675
+ ) VALUES (
676
+ db_id,
677
+ identifier,
678
+ payload,
679
+ queue_name,
680
+ schedule_info,
681
+ max_attempts,
682
+ priority
683
+ ) RETURNING * INTO v_job;
684
+ RETURN v_job;
766
685
  END;
767
686
  $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
768
687
 
769
- COMMENT ON FUNCTION app_jobs.tg_add_job_with_row IS 'Useful shortcut to create a job on insert or update. Pass the task name as the trigger argument, and the record data will automatically be available on the JSON payload.';
688
+ CREATE FUNCTION app_jobs.add_job(db_id uuid, 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$
689
+ DECLARE
690
+ v_job app_jobs.jobs;
691
+ BEGIN
692
+ IF job_key IS NOT NULL THEN
693
+ -- Upsert job
694
+ INSERT INTO app_jobs.jobs (
695
+ database_id,
696
+ task_identifier,
697
+ payload,
698
+ queue_name,
699
+ run_at,
700
+ max_attempts,
701
+ key,
702
+ priority
703
+ ) VALUES (
704
+ db_id,
705
+ identifier,
706
+ coalesce(payload,
707
+ '{}'::json),
708
+ queue_name,
709
+ coalesce(run_at, now()),
710
+ coalesce(max_attempts, 25),
711
+ job_key,
712
+ coalesce(priority, 0)
713
+ )
714
+ ON CONFLICT (key)
715
+ DO UPDATE SET
716
+ task_identifier = EXCLUDED.task_identifier,
717
+ payload = EXCLUDED.payload,
718
+ queue_name = EXCLUDED.queue_name,
719
+ max_attempts = EXCLUDED.max_attempts,
720
+ run_at = EXCLUDED.run_at,
721
+ priority = EXCLUDED.priority,
722
+ -- always reset error/retry state
723
+ attempts = 0, last_error = NULL
724
+ WHERE
725
+ jobs.locked_at IS NULL
726
+ RETURNING
727
+ * INTO v_job;
728
+
729
+ -- If upsert succeeded (insert or update), return early
730
+
731
+ IF NOT (v_job IS NULL) THEN
732
+ RETURN v_job;
733
+ END IF;
734
+
735
+ -- Upsert failed -> there must be an existing job that is locked. Remove
736
+ -- existing key to allow a new one to be inserted, and prevent any
737
+ -- subsequent retries by bumping attempts to the max allowed.
738
+
739
+ UPDATE
740
+ app_jobs.jobs
741
+ SET
742
+ KEY = NULL,
743
+ attempts = jobs.max_attempts
744
+ WHERE
745
+ KEY = job_key;
746
+ END IF;
747
+
748
+ INSERT INTO app_jobs.jobs (
749
+ database_id,
750
+ task_identifier,
751
+ payload,
752
+ queue_name,
753
+ run_at,
754
+ max_attempts,
755
+ priority
756
+ ) VALUES (
757
+ db_id,
758
+ identifier,
759
+ payload,
760
+ queue_name,
761
+ run_at,
762
+ max_attempts,
763
+ priority
764
+ )
765
+ RETURNING * INTO v_job;
766
+
767
+ RETURN v_job;
768
+ END;
769
+ $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;