inst-jobs 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/db/migrate/20181217155351_speed_up_max_concurrent_triggers.rb +95 -0
  3. data/db/migrate/20190726154743_make_critical_columns_not_null.rb +15 -0
  4. data/db/migrate/20200330230722_add_id_to_get_delayed_jobs_index.rb +25 -0
  5. data/db/migrate/20200824222232_speed_up_max_concurrent_delete_trigger.rb +95 -0
  6. data/db/migrate/20200825011002_add_strand_order_override.rb +126 -0
  7. data/lib/delayed/backend/active_record.rb +93 -14
  8. data/lib/delayed/backend/redis/job.rb +8 -2
  9. data/lib/delayed/batch.rb +1 -1
  10. data/lib/delayed/lifecycle.rb +1 -0
  11. data/lib/delayed/periodic.rb +7 -4
  12. data/lib/delayed/server.rb +19 -0
  13. data/lib/delayed/server/helpers.rb +1 -0
  14. data/lib/delayed/server/public/js/app.js +49 -1
  15. data/lib/delayed/server/views/index.erb +16 -1
  16. data/lib/delayed/server/views/layout.erb +5 -3
  17. data/lib/delayed/settings.rb +5 -1
  18. data/lib/delayed/version.rb +1 -1
  19. data/lib/delayed/work_queue/parent_process/server.rb +14 -4
  20. data/lib/delayed/worker.rb +28 -6
  21. data/lib/delayed/worker/consul_health_check.rb +1 -1
  22. data/lib/delayed/worker/health_check.rb +9 -5
  23. data/lib/delayed/worker/null_health_check.rb +7 -1
  24. data/spec/active_record_job_spec.rb +36 -35
  25. data/spec/delayed/server_spec.rb +43 -1
  26. data/spec/delayed/work_queue/parent_process/server_spec.rb +4 -1
  27. data/spec/delayed/worker/consul_health_check_spec.rb +2 -2
  28. data/spec/delayed/worker/health_check_spec.rb +2 -2
  29. data/spec/delayed/worker_spec.rb +67 -20
  30. data/spec/gemfiles/52.gemfile +7 -0
  31. data/spec/gemfiles/60.gemfile +7 -0
  32. data/spec/shared/delayed_batch.rb +11 -0
  33. data/spec/shared/shared_backend.rb +15 -0
  34. data/spec/shared/worker.rb +4 -0
  35. data/spec/spec_helper.rb +4 -1
  36. metadata +23 -27
  37. data/spec/gemfiles/42.gemfile.lock +0 -192
  38. data/spec/gemfiles/50.gemfile.lock +0 -187
  39. data/spec/gemfiles/51.gemfile.lock +0 -187
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6cfef8fa3325ff17a355286bf43ea4df18f817ac3ce6d5ca938fa551c117897c
4
- data.tar.gz: 02cecf31bd9e6629cd9f15552a29578e916239f7c1dab7d7bec93274ffb38478
3
+ metadata.gz: d1547b62f3c5fd86703cc209ec4b0feb6954db3c6fb4b58f4bcdbcf58dc9a9eb
4
+ data.tar.gz: 95ad1cacd71ac4511ee29eb0d3fda780037768234c232302b6fc10084de9e351
5
5
  SHA512:
