inst-jobs 0.15.18 → 0.16.0

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: 1f5fa07aabb776db7b07347664cd263fae34426050ea23b72e2f35ef6a665f46
4
- data.tar.gz: cfb860844d1a2c3654a8047f8c32331fab957f6fd1d5ac77f641f2704849d738
3
+ metadata.gz: d1547b62f3c5fd86703cc209ec4b0feb6954db3c6fb4b58f4bcdbcf58dc9a9eb
4
+ data.tar.gz: 95ad1cacd71ac4511ee29eb0d3fda780037768234c232302b6fc10084de9e351
5
5
  SHA512:
6
- metadata.gz: 9dd503fd8af0128fc999a78f96b199f759ea1c9df121f816ee72dd288d2e6bc6480b69bdece15f8f32c167e9785d18d9fb4b262abfd8459e08c12771d3dbe606
7
- data.tar.gz: cb4f63db74b2cd05fde625c43e100c07cd66ded4f3b0998d97bb19786befde179180b85b1dd45d193254bdd010aba9e2ddaf67b549d0e8cdc0657853b99146ed
6
+ metadata.gz: bd27fc7e6c0ca5a613ba98ea0928d7947f2344d05c57ab14f588ff615c69bab60a0b6f08e60d1b610b6746ad8a467d151237f1469129f6144d5e93ce20ea71ef
7
+ data.tar.gz: 25ce44b9f6eeedae9605c4bc2077a9e9b51a4b7df94ba4d68be1e78f70801af65273c332caba780c11bf523cff31daed9c44d8f068f697a8910781b09a2daf88
@@ -0,0 +1,95 @@
1
+ class SpeedUpMaxConcurrentDeleteTrigger < ActiveRecord::Migration[4.2]
2
+ def connection
3
+ Delayed::Job.connection
4
+ end
5
+
6
+ def up
7
+ if connection.adapter_name == 'PostgreSQL'
8
+ # tl;dr sacrifice some responsiveness to max_concurrent changes for faster performance
9
+ # don't get the count every single time - it's usually safe to just set the next one in line
10
+ # since the max_concurrent doesn't change all that often for a strand
11
+ execute(<<-SQL)
12
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
13
+ DECLARE
14
+ running_count integer;
15
+ should_lock boolean;
16
+ should_be_precise boolean;
17
+ BEGIN
18
+ IF OLD.strand IS NOT NULL THEN
19
+ should_lock := true;
20
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
21
+
22
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
23
+ running_count := (SELECT COUNT(*) FROM (
24
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
25
+ ) subquery_for_count);
26
+ should_lock := running_count < OLD.max_concurrent;
27
+ END IF;
28
+
29
+ IF should_lock THEN
30
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
31
+ END IF;
32
+
33
+ IF should_be_precise THEN
34
+ running_count := (SELECT COUNT(*) FROM (
35
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
36
+ ) subquery_for_count);
37
+ IF running_count < OLD.max_concurrent THEN
38
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id IN (
39
+ SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
40
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT (OLD.max_concurrent - running_count) FOR UPDATE
41
+ );
42
+ END IF;
43
+ ELSE
44
+ -- n-strands don't require precise ordering; we can make this query more performant
45
+ IF OLD.max_concurrent > 1 THEN
46
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id =
47
+ (SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
48
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT 1 FOR UPDATE SKIP LOCKED);
49
+ ELSE
50
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id =
51
+ (SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
52
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT 1 FOR UPDATE);
53
+ END IF;
54
+ END IF;
55
+ END IF;
56
+ RETURN OLD;
57
+ END;
58
+ $$ LANGUAGE plpgsql;
59
+ SQL
60
+ end
61
+ end
62
+
63
+ def down
64
+ if connection.adapter_name == 'PostgreSQL'
65
+ execute(<<-SQL)
66
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
67
+ DECLARE
68
+ running_count integer;
69
+ BEGIN
70
+ IF OLD.strand IS NOT NULL THEN
71
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
72
+ IF OLD.id % 20 = 0 THEN
73
+ running_count := (SELECT COUNT(*) FROM (
74
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
75
+ ) subquery_for_count);
76
+ IF running_count < OLD.max_concurrent THEN
77
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id IN (
78
+ SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
79
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT (OLD.max_concurrent - running_count) FOR UPDATE
80
+ );
81
+ END IF;
82
+ ELSE
83
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id =
84
+ (SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
85
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT 1 FOR UPDATE);
86
+ END IF;
87
+ END IF;
88
+ RETURN OLD;
89
+ END;
90
+ $$ LANGUAGE plpgsql;
91
+ SQL
92
+ end
93
+ end
94
+ end
95
+
@@ -0,0 +1,126 @@
1
+ class AddStrandOrderOverride < ActiveRecord::Migration[4.2]
2
+ disable_ddl_transaction! if respond_to?(:disable_ddl_transaction!)
3
+
4
+ def connection
5
+ Delayed::Job.connection
6
+ end
7
+
8
+ def up
9
+ add_column :delayed_jobs, :strand_order_override, :integer, default: 0, null: false
10
+ add_column :failed_jobs, :strand_order_override, :integer, default: 0, null: false
11
+ add_index :delayed_jobs, [:strand, :strand_order_override, :id],
12
+ algorithm: :concurrently,
13
+ where: "strand IS NOT NULL",
14
+ name: "next_in_strand_index"
15
+
16
+ if connection.adapter_name == 'PostgreSQL'
17
+ # Use the strand_order_override as the primary sorting mechanism (useful when moving between jobs queues without preserving ID ordering)
18
+ execute(<<-SQL)
19
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
20
+ DECLARE
21
+ running_count integer;
22
+ should_lock boolean;
23
+ should_be_precise boolean;
24
+ BEGIN
25
+ IF OLD.strand IS NOT NULL THEN
26
+ should_lock := true;
27
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
28
+
29
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
30
+ running_count := (SELECT COUNT(*) FROM (
31
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
32
+ ) subquery_for_count);
33
+ should_lock := running_count < OLD.max_concurrent;
34
+ END IF;
35
+
36
+ IF should_lock THEN
37
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
38
+ END IF;
39
+
40
+ IF should_be_precise THEN
41
+ running_count := (SELECT COUNT(*) FROM (
42
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
43
+ ) subquery_for_count);
44
+ IF running_count < OLD.max_concurrent THEN
45
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id IN (
46
+ SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
47
+ j2.strand = OLD.strand ORDER BY j2.strand_order_override ASC, j2.id ASC LIMIT (OLD.max_concurrent - running_count) FOR UPDATE
48
+ );
49
+ END IF;
50
+ ELSE
51
+ -- n-strands don't require precise ordering; we can make this query more performant
52
+ IF OLD.max_concurrent > 1 THEN
53
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id =
54
+ (SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
55
+ j2.strand = OLD.strand ORDER BY j2.strand_order_override ASC, j2.id ASC LIMIT 1 FOR UPDATE SKIP LOCKED);
56
+ ELSE
57
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id =
58
+ (SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
59
+ j2.strand = OLD.strand ORDER BY j2.strand_order_override ASC, j2.id ASC LIMIT 1 FOR UPDATE);
60
+ END IF;
61
+ END IF;
62
+ END IF;
63
+ RETURN OLD;
64
+ END;
65
+ $$ LANGUAGE plpgsql;
66
+ SQL
67
+ end
68
+ end
69
+
70
+ def down
71
+ remove_column :delayed_jobs, :strand_order_override, :integer
72
+ remove_column :failed_jobs, :strand_order_override, :integer
73
+
74
+ if connection.adapter_name == 'PostgreSQL'
75
+ execute(<<-SQL)
76
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
77
+ DECLARE
78
+ running_count integer;
79
+ should_lock boolean;
80
+ should_be_precise boolean;
81
+ BEGIN
82
+ IF OLD.strand IS NOT NULL THEN
83
+ should_lock := true;
84
+ should_be_precise := OLD.id % (OLD.max_concurrent * 4) = 0;
85
+
86
+ IF NOT should_be_precise AND OLD.max_concurrent > 16 THEN
87
+ running_count := (SELECT COUNT(*) FROM (
88
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
89
+ ) subquery_for_count);
90
+ should_lock := running_count < OLD.max_concurrent;
91
+ END IF;
92
+
93
+ IF should_lock THEN
94
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
95
+ END IF;
96
+
97
+ IF should_be_precise THEN
98
+ running_count := (SELECT COUNT(*) FROM (
99
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
100
+ ) subquery_for_count);
101
+ IF running_count < OLD.max_concurrent THEN
102
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id IN (
103
+ SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
104
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT (OLD.max_concurrent - running_count) FOR UPDATE
105
+ );
106
+ END IF;
107
+ ELSE
108
+ -- n-strands don't require precise ordering; we can make this query more performant
109
+ IF OLD.max_concurrent > 1 THEN
110
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id =
111
+ (SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
112
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT 1 FOR UPDATE SKIP LOCKED);
113
+ ELSE
114
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id =
115
+ (SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
116
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT 1 FOR UPDATE);
117
+ END IF;
118
+ END IF;
119
+ END IF;
120
+ RETURN OLD;
121
+ END;
122
+ $$ LANGUAGE plpgsql;
123
+ SQL
124
+ end
125
+ end
126
+ end
@@ -17,6 +17,8 @@ module Delayed
17
17
  include Delayed::Backend::Base
