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 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