6
- metadata.gz: ec8936d4c32b74eae509897903c3ed86d639883554ea647f611f635191df8858df9734ba9c2931c43cba8adab26c24973108610798df29ded204d3f5e612b2b2
7
- data.tar.gz: acc42e02a347d8c16ccb06967a3feb1a965a2a8746e801df7488ccc0f84e00dee334fff352885e0400bf52a2ac764e02af9757967773131f7db7f8da23613d9e
6
+ metadata.gz: bd27fc7e6c0ca5a613ba98ea0928d7947f2344d05c57ab14f588ff615c69bab60a0b6f08e60d1b610b6746ad8a467d151237f1469129f6144d5e93ce20ea71ef
7
+ data.tar.gz: 25ce44b9f6eeedae9605c4bc2077a9e9b51a4b7df94ba4d68be1e78f70801af65273c332caba780c11bf523cff31daed9c44d8f068f697a8910781b09a2daf88
@@ -0,0 +1,95 @@
1
+ class SpeedUpMaxConcurrentTriggers < 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(<<-CODE)
12
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
13
+ DECLARE
14
+ running_count integer;
15
+ BEGIN
16
+ IF OLD.strand IS NOT NULL THEN
17
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
18
+ IF OLD.id % 20 = 0 THEN
19
+ running_count := (SELECT COUNT(*) FROM (
20
+ SELECT 1 as one FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't' LIMIT OLD.max_concurrent
21
+ ) subquery_for_count);
22
+ IF running_count < OLD.max_concurrent THEN
23
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id IN (
24
+ SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
25
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT (OLD.max_concurrent - running_count) FOR UPDATE
26
+ );
27
+ END IF;
28
+ ELSE
29
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id =
30
+ (SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
31
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT 1 FOR UPDATE);
32
+ END IF;
33
+ END IF;
34
+ RETURN OLD;
35
+ END;
36
+ $$ LANGUAGE plpgsql;
37
+ CODE
38
+
39
+ # don't need the full count on insert
40
+ execute(<<-CODE)
41
+ CREATE OR REPLACE FUNCTION delayed_jobs_before_insert_row_tr_fn () RETURNS trigger AS $$
42
+ BEGIN
43
+ IF NEW.strand IS NOT NULL THEN
44
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(NEW.strand));
45
+ IF (SELECT COUNT(*) FROM (
46
+ SELECT 1 AS one FROM delayed_jobs WHERE strand = NEW.strand LIMIT NEW.max_concurrent
47
+ ) subquery_for_count) = NEW.max_concurrent THEN
48
+ NEW.next_in_strand := 'f';
49
+ END IF;
50
+ END IF;
51
+ RETURN NEW;
52
+ END;
53
+ $$ LANGUAGE plpgsql;
54
+ CODE
55
+ end
56
+ end
57
+
58
+ def down
59
+ if connection.adapter_name == 'PostgreSQL'
60
+ execute(<<-CODE)
61
+ CREATE OR REPLACE FUNCTION delayed_jobs_after_delete_row_tr_fn () RETURNS trigger AS $$
62
+ DECLARE
63
+ running_count integer;
64
+ BEGIN
65
+ IF OLD.strand IS NOT NULL THEN
66
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
67
+ running_count := (SELECT COUNT(*) FROM delayed_jobs WHERE strand = OLD.strand AND next_in_strand = 't');
68
+ IF running_count < OLD.max_concurrent THEN
69
+ UPDATE delayed_jobs SET next_in_strand = 't' WHERE id IN (
70
+ SELECT id FROM delayed_jobs j2 WHERE next_in_strand = 'f' AND
71
+ j2.strand = OLD.strand ORDER BY j2.id ASC LIMIT (OLD.max_concurrent - running_count) FOR UPDATE
72
+ );
73
+ END IF;
74
+ END IF;
75
+ RETURN OLD;
76
+ END;
77
+ $$ LANGUAGE plpgsql;
78
+ CODE
79
+
80
+ execute(<<-CODE)
81
+ CREATE OR REPLACE FUNCTION delayed_jobs_before_insert_row_tr_fn () RETURNS trigger AS $$
82
+ BEGIN
83
+ IF NEW.strand IS NOT NULL THEN
84
+ PERFORM pg_advisory_xact_lock(half_md5_as_bigint(NEW.strand));
85
+ IF (SELECT COUNT(*) FROM delayed_jobs WHERE strand = NEW.strand) >= NEW.max_concurrent THEN
86
+ NEW.next_in_strand := 'f';
87
+ END IF;
88
+ END IF;
89
+ RETURN NEW;
90
+ END;
91
+ $$ LANGUAGE plpgsql;
92
+ CODE
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,15 @@
1
+ class MakeCriticalColumnsNotNull < ActiveRecord::Migration[4.2]
2
+ def connection
3
+ Delayed::Job.connection
4
+ end
5
+
6
+ def up
7
+ change_column_null :delayed_jobs, :run_at, false
8
+ change_column_null :delayed_jobs, :queue, false
9
+ end
10
+
11
+ def down
12
+ change_column_null :delayed_jobs, :run_at, true
13
+ change_column_null :delayed_jobs, :queue, true
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ class AddIdToGetDelayedJobsIndex < 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
+ rename_index :delayed_jobs, "get_delayed_jobs_index", "get_delayed_jobs_index_old"
10
+ add_index :delayed_jobs, [:queue, :priority, :run_at, :id],
11
+ algorithm: :concurrently,
12
+ where: "locked_at IS NULL AND next_in_strand",
13
+ name: "get_delayed_jobs_index"
14
+ remove_index :delayed_jobs, name: "get_delayed_jobs_index_old"
15
+ end
16
+
17
+ def down
18
+ rename_index :delayed_jobs, "get_delayed_jobs_index", "get_delayed_jobs_index_old"
19
+ add_index :delayed_jobs, [:priority, :run_at, :queue],
20
+ algorithm: :concurrently,
21
+ where: "locked_at IS NULL AND next_in_strand",
22
+ name: "get_delayed_jobs_index"
23
+ remove_index :delayed_jobs, name: "get_delayed_jobs_index_old"
24
+ end
25
+ end
@@ -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,10 +17,68 @@ 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
23
25
 
26
+ class << self
27
+ def create(attributes, &block)
28
+ return super if connection.prepared_statements || Rails.version < '5.2'
29
+
30
+ # modified from ActiveRecord::Persistence.create and ActiveRecord::Persistence#_insert_record
31
+ job = new(attributes, &block)
32
+ job.single_step_create
33
+ end
34
+ end
35
+
36
+ def single_step_create
37
+ connection = self.class.connection
38
+ return save if connection.prepared_statements || Rails.version < '5.2'
39
+
40
+ # a before_save callback that we're skipping
41
+ initialize_defaults
42
+
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
+ end
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
80
+ end
81
+
24
82
  # be aware that some strand functionality is controlled by triggers on
25
83
  # the database. see