18
18
  self.table_name = :delayed_jobs
19
19
 
20
+ scope :next_in_strand_order, -> { order(:strand_order_override, :id) }
21
+
20
22
  def self.reconnect!
21
23
  clear_all_connections!
22
24
  end
@@ -27,42 +29,54 @@ module Delayed
27
29
 
28
30
  # modified from ActiveRecord::Persistence.create and ActiveRecord::Persistence#_insert_record
29
31
  job = new(attributes, &block)
30
- # a before_save callback that we're skipping
31
- job.initialize_defaults
32
+ job.single_step_create
33
+ end
34
+ end
32
35
 
33
- current_time = current_time_from_proper_timezone
36
+ def single_step_create
37
+ connection = self.class.connection
38
+ return save if connection.prepared_statements || Rails.version < '5.2'
34
39
 
35
- job.send(:all_timestamp_attributes_in_model).each do |column|
36
- if !job.attribute_present?(column)
37
- job._write_attribute(column, current_time)
38
- end
39
- end
40
+ # a before_save callback that we're skipping
41
+ initialize_defaults
40
42
 
41
- if Rails.version >= '6'
42
- attribute_names = job.send(:attribute_names_for_partial_writes)
43
- attribute_names = job.send(:attributes_for_create, attribute_names)
44
- values = job.send(:attributes_with_values, attribute_names)
45
- else
46
- attribute_names = job.partial_writes? ? job.send(:keys_for_partial_write) : self.attribute_names
47
- values = job.send(:attributes_with_values_for_create, attribute_names)
43
+ current_time = current_time_from_proper_timezone
44
+
45
+ all_timestamp_attributes_in_model.each do |column|
46
+ if !attribute_present?(column)
47
+ _write_attribute(column, current_time)
48
48
  end
