@pgpm/database-jobs 0.21.0 → 0.21.2

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 (26) hide show
  1. package/README.md +29 -35
  2. package/deploy/schemas/app_jobs/procedures/add_job.sql +38 -34
  3. package/deploy/schemas/app_jobs/procedures/add_scheduled_job.sql +31 -21
  4. package/deploy/schemas/app_jobs/procedures/force_unlock_workers.sql +20 -0
  5. package/deploy/schemas/app_jobs/procedures/get_job.sql +27 -51
  6. package/deploy/schemas/app_jobs/procedures/remove_job.sql +34 -0
  7. package/deploy/schemas/app_jobs/procedures/run_scheduled_job.sql +2 -0
  8. package/deploy/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +11 -2
  9. package/deploy/schemas/app_jobs/tables/jobs/table.sql +9 -5
  10. package/deploy/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +15 -5
  11. package/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql +7 -5
  12. package/deploy/schemas/app_jobs/triggers/tg_add_job_with_fields.sql +1 -1
  13. package/deploy/schemas/app_jobs/triggers/tg_add_job_with_row.sql +2 -2
  14. package/deploy/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql +2 -2
  15. package/package.json +5 -4
  16. package/pgpm-database-jobs.control +2 -2
  17. package/pgpm.plan +4 -2
  18. package/revert/schemas/app_jobs/procedures/force_unlock_workers.sql +7 -0
  19. package/revert/schemas/app_jobs/procedures/remove_job.sql +7 -0
  20. package/revert/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +3 -1
  21. package/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +3 -2
  22. package/verify/schemas/app_jobs/procedures/force_unlock_workers.sql +7 -0
  23. package/verify/schemas/app_jobs/procedures/remove_job.sql +7 -0
  24. package/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +2 -1
  25. package/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +1 -2
  26. package/sql/pgpm-database-jobs--0.15.3.sql +0 -805