26
84
  # db/migrate/20110831210257_add_delayed_jobs_next_in_strand.rb
@@ -71,8 +129,10 @@ module Delayed
71
129
  # 500.times { |i| "ohai".send_later_enqueue_args(:reverse, { :run_at => (12.hours.ago + (rand(24.hours.to_i))) }) }
72
130
  # then fire up your workers
73
131
  # you can check out strand correctness: diff test1.txt <(sort -n test1.txt)
74
- def self.ready_to_run
75
- where("run_at<=? AND locked_at IS NULL AND next_in_strand=?", db_time_now, true)
132
+ def self.ready_to_run(forced_latency: nil)
133
+ now = db_time_now
134
+ now -= forced_latency if forced_latency
135
+ where("run_at<=? AND locked_at IS NULL AND next_in_strand=?", now, true)
76
136
  end
77
137
  def self.by_priority
78
138
  order(:priority, :run_at, :id)
@@ -187,7 +247,7 @@ module Delayed
187
247
  end
188
248
 
189
249
  scope = scope.group(:tag).offset(offset).limit(limit)
190
- scope.order("COUNT(tag) DESC").count.map { |t,c| { :tag => t, :count => c } }
250
+ scope.order(Arel.sql("COUNT(tag) DESC")).count.map { |t,c| { :tag => t, :count => c } }
191
251
  end
192
252
 
193
253
  def self.maybe_silence_periodic_log(&block)
@@ -203,7 +263,8 @@ module Delayed
203
263
  min_priority = nil,
204
264
  max_priority = nil,
205
265
  prefetch: 0,
206
- prefetch_owner: nil)
266
+ prefetch_owner: nil,
267
+ forced_latency: nil)
207
268
 
208
269
  check_queue(queue)
209
270
  check_priorities(min_priority, max_priority)
@@ -217,9 +278,14 @@ module Delayed
217
278
  # jobs in a single query.
218
279
  effective_worker_names = Array(worker_names)
219
280
 
220
- target_jobs = all_available(queue, min_priority, max_priority).
281
+ lock = nil
282
+ lock = "FOR UPDATE SKIP LOCKED" if connection.postgresql_version >= 90500
283
+ target_jobs = all_available(queue,
284
+ min_priority,
285
+ max_priority,
286
+ forced_latency: forced_latency).
221
287
  limit(effective_worker_names.length + prefetch).
222
- lock
288
+ lock(lock)
223
289
  jobs_with_row_number = all.from(target_jobs).
224
290
  select("id, ROW_NUMBER() OVER () AS row_number")
225
291
  updates = "locked_by = CASE row_number "
@@ -295,23 +361,33 @@ module Delayed
295
361
 
296
362
  def self.all_available(queue = Delayed::Settings.queue,
297
363
  min_priority = nil,
298
- max_priority = nil)
364
+ max_priority = nil,
365
+ forced_latency: nil)
299
366
  min_priority ||= Delayed::MIN_PRIORITY
300
367
  max_priority ||= Delayed::MAX_PRIORITY
301
368
 
302
369
  check_queue(queue)
303
370
  check_priorities(min_priority, max_priority)
304
371
 
305
- self.ready_to_run.
372
+ self.ready_to_run(forced_latency: forced_latency).
306
373
  where(:priority => min_priority..max_priority, :queue => queue).
307
374
  by_priority
308
375
  end
309
376
 
310
377
  # used internally by create_singleton to take the appropriate lock
311
378
  # depending on the db driver
312
- def self.transaction_for_singleton(strand)
379
+ def self.transaction_for_singleton(strand, on_conflict)
380
+ return yield if on_conflict == :loose
313
381
  self.transaction do
314
- 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
315
391
  yield
316
392
  end
317
393
  end
@@ -323,18 +399,21 @@ module Delayed
323
399
  def self.create_singleton(options)
324
400
  strand = options[:strand]
325
401
  on_conflict = options.delete(:on_conflict) || :use_earliest
326
- transaction_for_singleton(strand) do
327
- job = self.where(:strand => strand, :locked_at => nil).order(:id).first
402
+ transaction_for_singleton(strand, on_conflict) do
403
+ job = self.where(:strand => strand, :locked_at => nil).next_in_strand_order.first
328
404
  new_job = new(options)
329
405
  if job
330
406
  new_job.initialize_defaults
331
407
  job.run_at =
332
408
  case on_conflict
333
- when :use_earliest
409
+ when :use_earliest, :patient
334
410
  [job.run_at, new_job.run_at].min
335
411
  when :overwrite
336
412
  new_job.run_at
413
+ when :loose
414
+ job.run_at
337
415
  end
416
+ job.handler = new_job.handler if on_conflict == :overwrite
338
417
  job.save! if job.changed?
339
418
  else
340
419
  new_job.save!
@@ -407,7 +486,7 @@ module Delayed
407
486
  raise "job already exists" unless new_record?
408
487
  self.locked_at = Delayed::Job.db_time_now
409
488
  self.locked_by = worker
410
- save!
489
+ single_step_create
411
490
  end
412
491
 
413
492
  def fail!