inst-jobs 2.4.3 → 2.4.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a5cde929174dd49e07d06025310bcea4cb0a6b55b3d83a40ea7b7bde9c9b775
4
- data.tar.gz: 1028828aa017afc111c89f410425a58939c42697066279b7a22174463384d1f1
3
+ metadata.gz: 48186ffba65e38c8e10702a68c93d9b4bb7f8ea13f98a7175e6755a1e693595f
4
+ data.tar.gz: 8a6e51516ffeaaa31d8b3bcb07a25e859259b9e734c5a8e14af019d842a01ed7
5
5
  SHA512:
6
- metadata.gz: ba08cb3d2e9ff3a72bdaf8c47d9265cc21a7bfd401b1657349c12043852955ac3c7e02d52c1ef1ab602207cbcc35e48e7293c77fceb34f12aec660d9e9638a7f
7
- data.tar.gz: d86f8ead17b6ddc679350dc9b9e5ee52a2ffc5b8ad62be52e5f375a28e0ccd9b9a1a6aad5255e08273a26fa9ed0d90a31afd618caee5e0378adf5481494bdc78
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
- horizon = db_time_now - (Settings.parent_process[:prefetched_jobs_timeout] * 4)
463
- where("locked_by LIKE 'prefetch:%' AND locked_at<?", horizon).update_all(locked_at: nil, locked_by: nil)
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)
@@ -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
- perform_audit!
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Delayed
4
- VERSION = "2.4.3"
4
+ VERSION = "2.4.7"
5
5
  end
@@ -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
- if jobs.first.locked_at < Time.now.utc - Settings.parent_process[:prefetched_jobs_timeout]
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.unlock(jobs)
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(described_class).to receive(:attempt_advisory_lock).and_return(false)
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
- Rails.logger = Logger.new(nil)
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.3
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-17 00:00:00.000000000 Z
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.16
515
+ rubygems_version: 3.2.24
515
516
  signing_key:
516
517
  specification_version: 4
517
518
  summary: Instructure-maintained fork of delayed_job