inst-jobs 2.4.3 → 2.4.7
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.
- checksums.yaml +4 -4
- data/db/migrate/20210917232626_add_delete_conflicting_singletons_before_unlock_trigger.rb +27 -0
- data/lib/delayed/backend/active_record.rb +24 -4
- data/lib/delayed/periodic.rb +7 -1
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed/work_queue/parent_process/server.rb +8 -3
- data/lib/delayed/worker/health_check.rb +1 -8
- data/spec/delayed/worker/health_check_spec.rb +1 -1
- data/spec/shared/shared_backend.rb +13 -0
- data/spec/spec_helper.rb +3 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48186ffba65e38c8e10702a68c93d9b4bb7f8ea13f98a7175e6755a1e693595f
|
|
4
|
+
data.tar.gz: 8a6e51516ffeaaa31d8b3bcb07a25e859259b9e734c5a8e14af019d842a01ed7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 385dcba329c82516cc6ac3b17aa7254cb427fce7d964c40c20ddd64c73a04af0cc687e4626e0dc2af890bf10ef1ec534f355e0dc8203416120c36ec453a9eb71
|
|
7
|
+
data.tar.gz: 42c9316bb0ab5ad237fb70719ddbfa9ba9dc7f4b8dfd75b13880e7be228f9a03b566eb94c0771bdd6672f92919b5c79db4dafd041b1194a3cd4a829a96c006c4
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddDeleteConflictingSingletonsBeforeUnlockTrigger < ActiveRecord::Migration[5.2]
|
|
4
|
+
def up
|
|
5
|
+
execute(<<~SQL)
|
|
6
|
+
CREATE FUNCTION delayed_jobs_before_unlock_delete_conflicting_singletons_row_fn () RETURNS trigger AS $$
|
|
7
|
+
BEGIN
|
|
8
|
+
IF EXISTS (SELECT 1 FROM delayed_jobs j2 WHERE j2.singleton=OLD.singleton) THEN
|
|
9
|
+
DELETE FROM delayed_jobs WHERE id<>OLD.id AND singleton=OLD.singleton;
|
|
10
|
+
END IF;
|
|
11
|
+
RETURN NEW;
|
|
12
|
+
END;
|
|
13
|
+
$$ LANGUAGE plpgsql;
|
|
14
|
+
SQL
|
|
15
|
+
execute(<<~SQL)
|
|
16
|
+
CREATE TRIGGER delayed_jobs_before_unlock_delete_conflicting_singletons_row_tr BEFORE UPDATE ON delayed_jobs FOR EACH ROW WHEN (
|
|
17
|
+
OLD.singleton IS NOT NULL AND
|
|
18
|
+
OLD.singleton=NEW.singleton AND
|
|
19
|
+
OLD.locked_by IS NOT NULL AND
|
|
20
|
+
NEW.locked_by IS NULL) EXECUTE PROCEDURE delayed_jobs_before_unlock_delete_conflicting_singletons_row_fn();
|
|
21
|
+
SQL
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def down
|
|
25
|
+
execute("DROP FUNCTION delayed_jobs_before_unlock_delete_conflicting_singletons_row_tr_fn()")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -38,6 +38,16 @@ module Delayed
|
|
|
38
38
|
job = new(attributes, &block)
|
|
39
39
|
job.single_step_create(on_conflict: on_conflict)
|
|
40
40
|
end
|
|
41
|
+
|
|
42
|
+
def attempt_advisory_lock(lock_name)
|
|
43
|
+
fn_name = connection.quote_table_name("half_md5_as_bigint")
|
|
44
|
+
connection.select_value("SELECT pg_try_advisory_xact_lock(#{fn_name}('#{lock_name}'));")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def advisory_lock(lock_name)
|
|
48
|
+
fn_name = connection.quote_table_name("half_md5_as_bigint")
|
|
49
|
+
connection.execute("SELECT pg_advisory_xact_lock(#{fn_name}('#{lock_name}'));")
|
|
50
|
+
end
|
|
41
51
|
end
|
|
42
52
|
|
|
43
53
|
def single_step_create(on_conflict: nil)
|
|
@@ -98,11 +108,11 @@ module Delayed
|
|
|
98
108
|
fn_name = connection.quote_table_name("half_md5_as_bigint")
|
|
99
109
|
sql = "SELECT pg_advisory_xact_lock(#{fn_name}(#{connection.quote(values['strand'])})); #{sql}"
|
|
100
110
|
end
|
|
101
|
-
result = connection.execute(sql, "#{self} Create")
|
|
111
|
+
result = connection.execute(sql, "#{self.class} Create")
|
|
102
112
|
self.id = result.values.first&.first
|
|
103
113
|
result.clear
|
|
104
114
|
else
|
|
105
|
-
result = connection.exec_query(sql, "#{self} Create", binds)
|
|
115
|
+
result = connection.exec_query(sql, "#{self.class} Create", binds)
|
|
106
116
|
self.id = connection.send(:last_inserted_id, result)
|
|
107
117
|
end
|
|
108
118
|
|
|
@@ -458,9 +468,19 @@ module Delayed
|
|
|
458
468
|
where("locked_by LIKE ?", "#{name}:%").pluck(:locked_by).map { |locked_by| locked_by.split(":").last.to_i }
|
|
459
469
|
end
|
|
460
470
|
|
|
471
|
+
def self.prefetch_jobs_lock_name
|
|
472
|
+
"Delayed::Job.unlock_orphaned_prefetched_jobs"
|
|
473
|
+
end
|
|
474
|
+
|
|
461
475
|
def self.unlock_orphaned_prefetched_jobs
|
|
462
|
-
|
|
463
|
-
|
|
476
|
+
transaction do
|
|
477
|
+
# for db performance reasons, we only need one process doing this at a time
|
|
478
|
+
# so if we can't get an advisory lock, just abort. we'll try again soon
|
|
479
|
+
return unless attempt_advisory_lock(prefetch_jobs_lock_name)
|
|
480
|
+
|
|
481
|
+
horizon = db_time_now - (Settings.parent_process[:prefetched_jobs_timeout] * 4)
|
|
482
|
+
where("locked_by LIKE 'prefetch:%' AND locked_at<?", horizon).update_all(locked_at: nil, locked_by: nil)
|
|
483
|
+
end
|
|
464
484
|
end
|
|
465
485
|
|
|
466
486
|
def self.unlock(jobs)
|
data/lib/delayed/periodic.rb
CHANGED
|
@@ -33,7 +33,13 @@ module Delayed
|
|
|
33
33
|
# we used to queue up a job in a strand here, and perform the audit inside that job
|
|
34
34
|
# however, now that we're using singletons for scheduling periodic jobs,
|
|
35
35
|
# it's fine to just do the audit in-line here without risk of creating duplicates
|
|
36
|
-
|
|
36
|
+
Delayed::Job.transaction do
|
|
37
|
+
# for db performance reasons, we only need one process doing this at a time
|
|
38
|
+
# so if we can't get an advisory lock, just abort. we'll try again soon
|
|
39
|
+
return unless Delayed::Job.attempt_advisory_lock("Delayed::Periodic#audit_queue")
|
|
40
|
+
|
|
41
|
+
perform_audit!
|
|
42
|
+
end
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
# make sure all periodic jobs are scheduled for their next run in the job queue
|
data/lib/delayed/version.rb
CHANGED
|
@@ -195,11 +195,13 @@ module Delayed
|
|
|
195
195
|
def unlock_timed_out_prefetched_jobs
|
|
196
196
|
@prefetched_jobs.each do |(worker_config, jobs)|
|
|
197
197
|
next if jobs.empty?
|
|
198
|
+
next unless jobs.first.locked_at < Time.now.utc - Settings.parent_process[:prefetched_jobs_timeout]
|
|
198
199
|
|
|
199
|
-
|
|
200
|
+
Delayed::Job.transaction do
|
|
201
|
+
Delayed::Job.advisory_lock(Delayed::Job.prefetch_jobs_lock_name)
|
|
200
202
|
Delayed::Job.unlock(jobs)
|
|
201
|
-
@prefetched_jobs[worker_config] = []
|
|
202
203
|
end
|
|
204
|
+
@prefetched_jobs[worker_config] = []
|
|
203
205
|
end
|
|
204
206
|
end
|
|
205
207
|
|
|
@@ -207,7 +209,10 @@ module Delayed
|
|
|
207
209
|
@prefetched_jobs.each do |(_worker_config, jobs)|
|
|
208
210
|
next if jobs.empty?
|
|
209
211
|
|
|
210
|
-
Delayed::Job.
|
|
212
|
+
Delayed::Job.transaction do
|
|
213
|
+
Delayed::Job.advisory_lock(Delayed::Job.prefetch_jobs_lock_name)
|
|
214
|
+
Delayed::Job.unlock(jobs)
|
|
215
|
+
end
|
|
211
216
|
end
|
|
212
217
|
@prefetched_jobs = {}
|
|
213
218
|
end
|
|
@@ -33,7 +33,7 @@ module Delayed
|
|
|
33
33
|
# and we try to get an advisory lock when it runs. If we succeed,
|
|
34
34
|
# no other worker is trying to do this right now (and if we abandon the
|
|
35
35
|
# operation, the transaction will end, releasing the advisory lock).
|
|
36
|
-
result = attempt_advisory_lock
|
|
36
|
+
result = Delayed::Job.attempt_advisory_lock("Delayed::Worker::HealthCheck#reschedule_abandoned_jobs")
|
|
37
37
|
return unless result
|
|
38
38
|
|
|
39
39
|
checker = Worker::HealthCheck.build(
|
|
@@ -65,13 +65,6 @@ module Delayed
|
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
|
-
|
|
69
|
-
def attempt_advisory_lock
|
|
70
|
-
lock_name = "Delayed::Worker::HealthCheck#reschedule_abandoned_jobs"
|
|
71
|
-
conn = ActiveRecord::Base.connection
|
|
72
|
-
fn_name = conn.quote_table_name("half_md5_as_bigint")
|
|
73
|
-
conn.select_value("SELECT pg_try_advisory_xact_lock(#{fn_name}('#{lock_name}'));")
|
|
74
|
-
end
|
|
75
68
|
end
|
|
76
69
|
|
|
77
70
|
attr_accessor :config, :worker_name
|
|
@@ -111,7 +111,7 @@ RSpec.describe Delayed::Worker::HealthCheck do
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
it "bails immediately if advisory lock already taken" do
|
|
114
|
-
allow(
|
|
114
|
+
allow(Delayed::Job).to receive(:attempt_advisory_lock).and_return(false)
|
|
115
115
|
described_class.reschedule_abandoned_jobs
|
|
116
116
|
@dead_job.reload
|
|
117
117
|
expect(@dead_job.run_at.to_i).to eq(initial_run_at.to_i)
|
|
@@ -491,6 +491,19 @@ shared_examples_for "a backend" do
|
|
|
491
491
|
expect(@job2).to be_new_record
|
|
492
492
|
end
|
|
493
493
|
end
|
|
494
|
+
|
|
495
|
+
context "when unlocking with another singleton pending" do
|
|
496
|
+
it "deletes the pending singleton" do
|
|
497
|
+
@job1 = create_job(singleton: "myjobs", max_attempts: 2)
|
|
498
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(@job1)
|
|
499
|
+
|
|
500
|
+
@job2 = create_job(singleton: "myjobs", max_attempts: 2)
|
|
501
|
+
|
|
502
|
+
@job1.reload.reschedule
|
|
503
|
+
expect { @job1.reload }.not_to raise_error
|
|
504
|
+
expect { @job2.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
|
505
|
+
end
|
|
506
|
+
end
|
|
494
507
|
end
|
|
495
508
|
end
|
|
496
509
|
|
data/spec/spec_helper.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "delayed_job"
|
|
|
4
4
|
require "delayed/testing"
|
|
5
5
|
|
|
6
6
|
require "database_cleaner"
|
|
7
|
+
require "fileutils"
|
|
7
8
|
require "rack/test"
|
|
8
9
|
require "timecop"
|
|
9
10
|
require "webmock/rspec"
|
|
@@ -80,7 +81,8 @@ Delayed::Backend::ActiveRecord::Job.reset_column_information
|
|
|
80
81
|
Delayed::Backend::ActiveRecord::Job::Failed.reset_column_information
|
|
81
82
|
|
|
82
83
|
Time.zone = "UTC" # rubocop:disable Rails/TimeZoneAssignment
|
|
83
|
-
|
|
84
|
+
FileUtils.mkdir_p("tmp")
|
|
85
|
+
ActiveRecord::Base.logger = Rails.logger = Logger.new("tmp/test.log")
|
|
84
86
|
|
|
85
87
|
# Purely useful for test cases...
|
|
86
88
|
class Story < ActiveRecord::Base
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inst-jobs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.4.
|
|
4
|
+
version: 2.4.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tobias Luetke
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2021-09-
|
|
12
|
+
date: 2021-09-21 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activerecord
|
|
@@ -431,6 +431,7 @@ files:
|
|
|
431
431
|
- db/migrate/20200825011002_add_strand_order_override.rb
|
|
432
432
|
- db/migrate/20210809145804_add_n_strand_index.rb
|
|
433
433
|
- db/migrate/20210812210128_add_singleton_column.rb
|
|
434
|
+
- db/migrate/20210917232626_add_delete_conflicting_singletons_before_unlock_trigger.rb
|
|
434
435
|
- exe/inst_jobs
|
|
435
436
|
- lib/delayed/backend/active_record.rb
|
|
436
437
|
- lib/delayed/backend/base.rb
|
|
@@ -511,7 +512,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
511
512
|
- !ruby/object:Gem::Version
|
|
512
513
|
version: '0'
|
|
513
514
|
requirements: []
|
|
514
|
-
rubygems_version: 3.2.
|
|
515
|
+
rubygems_version: 3.2.24
|
|
515
516
|
signing_key:
|
|
516
517
|
specification_version: 4
|
|
517
518
|
summary: Instructure-maintained fork of delayed_job
|