49
- im = arel_table.compile_insert(_substitute_values(values))
50
- sql, _binds = connection.send(:to_sql_and_binds, im, [])
51
-
52
- # https://www.postgresql.org/docs/9.5/libpq-exec.html
53
- sql = "#{sql} RETURNING id"
54
- # > Multiple queries sent in a single PQexec call are processed in a single transaction,
55
- # unless there are explicit BEGIN/COMMIT commands included in the query string to divide
56
- # it into multiple transactions.
57
- sql = "SELECT pg_advisory_xact_lock(#{connection.quote_table_name('half_md5_as_bigint')}(#{connection.quote(values['strand'])})); #{sql}" if values["strand"]
58
- result = connection.execute(sql, "#{self} Create")
59
- job.id = result.values.first.first
60
- result.clear
61
- job.instance_variable_set(:@new_record, false)
62
- job.changes_applied
63
-
64
- job
65
49
  end
50
+
51
+ if Rails.version >= '6'
52
+ attribute_names = attribute_names_for_partial_writes
53
+ attribute_names = attributes_for_create(attribute_names)
54
+ values = attributes_with_values(attribute_names)
55
+ else
56
+ attribute_names = partial_writes? ? keys_for_partial_write : self.attribute_names
57
+ values = attributes_with_values_for_create(attribute_names)
58
+ end
59
+ im = self.class.arel_table.compile_insert(self.class.send(:_substitute_values, values))
60
+ sql, _binds = connection.send(:to_sql_and_binds, im, [])
61
+
62
+ # https://www.postgresql.org/docs/9.5/libpq-exec.html
63
+ sql = "#{sql} RETURNING id"
64
+ # > Multiple queries sent in a single PQexec call are processed in a single transaction,
65
+ # unless there are explicit BEGIN/COMMIT commands included in the query string to divide
66
+ # it into multiple transactions.
67
+ sql = "SELECT pg_advisory_xact_lock(#{connection.quote_table_name('half_md5_as_bigint')}(#{connection.quote(values['strand'])})); #{sql}" if values["strand"]
68
+ result = connection.execute(sql, "#{self} Create")
69
+ self.id = result.values.first.first
70
+ result.clear
71
+ @new_record = false
72
+ changes_applied
73
+
74
+ self
75
+ end
76
+
77
+ def destroy
78
+ # skip transaction and callbacks
79
+ destroy_row
66
80
  end