@@ -1,805 +0,0 @@
1
- \echo Use "CREATE EXTENSION pgpm-database-jobs" to load this file. \quit
2
- CREATE SCHEMA IF NOT EXISTS app_jobs;
3
-
4
- GRANT USAGE ON SCHEMA app_jobs TO administrator;
5
-
6
- ALTER DEFAULT PRIVILEGES IN SCHEMA app_jobs
7
- GRANT EXECUTE ON FUNCTIONS TO administrator;
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
-
56
- CREATE FUNCTION app_jobs.json_build_object_apply(arguments text[]) RETURNS pg_catalog.json AS $EOFCODE$
57
- DECLARE
58
- arg text;
59
- _sql text;
60
- _res json;
61
- args text[];
62
- BEGIN
63
- _sql = 'SELECT json_build_object(';
64
- FOR arg IN
65
- SELECT
66
- unnest(arguments)
67
- LOOP
68
- args = array_append(args, format('''%s''', arg));
69
- END LOOP;
70
- _sql = _sql || format('%s);', array_to_string(args, ','));
71
- EXECUTE _sql INTO _res;
72
- RETURN _res;
73
- END;
74
- $EOFCODE$ LANGUAGE plpgsql;
75
-
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 (
116
- id bigserial PRIMARY KEY,
117
- database_id uuid NOT NULL,
118
- queue_name text DEFAULT (public.gen_random_uuid())::text,
119
- task_identifier text NOT NULL,
120
- payload pg_catalog.json DEFAULT '{}'::json NOT NULL,
121
- priority int DEFAULT 0 NOT NULL,
122
- max_attempts int DEFAULT 25 NOT NULL,
123
- key text,
124
- locked_at timestamptz,
125
- locked_by text,
126
- schedule_info pg_catalog.json NOT NULL,
127
- last_scheduled timestamptz,
128
- last_scheduled_id bigint,
129
- CHECK (length(key) < 513),
130
- CHECK (length(task_identifier) < 127),
131
- CHECK (max_attempts > 0),
132
- CHECK (length(queue_name) < 127),
133
- CHECK (length(locked_by) > 3),
134
- UNIQUE (key)
135
- );
136
-
137
- COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions with database scoping: each row spawns jobs on a schedule for a specific database';
138
- COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier';
139
- COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to, for multi-tenant isolation';
140
- COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into';
141
- COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs';
142
- COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job';
143
- COMMENT ON COLUMN app_jobs.scheduled_jobs.priority IS 'Priority assigned to spawned jobs (lower = higher priority)';
144
- COMMENT ON COLUMN app_jobs.scheduled_jobs.max_attempts IS 'Max retry attempts for spawned jobs';
145
- COMMENT ON COLUMN app_jobs.scheduled_jobs.key IS 'Optional unique deduplication key';
146
- COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_at IS 'Timestamp when the scheduler locked this record for processing';
147
- COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_by IS 'Identifier of the scheduler worker holding the lock';
148
- COMMENT ON COLUMN app_jobs.scheduled_jobs.schedule_info IS 'JSON schedule configuration (e.g. cron expression, interval)';
149
- COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled IS 'Timestamp when a job was last spawned from this schedule';
150
- COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled_id IS 'ID of the last job spawned from this schedule';
151
-
152
- CREATE FUNCTION app_jobs.do_notify() RETURNS trigger AS $EOFCODE$
153
- BEGIN
154
- PERFORM
155
- pg_notify(TG_ARGV[0], '');
156
- RETURN NEW;
157
- END;
158
- $EOFCODE$ LANGUAGE plpgsql;
159
-
160
- CREATE TRIGGER _900_notify_scheduled_job
161
- AFTER INSERT
162
- ON app_jobs.scheduled_jobs
163
- FOR EACH ROW
164
- EXECUTE PROCEDURE app_jobs.do_notify('scheduled_jobs:insert');
165
-
166
- CREATE INDEX scheduled_jobs_priority_id_idx ON app_jobs.scheduled_jobs (priority, id);
167
-
168
- CREATE INDEX scheduled_jobs_locked_by_idx ON app_jobs.scheduled_jobs (locked_by);
169
-
170
- GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.scheduled_jobs TO administrator;
171
-
172
- CREATE TABLE app_jobs.jobs (
173
- id bigserial PRIMARY KEY,
174
- database_id uuid NOT NULL,
175
- queue_name text DEFAULT (public.gen_random_uuid())::text,
176
- task_identifier text NOT NULL,
177
- payload pg_catalog.json DEFAULT '{}'::json NOT NULL,
178
- priority int DEFAULT 0 NOT NULL,
179
- run_at timestamptz DEFAULT now() NOT NULL,
180
- attempts int DEFAULT 0 NOT NULL,
181
- max_attempts int DEFAULT 25 NOT NULL,
182
- key text,
183
- last_error text,
184
- locked_at timestamptz,
185
- locked_by text,
186
- CHECK (length(key) < 513),
187
- CHECK (length(task_identifier) < 127),
188
- CHECK (max_attempts > 0),
189
- CHECK (length(queue_name) < 127),
190
- CHECK (length(locked_by) > 3),
191
- UNIQUE (key)
192
- );
193
-
194
- COMMENT ON TABLE app_jobs.jobs IS 'Background job queue with database scoping: each row is a pending or in-progress task for a specific database';
195
- COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier';
196
- COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to, for multi-tenant job isolation';
197
- COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control';
198
- COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)';
199
- COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler';
200
- COMMENT ON COLUMN app_jobs.jobs.priority IS 'Execution priority; lower numbers run first (default 0)';
201
- COMMENT ON COLUMN app_jobs.jobs.run_at IS 'Earliest time this job should be executed; used for delayed/scheduled execution';
202
- COMMENT ON COLUMN app_jobs.jobs.attempts IS 'Number of times this job has been attempted so far';
203
- COMMENT ON COLUMN app_jobs.jobs.max_attempts IS 'Maximum retry attempts before the job is considered permanently failed';
204
- COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; prevents duplicate jobs with the same key';
205
- COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt';
206
- COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing';
207
- COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock';
208
-
209
- ALTER TABLE app_jobs.jobs
210
- ADD COLUMN created_at timestamptz;
211
-
212
- ALTER TABLE app_jobs.jobs
213
- ALTER COLUMN created_at SET DEFAULT now();
214
-
215
- ALTER TABLE app_jobs.jobs
216
- ADD COLUMN updated_at timestamptz;
217
-
218
- ALTER TABLE app_jobs.jobs
219
- ALTER COLUMN updated_at SET DEFAULT now();
220
-
221
- CREATE TRIGGER _100_update_jobs_modtime_tg
222
- BEFORE INSERT OR UPDATE
223
- ON app_jobs.jobs
224
- FOR EACH ROW
225
- EXECUTE PROCEDURE app_jobs.tg_update_timestamps();
226
-
227
- CREATE FUNCTION app_jobs.tg_increase_job_queue_count() RETURNS trigger AS $EOFCODE$
228
- BEGIN
229
- INSERT INTO app_jobs.job_queues (queue_name, job_count)
230
- VALUES (NEW.queue_name, 1)
231
- ON CONFLICT (queue_name)
232
- DO UPDATE SET
233
- job_count = job_queues.job_count + 1;
234
- RETURN NEW;
235
- END;
236
- $EOFCODE$ LANGUAGE plpgsql VOLATILE;
237
-
238
- CREATE TRIGGER _500_increase_job_queue_count_on_insert
239
- AFTER INSERT
240
- ON app_jobs.jobs
241
- FOR EACH ROW
242
- WHEN (new.queue_name IS NOT NULL)
243
- EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count();
244
-
245
- CREATE TRIGGER _500_increase_job_queue_count_on_update
246
- AFTER UPDATE OF queue_name
247
- ON app_jobs.jobs
248
- FOR EACH ROW
249
- WHEN (new.queue_name IS DISTINCT FROM old.queue_name
250
- AND new.queue_name IS NOT NULL)
251
- EXECUTE PROCEDURE app_jobs.tg_increase_job_queue_count();
252
-
253
- CREATE TRIGGER _900_notify_worker
254
- AFTER INSERT
255
- ON app_jobs.jobs
256
- FOR EACH ROW
257
- EXECUTE PROCEDURE app_jobs.do_notify('jobs:insert');
258
-
259
- CREATE FUNCTION app_jobs.tg_decrease_job_queue_count() RETURNS trigger AS $EOFCODE$
260
- DECLARE
261
- v_new_job_count int;
262
- BEGIN
263
- UPDATE
264
- app_jobs.job_queues
265
- SET
266
- job_count = job_queues.job_count - 1
267
- WHERE
268
- queue_name = OLD.queue_name
269
- RETURNING
270
- job_count INTO v_new_job_count;
271
- IF v_new_job_count <= 0 THEN
272
- DELETE FROM app_jobs.job_queues
273
- WHERE queue_name = OLD.queue_name
274
- AND job_count <= 0;
275
- END IF;
276
- RETURN OLD;
277
- END;
278
- $EOFCODE$ LANGUAGE plpgsql VOLATILE;
279
-
280
- CREATE TRIGGER decrease_job_queue_count_on_delete
281
- AFTER DELETE
282
- ON app_jobs.jobs
283
- FOR EACH ROW
284
- WHEN (old.queue_name IS NOT NULL)
285
- EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count();
286
-
287
- CREATE TRIGGER decrease_job_queue_count_on_update
288
- AFTER UPDATE OF queue_name
289
- ON app_jobs.jobs
290
- FOR EACH ROW
291
- WHEN (new.queue_name IS DISTINCT FROM old.queue_name
292
- AND old.queue_name IS NOT NULL)
293
- EXECUTE PROCEDURE app_jobs.tg_decrease_job_queue_count();
294
-
295
- CREATE INDEX priority_run_at_id_idx ON app_jobs.jobs (priority, run_at, id);
296
-
297
- CREATE INDEX jobs_locked_by_idx ON app_jobs.jobs (locked_by);
298
-
299
- GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.jobs TO administrator;
300
-
301
- CREATE TABLE app_jobs.job_queues (
302
- queue_name text NOT NULL PRIMARY KEY,
303
- job_count int DEFAULT 0 NOT NULL,
304
- locked_at timestamptz,
305
- locked_by text
306
- );
307
-
308
- COMMENT ON TABLE app_jobs.job_queues IS 'Queue metadata: tracks job counts and locking state for each named queue';
309
- COMMENT ON COLUMN app_jobs.job_queues.queue_name IS 'Unique name identifying this queue';
310
- COMMENT ON COLUMN app_jobs.job_queues.job_count IS 'Number of pending jobs in this queue';
311
- COMMENT ON COLUMN app_jobs.job_queues.locked_at IS 'Timestamp when this queue was locked for batch processing';
312
- COMMENT ON COLUMN app_jobs.job_queues.locked_by IS 'Identifier of the worker that currently holds the queue lock';
313
-
314
- CREATE INDEX job_queues_locked_by_idx ON app_jobs.job_queues (locked_by);
315
-
316
- GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.job_queues TO administrator;
317
-
318
- CREATE FUNCTION app_jobs.run_scheduled_job(id bigint, job_expiry interval DEFAULT '1 hours') RETURNS app_jobs.jobs AS $EOFCODE$
319
- DECLARE
320
- j app_jobs.jobs;
321
- last_id bigint;
322
- lkd_by text;
323
- BEGIN
324
- -- check last scheduled
325
- SELECT
326
- last_scheduled_id
327
- FROM
328
- app_jobs.scheduled_jobs s
329
- WHERE
330
- s.id = run_scheduled_job.id INTO last_id;
331
-
332
- -- if it's been scheduled check if it's been run
333
-
334
- IF (last_id IS NOT NULL) THEN
335
- SELECT
336
- locked_by
337
- FROM
338
- app_jobs.jobs js
339
- WHERE
340
- js.id = last_id
341
- AND (js.locked_at IS NULL -- never been run
342
- OR js.locked_at >= (NOW() - job_expiry)
343
- -- still running within a safe interval
344
- ) INTO lkd_by;
345
- IF (FOUND) THEN
346
- RAISE EXCEPTION 'ALREADY_SCHEDULED';
347
- END IF;
348
- END IF;
349
-
350
- -- insert new job
351
- INSERT INTO app_jobs.jobs (
352
- database_id,
353
- queue_name,
354
- task_identifier,
355
- payload,
356
- priority,
357
- max_attempts,
358
- key
359
- ) SELECT
360
- database_id,
361
- queue_name,
362
- task_identifier,
363
- payload,
364
- priority,
365
- max_attempts,
366
- key
367
- FROM
368
- app_jobs.scheduled_jobs s
369
- WHERE
370
- s.id = run_scheduled_job.id
371
- RETURNING
372
- * INTO j;
373
- -- update the scheduled job
374
- UPDATE
375
- app_jobs.scheduled_jobs s
376
- SET
377
- last_scheduled = NOW(),
378
- last_scheduled_id = j.id
379
- WHERE
380
- s.id = run_scheduled_job.id;
381
- RETURN j;
382
- END;
383
- $EOFCODE$ LANGUAGE plpgsql VOLATILE;
384
-
385
- 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$
386
- UPDATE
387
- app_jobs.jobs
388
- SET
389
- run_at = coalesce(reschedule_jobs.run_at, jobs.run_at),
390
- priority = coalesce(reschedule_jobs.priority, jobs.priority),
391
- attempts = coalesce(reschedule_jobs.attempts, jobs.attempts),
392
- max_attempts = coalesce(reschedule_jobs.max_attempts, jobs.max_attempts)
393
- WHERE
394
- id = ANY (job_ids)
395
- AND (locked_by IS NULL
396
- OR locked_at < NOW() - interval '4 hours')
397
- RETURNING
398
- *;
399
- $EOFCODE$;
400
-
401
- CREATE FUNCTION app_jobs.release_scheduled_jobs(worker_id text, ids bigint[] DEFAULT NULL) RETURNS void AS $EOFCODE$
402
- DECLARE
403
- BEGIN
404
- -- clear the scheduled job
405
- UPDATE
406
- app_jobs.scheduled_jobs s
407
- SET
408
- locked_at = NULL,
409
- locked_by = NULL
410
- WHERE
411
- locked_by = worker_id
412
- AND (ids IS NULL
413
- OR s.id = ANY (ids));
414
- END;
415
- $EOFCODE$ LANGUAGE plpgsql VOLATILE;
416
-
417
- CREATE FUNCTION app_jobs.release_jobs(worker_id text) RETURNS void AS $EOFCODE$
418
- DECLARE
419
- BEGIN
420
- -- clear the job
421
- UPDATE
422
- app_jobs.jobs
423
- SET
424
- locked_at = NULL,
425
- locked_by = NULL,
426
- attempts = GREATEST (attempts - 1, 0)
427
- WHERE
428
- locked_by = worker_id;
429
- -- clear the queue
430
- UPDATE
431
- app_jobs.job_queues
432
- SET
433
- locked_at = NULL,
434
- locked_by = NULL
435
- WHERE
436
- locked_by = worker_id;
437
- END;
438
- $EOFCODE$ LANGUAGE plpgsql VOLATILE;
439
-
440
- CREATE FUNCTION app_jobs.permanently_fail_jobs(job_ids bigint[], error_message text DEFAULT NULL) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$
441
- UPDATE
442
- app_jobs.jobs
443
- SET
444
- last_error = coalesce(error_message, 'Manually marked as failed'),
445
- attempts = max_attempts
446
- WHERE
447
- id = ANY (job_ids)
448
- AND (locked_by IS NULL
449
- OR locked_at < NOW() - interval '4 hours')
450
- RETURNING
451
- *;
452
- $EOFCODE$;
453
-
454
- CREATE FUNCTION app_jobs.get_scheduled_job(worker_id text, task_identifiers text[] DEFAULT NULL) RETURNS app_jobs.scheduled_jobs LANGUAGE plpgsql AS $EOFCODE$
455
- DECLARE
456
- v_job_id bigint;
457
- v_row app_jobs.scheduled_jobs;
458
- BEGIN
459
-
460
- --
461
-
462
- IF worker_id IS NULL THEN
463
- RAISE exception 'INVALID_WORKER_ID';
464
- END IF;
465
-
466
- --
467
-
468
- SELECT
469
- scheduled_jobs.id INTO v_job_id
470
- FROM
471
- app_jobs.scheduled_jobs
472
- WHERE (scheduled_jobs.locked_at IS NULL)
473
- AND (task_identifiers IS NULL
474
- OR task_identifier = ANY (task_identifiers))
475
- ORDER BY
476
- priority ASC,
477
- id ASC
478
- LIMIT 1
479
- FOR UPDATE
480
- SKIP LOCKED;
481
-
482
- --
483
-
484
- IF v_job_id IS NULL THEN
485
- RETURN NULL;
486
- END IF;
487
-
488
- --
489
-
490
- UPDATE
491
- app_jobs.scheduled_jobs
492
- SET
493
- locked_by = worker_id,
494
- locked_at = NOW()
495
- WHERE
496
- id = v_job_id
497
- RETURNING
498
- * INTO v_row;
499
-
500
- --
501
-
502
- RETURN v_row;
503
- END;
504
- $EOFCODE$;
505
-
506
- 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$
507
- DECLARE
508
- v_job_id bigint;
509
- v_queue_name text;
510
- v_row app_jobs.jobs;
511
- v_now timestamptz = now();
512
- BEGIN
513
-
514
- IF worker_id IS NULL THEN
515
- RAISE exception 'INVALID_WORKER_ID';
516
- END IF;
517
-
518
- --
519
-
520
- SELECT
521
- jobs.queue_name,
522
- jobs.id INTO v_queue_name,
523
- v_job_id
524
- FROM
525
- app_jobs.jobs
526
- WHERE (jobs.locked_at IS NULL
527
- OR jobs.locked_at < (v_now - job_expiry))
528
- AND (jobs.queue_name IS NULL
529
- OR EXISTS (
530
- SELECT
531
- 1
532
- FROM
533
- app_jobs.job_queues
534
- WHERE
535
- job_queues.queue_name = jobs.queue_name
536
- AND (job_queues.locked_at IS NULL
537
- OR job_queues.locked_at < (v_now - job_expiry))
538
- FOR UPDATE
539
- SKIP LOCKED))
540
- AND run_at <= v_now
541
- AND attempts < max_attempts
542
- AND (task_identifiers IS NULL
543
- OR task_identifier = ANY (task_identifiers))
544
- ORDER BY
545
- priority ASC,
546
- run_at ASC,
547
- id ASC
548
- LIMIT 1
549
- FOR UPDATE
550
- SKIP LOCKED;
551
-
552
- --
553
-
554
- IF v_job_id IS NULL THEN
555
- RETURN NULL;
556
- END IF;
557
-
558
- --
559
-
560
- IF v_queue_name IS NOT NULL THEN
561
- UPDATE
562
- app_jobs.job_queues
563
- SET
564
- locked_by = worker_id,
565
- locked_at = v_now
566
- WHERE
567
- job_queues.queue_name = v_queue_name;
568
- END IF;
569
-
570
- --
571
-
572
- UPDATE
573
- app_jobs.jobs
574
- SET
575
- attempts = attempts + 1,
576
- locked_by = worker_id,
577
- locked_at = v_now
578
- WHERE
579
- id = v_job_id
580
- RETURNING
581
- * INTO v_row;
582
-
583
- --
584
- RETURN v_row;
585
- END;
586
- $EOFCODE$;
587
-
588
- CREATE FUNCTION app_jobs.fail_job(worker_id text, job_id bigint, error_message text) RETURNS app_jobs.jobs LANGUAGE plpgsql STRICT AS $EOFCODE$
589
- DECLARE
590
- v_row app_jobs.jobs;
591
- BEGIN
592
- UPDATE
593
- app_jobs.jobs
594
- SET
595
- last_error = error_message,
596
- run_at = greatest (now(), run_at) + (exp(least (attempts, 10))::text || ' seconds')::interval,
597
- locked_by = NULL,
598
- locked_at = NULL
599
- WHERE
600
- id = job_id
601
- AND locked_by = worker_id
602
- RETURNING
603
- * INTO v_row;
604
- IF v_row.queue_name IS NOT NULL THEN
605
- UPDATE
606
- app_jobs.job_queues
607
- SET
608
- locked_by = NULL,
609
- locked_at = NULL
610
- WHERE
611
- queue_name = v_row.queue_name
612
- AND locked_by = worker_id;
613
- END IF;
614
- RETURN v_row;
615
- END;
616
- $EOFCODE$;
617
-
618
- CREATE FUNCTION app_jobs.complete_jobs(job_ids bigint[]) RETURNS SETOF app_jobs.jobs LANGUAGE sql AS $EOFCODE$
619
- DELETE FROM app_jobs.jobs
620
- WHERE id = ANY (job_ids)
621
- AND (locked_by IS NULL
622
- OR locked_at < NOW() - interval '4 hours')
623
- RETURNING
624
- *;
625
- $EOFCODE$;
626
-
627
- CREATE FUNCTION app_jobs.complete_job(worker_id text, job_id bigint) RETURNS app_jobs.jobs LANGUAGE plpgsql AS $EOFCODE$
628
- DECLARE
629
- v_row app_jobs.jobs;
630
- BEGIN
631
- DELETE FROM app_jobs.jobs
632
- WHERE id = job_id
633
- RETURNING
634
- * INTO v_row;
635
- IF v_row.queue_name IS NOT NULL THEN
636
- UPDATE
637
- app_jobs.job_queues
638
- SET
639
- locked_by = NULL,
640
- locked_at = NULL
641
- WHERE
642
- queue_name = v_row.queue_name
643
- AND locked_by = worker_id;
644
- END IF;
645
- RETURN v_row;
646
- END;
647
- $EOFCODE$;
648
-
649
- 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$
650
- DECLARE
651
- v_job app_jobs.scheduled_jobs;
652
- BEGIN
653
- IF job_key IS NOT NULL THEN
654
-
655
- -- Upsert job
656
- INSERT INTO app_jobs.scheduled_jobs (
657
- database_id,
658
- task_identifier,
659
- payload,
660
- queue_name,
661
- schedule_info,
662
- max_attempts,
663
- key,
664
- priority
665
- ) VALUES (
666
- db_id,
667
- identifier,
668
- coalesce(payload, '{}'::json),
669
- queue_name,
670
- schedule_info,
671
- coalesce(max_attempts, 25),
672
- job_key,
673
- coalesce(priority, 0)
674
- )
675
- ON CONFLICT (key)
676
- DO UPDATE SET
677
- task_identifier = EXCLUDED.task_identifier,
678
- payload = EXCLUDED.payload,
679
- queue_name = EXCLUDED.queue_name,
680
- max_attempts = EXCLUDED.max_attempts,
681
- schedule_info = EXCLUDED.schedule_info,
682
- priority = EXCLUDED.priority
683
- WHERE
684
- scheduled_jobs.locked_at IS NULL
685
- RETURNING
686
- * INTO v_job;
687
-
688
- -- If upsert succeeded (insert or update), return early
689
-
690
- IF NOT (v_job IS NULL) THEN
691
- RETURN v_job;
692
- END IF;
693
-
694
- -- Upsert failed -> there must be an existing scheduled job that is locked. Remove
695
- -- and allow a new one to be inserted
696
-
697
- DELETE FROM
698
- app_jobs.scheduled_jobs
699
- WHERE
700
- KEY = job_key;
701
- END IF;
702
-
703
- INSERT INTO app_jobs.scheduled_jobs (
704
- database_id,
705
- task_identifier,
706
- payload,
707
- queue_name,
708
- schedule_info,
709
- max_attempts,
710
- priority
711
- ) VALUES (
712
- db_id,
713
- identifier,
714
- payload,
715
- queue_name,
716
- schedule_info,
717
- max_attempts,
718
- priority
719
- ) RETURNING * INTO v_job;
720
- RETURN v_job;
721
- END;
722
- $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
723
-
724
- 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$
725
- DECLARE
726
- v_job app_jobs.jobs;
727
- BEGIN
728
- IF job_key IS NOT NULL THEN
729
- -- Upsert job
730
- INSERT INTO app_jobs.jobs (
731
- database_id,
732
- task_identifier,
733
- payload,
734
- queue_name,
735
- run_at,
736
- max_attempts,
737
- key,
738
- priority
739
- ) VALUES (
740
- db_id,
741
- identifier,
742
- coalesce(payload,
743
- '{}'::json),
744
- queue_name,
745
- coalesce(run_at, now()),
746
- coalesce(max_attempts, 25),
747
- job_key,
748
- coalesce(priority, 0)
749
- )
750
- ON CONFLICT (key)
751
- DO UPDATE SET
752
- task_identifier = EXCLUDED.task_identifier,
753
- payload = EXCLUDED.payload,
754
- queue_name = EXCLUDED.queue_name,
755
- max_attempts = EXCLUDED.max_attempts,
756
- run_at = EXCLUDED.run_at,
757
- priority = EXCLUDED.priority,
758
- -- always reset error/retry state
759
- attempts = 0, last_error = NULL
760
- WHERE
761
- jobs.locked_at IS NULL
762
- RETURNING
763
- * INTO v_job;
764
-
765
- -- If upsert succeeded (insert or update), return early
766
-
767
- IF NOT (v_job IS NULL) THEN
768
- RETURN v_job;
769
- END IF;
770
-
771
- -- Upsert failed -> there must be an existing job that is locked. Remove
772
- -- existing key to allow a new one to be inserted, and prevent any
773
- -- subsequent retries by bumping attempts to the max allowed.
774
-
775
- UPDATE
776
- app_jobs.jobs
777
- SET
778
- KEY = NULL,
779
- attempts = jobs.max_attempts
780
- WHERE
781
- KEY = job_key;
782
- END IF;
783
-
784
- INSERT INTO app_jobs.jobs (
785
- database_id,
786
- task_identifier,
787
- payload,
788
- queue_name,
789
- run_at,
790
- max_attempts,
791
- priority
792
- ) VALUES (
793
- db_id,
794
- identifier,
795
- payload,
796
- queue_name,
797
- run_at,
798
- max_attempts,
799
- priority
800
- )
801
- RETURNING * INTO v_job;
802
-
803
- RETURN v_job;
804
- END;
805
- $EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;