inst-jobs 2.0.0 → 3.1.0
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/20101216224513_create_delayed_jobs.rb +9 -7
- data/db/migrate/20110531144916_cleanup_delayed_jobs_indexes.rb +8 -13
- data/db/migrate/20110610213249_optimize_delayed_jobs.rb +8 -8
- data/db/migrate/20110831210257_add_delayed_jobs_next_in_strand.rb +25 -25
- data/db/migrate/20120510004759_delayed_jobs_delete_trigger_lock_for_update.rb +4 -8
- data/db/migrate/20120531150712_drop_psql_jobs_pop_fn.rb +1 -3
- data/db/migrate/20120607164022_delayed_jobs_use_advisory_locks.rb +11 -15
- data/db/migrate/20120607181141_index_jobs_on_locked_by.rb +1 -1
- data/db/migrate/20120608191051_add_jobs_run_at_index.rb +2 -2
- data/db/migrate/20120927184213_change_delayed_jobs_handler_to_text.rb +1 -1
- data/db/migrate/20140505215510_copy_failed_jobs_original_id.rb +2 -3
- data/db/migrate/20150807133223_add_max_concurrent_to_jobs.rb +9 -13
- data/db/migrate/20151210162949_improve_max_concurrent.rb +4 -8
- data/db/migrate/20161206323555_add_back_default_string_limits_jobs.rb +3 -2
- data/db/migrate/20181217155351_speed_up_max_concurrent_triggers.rb +13 -17
- data/db/migrate/20200330230722_add_id_to_get_delayed_jobs_index.rb +8 -8
- data/db/migrate/20200824222232_speed_up_max_concurrent_delete_trigger.rb +72 -77
- data/db/migrate/20200825011002_add_strand_order_override.rb +93 -97
- data/db/migrate/20210809145804_add_n_strand_index.rb +12 -0
- data/db/migrate/20210812210128_add_singleton_column.rb +200 -0
- data/db/migrate/20210917232626_add_delete_conflicting_singletons_before_unlock_trigger.rb +27 -0
- data/db/migrate/20210928174754_fix_singleton_condition_in_before_insert.rb +56 -0
- data/db/migrate/20210929204903_update_conflicting_singleton_function_to_use_index.rb +27 -0
- data/db/migrate/20211101190934_update_after_delete_trigger_for_singleton_index.rb +137 -0
- data/db/migrate/20211207094200_update_after_delete_trigger_for_singleton_transition_cases.rb +171 -0
- data/db/migrate/20211220112800_fix_singleton_race_condition_insert.rb +59 -0
- data/db/migrate/20211220113000_fix_singleton_race_condition_delete.rb +207 -0
- data/db/migrate/20220127091200_fix_singleton_unique_constraint.rb +31 -0
- data/db/migrate/20220128084800_update_insert_trigger_for_singleton_unique_constraint_change.rb +60 -0
- data/db/migrate/20220128084900_update_delete_trigger_for_singleton_unique_constraint_change.rb +209 -0
- data/db/migrate/20220203063200_remove_old_singleton_index.rb +31 -0
- data/db/migrate/20220328152900_add_failed_jobs_indicies.rb +12 -0
- data/exe/inst_jobs +3 -2
- data/lib/delayed/backend/active_record.rb +226 -168
- data/lib/delayed/backend/base.rb +119 -72
- data/lib/delayed/batch.rb +11 -9
- data/lib/delayed/cli.rb +98 -84
- data/lib/delayed/core_ext/kernel.rb +4 -2
- data/lib/delayed/daemon.rb +70 -74
- data/lib/delayed/job_tracking.rb +26 -25
- data/lib/delayed/lifecycle.rb +28 -23
- data/lib/delayed/log_tailer.rb +17 -17
- data/lib/delayed/logging.rb +13 -16
- data/lib/delayed/message_sending.rb +43 -52
- data/lib/delayed/performable_method.rb +6 -8
- data/lib/delayed/periodic.rb +72 -68
- data/lib/delayed/plugin.rb +2 -4
- data/lib/delayed/pool.rb +205 -168
- data/lib/delayed/rails_reloader_plugin.rb +30 -0
- data/lib/delayed/server/helpers.rb +6 -6
- data/lib/delayed/server.rb +51 -54
- data/lib/delayed/settings.rb +96 -81
- data/lib/delayed/testing.rb +21 -22
- data/lib/delayed/version.rb +1 -1
- data/lib/delayed/work_queue/in_process.rb +21 -17
- data/lib/delayed/work_queue/parent_process/client.rb +55 -53
- data/lib/delayed/work_queue/parent_process/server.rb +245 -207
- data/lib/delayed/work_queue/parent_process.rb +52 -53
- data/lib/delayed/worker/consul_health_check.rb +32 -33
- data/lib/delayed/worker/health_check.rb +35 -27
- data/lib/delayed/worker/null_health_check.rb +3 -1
- data/lib/delayed/worker/process_helper.rb +11 -12
- data/lib/delayed/worker.rb +257 -244
- data/lib/delayed/yaml_extensions.rb +12 -10
- data/lib/delayed_job.rb +37 -37
- data/lib/inst-jobs.rb +1 -1
- data/spec/active_record_job_spec.rb +152 -139
- data/spec/delayed/cli_spec.rb +7 -7
- data/spec/delayed/daemon_spec.rb +10 -9
- data/spec/delayed/message_sending_spec.rb +16 -9
- data/spec/delayed/periodic_spec.rb +14 -21
- data/spec/delayed/server_spec.rb +38 -38
- data/spec/delayed/settings_spec.rb +26 -25
- data/spec/delayed/work_queue/in_process_spec.rb +8 -9
- data/spec/delayed/work_queue/parent_process/client_spec.rb +17 -12
- data/spec/delayed/work_queue/parent_process/server_spec.rb +118 -42
- data/spec/delayed/work_queue/parent_process_spec.rb +21 -23
- data/spec/delayed/worker/consul_health_check_spec.rb +37 -50
- data/spec/delayed/worker/health_check_spec.rb +60 -52
- data/spec/delayed/worker_spec.rb +53 -24
- data/spec/sample_jobs.rb +45 -15
- data/spec/shared/delayed_batch.rb +74 -67
- data/spec/shared/delayed_method.rb +143 -102
- data/spec/shared/performable_method.rb +39 -38
- data/spec/shared/shared_backend.rb +801 -440
- data/spec/shared/testing.rb +14 -14
- data/spec/shared/worker.rb +157 -149
- data/spec/shared_jobs_specs.rb +13 -13
- data/spec/spec_helper.rb +57 -56
- metadata +183 -103
- data/lib/delayed/backend/redis/bulk_update.lua +0 -50
- data/lib/delayed/backend/redis/destroy_job.lua +0 -2
- data/lib/delayed/backend/redis/enqueue.lua +0 -29
- data/lib/delayed/backend/redis/fail_job.lua +0 -5
- data/lib/delayed/backend/redis/find_available.lua +0 -3
- data/lib/delayed/backend/redis/functions.rb +0 -59
- data/lib/delayed/backend/redis/get_and_lock_next_available.lua +0 -17
- data/lib/delayed/backend/redis/includes/jobs_common.lua +0 -203
- data/lib/delayed/backend/redis/job.rb +0 -535
- data/lib/delayed/backend/redis/set_running.lua +0 -5
- data/lib/delayed/backend/redis/tickle_strand.lua +0 -2
- data/spec/gemfiles/42.gemfile +0 -7
- data/spec/gemfiles/50.gemfile +0 -7
- data/spec/gemfiles/51.gemfile +0 -7
- data/spec/gemfiles/52.gemfile +0 -7
- data/spec/gemfiles/60.gemfile +0 -7
- data/spec/redis_job_spec.rb +0 -148
@@ -1,220 +1,243 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
module InDelayedJobTest
|
6
|
+
def self.check_in_job
|
7
|
+
Delayed::Job.in_delayed_job?.should == true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
shared_examples_for "a backend" do
|
4
12
|
def create_job(opts = {})
|
5
|
-
Delayed::Job.enqueue(SimpleJob.new, **{ :
|
13
|
+
Delayed::Job.enqueue(SimpleJob.new, **{ queue: nil }.merge(opts))
|
6
14
|
end
|
7
15
|
|
8
16
|
before do
|
9
17
|
SimpleJob.runs = 0
|
10
18
|
end
|
11
19
|
|
12
|
-
it "
|
13
|
-
Delayed::Job.create(:
|
20
|
+
it "sets run_at automatically if not set" do
|
21
|
+
expect(Delayed::Job.create(payload_object: ErrorJob.new).run_at).not_to be_nil
|
14
22
|
end
|
15
23
|
|
16
|
-
it "
|
24
|
+
it "does not set run_at automatically if already set" do
|
17
25
|
later = Delayed::Job.db_time_now + 5.minutes
|
18
|
-
Delayed::Job.create(:
|
26
|
+
expect(Delayed::Job.create(payload_object: ErrorJob.new, run_at: later).run_at).to be_within(1).of(later)
|
19
27
|
end
|
20
28
|
|
21
|
-
it "
|
22
|
-
|
29
|
+
it "raises ArgumentError when handler doesn't respond_to :perform" do
|
30
|
+
expect { Delayed::Job.enqueue(Object.new) }.to raise_error(ArgumentError)
|
23
31
|
end
|
24
32
|
|
25
|
-
it "
|
33
|
+
it "increases count after enqueuing items" do
|
26
34
|
Delayed::Job.enqueue SimpleJob.new
|
27
|
-
Delayed::Job.jobs_count(:current).
|
35
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(1)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "triggers the lifecycle event around the create" do
|
39
|
+
called = false
|
40
|
+
called_args = nil
|
41
|
+
|
42
|
+
Delayed::Worker.lifecycle.after(:create) do |args|
|
43
|
+
called = true
|
44
|
+
called_args = args
|
45
|
+
end
|
46
|
+
|
47
|
+
job = SimpleJob.new
|
48
|
+
Delayed::Job.enqueue(job)
|
49
|
+
|
50
|
+
expect(called).to be_truthy
|
51
|
+
expect(called_args[:payload_object]).to eq job
|
28
52
|
end
|
29
53
|
|
30
|
-
it "
|
31
|
-
@job = Delayed::Job.enqueue SimpleJob.new, :
|
32
|
-
@job.priority.
|
54
|
+
it "is able to set priority when enqueuing items" do
|
55
|
+
@job = Delayed::Job.enqueue SimpleJob.new, priority: 5
|
56
|
+
expect(@job.priority).to eq(5)
|
33
57
|
end
|
34
58
|
|
35
|
-
it "
|
59
|
+
it "uses the default priority when enqueuing items" do
|
36
60
|
Delayed::Job.default_priority = 0
|
37
61
|
@job = Delayed::Job.enqueue SimpleJob.new
|
38
|
-
@job.priority.
|
62
|
+
expect(@job.priority).to eq(0)
|
39
63
|
Delayed::Job.default_priority = 10
|
40
64
|
@job = Delayed::Job.enqueue SimpleJob.new
|
41
|
-
@job.priority.
|
65
|
+
expect(@job.priority).to eq(10)
|
42
66
|
Delayed::Job.default_priority = 0
|
43
67
|
end
|
44
68
|
|
45
|
-
it "
|
69
|
+
it "is able to set run_at when enqueuing items" do
|
46
70
|
later = Delayed::Job.db_time_now + 5.minutes
|
47
|
-
@job = Delayed::Job.enqueue SimpleJob.new, :
|
48
|
-
@job.run_at.
|
71
|
+
@job = Delayed::Job.enqueue SimpleJob.new, priority: 5, run_at: later
|
72
|
+
expect(@job.run_at).to be_within(1).of(later)
|
49
73
|
end
|
50
74
|
|
51
|
-
it "
|
75
|
+
it "is able to set expires_at when enqueuing items" do
|
52
76
|
later = Delayed::Job.db_time_now + 1.day
|
53
|
-
@job = Delayed::Job.enqueue SimpleJob.new, :
|
54
|
-
@job.expires_at.
|
77
|
+
@job = Delayed::Job.enqueue SimpleJob.new, expires_at: later
|
78
|
+
expect(@job.expires_at).to be_within(1).of(later)
|
55
79
|
end
|
56
80
|
|
57
|
-
it "
|
81
|
+
it "works with jobs in modules" do
|
58
82
|
M::ModuleJob.runs = 0
|
59
83
|
job = Delayed::Job.enqueue M::ModuleJob.new
|
60
|
-
|
84
|
+
expect { job.invoke_job }.to change { M::ModuleJob.runs }.from(0).to(1)
|
61
85
|
end
|
62
86
|
|
63
|
-
it "
|
64
|
-
job = Delayed::Job.new :
|
65
|
-
|
87
|
+
it "raises an DeserializationError when the job class is totally unknown" do
|
88
|
+
job = Delayed::Job.new handler: "--- !ruby/object:JobThatDoesNotExist {}"
|
89
|
+
expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError)
|
66
90
|
end
|
67
91
|
|
68
|
-
it "
|
69
|
-
job = Delayed::Job.new :
|
70
|
-
|
92
|
+
it "tries to load the class when it is unknown at the time of the deserialization" do
|
93
|
+
job = Delayed::Job.new handler: "--- !ruby/object:JobThatDoesNotExist {}"
|
94
|
+
expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError)
|
71
95
|
end
|
72
96
|
|
73
|
-
it "
|
74
|
-
job = Delayed::Job.new :
|
75
|
-
|
97
|
+
it "tries include the namespace when loading unknown objects" do
|
98
|
+
job = Delayed::Job.new handler: "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
|
99
|
+
expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError)
|
76
100
|
end
|
77
101
|
|
78
|
-
it "
|
79
|
-
job = Delayed::Job.new :
|
80
|
-
|
102
|
+
it "alsoes try to load structs when they are unknown (raises TypeError)" do
|
103
|
+
job = Delayed::Job.new handler: "--- !ruby/struct:JobThatDoesNotExist {}"
|
104
|
+
expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError)
|
81
105
|
end
|
82
106
|
|
83
|
-
it "
|
84
|
-
job = Delayed::Job.new :
|
85
|
-
|
107
|
+
it "tries include the namespace when loading unknown structs" do
|
108
|
+
job = Delayed::Job.new handler: "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
|
109
|
+
expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError)
|
86
110
|
end
|
87
111
|
|
88
|
-
it "
|
89
|
-
job = Delayed::Job.new :
|
90
|
-
|
112
|
+
it "raises an DeserializationError when the handler is invalid YAML" do
|
113
|
+
job = Delayed::Job.new handler: %(test: ""11")
|
114
|
+
expect { job.payload_object.perform }.to raise_error(Delayed::Backend::DeserializationError, /parsing error/)
|
91
115
|
end
|
92
116
|
|
93
117
|
describe "find_available" do
|
94
|
-
it "
|
95
|
-
@job = create_job :
|
118
|
+
it "does not find failed jobs" do
|
119
|
+
@job = create_job attempts: 50
|
96
120
|
@job.fail!
|
97
|
-
Delayed::Job.find_available(5).
|
121
|
+
expect(Delayed::Job.find_available(5)).not_to include(@job)
|
98
122
|
end
|
99
123
|
|
100
|
-
it "
|
101
|
-
@job = create_job :
|
102
|
-
Delayed::Job.find_available(5).
|
124
|
+
it "does not find jobs scheduled for the future" do
|
125
|
+
@job = create_job run_at: (Delayed::Job.db_time_now + 1.minute)
|
126
|
+
expect(Delayed::Job.find_available(5)).not_to include(@job)
|
103
127
|
end
|
104
128
|
|
105
|
-
it "
|
129
|
+
it "does not find jobs locked by another worker" do
|
106
130
|
@job = create_job
|
107
|
-
Delayed::Job.get_and_lock_next_available(
|
108
|
-
Delayed::Job.find_available(5).
|
131
|
+
expect(Delayed::Job.get_and_lock_next_available("other_worker")).to eq(@job)
|
132
|
+
expect(Delayed::Job.find_available(5)).not_to include(@job)
|
109
133
|
end
|
110
134
|
|
111
|
-
it "
|
135
|
+
it "finds open jobs" do
|
112
136
|
@job = create_job
|
113
|
-
Delayed::Job.find_available(5).
|
137
|
+
expect(Delayed::Job.find_available(5)).to include(@job)
|
114
138
|
end
|
115
139
|
|
116
140
|
it "returns an empty hash when asking for multiple jobs, and there aren't any" do
|
117
|
-
locked_jobs = Delayed::Job.get_and_lock_next_available([
|
118
|
-
locked_jobs.
|
141
|
+
locked_jobs = Delayed::Job.get_and_lock_next_available(%w[worker1 worker2])
|
142
|
+
expect(locked_jobs).to eq({})
|
119
143
|
end
|
120
144
|
end
|
121
145
|
|
122
146
|
context "when another worker is already performing an task, it" do
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
Delayed::Job.get_and_lock_next_available('worker1').should == @job
|
147
|
+
before do
|
148
|
+
@job = Delayed::Job.create payload_object: SimpleJob.new
|
149
|
+
expect(Delayed::Job.get_and_lock_next_available("worker1")).to eq(@job)
|
127
150
|
end
|
128
151
|
|
129
|
-
it "
|
130
|
-
Delayed::Job.get_and_lock_next_available(
|
152
|
+
it "does not allow a second worker to get exclusive access" do
|
153
|
+
expect(Delayed::Job.get_and_lock_next_available("worker2")).to be_nil
|
131
154
|
end
|
132
155
|
|
133
|
-
it "
|
134
|
-
Delayed::Job.find_available(1).length.
|
156
|
+
it "is not found by another worker" do
|
157
|
+
expect(Delayed::Job.find_available(1).length).to eq(0)
|
135
158
|
end
|
136
159
|
end
|
137
160
|
|
138
|
-
|
139
|
-
it "
|
140
|
-
Delayed::Job.create(:
|
161
|
+
describe "#name" do
|
162
|
+
it "is the class name of the job that was enqueued" do
|
163
|
+
expect(Delayed::Job.create(payload_object: ErrorJob.new).name).to eq("ErrorJob")
|
141
164
|
end
|
142
165
|
|
143
|
-
it "
|
166
|
+
it "is the method that will be called if its a performable method object" do
|
144
167
|
@job = Story.delay(ignore_transaction: true).create
|
145
|
-
@job.name.
|
168
|
+
expect(@job.name).to eq("Story.create")
|
146
169
|
end
|
147
170
|
|
148
|
-
it "
|
149
|
-
@job = Story.create(:
|
150
|
-
@job.name.
|
171
|
+
it "is the instance method that will be called if its a performable method object" do
|
172
|
+
@job = Story.create(text: "...").delay(ignore_transaction: true).save
|
173
|
+
expect(@job.name).to eq("Story#save")
|
151
174
|
end
|
152
175
|
end
|
153
176
|
|
154
177
|
context "worker prioritization" do
|
155
|
-
it "
|
156
|
-
10.times { create_job :
|
178
|
+
it "fetches jobs ordered by priority" do
|
179
|
+
10.times { create_job priority: rand(10) }
|
157
180
|
jobs = Delayed::Job.find_available(10)
|
158
|
-
jobs.size.
|
181
|
+
expect(jobs.size).to eq(10)
|
159
182
|
jobs.each_cons(2) do |a, b|
|
160
|
-
a.priority.
|
183
|
+
expect(a.priority).to be <= b.priority
|
161
184
|
end
|
162
185
|
end
|
163
186
|
|
164
|
-
it "
|
165
|
-
|
166
|
-
found = Delayed::Job.get_and_lock_next_available(
|
167
|
-
found.
|
168
|
-
job2 = create_job :
|
169
|
-
found = Delayed::Job.get_and_lock_next_available(
|
170
|
-
found.
|
171
|
-
job3 = create_job :
|
172
|
-
found = Delayed::Job.get_and_lock_next_available(
|
173
|
-
found.
|
174
|
-
end
|
175
|
-
|
176
|
-
it "
|
177
|
-
|
178
|
-
found = Delayed::Job.get_and_lock_next_available(
|
179
|
-
found.
|
180
|
-
job2 = create_job :
|
181
|
-
found = Delayed::Job.get_and_lock_next_available(
|
182
|
-
found.
|
183
|
-
job3 = create_job :
|
184
|
-
found = Delayed::Job.get_and_lock_next_available(
|
185
|
-
found.
|
187
|
+
it "does not find jobs lower than the given priority" do
|
188
|
+
create_job priority: 5
|
189
|
+
found = Delayed::Job.get_and_lock_next_available("test1", Delayed::Settings.queue, 10, 20)
|
190
|
+
expect(found).to be_nil
|
191
|
+
job2 = create_job priority: 10
|
192
|
+
found = Delayed::Job.get_and_lock_next_available("test1", Delayed::Settings.queue, 10, 20)
|
193
|
+
expect(found).to eq(job2)
|
194
|
+
job3 = create_job priority: 15
|
195
|
+
found = Delayed::Job.get_and_lock_next_available("test2", Delayed::Settings.queue, 10, 20)
|
196
|
+
expect(found).to eq(job3)
|
197
|
+
end
|
198
|
+
|
199
|
+
it "does not find jobs higher than the given priority" do
|
200
|
+
create_job priority: 25
|
201
|
+
found = Delayed::Job.get_and_lock_next_available("test1", Delayed::Settings.queue, 10, 20)
|
202
|
+
expect(found).to be_nil
|
203
|
+
job2 = create_job priority: 20
|
204
|
+
found = Delayed::Job.get_and_lock_next_available("test1", Delayed::Settings.queue, 10, 20)
|
205
|
+
expect(found).to eq(job2)
|
206
|
+
job3 = create_job priority: 15
|
207
|
+
found = Delayed::Job.get_and_lock_next_available("test2", Delayed::Settings.queue, 10, 20)
|
208
|
+
expect(found).to eq(job3)
|
186
209
|
end
|
187
210
|
end
|
188
211
|
|
189
212
|
context "clear_locks!" do
|
190
213
|
before do
|
191
|
-
@job = create_job(:
|
214
|
+
@job = create_job(locked_by: "worker", locked_at: Delayed::Job.db_time_now)
|
192
215
|
end
|
193
216
|
|
194
|
-
it "
|
195
|
-
Delayed::Job.clear_locks!(
|
196
|
-
Delayed::Job.find_available(5).
|
217
|
+
it "clears locks for the given worker" do
|
218
|
+
Delayed::Job.clear_locks!("worker")
|
219
|
+
expect(Delayed::Job.find_available(5)).to include(@job)
|
197
220
|
end
|
198
221
|
|
199
|
-
it "
|
200
|
-
Delayed::Job.clear_locks!(
|
201
|
-
Delayed::Job.find_available(5).
|
222
|
+
it "does not clear locks for other workers" do
|
223
|
+
Delayed::Job.clear_locks!("worker1")
|
224
|
+
expect(Delayed::Job.find_available(5)).not_to include(@job)
|
202
225
|
end
|
203
226
|
end
|
204
227
|
|
205
228
|
context "unlock" do
|
206
229
|
before do
|
207
|
-
@job = create_job(:
|
230
|
+
@job = create_job(locked_by: "worker", locked_at: Delayed::Job.db_time_now)
|
208
231
|
end
|
209
232
|
|
210
|
-
it "
|
233
|
+
it "clears locks" do
|
211
234
|
@job.unlock
|
212
|
-
@job.locked_by.
|
213
|
-
@job.locked_at.
|
235
|
+
expect(@job.locked_by).to be_nil
|
236
|
+
expect(@job.locked_at).to be_nil
|
214
237
|
end
|
215
238
|
|
216
239
|
it "clears locks from multiple jobs" do
|
217
|
-
job2 = create_job(:
|
240
|
+
job2 = create_job(locked_by: "worker", locked_at: Delayed::Job.db_time_now)
|
218
241
|
Delayed::Job.unlock([@job, job2])
|
219
242
|
expect(@job.locked_at).to be_nil
|
220
243
|
expect(job2.locked_at).to be_nil
|
@@ -225,295 +248,621 @@ shared_examples_for 'a backend' do
|
|
225
248
|
|
226
249
|
describe "#transfer_lock" do
|
227
250
|
it "works" do
|
228
|
-
job = create_job(:
|
229
|
-
expect(job.transfer_lock!(from:
|
230
|
-
expect(Delayed::Job.find(job.id).locked_by).to eq
|
251
|
+
job = create_job(locked_by: "worker", locked_at: Delayed::Job.db_time_now)
|
252
|
+
expect(job.transfer_lock!(from: "worker", to: "worker2")).to be true
|
253
|
+
expect(Delayed::Job.find(job.id).locked_by).to eq "worker2"
|
231
254
|
end
|
232
255
|
end
|
233
256
|
|
234
257
|
context "strands" do
|
235
|
-
it "
|
236
|
-
job1 = create_job(:
|
237
|
-
job2 = create_job(:
|
238
|
-
Delayed::Job.get_and_lock_next_available(
|
239
|
-
Delayed::Job.get_and_lock_next_available(
|
258
|
+
it "runs strand jobs in strict order" do
|
259
|
+
job1 = create_job(strand: "myjobs")
|
260
|
+
job2 = create_job(strand: "myjobs")
|
261
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job1)
|
262
|
+
expect(Delayed::Job.get_and_lock_next_available("w2")).to be_nil
|
240
263
|
job1.destroy
|
241
264
|
# update time since the failed lock pushed it forward
|
242
265
|
job2.run_at = 1.minute.ago
|
243
266
|
job2.save!
|
244
|
-
Delayed::Job.get_and_lock_next_available(
|
245
|
-
Delayed::Job.get_and_lock_next_available(
|
267
|
+
expect(Delayed::Job.get_and_lock_next_available("w3")).to eq(job2)
|
268
|
+
expect(Delayed::Job.get_and_lock_next_available("w4")).to be_nil
|
246
269
|
end
|
247
270
|
|
248
|
-
it "
|
249
|
-
job1 = create_job(:
|
250
|
-
job2 = create_job(:
|
251
|
-
Delayed::Job.find_available(2).
|
252
|
-
Delayed::Job.find_available(2).
|
271
|
+
it "fails to lock if an earlier job gets locked" do
|
272
|
+
job1 = create_job(strand: "myjobs")
|
273
|
+
job2 = create_job(strand: "myjobs")
|
274
|
+
expect(Delayed::Job.find_available(2)).to eq([job1])
|
275
|
+
expect(Delayed::Job.find_available(2)).to eq([job1])
|
253
276
|
|
254
277
|
# job1 gets locked by w1
|
255
|
-
Delayed::Job.get_and_lock_next_available(
|
278
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job1)
|
256
279
|
|
257
280
|
# normally w2 would now be able to lock job2, but strands prevent it
|
258
|
-
Delayed::Job.get_and_lock_next_available(
|
281
|
+
expect(Delayed::Job.get_and_lock_next_available("w2")).to be_nil
|
259
282
|
|
260
283
|
# now job1 is done
|
261
284
|
job1.destroy
|
262
285
|
# update time since the failed lock pushed it forward
|
263
286
|
job2.run_at = 1.minute.ago
|
264
287
|
job2.save!
|
265
|
-
Delayed::Job.get_and_lock_next_available(
|
288
|
+
expect(Delayed::Job.get_and_lock_next_available("w2")).to eq(job2)
|
266
289
|
end
|
267
290
|
|
268
|
-
it "
|
269
|
-
job1 = create_job(:
|
270
|
-
job2 = create_job(:
|
271
|
-
job3 = create_job(:
|
272
|
-
Delayed::Job.get_and_lock_next_available(
|
273
|
-
Delayed::Job.find_available(1).
|
291
|
+
it "keeps strand jobs in order as they are rescheduled" do
|
292
|
+
job1 = create_job(strand: "myjobs")
|
293
|
+
job2 = create_job(strand: "myjobs")
|
294
|
+
job3 = create_job(strand: "myjobs")
|
295
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job1)
|
296
|
+
expect(Delayed::Job.find_available(1)).to eq([])
|
274
297
|
job1.destroy
|
275
|
-
Delayed::Job.find_available(1).
|
298
|
+
expect(Delayed::Job.find_available(1)).to eq([job2])
|
276
299
|
# move job2's time forward
|
277
300
|
job2.run_at = 1.second.ago
|
278
301
|
job2.save!
|
279
302
|
job3.run_at = 5.seconds.ago
|
280
303
|
job3.save!
|
281
304
|
# we should still get job2, not job3
|
282
|
-
Delayed::Job.get_and_lock_next_available(
|
305
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job2)
|
283
306
|
end
|
284
307
|
|
285
|
-
it "
|
286
|
-
job1 = create_job(:
|
287
|
-
job2 = create_job(:
|
308
|
+
it "allows to run the next job if a failed job is present" do
|
309
|
+
job1 = create_job(strand: "myjobs")
|
310
|
+
job2 = create_job(strand: "myjobs")
|
288
311
|
job1.fail!
|
289
|
-
Delayed::Job.get_and_lock_next_available(
|
312
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job2)
|
290
313
|
end
|
291
314
|
|
292
|
-
it "
|
293
|
-
jobs = [create_job(:
|
294
|
-
locked = [Delayed::Job.get_and_lock_next_available(
|
295
|
-
Delayed::Job.get_and_lock_next_available(
|
296
|
-
jobs.
|
297
|
-
Delayed::Job.get_and_lock_next_available(
|
315
|
+
it "does not interfere with jobs with no strand" do
|
316
|
+
jobs = [create_job(strand: nil), create_job(strand: "myjobs")]
|
317
|
+
locked = [Delayed::Job.get_and_lock_next_available("w1"),
|
318
|
+
Delayed::Job.get_and_lock_next_available("w2")]
|
319
|
+
expect(jobs).to eq locked
|
320
|
+
expect(Delayed::Job.get_and_lock_next_available("w3")).to be_nil
|
298
321
|
end
|
299
322
|
|
300
|
-
it "
|
301
|
-
jobs = [create_job(:
|
302
|
-
locked = [Delayed::Job.get_and_lock_next_available(
|
303
|
-
Delayed::Job.get_and_lock_next_available(
|
304
|
-
jobs.
|
305
|
-
Delayed::Job.get_and_lock_next_available(
|
323
|
+
it "does not interfere with jobs in other strands" do
|
324
|
+
jobs = [create_job(strand: "strand1"), create_job(strand: "strand2")]
|
325
|
+
locked = [Delayed::Job.get_and_lock_next_available("w1"),
|
326
|
+
Delayed::Job.get_and_lock_next_available("w2")]
|
327
|
+
expect(jobs).to eq locked
|
328
|
+
expect(Delayed::Job.get_and_lock_next_available("w3")).to be_nil
|
306
329
|
end
|
307
330
|
|
308
|
-
it "
|
309
|
-
jobs = [create_job(:
|
310
|
-
first = Delayed::Job.get_and_lock_next_available(
|
311
|
-
second = Delayed::Job.get_and_lock_next_available(
|
331
|
+
it "does not find next jobs when given no priority" do
|
332
|
+
jobs = [create_job(strand: "strand1"), create_job(strand: "strand1")]
|
333
|
+
first = Delayed::Job.get_and_lock_next_available("w1", Delayed::Settings.queue, nil, nil)
|
334
|
+
second = Delayed::Job.get_and_lock_next_available("w2", Delayed::Settings.queue, nil, nil)
|
312
335
|
expect(first).to eq jobs.first
|
313
|
-
expect(second).to
|
336
|
+
expect(second).to be_nil
|
314
337
|
end
|
315
338
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
339
|
+
it "complains if you pass more than one strand-based option" do
|
340
|
+
expect { create_job(strand: "a", n_strand: "b") }.to raise_error(ArgumentError)
|
341
|
+
end
|
342
|
+
|
343
|
+
context "singleton" do
|
344
|
+
it "creates if there's no jobs on the strand" do
|
345
|
+
@job = create_job(singleton: "myjobs")
|
346
|
+
expect(@job).to be_present
|
347
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(@job)
|
321
348
|
end
|
322
349
|
|
323
|
-
it "
|
324
|
-
@job = create_job(:
|
325
|
-
@job.
|
326
|
-
Delayed::Job.get_and_lock_next_available(
|
350
|
+
it "creates if there's another job on the strand, but it's running" do
|
351
|
+
@job = create_job(singleton: "myjobs")
|
352
|
+
expect(@job).to be_present
|
353
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(@job)
|
327
354
|
|
328
|
-
@job2 = create_job(:
|
329
|
-
@job.
|
330
|
-
@job2.
|
355
|
+
@job2 = create_job(singleton: "myjobs")
|
356
|
+
expect(@job).to be_present
|
357
|
+
expect(@job2).not_to eq(@job)
|
331
358
|
end
|
332
359
|
|
333
|
-
it "
|
334
|
-
@job = create_job(:
|
335
|
-
@job.
|
360
|
+
it "does not create if there's another non-running job on the strand" do
|
361
|
+
@job = create_job(singleton: "myjobs")
|
362
|
+
expect(@job).to be_present
|
336
363
|
|
337
|
-
@job2 = create_job(:
|
338
|
-
@job2.
|
364
|
+
@job2 = create_job(singleton: "myjobs")
|
365
|
+
expect(@job2).to be_new_record
|
339
366
|
end
|
340
367
|
|
341
|
-
it "
|
342
|
-
@job = create_job(:
|
343
|
-
@job.
|
344
|
-
Delayed::Job.get_and_lock_next_available(
|
368
|
+
it "does not create if there's a job running and one waiting on the strand" do
|
369
|
+
@job = create_job(singleton: "myjobs")
|
370
|
+
expect(@job).to be_present
|
371
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(@job)
|
345
372
|
|
346
|
-
@job2 = create_job(:
|
347
|
-
@job2.
|
348
|
-
@job2.
|
373
|
+
@job2 = create_job(singleton: "myjobs")
|
374
|
+
expect(@job2).to be_present
|
375
|
+
expect(@job2).not_to eq(@job)
|
349
376
|
|
350
|
-
@job3 = create_job(:
|
351
|
-
@job3.
|
377
|
+
@job3 = create_job(singleton: "myjobs")
|
378
|
+
expect(@job3).to be_new_record
|
352
379
|
end
|
353
380
|
|
354
|
-
it "
|
355
|
-
job1 = create_job(singleton:
|
356
|
-
job2 = create_job(singleton:
|
357
|
-
job2.
|
381
|
+
it "updates existing job if new job is set to run sooner" do
|
382
|
+
job1 = create_job(singleton: "myjobs", run_at: 1.hour.from_now)
|
383
|
+
job2 = create_job(singleton: "myjobs")
|
384
|
+
expect(job2).to eq(job1)
|
358
385
|
# it should be scheduled to run immediately
|
359
|
-
Delayed::Job.get_and_lock_next_available(
|
386
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job1)
|
360
387
|
end
|
361
388
|
|
362
|
-
it "
|
389
|
+
it "updates existing job to a later date if requested" do
|
363
390
|
t1 = 1.hour.from_now
|
364
391
|
t2 = 2.hours.from_now
|
365
|
-
job1 = create_job(singleton:
|
366
|
-
job2 = create_job(singleton:
|
367
|
-
job2.
|
368
|
-
|
369
|
-
|
370
|
-
job3
|
371
|
-
job3.
|
372
|
-
job3.run_at.to_i.should == t2.to_i
|
392
|
+
job1 = create_job(singleton: "myjobs", run_at: t1)
|
393
|
+
job2 = create_job(singleton: "myjobs", run_at: t2)
|
394
|
+
expect(job2).to be_new_record
|
395
|
+
|
396
|
+
job3 = create_job(singleton: "myjobs", run_at: t2, on_conflict: :overwrite)
|
397
|
+
expect(job3).to eq(job1)
|
398
|
+
expect(job3.run_at.to_i).to eq(t2.to_i)
|
373
399
|
end
|
374
400
|
|
375
|
-
it "
|
376
|
-
job1 = Delayed::Job.enqueue(SimpleJob.new, queue: nil, singleton:
|
377
|
-
job2 = Delayed::Job.enqueue(ErrorJob.new, queue: nil, singleton:
|
378
|
-
job2.
|
379
|
-
expect(
|
401
|
+
it "updates existing singleton job handler if requested" do
|
402
|
+
job1 = Delayed::Job.enqueue(SimpleJob.new, queue: nil, singleton: "myjobs", on_conflict: :overwrite)
|
403
|
+
job2 = Delayed::Job.enqueue(ErrorJob.new, queue: nil, singleton: "myjobs", on_conflict: :overwrite)
|
404
|
+
expect(job2).to eq(job1)
|
405
|
+
expect(job1.reload.handler).to include("ErrorJob")
|
380
406
|
end
|
381
407
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
408
|
+
context "next_in_strand management - deadlocks and race conditions", non_transactional: true, slow: true do
|
409
|
+
# The following unit tests are fairly slow and non-deterministic. It may be
|
410
|
+
# easier to make them fail quicker and more consistently by adding a random
|
411
|
+
# sleep into the appropriate trigger(s).
|
412
|
+
|
413
|
+
def loop_secs(val)
|
414
|
+
loop_start = Time.now.utc
|
415
|
+
|
416
|
+
loop do
|
417
|
+
break if Time.now.utc >= loop_start + val
|
418
|
+
|
419
|
+
yield
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def loop_until_found(params)
|
424
|
+
found = false
|
425
|
+
|
426
|
+
loop_secs(10.seconds) do
|
427
|
+
if Delayed::Job.exists?(**params)
|
428
|
+
found = true
|
429
|
+
break
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
raise "timed out waiting for condition" unless found
|
434
|
+
end
|
435
|
+
|
436
|
+
def thread_body
|
437
|
+
yield
|
438
|
+
rescue
|
439
|
+
Thread.current.thread_variable_set(:fail, true)
|
440
|
+
raise
|
441
|
+
end
|
442
|
+
|
443
|
+
it "doesn't orphan the singleton when two are queued consecutively" do
|
444
|
+
# In order to reproduce this one efficiently, you'll probably want to add
|
445
|
+
# a sleep within delayed_jobs_before_insert_row_tr_fn.
|
446
|
+
# IF NEW.singleton IS NOT NULL THEN
|
447
|
+
# ...
|
448
|
+
# PERFORM pg_sleep(random() * 2);
|
449
|
+
# END IF;
|
450
|
+
|
451
|
+
threads = []
|
452
|
+
|
453
|
+
threads << Thread.new do
|
454
|
+
thread_body do
|
455
|
+
loop do
|
456
|
+
create_job(singleton: "singleton_job")
|
457
|
+
create_job(singleton: "singleton_job")
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
threads << Thread.new do
|
463
|
+
thread_body do
|
464
|
+
loop do
|
465
|
+
Delayed::Job.get_and_lock_next_available("w1")&.destroy
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
threads << Thread.new do
|
471
|
+
thread_body do
|
472
|
+
loop do
|
473
|
+
loop_until_found(singleton: "singleton_job", next_in_strand: true)
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
begin
|
479
|
+
loop_secs(60.seconds) do
|
480
|
+
if threads.any? { |x| x.thread_variable_get(:fail) }
|
481
|
+
raise "at least one job became orphaned or other error"
|
482
|
+
end
|
483
|
+
end
|
484
|
+
ensure
|
485
|
+
threads.each(&:kill)
|
486
|
+
threads.each(&:join)
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
it "doesn't deadlock when transitioning from strand_a to strand_b" do
|
491
|
+
# In order to reproduce this one efficiently, you'll probably want to add
|
492
|
+
# a sleep within delayed_jobs_after_delete_row_tr_fn.
|
493
|
+
# PERFORM pg_advisory_xact_lock(half_md5_as_bigint(OLD.strand));
|
494
|
+
# PERFORM pg_sleep(random() * 2);
|
495
|
+
|
496
|
+
threads = []
|
497
|
+
|
498
|
+
threads << Thread.new do
|
499
|
+
thread_body do
|
500
|
+
loop do
|
501
|
+
j1 = create_job(singleton: "myjobs", strand: "myjobs2", locked_by: "w1")
|
502
|
+
j2 = create_job(singleton: "myjobs", strand: "myjobs")
|
503
|
+
|
504
|
+
j1.delete
|
505
|
+
j2.delete
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
threads << Thread.new do
|
511
|
+
thread_body do
|
512
|
+
loop do
|
513
|
+
j1 = create_job(singleton: "myjobs2", strand: "myjobs", locked_by: "w1")
|
514
|
+
j2 = create_job(singleton: "myjobs2", strand: "myjobs2")
|
515
|
+
|
516
|
+
j1.delete
|
517
|
+
j2.delete
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
threads << Thread.new do
|
523
|
+
thread_body do
|
524
|
+
loop do
|
525
|
+
loop_until_found(singleton: "myjobs", next_in_strand: true)
|
526
|
+
end
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
threads << Thread.new do
|
531
|
+
thread_body do
|
532
|
+
loop do
|
533
|
+
loop_until_found(singleton: "myjobs2", next_in_strand: true)
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
begin
|
539
|
+
loop_secs(60.seconds) do
|
540
|
+
if threads.any? { |x| x.thread_variable_get(:fail) }
|
541
|
+
raise "at least one thread hit a deadlock or other error"
|
542
|
+
end
|
543
|
+
end
|
544
|
+
ensure
|
545
|
+
threads.each(&:kill)
|
546
|
+
threads.each(&:join)
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
context "next_in_strand management" do
|
552
|
+
it "handles transitions correctly when going from stranded to not stranded" do
|
553
|
+
@job1 = create_job(singleton: "myjobs", strand: "myjobs")
|
554
|
+
Delayed::Job.get_and_lock_next_available("w1")
|
555
|
+
@job2 = create_job(singleton: "myjobs")
|
556
|
+
|
557
|
+
expect(@job1.reload.next_in_strand).to be true
|
558
|
+
expect(@job2.reload.next_in_strand).to be false
|
559
|
+
|
560
|
+
@job1.destroy
|
561
|
+
expect(@job2.reload.next_in_strand).to be true
|
562
|
+
end
|
563
|
+
|
564
|
+
it "handles transitions correctly when going from not stranded to stranded" do
|
565
|
+
@job1 = create_job(singleton: "myjobs2", strand: "myjobs")
|
566
|
+
@job2 = create_job(singleton: "myjobs")
|
567
|
+
Delayed::Job.get_and_lock_next_available("w1")
|
568
|
+
Delayed::Job.get_and_lock_next_available("w1")
|
569
|
+
@job3 = create_job(singleton: "myjobs", strand: "myjobs2")
|
570
|
+
|
571
|
+
expect(@job1.reload.next_in_strand).to be true
|
572
|
+
expect(@job2.reload.next_in_strand).to be true
|
573
|
+
expect(@job3.reload.next_in_strand).to be false
|
574
|
+
|
575
|
+
@job2.destroy
|
576
|
+
expect(@job1.reload.next_in_strand).to be true
|
577
|
+
expect(@job3.reload.next_in_strand).to be true
|
578
|
+
end
|
579
|
+
|
580
|
+
it "does not violate n_strand=1 constraints when going from not stranded to stranded" do
|
581
|
+
@job1 = create_job(singleton: "myjobs2", strand: "myjobs")
|
582
|
+
@job2 = create_job(singleton: "myjobs")
|
583
|
+
Delayed::Job.get_and_lock_next_available("w1")
|
584
|
+
Delayed::Job.get_and_lock_next_available("w1")
|
585
|
+
@job3 = create_job(singleton: "myjobs", strand: "myjobs")
|
586
|
+
|
587
|
+
expect(@job1.reload.next_in_strand).to be true
|
588
|
+
expect(@job2.reload.next_in_strand).to be true
|
589
|
+
expect(@job3.reload.next_in_strand).to be false
|
590
|
+
|
591
|
+
@job2.destroy
|
592
|
+
expect(@job1.reload.next_in_strand).to be true
|
593
|
+
expect(@job3.reload.next_in_strand).to be false
|
594
|
+
end
|
595
|
+
|
596
|
+
it "handles transitions correctly when going from stranded to another strand" do
|
597
|
+
@job1 = create_job(singleton: "myjobs", strand: "myjobs")
|
598
|
+
Delayed::Job.get_and_lock_next_available("w1")
|
599
|
+
@job2 = create_job(singleton: "myjobs", strand: "myjobs2")
|
600
|
+
|
601
|
+
expect(@job1.reload.next_in_strand).to be true
|
602
|
+
expect(@job2.reload.next_in_strand).to be false
|
603
|
+
|
604
|
+
@job1.destroy
|
605
|
+
expect(@job2.reload.next_in_strand).to be true
|
606
|
+
end
|
607
|
+
|
608
|
+
it "does not violate n_strand=1 constraints when going from stranded to another strand" do
|
609
|
+
@job1 = create_job(singleton: "myjobs2", strand: "myjobs2")
|
610
|
+
@job2 = create_job(singleton: "myjobs", strand: "myjobs")
|
611
|
+
Delayed::Job.get_and_lock_next_available("w1")
|
612
|
+
Delayed::Job.get_and_lock_next_available("w1")
|
613
|
+
@job3 = create_job(singleton: "myjobs", strand: "myjobs2")
|
614
|
+
|
615
|
+
expect(@job1.reload.next_in_strand).to be true
|
616
|
+
expect(@job2.reload.next_in_strand).to be true
|
617
|
+
expect(@job3.reload.next_in_strand).to be false
|
618
|
+
|
619
|
+
@job2.destroy
|
620
|
+
expect(@job1.reload.next_in_strand).to be true
|
621
|
+
expect(@job3.reload.next_in_strand).to be false
|
622
|
+
end
|
623
|
+
|
624
|
+
it "creates first as true, and second as false, then transitions to second when deleted" do
|
625
|
+
@job1 = create_job(singleton: "myjobs")
|
626
|
+
Delayed::Job.get_and_lock_next_available("w1")
|
627
|
+
@job2 = create_job(singleton: "myjobs")
|
628
|
+
expect(@job1.reload.next_in_strand).to be true
|
629
|
+
expect(@job2.reload.next_in_strand).to be false
|
630
|
+
|
631
|
+
@job1.destroy
|
632
|
+
expect(@job2.reload.next_in_strand).to be true
|
633
|
+
end
|
634
|
+
|
635
|
+
it "when combined with a strand" do
|
636
|
+
job1 = create_job(singleton: "singleton", strand: "strand")
|
637
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job1
|
638
|
+
job2 = create_job(singleton: "singleton", strand: "strand")
|
639
|
+
expect(job2).not_to eq job1
|
640
|
+
expect(job2).not_to be_new_record
|
641
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
642
|
+
job3 = create_job(strand: "strand")
|
643
|
+
job4 = create_job(strand: "strand")
|
644
|
+
expect(job3.reload).not_to be_next_in_strand
|
645
|
+
expect(job4.reload).not_to be_next_in_strand
|
646
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
647
|
+
job1.destroy
|
648
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job2
|
649
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
650
|
+
job2.destroy
|
651
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job3
|
652
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
653
|
+
job3.destroy
|
654
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job4
|
655
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
656
|
+
end
|
657
|
+
|
658
|
+
it "when combined with a small n_strand" do
|
659
|
+
allow(Delayed::Settings).to receive(:num_strands).and_return(->(*) { 2 })
|
660
|
+
|
661
|
+
job1 = create_job(singleton: "singleton", n_strand: "strand")
|
662
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job1
|
663
|
+
job2 = create_job(singleton: "singleton", n_strand: "strand")
|
664
|
+
expect(job2).not_to eq job1
|
665
|
+
expect(job2).not_to be_new_record
|
666
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
667
|
+
job3 = create_job(n_strand: "strand")
|
668
|
+
job4 = create_job(n_strand: "strand")
|
669
|
+
expect(job3.reload).to be_next_in_strand
|
670
|
+
expect(job4.reload).not_to be_next_in_strand
|
671
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job3
|
672
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
673
|
+
# this doesn't unlock job2, even though it's ahead of job4
|
674
|
+
job3.destroy
|
675
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job4
|
676
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
677
|
+
job4.destroy
|
678
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
679
|
+
job1.destroy
|
680
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job2
|
681
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
682
|
+
end
|
683
|
+
|
684
|
+
it "when combined with a larger n_strand" do
|
685
|
+
allow(Delayed::Settings).to receive(:num_strands).and_return(->(*) { 10 })
|
686
|
+
|
687
|
+
job1 = create_job(singleton: "singleton", n_strand: "strand")
|
688
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job1
|
689
|
+
job2 = create_job(singleton: "singleton", n_strand: "strand")
|
690
|
+
expect(job2).not_to eq job1
|
691
|
+
expect(job2).not_to be_new_record
|
692
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
693
|
+
job3 = create_job(n_strand: "strand")
|
694
|
+
job4 = create_job(n_strand: "strand")
|
695
|
+
expect(job3.reload).to be_next_in_strand
|
696
|
+
expect(job4.reload).to be_next_in_strand
|
697
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job3
|
698
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job4
|
699
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
700
|
+
# this doesn't unlock job2
|
701
|
+
job3.destroy
|
702
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
703
|
+
job4.destroy
|
704
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
705
|
+
job1.destroy
|
706
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq job2
|
707
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
context "with on_conflict: loose and strand-inferred-from-singleton" do
|
712
|
+
around do |example|
|
713
|
+
Delayed::Settings.infer_strand_from_singleton = true
|
714
|
+
example.call
|
715
|
+
ensure
|
716
|
+
Delayed::Settings.infer_strand_from_singleton = false
|
717
|
+
end
|
718
|
+
|
719
|
+
it "does not create if there's another non-running job on the strand" do
|
720
|
+
@job = create_job(singleton: "myjobs", on_conflict: :loose)
|
721
|
+
expect(@job).to be_present
|
722
|
+
|
723
|
+
@job2 = create_job(singleton: "myjobs", on_conflict: :loose)
|
724
|
+
expect(@job2).to be_new_record
|
725
|
+
end
|
726
|
+
end
|
727
|
+
|
728
|
+
context "when unlocking with another singleton pending" do
|
729
|
+
it "deletes the pending singleton" do
|
730
|
+
@job1 = create_job(singleton: "myjobs", max_attempts: 2)
|
731
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(@job1)
|
732
|
+
|
733
|
+
@job2 = create_job(singleton: "myjobs", max_attempts: 2)
|
734
|
+
|
735
|
+
@job1.reload.reschedule
|
736
|
+
expect { @job1.reload }.not_to raise_error
|
737
|
+
expect { @job2.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
738
|
+
end
|
388
739
|
end
|
389
740
|
end
|
390
741
|
end
|
391
742
|
|
392
743
|
context "on hold" do
|
393
|
-
it "
|
394
|
-
job1 = create_job
|
744
|
+
it "hold/unholds jobs" do
|
745
|
+
job1 = create_job
|
395
746
|
job1.hold!
|
396
|
-
Delayed::Job.get_and_lock_next_available(
|
747
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to be_nil
|
397
748
|
|
398
749
|
job1.unhold!
|
399
|
-
Delayed::Job.get_and_lock_next_available(
|
750
|
+
expect(Delayed::Job.get_and_lock_next_available("w1")).to eq(job1)
|
400
751
|
end
|
401
752
|
end
|
402
753
|
|
403
754
|
context "periodic jobs" do
|
404
|
-
before
|
755
|
+
before do
|
405
756
|
# make the periodic job get scheduled in the past
|
406
757
|
@cron_time = 10.minutes.ago
|
407
758
|
allow(Delayed::Periodic).to receive(:now).and_return(@cron_time)
|
408
759
|
Delayed::Periodic.scheduled = {}
|
409
|
-
Delayed::Periodic.cron(
|
760
|
+
Delayed::Periodic.cron("my SimpleJob", "*/5 * * * * *") do
|
410
761
|
Delayed::Job.enqueue(SimpleJob.new)
|
411
762
|
end
|
412
763
|
end
|
413
764
|
|
414
|
-
it "
|
415
|
-
Delayed::Job.jobs_count(:current).
|
765
|
+
it "schedules jobs if they aren't scheduled yet" do
|
766
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(0)
|
416
767
|
Delayed::Periodic.perform_audit!
|
417
|
-
Delayed::Job.jobs_count(:current).
|
418
|
-
job = Delayed::Job.get_and_lock_next_available(
|
419
|
-
job.tag.
|
420
|
-
job.payload_object.
|
421
|
-
job.run_at.
|
422
|
-
job.run_at.
|
423
|
-
job.
|
768
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(1)
|
769
|
+
job = Delayed::Job.get_and_lock_next_available("test1")
|
770
|
+
expect(job.tag).to eq("periodic: my SimpleJob")
|
771
|
+
expect(job.payload_object).to eq(Delayed::Periodic.scheduled["my SimpleJob"])
|
772
|
+
expect(job.run_at).to be >= @cron_time
|
773
|
+
expect(job.run_at).to be <= @cron_time + 6.minutes
|
774
|
+
expect(job.singleton).to eq(job.tag)
|
424
775
|
end
|
425
776
|
|
426
|
-
it "
|
427
|
-
Delayed::Job.jobs_count(:current).
|
777
|
+
it "schedules jobs if there are only failed jobs on the queue" do
|
778
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(0)
|
428
779
|
expect { Delayed::Periodic.perform_audit! }.to change { Delayed::Job.jobs_count(:current) }.by(1)
|
429
|
-
Delayed::Job.jobs_count(:current).
|
430
|
-
job = Delayed::Job.get_and_lock_next_available(
|
780
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(1)
|
781
|
+
job = Delayed::Job.get_and_lock_next_available("test1")
|
431
782
|
job.fail!
|
432
|
-
expect { Delayed::Periodic.perform_audit! }.to change{ Delayed::Job.jobs_count(:current) }.by(1)
|
783
|
+
expect { Delayed::Periodic.perform_audit! }.to change { Delayed::Job.jobs_count(:current) }.by(1)
|
433
784
|
end
|
434
785
|
|
435
|
-
it "
|
436
|
-
Delayed::Job.jobs_count(:current).
|
786
|
+
it "does not schedule jobs that are already scheduled" do
|
787
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(0)
|
437
788
|
Delayed::Periodic.perform_audit!
|
438
|
-
Delayed::Job.jobs_count(:current).
|
789
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(1)
|
439
790
|
job = Delayed::Job.find_available(1).first
|
440
791
|
Delayed::Periodic.perform_audit!
|
441
|
-
Delayed::Job.jobs_count(:current).
|
792
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(1)
|
442
793
|
# verify that the same job still exists, it wasn't just replaced with a new one
|
443
|
-
job.
|
794
|
+
expect(job).to eq(Delayed::Job.find_available(1).first)
|
444
795
|
end
|
445
796
|
|
446
|
-
it "
|
447
|
-
Delayed::Job.jobs_count(:current).
|
797
|
+
it "schedules the next job run after performing" do
|
798
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(0)
|
448
799
|
Delayed::Periodic.perform_audit!
|
449
|
-
Delayed::Job.jobs_count(:current).
|
450
|
-
job = Delayed::Job.get_and_lock_next_available(
|
800
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(1)
|
801
|
+
job = Delayed::Job.get_and_lock_next_available("test")
|
451
802
|
run_job(job)
|
452
803
|
|
453
|
-
job = Delayed::Job.get_and_lock_next_available(
|
454
|
-
job.tag.
|
804
|
+
job = Delayed::Job.get_and_lock_next_available("test1")
|
805
|
+
expect(job.tag).to eq("SimpleJob#perform")
|
455
806
|
|
456
|
-
next_scheduled = Delayed::Job.get_and_lock_next_available(
|
457
|
-
next_scheduled.tag.
|
458
|
-
next_scheduled.payload_object.
|
807
|
+
next_scheduled = Delayed::Job.get_and_lock_next_available("test2")
|
808
|
+
expect(next_scheduled.tag).to eq("periodic: my SimpleJob")
|
809
|
+
expect(next_scheduled.payload_object).to be_is_a(Delayed::Periodic)
|
459
810
|
end
|
460
811
|
|
461
|
-
it "
|
462
|
-
|
812
|
+
it "rejects duplicate named jobs" do
|
813
|
+
expect { Delayed::Periodic.cron("my SimpleJob", "*/15 * * * * *") { nil } }.to raise_error(ArgumentError)
|
463
814
|
end
|
464
815
|
|
465
|
-
it "
|
816
|
+
it "handles jobs that are no longer scheduled" do
|
466
817
|
Delayed::Periodic.perform_audit!
|
467
818
|
Delayed::Periodic.scheduled = {}
|
468
|
-
job = Delayed::Job.get_and_lock_next_available(
|
819
|
+
job = Delayed::Job.get_and_lock_next_available("test")
|
469
820
|
run_job(job)
|
470
821
|
# shouldn't error, and the job should now be deleted
|
471
|
-
Delayed::Job.jobs_count(:current).
|
822
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(0)
|
472
823
|
end
|
473
824
|
|
474
|
-
it "
|
475
|
-
change_setting(Delayed::Periodic, :overrides, {
|
825
|
+
it "allows overriding schedules using periodic_jobs.yml" do
|
826
|
+
change_setting(Delayed::Periodic, :overrides, { "my ChangedJob" => "*/10 * * * * *" }) do
|
476
827
|
Delayed::Periodic.scheduled = {}
|
477
|
-
Delayed::Periodic.cron(
|
828
|
+
Delayed::Periodic.cron("my ChangedJob", "*/5 * * * * *") do
|
478
829
|
Delayed::Job.enqueue(SimpleJob.new)
|
479
830
|
end
|
480
|
-
Delayed::Periodic.scheduled[
|
831
|
+
expect(Delayed::Periodic.scheduled["my ChangedJob"].cron.original).to eq("*/10 * * * * *")
|
481
832
|
end
|
482
833
|
end
|
483
834
|
|
484
|
-
it "
|
485
|
-
change_setting(Delayed::Periodic, :overrides, {
|
835
|
+
it "fails if the override cron line is invalid" do
|
836
|
+
change_setting(Delayed::Periodic, :overrides, { "my ChangedJob" => "*/10 * * * * * *" }) do # extra asterisk
|
486
837
|
Delayed::Periodic.scheduled = {}
|
487
|
-
expect
|
488
|
-
Delayed::
|
489
|
-
|
838
|
+
expect do
|
839
|
+
Delayed::Periodic.cron("my ChangedJob", "*/5 * * * * *") do
|
840
|
+
Delayed::Job.enqueue(SimpleJob.new)
|
841
|
+
end
|
842
|
+
end.to raise_error(ArgumentError)
|
490
843
|
end
|
491
844
|
|
492
|
-
expect
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
module InDelayedJobTest
|
497
|
-
def self.check_in_job
|
498
|
-
Delayed::Job.in_delayed_job?.should == true
|
845
|
+
expect do
|
846
|
+
Delayed::Periodic.add_overrides({ "my ChangedJob" => "*/10 * * * * * *" })
|
847
|
+
end.to raise_error(ArgumentError)
|
499
848
|
end
|
500
849
|
end
|
501
850
|
|
502
|
-
it "
|
851
|
+
it "sets in_delayed_job?" do
|
503
852
|
job = InDelayedJobTest.delay(ignore_transaction: true).check_in_job
|
504
|
-
Delayed::Job.in_delayed_job
|
853
|
+
expect(Delayed::Job.in_delayed_job?).to be(false)
|
505
854
|
job.invoke_job
|
506
|
-
Delayed::Job.in_delayed_job
|
855
|
+
expect(Delayed::Job.in_delayed_job?).to be(false)
|
507
856
|
end
|
508
857
|
|
509
|
-
it "
|
510
|
-
story = Story.new :
|
511
|
-
|
858
|
+
it "fails on job creation if an unsaved AR object is used" do
|
859
|
+
story = Story.new text: "Once upon..."
|
860
|
+
expect { story.delay.text }.to raise_error(RuntimeError)
|
512
861
|
|
513
862
|
reader = StoryReader.new
|
514
|
-
|
863
|
+
expect { reader.delay.read(story) }.to raise_error(RuntimeError)
|
515
864
|
|
516
|
-
|
865
|
+
expect { [story, 1, story, false].delay.first }.to raise_error(RuntimeError)
|
517
866
|
end
|
518
867
|
|
519
868
|
# the sort order of current_jobs and list_jobs depends on the back-end
|
@@ -521,62 +870,62 @@ shared_examples_for 'a backend' do
|
|
521
870
|
describe "current jobs, queue size, strand_size" do
|
522
871
|
before do
|
523
872
|
@jobs = []
|
524
|
-
3.times { @jobs << create_job(:
|
525
|
-
@jobs.unshift create_job(:
|
526
|
-
@jobs.unshift create_job(:
|
527
|
-
@jobs << create_job(:
|
528
|
-
@future_job = create_job(:
|
529
|
-
2.times { @jobs << create_job(:
|
530
|
-
@jobs << create_job(:
|
531
|
-
@failed_job = create_job.tap
|
532
|
-
@other_queue_job = create_job(:
|
873
|
+
3.times { @jobs << create_job(priority: 3) }
|
874
|
+
@jobs.unshift create_job(priority: 2)
|
875
|
+
@jobs.unshift create_job(priority: 1)
|
876
|
+
@jobs << create_job(priority: 3, strand: "test1")
|
877
|
+
@future_job = create_job(run_at: 5.hours.from_now)
|
878
|
+
2.times { @jobs << create_job(priority: 3) }
|
879
|
+
@jobs << create_job(priority: 3, strand: "test1")
|
880
|
+
@failed_job = create_job.tap(&:fail!)
|
881
|
+
@other_queue_job = create_job(queue: "another")
|
533
882
|
end
|
534
883
|
|
535
|
-
it "
|
536
|
-
Delayed::Job.list_jobs(:current, 100).map(&:id).sort.
|
884
|
+
it "returns the queued jobs" do
|
885
|
+
expect(Delayed::Job.list_jobs(:current, 100).map(&:id).sort).to eq(@jobs.map(&:id).sort)
|
537
886
|
end
|
538
887
|
|
539
|
-
it "
|
888
|
+
it "paginates the returned jobs" do
|
540
889
|
@returned = []
|
541
890
|
@returned += Delayed::Job.list_jobs(:current, 3, 0)
|
542
891
|
@returned += Delayed::Job.list_jobs(:current, 4, 3)
|
543
892
|
@returned += Delayed::Job.list_jobs(:current, 100, 7)
|
544
|
-
@returned.sort_by
|
893
|
+
expect(@returned.sort_by(&:id)).to eq(@jobs.sort_by(&:id))
|
545
894
|
end
|
546
895
|
|
547
|
-
it "
|
548
|
-
Delayed::Job.list_jobs(:current, 5, 0, "another").
|
896
|
+
it "returns other queues" do
|
897
|
+
expect(Delayed::Job.list_jobs(:current, 5, 0, "another")).to eq([@other_queue_job])
|
549
898
|
end
|
550
899
|
|
551
|
-
it "
|
552
|
-
Delayed::Job.jobs_count(:current).
|
553
|
-
Delayed::Job.jobs_count(:current, "another").
|
554
|
-
Delayed::Job.jobs_count(:current, "bogus").
|
900
|
+
it "returns queue size" do
|
901
|
+
expect(Delayed::Job.jobs_count(:current)).to eq(@jobs.size)
|
902
|
+
expect(Delayed::Job.jobs_count(:current, "another")).to eq(1)
|
903
|
+
expect(Delayed::Job.jobs_count(:current, "bogus")).to eq(0)
|
555
904
|
end
|
556
905
|
|
557
|
-
it "
|
558
|
-
Delayed::Job.strand_size("test1").
|
559
|
-
Delayed::Job.strand_size("bogus").
|
906
|
+
it "returns strand size" do
|
907
|
+
expect(Delayed::Job.strand_size("test1")).to eq(2)
|
908
|
+
expect(Delayed::Job.strand_size("bogus")).to eq(0)
|
560
909
|
end
|
561
910
|
end
|
562
911
|
|
563
|
-
it "
|
912
|
+
it "returns the jobs in a strand" do
|
564
913
|
strand_jobs = []
|
565
|
-
3.times { strand_jobs << create_job(:
|
566
|
-
2.times { create_job(:
|
567
|
-
strand_jobs << create_job(:
|
914
|
+
3.times { strand_jobs << create_job(strand: "test1") }
|
915
|
+
2.times { create_job(strand: "test2") }
|
916
|
+
strand_jobs << create_job(strand: "test1", run_at: 5.hours.from_now)
|
568
917
|
create_job
|
569
918
|
|
570
919
|
jobs = Delayed::Job.list_jobs(:strand, 3, 0, "test1")
|
571
|
-
jobs.size.
|
920
|
+
expect(jobs.size).to eq(3)
|
572
921
|
|
573
922
|
jobs += Delayed::Job.list_jobs(:strand, 3, 3, "test1")
|
574
|
-
jobs.size.
|
923
|
+
expect(jobs.size).to eq(4)
|
575
924
|
|
576
|
-
jobs.sort_by
|
925
|
+
expect(jobs.sort_by(&:id)).to eq(strand_jobs.sort_by(&:id))
|
577
926
|
end
|
578
927
|
|
579
|
-
it "
|
928
|
+
it "returns the jobs for a tag" do
|
580
929
|
tag_jobs = []
|
581
930
|
3.times { tag_jobs << "test".delay(ignore_transaction: true).to_s }
|
582
931
|
2.times { "test".delay.to_i }
|
@@ -586,62 +935,62 @@ shared_examples_for 'a backend' do
|
|
586
935
|
create_job
|
587
936
|
|
588
937
|
jobs = Delayed::Job.list_jobs(:tag, 3, 0, "String#to_s")
|
589
|
-
jobs.size.
|
938
|
+
expect(jobs.size).to eq(3)
|
590
939
|
|
591
940
|
jobs += Delayed::Job.list_jobs(:tag, 3, 3, "String#to_s")
|
592
|
-
jobs.size.
|
941
|
+
expect(jobs.size).to eq(5)
|
593
942
|
|
594
|
-
jobs.sort_by
|
943
|
+
expect(jobs.sort_by(&:id)).to eq(tag_jobs.sort_by(&:id))
|
595
944
|
end
|
596
945
|
|
597
946
|
describe "running_jobs" do
|
598
|
-
it "
|
947
|
+
it "returns the running jobs, ordered by locked_at" do
|
599
948
|
Timecop.freeze(10.minutes.ago) { 3.times { create_job } }
|
600
|
-
j1 = Timecop.freeze(2.minutes.ago) { Delayed::Job.get_and_lock_next_available(
|
601
|
-
j2 = Timecop.freeze(5.minutes.ago) { Delayed::Job.get_and_lock_next_available(
|
602
|
-
j3 = Timecop.freeze(5.seconds.ago) { Delayed::Job.get_and_lock_next_available(
|
603
|
-
[j1, j2, j3].compact.size.
|
949
|
+
j1 = Timecop.freeze(2.minutes.ago) { Delayed::Job.get_and_lock_next_available("w1") }
|
950
|
+
j2 = Timecop.freeze(5.minutes.ago) { Delayed::Job.get_and_lock_next_available("w2") }
|
951
|
+
j3 = Timecop.freeze(5.seconds.ago) { Delayed::Job.get_and_lock_next_available("w3") }
|
952
|
+
expect([j1, j2, j3].compact.size).to eq(3)
|
604
953
|
|
605
|
-
Delayed::Job.running_jobs.
|
954
|
+
expect(Delayed::Job.running_jobs).to eq([j2, j1, j3])
|
606
955
|
end
|
607
956
|
end
|
608
957
|
|
609
958
|
describe "future jobs" do
|
610
|
-
it "
|
611
|
-
Timecop.freeze
|
612
|
-
@job = create_job :
|
959
|
+
it "finds future jobs once their run_at rolls by" do
|
960
|
+
Timecop.freeze do
|
961
|
+
@job = create_job run_at: 5.minutes.from_now
|
613
962
|
expect(Delayed::Job.find_available(5)).not_to include(@job)
|
614
|
-
|
615
|
-
Timecop.freeze(1.hour.from_now)
|
963
|
+
end
|
964
|
+
Timecop.freeze(1.hour.from_now) do
|
616
965
|
expect(Delayed::Job.find_available(5)).to include(@job)
|
617
|
-
Delayed::Job.get_and_lock_next_available(
|
618
|
-
|
966
|
+
expect(Delayed::Job.get_and_lock_next_available("test")).to eq(@job)
|
967
|
+
end
|
619
968
|
end
|
620
969
|
|
621
|
-
it "
|
970
|
+
it "returns future jobs sorted by their run_at" do
|
622
971
|
@j1 = create_job
|
623
|
-
@j2 = create_job :
|
624
|
-
@j3 = create_job :
|
625
|
-
Delayed::Job.list_jobs(:future, 1).
|
626
|
-
Delayed::Job.list_jobs(:future, 5).
|
627
|
-
Delayed::Job.list_jobs(:future, 1, 1).
|
972
|
+
@j2 = create_job run_at: 1.hour.from_now
|
973
|
+
@j3 = create_job run_at: 30.minutes.from_now
|
974
|
+
expect(Delayed::Job.list_jobs(:future, 1)).to eq([@j3])
|
975
|
+
expect(Delayed::Job.list_jobs(:future, 5)).to eq([@j3, @j2])
|
976
|
+
expect(Delayed::Job.list_jobs(:future, 1, 1)).to eq([@j2])
|
628
977
|
end
|
629
978
|
end
|
630
979
|
|
631
980
|
describe "failed jobs" do
|
632
981
|
# the sort order of failed_jobs depends on the back-end implementation,
|
633
982
|
# so sort order isn't tested here
|
634
|
-
it "
|
983
|
+
it "returns the list of failed jobs" do
|
635
984
|
jobs = []
|
636
|
-
3.times { jobs << create_job(:
|
637
|
-
jobs = jobs.sort_by
|
638
|
-
Delayed::Job.list_jobs(:failed, 1).
|
985
|
+
3.times { jobs << create_job(priority: 3) }
|
986
|
+
jobs = jobs.sort_by(&:id)
|
987
|
+
expect(Delayed::Job.list_jobs(:failed, 1)).to eq([])
|
639
988
|
jobs[0].fail!
|
640
989
|
jobs[1].fail!
|
641
|
-
failed = (Delayed::Job.list_jobs(:failed, 1, 0) + Delayed::Job.list_jobs(:failed, 1, 1)).sort_by
|
642
|
-
failed.size.
|
643
|
-
failed[0].original_job_id.
|
644
|
-
failed[1].original_job_id.
|
990
|
+
failed = (Delayed::Job.list_jobs(:failed, 1, 0) + Delayed::Job.list_jobs(:failed, 1, 1)).sort_by(&:id)
|
991
|
+
expect(failed.size).to eq(2)
|
992
|
+
expect(failed[0].original_job_id).to eq(jobs[0].id)
|
993
|
+
expect(failed[1].original_job_id).to eq(jobs[1].id)
|
645
994
|
end
|
646
995
|
end
|
647
996
|
|
@@ -652,126 +1001,123 @@ shared_examples_for 'a backend' do
|
|
652
1001
|
@ignored_jobs = []
|
653
1002
|
end
|
654
1003
|
|
655
|
-
it "
|
656
|
-
@affected_jobs.all?
|
657
|
-
@ignored_jobs.any?
|
658
|
-
Delayed::Job.bulk_update(
|
1004
|
+
it "holds and unhold a scope of jobs" do
|
1005
|
+
expect(@affected_jobs.all?(&:on_hold?)).to be false
|
1006
|
+
expect(@ignored_jobs.any?(&:on_hold?)).to be false
|
1007
|
+
expect(Delayed::Job.bulk_update("hold", flavor: @flavor, query: @query)).to eq(@affected_jobs.size)
|
659
1008
|
|
660
|
-
@affected_jobs.all? { |j| Delayed::Job.find(j.id).on_hold? }.
|
661
|
-
@ignored_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }.
|
1009
|
+
expect(@affected_jobs.all? { |j| Delayed::Job.find(j.id).on_hold? }).to be true
|
1010
|
+
expect(@ignored_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }).to be false
|
662
1011
|
|
663
|
-
|
664
|
-
# to un-hold them
|
665
|
-
next if Delayed::Job == Delayed::Backend::Redis::Job
|
666
|
-
Delayed::Job.bulk_update('unhold', :flavor => @flavor, :query => @query).should == @affected_jobs.size
|
1012
|
+
expect(Delayed::Job.bulk_update("unhold", flavor: @flavor, query: @query)).to eq(@affected_jobs.size)
|
667
1013
|
|
668
|
-
@affected_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }.
|
669
|
-
@ignored_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }.
|
1014
|
+
expect(@affected_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }).to be false
|
1015
|
+
expect(@ignored_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }).to be false
|
670
1016
|
end
|
671
1017
|
|
672
|
-
it "
|
673
|
-
Delayed::Job.bulk_update(
|
674
|
-
|
675
|
-
|
1018
|
+
it "deletes a scope of jobs" do
|
1019
|
+
expect(Delayed::Job.bulk_update("destroy", flavor: @flavor, query: @query)).to eq(@affected_jobs.size)
|
1020
|
+
expect(Delayed::Job.where(id: @affected_jobs.map(&:id))).not_to exist
|
1021
|
+
expect(Delayed::Job.where(id: @ignored_jobs.map(&:id)).count).to eq @ignored_jobs.size
|
676
1022
|
end
|
677
1023
|
end
|
678
1024
|
|
679
1025
|
describe "scope: current" do
|
680
1026
|
include_examples "scope"
|
681
|
-
before do
|
682
|
-
@flavor =
|
1027
|
+
before do # rubocop:disable RSpec/HooksBeforeExamples
|
1028
|
+
@flavor = "current"
|
683
1029
|
Timecop.freeze(5.minutes.ago) do
|
684
1030
|
3.times { @affected_jobs << create_job }
|
685
|
-
@ignored_jobs << create_job(:
|
686
|
-
@ignored_jobs << create_job(:
|
1031
|
+
@ignored_jobs << create_job(run_at: 2.hours.from_now)
|
1032
|
+
@ignored_jobs << create_job(queue: "q2")
|
687
1033
|
end
|
688
1034
|
end
|
689
1035
|
end
|
690
1036
|
|
691
1037
|
describe "scope: future" do
|
692
1038
|
include_examples "scope"
|
693
|
-
before do
|
694
|
-
@flavor =
|
1039
|
+
before do # rubocop:disable RSpec/HooksBeforeExamples
|
1040
|
+
@flavor = "future"
|
695
1041
|
Timecop.freeze(5.minutes.ago) do
|
696
|
-
3.times { @affected_jobs << create_job(:
|
1042
|
+
3.times { @affected_jobs << create_job(run_at: 2.hours.from_now) }
|
697
1043
|
@ignored_jobs << create_job
|
698
|
-
@ignored_jobs << create_job(:
|
1044
|
+
@ignored_jobs << create_job(queue: "q2", run_at: 2.hours.from_now)
|
699
1045
|
end
|
700
1046
|
end
|
701
1047
|
end
|
702
1048
|
|
703
1049
|
describe "scope: strand" do
|
704
1050
|
include_examples "scope"
|
705
|
-
before do
|
706
|
-
@flavor =
|
707
|
-
@query =
|
1051
|
+
before do # rubocop:disable RSpec/HooksBeforeExamples
|
1052
|
+
@flavor = "strand"
|
1053
|
+
@query = "s1"
|
708
1054
|
Timecop.freeze(5.minutes.ago) do
|
709
|
-
@affected_jobs << create_job(:
|
710
|
-
@affected_jobs << create_job(:
|
1055
|
+
@affected_jobs << create_job(strand: "s1")
|
1056
|
+
@affected_jobs << create_job(strand: "s1", run_at: 2.hours.from_now)
|
711
1057
|
@ignored_jobs << create_job
|
712
|
-
@ignored_jobs << create_job(:
|
713
|
-
@ignored_jobs << create_job(:
|
1058
|
+
@ignored_jobs << create_job(strand: "s2")
|
1059
|
+
@ignored_jobs << create_job(strand: "s2", run_at: 2.hours.from_now)
|
714
1060
|
end
|
715
1061
|
end
|
716
1062
|
end
|
717
1063
|
|
718
1064
|
describe "scope: tag" do
|
719
1065
|
include_examples "scope"
|
720
|
-
before do
|
721
|
-
@flavor =
|
722
|
-
@query =
|
1066
|
+
before do # rubocop:disable RSpec/HooksBeforeExamples
|
1067
|
+
@flavor = "tag"
|
1068
|
+
@query = "String#to_i"
|
723
1069
|
Timecop.freeze(5.minutes.ago) do
|
724
1070
|
@affected_jobs << "test".delay(ignore_transaction: true).to_i
|
725
|
-
@affected_jobs << "test".delay(strand:
|
1071
|
+
@affected_jobs << "test".delay(strand: "s1", ignore_transaction: true).to_i
|
726
1072
|
@affected_jobs << "test".delay(run_at: 2.hours.from_now, ignore_transaction: true).to_i
|
727
1073
|
@ignored_jobs << create_job
|
728
|
-
@ignored_jobs << create_job(:
|
1074
|
+
@ignored_jobs << create_job(run_at: 1.hour.from_now)
|
729
1075
|
end
|
730
1076
|
end
|
731
1077
|
end
|
732
1078
|
|
733
|
-
it "
|
1079
|
+
it "holds and un-hold given job ids" do
|
734
1080
|
j1 = "test".delay(ignore_transaction: true).to_i
|
735
|
-
j2 = create_job(:
|
736
|
-
j3 = "test".delay(strand:
|
737
|
-
Delayed::Job.bulk_update(
|
738
|
-
Delayed::Job.find(j1.id).on_hold
|
739
|
-
Delayed::Job.find(j2.id).on_hold
|
740
|
-
Delayed::Job.find(j3.id).on_hold
|
1081
|
+
j2 = create_job(run_at: 2.hours.from_now)
|
1082
|
+
j3 = "test".delay(strand: "s1", ignore_transaction: true).to_i
|
1083
|
+
expect(Delayed::Job.bulk_update("hold", ids: [j1.id, j2.id])).to eq(2)
|
1084
|
+
expect(Delayed::Job.find(j1.id).on_hold?).to be true
|
1085
|
+
expect(Delayed::Job.find(j2.id).on_hold?).to be true
|
1086
|
+
expect(Delayed::Job.find(j3.id).on_hold?).to be false
|
741
1087
|
|
742
|
-
Delayed::Job.bulk_update(
|
743
|
-
Delayed::Job.find(j1.id).on_hold
|
744
|
-
Delayed::Job.find(j2.id).on_hold
|
745
|
-
Delayed::Job.find(j3.id).on_hold
|
1088
|
+
expect(Delayed::Job.bulk_update("unhold", ids: [j2.id, j3.id])).to eq(1)
|
1089
|
+
expect(Delayed::Job.find(j1.id).on_hold?).to be true
|
1090
|
+
expect(Delayed::Job.find(j2.id).on_hold?).to be false
|
1091
|
+
expect(Delayed::Job.find(j3.id).on_hold?).to be false
|
746
1092
|
end
|
747
1093
|
|
748
|
-
it "
|
749
|
-
job1 = Delayed::Job.new(:
|
1094
|
+
it "does not hold locked jobs" do
|
1095
|
+
job1 = Delayed::Job.new(tag: "tag")
|
750
1096
|
job1.create_and_lock!("worker")
|
751
|
-
job1.on_hold
|
752
|
-
Delayed::Job.bulk_update(
|
753
|
-
Delayed::Job.find(job1.id).on_hold
|
1097
|
+
expect(job1.on_hold?).to be false
|
1098
|
+
expect(Delayed::Job.bulk_update("hold", ids: [job1.id])).to eq(0)
|
1099
|
+
expect(Delayed::Job.find(job1.id).on_hold?).to be false
|
754
1100
|
end
|
755
1101
|
|
756
|
-
it "
|
757
|
-
job1 = Delayed::Job.new(:
|
1102
|
+
it "does not unhold locked jobs" do
|
1103
|
+
job1 = Delayed::Job.new(tag: "tag")
|
758
1104
|
job1.create_and_lock!("worker")
|
759
|
-
Delayed::Job.bulk_update(
|
760
|
-
Delayed::Job.find(job1.id).on_hold
|
761
|
-
Delayed::Job.find(job1.id).locked
|
1105
|
+
expect(Delayed::Job.bulk_update("unhold", ids: [job1.id])).to eq(0)
|
1106
|
+
expect(Delayed::Job.find(job1.id).on_hold?).to be false
|
1107
|
+
expect(Delayed::Job.find(job1.id).locked?).to be true
|
762
1108
|
end
|
763
1109
|
|
764
|
-
it "
|
1110
|
+
it "deletes given job ids" do
|
765
1111
|
jobs = (0..2).map { create_job }
|
766
|
-
Delayed::Job.bulk_update(
|
767
|
-
|
1112
|
+
expect(Delayed::Job.bulk_update("destroy", ids: jobs[0, 2].map(&:id))).to eq(2)
|
1113
|
+
expect(Delayed::Job.order(:id).where(id: jobs.map(&:id))).to eq jobs[2, 1]
|
768
1114
|
end
|
769
1115
|
|
770
|
-
it "
|
771
|
-
job1 = Delayed::Job.new(:
|
1116
|
+
it "does not delete locked jobs" do
|
1117
|
+
job1 = Delayed::Job.new(tag: "tag")
|
772
1118
|
job1.create_and_lock!("worker")
|
773
|
-
Delayed::Job.bulk_update(
|
774
|
-
Delayed::Job.find(job1.id).locked
|
1119
|
+
expect(Delayed::Job.bulk_update("destroy", ids: [job1.id])).to eq(0)
|
1120
|
+
expect(Delayed::Job.find(job1.id).locked?).to be true
|
775
1121
|
end
|
776
1122
|
end
|
777
1123
|
|
@@ -779,7 +1125,7 @@ shared_examples_for 'a backend' do
|
|
779
1125
|
before do
|
780
1126
|
@cur = []
|
781
1127
|
3.times { @cur << "test".delay(ignore_transaction: true).to_s }
|
782
|
-
5.times { @cur << "test".delay(ignore_transaction: true).to_i}
|
1128
|
+
5.times { @cur << "test".delay(ignore_transaction: true).to_i }
|
783
1129
|
2.times { @cur << "test".delay(ignore_transaction: true).upcase }
|
784
1130
|
"test".delay(ignore_transaction: true).downcase.fail!
|
785
1131
|
@future = []
|
@@ -787,48 +1133,48 @@ shared_examples_for 'a backend' do
|
|
787
1133
|
@cur << "test".delay(ignore_transaction: true).downcase
|
788
1134
|
end
|
789
1135
|
|
790
|
-
it "
|
791
|
-
Delayed::Job.tag_counts(:current, 1).
|
792
|
-
Delayed::Job.tag_counts(:current, 1, 1).
|
793
|
-
Delayed::Job.tag_counts(:current, 5).
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
@cur[0,4].each
|
1136
|
+
it "returns a sorted list of popular current tags" do
|
1137
|
+
expect(Delayed::Job.tag_counts(:current, 1)).to eq([{ tag: "String#to_i", count: 5 }])
|
1138
|
+
expect(Delayed::Job.tag_counts(:current, 1, 1)).to eq([{ tag: "String#to_s", count: 3 }])
|
1139
|
+
expect(Delayed::Job.tag_counts(:current, 5)).to eq([{ tag: "String#to_i", count: 5 },
|
1140
|
+
{ tag: "String#to_s", count: 3 },
|
1141
|
+
{ tag: "String#upcase", count: 2 },
|
1142
|
+
{ tag: "String#downcase", count: 1 }])
|
1143
|
+
@cur[0, 4].each(&:destroy)
|
798
1144
|
@future[0].run_at = @future[1].run_at = 1.hour.ago
|
799
1145
|
@future[0].save!
|
800
1146
|
@future[1].save!
|
801
1147
|
|
802
|
-
Delayed::Job.tag_counts(:current, 5).
|
803
|
-
|
804
|
-
|
1148
|
+
expect(Delayed::Job.tag_counts(:current, 5)).to eq([{ tag: "String#to_i", count: 4 },
|
1149
|
+
{ tag: "String#downcase", count: 3 },
|
1150
|
+
{ tag: "String#upcase", count: 2 }])
|
805
1151
|
end
|
806
1152
|
|
807
|
-
it "
|
808
|
-
Delayed::Job.tag_counts(:all, 1).
|
809
|
-
Delayed::Job.tag_counts(:all, 1, 1).
|
810
|
-
Delayed::Job.tag_counts(:all, 5).
|
811
|
-
|
812
|
-
|
813
|
-
|
1153
|
+
it "returns a sorted list of all popular tags" do
|
1154
|
+
expect(Delayed::Job.tag_counts(:all, 1)).to eq([{ tag: "String#downcase", count: 6 }])
|
1155
|
+
expect(Delayed::Job.tag_counts(:all, 1, 1)).to eq([{ tag: "String#to_i", count: 5 }])
|
1156
|
+
expect(Delayed::Job.tag_counts(:all, 5)).to eq([{ tag: "String#downcase", count: 6 },
|
1157
|
+
{ tag: "String#to_i", count: 5 },
|
1158
|
+
{ tag: "String#to_s", count: 3 },
|
1159
|
+
{ tag: "String#upcase", count: 2 }])
|
814
1160
|
|
815
|
-
@cur[0,4].each
|
1161
|
+
@cur[0, 4].each(&:destroy)
|
816
1162
|
@future[0].destroy
|
817
1163
|
@future[1].fail!
|
818
1164
|
@future[2].fail!
|
819
1165
|
|
820
|
-
Delayed::Job.tag_counts(:all, 5).
|
821
|
-
|
822
|
-
|
1166
|
+
expect(Delayed::Job.tag_counts(:all, 5)).to eq([{ tag: "String#to_i", count: 4 },
|
1167
|
+
{ tag: "String#downcase", count: 3 },
|
1168
|
+
{ tag: "String#upcase", count: 2 }])
|
823
1169
|
end
|
824
1170
|
end
|
825
1171
|
|
826
|
-
it "
|
1172
|
+
it "unlocks orphaned jobs" do
|
827
1173
|
change_setting(Delayed::Settings, :max_attempts, 2) do
|
828
|
-
job1 = Delayed::Job.new(:
|
829
|
-
job2 = Delayed::Job.new(:
|
830
|
-
job3 = Delayed::Job.new(:
|
831
|
-
job4 = Delayed::Job.new(:
|
1174
|
+
job1 = Delayed::Job.new(tag: "tag")
|
1175
|
+
job2 = Delayed::Job.new(tag: "tag")
|
1176
|
+
job3 = Delayed::Job.new(tag: "tag")
|
1177
|
+
job4 = Delayed::Job.new(tag: "tag")
|
832
1178
|
job1.create_and_lock!("Jobworker:#{Process.pid}")
|
833
1179
|
`echo ''`
|
834
1180
|
child_pid = $?.pid
|
@@ -836,23 +1182,38 @@ shared_examples_for 'a backend' do
|
|
836
1182
|
job3.create_and_lock!("someoneelse:#{Process.pid}")
|
837
1183
|
job4.create_and_lock!("Jobworker:notanumber")
|
838
1184
|
|
839
|
-
Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker").
|
1185
|
+
expect(Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker")).to eq(1)
|
840
1186
|
|
841
|
-
Delayed::Job.find(job1.id).locked_by.
|
842
|
-
Delayed::Job.find(job2.id).locked_by.
|
843
|
-
Delayed::Job.find(job3.id).locked_by.
|
844
|
-
Delayed::Job.find(job4.id).locked_by.
|
1187
|
+
expect(Delayed::Job.find(job1.id).locked_by).not_to be_nil
|
1188
|
+
expect(Delayed::Job.find(job2.id).locked_by).to be_nil
|
1189
|
+
expect(Delayed::Job.find(job3.id).locked_by).not_to be_nil
|
1190
|
+
expect(Delayed::Job.find(job4.id).locked_by).not_to be_nil
|
845
1191
|
|
846
|
-
Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker").
|
1192
|
+
expect(Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker")).to eq(0)
|
1193
|
+
end
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
it "removes an un-reschedulable job" do
|
1197
|
+
change_setting(Delayed::Settings, :max_attempts, -1) do
|
1198
|
+
job = Delayed::Job.new(tag: "tag")
|
1199
|
+
`echo ''`
|
1200
|
+
child_pid = $?.pid
|
1201
|
+
job.create_and_lock!("Jobworker:#{child_pid}")
|
1202
|
+
Timeout.timeout(1) do
|
1203
|
+
# if this takes longer than a second it's hung
|
1204
|
+
# in an infinite loop, which would be bad.
|
1205
|
+
expect(Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker")).to eq(1)
|
1206
|
+
end
|
1207
|
+
expect { Delayed::Job.find(job.id) }.to raise_error(ActiveRecord::RecordNotFound)
|
847
1208
|
end
|
848
1209
|
end
|
849
1210
|
|
850
|
-
it "
|
1211
|
+
it "unlocks orphaned jobs given a pid" do
|
851
1212
|
change_setting(Delayed::Settings, :max_attempts, 2) do
|
852
|
-
job1 = Delayed::Job.new(:
|
853
|
-
job2 = Delayed::Job.new(:
|
854
|
-
job3 = Delayed::Job.new(:
|
855
|
-
job4 = Delayed::Job.new(:
|
1213
|
+
job1 = Delayed::Job.new(tag: "tag")
|
1214
|
+
job2 = Delayed::Job.new(tag: "tag")
|
1215
|
+
job3 = Delayed::Job.new(tag: "tag")
|
1216
|
+
job4 = Delayed::Job.new(tag: "tag")
|
856
1217
|
job1.create_and_lock!("Jobworker:#{Process.pid}")
|
857
1218
|
`echo ''`
|
858
1219
|
child_pid = $?.pid
|
@@ -862,15 +1223,15 @@ shared_examples_for 'a backend' do
|
|
862
1223
|
job3.create_and_lock!("someoneelse:#{Process.pid}")
|
863
1224
|
job4.create_and_lock!("Jobworker:notanumber")
|
864
1225
|
|
865
|
-
Delayed::Job.unlock_orphaned_jobs(child_pid2, "Jobworker").
|
866
|
-
Delayed::Job.unlock_orphaned_jobs(child_pid, "Jobworker").
|
1226
|
+
expect(Delayed::Job.unlock_orphaned_jobs(child_pid2, "Jobworker")).to eq(0)
|
1227
|
+
expect(Delayed::Job.unlock_orphaned_jobs(child_pid, "Jobworker")).to eq(1)
|
867
1228
|
|
868
|
-
Delayed::Job.find(job1.id).locked_by.
|
869
|
-
Delayed::Job.find(job2.id).locked_by.
|
870
|
-
Delayed::Job.find(job3.id).locked_by.
|
871
|
-
Delayed::Job.find(job4.id).locked_by.
|
1229
|
+
expect(Delayed::Job.find(job1.id).locked_by).not_to be_nil
|
1230
|
+
expect(Delayed::Job.find(job2.id).locked_by).to be_nil
|
1231
|
+
expect(Delayed::Job.find(job3.id).locked_by).not_to be_nil
|
1232
|
+
expect(Delayed::Job.find(job4.id).locked_by).not_to be_nil
|
872
1233
|
|
873
|
-
Delayed::Job.unlock_orphaned_jobs(child_pid, "Jobworker").
|
1234
|
+
expect(Delayed::Job.unlock_orphaned_jobs(child_pid, "Jobworker")).to eq(0)
|
874
1235
|
end
|
875
1236
|
end
|
876
1237
|
end
|