67
81
 
68
82
  # be aware that some strand functionality is controlled by triggers on
@@ -365,7 +379,15 @@ module Delayed
365
379
  def self.transaction_for_singleton(strand, on_conflict)
366
380
  return yield if on_conflict == :loose
367
381
  self.transaction do
368
- connection.execute(sanitize_sql(["SELECT pg_advisory_xact_lock(#{connection.quote_table_name('half_md5_as_bigint')}(?))", strand]))
382
+ if on_conflict == :patient
383
+ pg_function = 'pg_try_advisory_xact_lock'
384
+ execute_method = :select_value
385
+ else
386
+ pg_function = 'pg_advisory_xact_lock'
387
+ execute_method = :execute
388
+ end
389
+ result = connection.send(execute_method, sanitize_sql(["SELECT #{pg_function}(#{connection.quote_table_name('half_md5_as_bigint')}(?))", strand]))
390
+ return if result == false && on_conflict == :patient
369
391
  yield
370
392
  end
371
393
  end
@@ -378,13 +400,13 @@ module Delayed
378
400
  strand = options[:strand]
379
401
  on_conflict = options.delete(:on_conflict) || :use_earliest
380
402
  transaction_for_singleton(strand, on_conflict) do
381
- job = self.where(:strand => strand, :locked_at => nil).order(:id).first
403
+ job = self.where(:strand => strand, :locked_at => nil).next_in_strand_order.first
382
404
  new_job = new(options)
383
405
  if job
384
406
  new_job.initialize_defaults
385
407
  job.run_at =
386
408
  case on_conflict
387
- when :use_earliest
409
+ when :use_earliest, :patient
388
410
  [job.run_at, new_job.run_at].min
389
411
  when :overwrite
390
412
  new_job.run_at
@@ -464,7 +486,7 @@ module Delayed
464
486
  raise "job already exists" unless new_record?
465
487
  self.locked_at = Delayed::Job.db_time_now
466
488
  self.locked_by = worker
467
- save!
489
+ single_step_create
468
490
  end
469
491
 
470
492
  def fail!
@@ -47,7 +47,10 @@ class Periodic
47
47
  end
48
48
 
49
49
  def enqueue
50
- Delayed::Job.enqueue(self, @job_args.merge(:max_attempts => 1, :run_at => @cron.next_time(Delayed::Periodic.now).utc.to_time, :singleton => tag))
50
+ Delayed::Job.enqueue(self, @job_args.merge(:max_attempts => 1,
51
+ :run_at => @cron.next_time(Delayed::Periodic.now).utc.to_time,
52
+ :singleton => tag,
53
+ on_conflict: :patient))
51
54
  end
52
55
 
53
56
  def perform
@@ -23,7 +23,10 @@ module Delayed
23
23
  :worker_health_check_config,
24
24
  :worker_procname_prefix,
25
25
  ]
26
- SETTINGS_WITH_ARGS = [ :num_strands ]
26
+ SETTINGS_WITH_ARGS = [
27
+ :job_detailed_log_format,
28
+ :num_strands
29
+ ]
27
30
 
28
31
  SETTINGS.each do |setting|
29
32
  mattr_writer(setting)
@@ -65,6 +68,7 @@ module Delayed
65
68
 
66
69
  self.num_strands = ->(strand_name){ nil }
67
70
  self.default_job_options = ->{ Hash.new }
71
+ self.job_detailed_log_format = ->(job){ job.to_json(include_root: false, only: %w(tag strand priority attempts created_at max_attempts source)) }
68
72
 
69
73
  # Send workers KILL after QUIT if they haven't exited within the
70
74
  # slow_exit_timeout
@@ -1,3 +1,3 @@
1
1
  module Delayed
2
- VERSION = "0.15.18"
2
+ VERSION = "0.16.0"
3
3
  end
@@ -5,7 +5,7 @@ class ParentProcess
5
5
  attr_reader :clients, :listen_socket
6
6
 
7
7
  include Delayed::Logging
8
- SIGNALS = %i{INT TERM QUIT CHLD}
8
+ SIGNALS = %i{INT TERM QUIT}
9
9
 
10
10
  def initialize(listen_socket, parent_pid: nil, config: Settings.parent_process)
11
11
  @listen_socket = listen_socket
@@ -253,7 +253,7 @@ class Worker
253
253
  def log_job(job, format = :short)
254
254
  case format
255
255
  when :long
256
- "#{job.full_name} #{ job.to_json(:include_root => false, :only => %w(tag strand priority attempts created_at max_attempts source)) }"
256
+ "#{job.full_name} #{ Settings.job_detailed_log_format.call(job) }"
257
257
  else
258
258
  job.full_name
259
259
  end
@@ -38,6 +38,33 @@ describe Delayed::Worker do
38
38
  end
39
39
  end
40
40
 
41
+ describe "#log_job" do
42
+ around(:each) do |block|
43
+ prev_logger = Delayed::Settings.job_detailed_log_format
44
+ block.call
45
+ Delayed::Settings.job_detailed_log_format = prev_logger
46
+ end
47
+
48
+ it "has a reasonable default format" do
49
+ payload = double(perform: nil)
50
+ job = Delayed::Job.new(payload_object: payload, priority: 25, strand: "test_jobs")
51
+ short_log_format = subject.log_job(job, :short)
52
+ expect(short_log_format).to eq("RSpec::Mocks::Double")
53
+ long_format = subject.log_job(job, :long)
54
+ expect(long_format).to eq("RSpec::Mocks::Double {\"priority\":25,\"attempts\":0,\"created_at\":null,\"tag\":\"RSpec::Mocks::Double#perform\",\"max_attempts\":null,\"strand\":\"test_jobs\",\"source\":null}")
55
+ end
56
+
57
+ it "logging format can be changed with settings" do
58
+ Delayed::Settings.job_detailed_log_format = ->(job){ "override format #{job.strand}"}
59
+ payload = double(perform: nil)
60
+ job = Delayed::Job.new(payload_object: payload, priority: 25, strand: "test_jobs")
61
+ short_log_format = subject.log_job(job, :short)
62
+ expect(short_log_format).to eq("RSpec::Mocks::Double")
63
+ long_format = subject.log_job(job, :long)
64
+ expect(long_format).to eq("RSpec::Mocks::Double override format test_jobs")
65
+ end
66
+ end
67
+
41
68
  describe "#run" do
42
69
  it "passes extra config options through to the WorkQueue" do
43
70
  expect(subject.work_queue).to receive(:get_and_lock_next_available).