inst-jobs 0.11.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.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/bin/inst_job +4 -0
  3. data/db/migrate/20101216224513_create_delayed_jobs.rb +40 -0
  4. data/db/migrate/20110208031356_add_delayed_jobs_tag.rb +14 -0
  5. data/db/migrate/20110426161613_add_delayed_jobs_max_attempts.rb +13 -0
  6. data/db/migrate/20110516225834_add_delayed_jobs_strand.rb +14 -0
  7. data/db/migrate/20110531144916_cleanup_delayed_jobs_indexes.rb +26 -0
  8. data/db/migrate/20110610213249_optimize_delayed_jobs.rb +40 -0
  9. data/db/migrate/20110831210257_add_delayed_jobs_next_in_strand.rb +52 -0
  10. data/db/migrate/20120510004759_delayed_jobs_delete_trigger_lock_for_update.rb +31 -0
  11. data/db/migrate/20120531150712_drop_psql_jobs_pop_fn.rb +15 -0
  12. data/db/migrate/20120607164022_delayed_jobs_use_advisory_locks.rb +80 -0
  13. data/db/migrate/20120607181141_index_jobs_on_locked_by.rb +15 -0
  14. data/db/migrate/20120608191051_add_jobs_run_at_index.rb +15 -0
  15. data/db/migrate/20120927184213_change_delayed_jobs_handler_to_text.rb +13 -0
  16. data/db/migrate/20140505215131_add_failed_jobs_original_job_id.rb +13 -0
  17. data/db/migrate/20140505215510_copy_failed_jobs_original_id.rb +13 -0
  18. data/db/migrate/20140505223637_drop_failed_jobs_original_id.rb +13 -0
  19. data/db/migrate/20140512213941_add_source_to_jobs.rb +15 -0
  20. data/db/migrate/20150807133223_add_max_concurrent_to_jobs.rb +70 -0
  21. data/db/migrate/20151123210429_add_expires_at_to_jobs.rb +15 -0
  22. data/db/migrate/20151210162949_improve_max_concurrent.rb +50 -0
  23. data/lib/delayed/backend/active_record.rb +340 -0
  24. data/lib/delayed/backend/base.rb +335 -0
  25. data/lib/delayed/backend/redis/bulk_update.lua +50 -0
  26. data/lib/delayed/backend/redis/destroy_job.lua +2 -0
  27. data/lib/delayed/backend/redis/enqueue.lua +29 -0
  28. data/lib/delayed/backend/redis/fail_job.lua +5 -0
  29. data/lib/delayed/backend/redis/find_available.lua +3 -0
  30. data/lib/delayed/backend/redis/functions.rb +57 -0
  31. data/lib/delayed/backend/redis/get_and_lock_next_available.lua +17 -0
  32. data/lib/delayed/backend/redis/includes/jobs_common.lua +203 -0
  33. data/lib/delayed/backend/redis/job.rb +497 -0
  34. data/lib/delayed/backend/redis/set_running.lua +5 -0
  35. data/lib/delayed/backend/redis/tickle_strand.lua +2 -0
  36. data/lib/delayed/batch.rb +56 -0
  37. data/lib/delayed/cli.rb +101 -0
  38. data/lib/delayed/daemon.rb +103 -0
  39. data/lib/delayed/engine.rb +4 -0
  40. data/lib/delayed/job_tracking.rb +31 -0
  41. data/lib/delayed/lifecycle.rb +90 -0
  42. data/lib/delayed/log_tailer.rb +22 -0
  43. data/lib/delayed/message_sending.rb +134 -0
  44. data/lib/delayed/performable_method.rb +52 -0
  45. data/lib/delayed/periodic.rb +85 -0
  46. data/lib/delayed/plugin.rb +22 -0
  47. data/lib/delayed/pool.rb +161 -0
  48. data/lib/delayed/server/helpers.rb +28 -0
  49. data/lib/delayed/server/public/css/app.css +12 -0
  50. data/lib/delayed/server/public/js/app.js +132 -0
  51. data/lib/delayed/server/views/index.erb +90 -0
  52. data/lib/delayed/server/views/layout.erb +47 -0
  53. data/lib/delayed/server.rb +120 -0
  54. data/lib/delayed/settings.rb +90 -0
  55. data/lib/delayed/testing.rb +32 -0
  56. data/lib/delayed/version.rb +3 -0
  57. data/lib/delayed/work_queue/in_process.rb +13 -0
  58. data/lib/delayed/work_queue/parent_process.rb +180 -0
  59. data/lib/delayed/worker.rb +234 -0
  60. data/lib/delayed/yaml_extensions.rb +109 -0
  61. data/lib/delayed_job.rb +46 -0
  62. data/lib/inst-jobs.rb +1 -0
  63. data/spec/active_record_job_spec.rb +246 -0
  64. data/spec/delayed/cli_spec.rb +23 -0
  65. data/spec/delayed/daemon_spec.rb +35 -0
  66. data/spec/delayed/server_spec.rb +63 -0
  67. data/spec/delayed/settings_spec.rb +32 -0
  68. data/spec/delayed/work_queue/in_process_spec.rb +31 -0
  69. data/spec/delayed/work_queue/parent_process_spec.rb +159 -0
  70. data/spec/delayed/worker_spec.rb +16 -0
  71. data/spec/gemfiles/32.gemfile +6 -0
  72. data/spec/gemfiles/40.gemfile +5 -0
  73. data/spec/gemfiles/41.gemfile +5 -0
  74. data/spec/gemfiles/42.gemfile +5 -0
  75. data/spec/migrate/20140924140513_add_story_table.rb +7 -0
  76. data/spec/redis_job_spec.rb +140 -0
  77. data/spec/sample_jobs.rb +28 -0
  78. data/spec/shared/delayed_batch.rb +85 -0
  79. data/spec/shared/delayed_method.rb +419 -0
  80. data/spec/shared/performable_method.rb +66 -0
  81. data/spec/shared/shared_backend.rb +819 -0
  82. data/spec/shared/testing.rb +48 -0
  83. data/spec/shared/worker.rb +378 -0
  84. data/spec/shared_jobs_specs.rb +15 -0
  85. data/spec/spec_helper.rb +97 -0
  86. metadata +390 -0
@@ -0,0 +1,246 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ describe 'Delayed::Backed::ActiveRecord::Job' do
4
+ before :all do
5
+ Delayed.select_backend(Delayed::Backend::ActiveRecord::Job)
6
+ end
7
+
8
+ after :all do
9
+ Delayed.send(:remove_const, :Job)
10
+ end
11
+
12
+ before do
13
+ Delayed::Testing.clear_all!
14
+ end
15
+
16
+ include_examples 'a delayed_jobs implementation'
17
+
18
+ it "should recover as well as possible from a failure failing a job" do
19
+ allow(Delayed::Job::Failed).to receive(:create).and_raise(RuntimeError)
20
+ job = "test".send_later_enqueue_args :reverse, no_delay: true
21
+ job_id = job.id
22
+ proc { job.fail! }.should raise_error(RuntimeError)
23
+ proc { Delayed::Job.find(job_id) }.should raise_error(ActiveRecord::RecordNotFound)
24
+ Delayed::Job.count.should == 0
25
+ end
26
+
27
+ context "when another worker has worked on a task since the job was found to be available, it" do
28
+ before :each do
29
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
30
+ @job_copy_for_worker_2 = Delayed::Job.find(@job.id)
31
+ end
32
+
33
+ it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
34
+ @job.destroy
35
+ @job_copy_for_worker_2.send(:lock_exclusively!, 'worker2').should == false
36
+ end
37
+
38
+ it "should not allow a second worker to get exclusive access if failed to be processed by worker1 and run_at time is now in future (due to backing off behaviour)" do
39
+ @job.update_attributes(:attempts => 1, :run_at => 1.day.from_now)
40
+ @job_copy_for_worker_2.send(:lock_exclusively!, 'worker2').should == false
41
+ end
42
+
43
+ it "should select the next job at random if enabled" do
44
+ begin
45
+ Delayed::Settings.select_random_from_batch = true
46
+ 15.times { "test".send_later :length }
47
+ founds = []
48
+ 15.times do
49
+ job = Delayed::Job.get_and_lock_next_available('tester')
50
+ founds << job
51
+ job.unlock
52
+ job.save!
53
+ end
54
+ founds.uniq.size.should > 1
55
+ ensure
56
+ Delayed::Settings.select_random_from_batch = false
57
+ end
58
+ end
59
+ end
60
+
61
+ it "should unlock a successfully locked job and persist the job's unlocked state" do
62
+ job = Delayed::Job.create :payload_object => SimpleJob.new
63
+ job.send(:lock_exclusively!, 'worker1').should == true
64
+ job.unlock
65
+ job.save!
66
+ job.reload
67
+ job.locked_by.should == nil
68
+ job.locked_at.should == nil
69
+ end
70
+
71
+ describe "bulk_update failed jobs" do
72
+ context "holding/unholding failed jobs" do
73
+ before :each do
74
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
75
+ Delayed::Job.get_and_lock_next_available('worker1').should == @job
76
+ @job.fail!
77
+ end
78
+
79
+ it "should raise error when holding failed jobs" do
80
+ expect { Delayed::Job.bulk_update('hold', :flavor => 'failed', :query => @query) }.to raise_error(RuntimeError)
81
+ end
82
+
83
+ it "should raise error unholding failed jobs" do
84
+ expect { Delayed::Job.bulk_update('unhold', :flavor => 'failed', :query => @query) }.to raise_error(RuntimeError)
85
+ end
86
+ end
87
+
88
+ context "deleting failed jobs" do
89
+ before :each do
90
+ 2.times {
91
+ j = Delayed::Job.create(:payload_object => SimpleJob.new)
92
+ j.send(:lock_exclusively!, 'worker1').should == true
93
+ j.fail!
94
+ }
95
+ end
96
+
97
+ it "should delete failed jobs by id" do
98
+ target_ids = Delayed::Job::Failed.all[0..2].map { |j| j.id }
99
+ Delayed::Job.bulk_update('destroy', :ids => target_ids, :flavor => 'failed', :query => @query).should == target_ids.length
100
+ end
101
+
102
+ it "should delete all failed jobs" do
103
+ failed_count = Delayed::Job::Failed.count
104
+ Delayed::Job.bulk_update('destroy', :flavor => 'failed', :query => @query).should == failed_count
105
+ end
106
+ end
107
+ end
108
+
109
+ context 'n_strand' do
110
+ it "should default to 1" do
111
+ expect(Delayed::Job).to receive(:rand).never
112
+ job = Delayed::Job.enqueue(SimpleJob.new, :n_strand => 'njobs')
113
+ job.strand.should == "njobs"
114
+ end
115
+
116
+ it "should set max_concurrent based on num_strands" do
117
+ change_setting(Delayed::Settings, :num_strands, ->(strand_name) { expect(strand_name).to eql "njobs"; "3" }) do
118
+ job = Delayed::Job.enqueue(SimpleJob.new, :n_strand => 'njobs')
119
+ job.strand.should == "njobs"
120
+ job.max_concurrent.should == 3
121
+ end
122
+ end
123
+
124
+ context "with two parameters" do
125
+ it "should use the first param as the setting to read" do
126
+ job = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs", "123"])
127
+ job.strand.should == "njobs/123"
128
+ change_setting(Delayed::Settings, :num_strands, ->(strand_name) {
129
+ case strand_name
130
+ when "njobs"; 3
131
+ else nil
132
+ end
133
+ }) do
134
+ job = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs", "123"])
135
+ job.strand.should == "njobs/123"
136
+ job.max_concurrent.should == 3
137
+ end
138
+ end
139
+
140
+ it "should allow overridding the setting based on the second param" do
141
+ change_setting(Delayed::Settings, :num_strands, ->(strand_name) {
142
+ case strand_name
143
+ when "njobs/123"; 5
144
+ else nil
145
+ end
146
+ }) do
147
+ job = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs", "123"])
148
+ job.strand.should == "njobs/123"
149
+ job.max_concurrent.should == 5
150
+ job = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs", "456"])
151
+ job.strand.should == "njobs/456"
152
+ job.max_concurrent.should == 1
153
+ end
154
+
155
+ change_setting(Delayed::Settings, :num_strands, ->(strand_name) {
156
+ case strand_name
157
+ when "njobs/123"; 5
158
+ when "njobs"; 3
159
+ else nil
160
+ end
161
+ }) do
162
+ job = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs", "123"])
163
+ job.strand.should == "njobs/123"
164
+ job.max_concurrent.should == 5
165
+ job = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs", "456"])
166
+ job.strand.should == "njobs/456"
167
+ job.max_concurrent.should == 3
168
+ end
169
+ end
170
+ end
171
+
172
+ context "max_concurrent triggers" do
173
+ before do
174
+ skip("postgres specific") unless ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
175
+ end
176
+
177
+ it "should set one job as next_in_strand at a time with max_concurrent of 1" do
178
+ job1 = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs"])
179
+ job1.reload
180
+ job1.next_in_strand.should == true
181
+ job2 = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs"])
182
+ job2.reload
183
+ job2.next_in_strand.should == false
184
+ run_job(job1)
185
+ job2.reload
186
+ job2.next_in_strand.should == true
187
+ end
188
+
189
+ it "should set multiple jobs as next_in_strand at a time based on max_concurrent" do
190
+ change_setting(Delayed::Settings, :num_strands, ->(strand_name) {
191
+ case strand_name
192
+ when "njobs"; 2
193
+ else nil
194
+ end
195
+ }) do
196
+ job1 = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs"])
197
+ job1.reload
198
+ job1.next_in_strand.should == true
199
+ job2 = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs"])
200
+ job2.reload
201
+ job2.next_in_strand.should == true
202
+ job3 = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs"])
203
+ job3.reload
204
+ job3.next_in_strand.should == false
205
+ run_job(job1)
206
+ job3.reload
207
+ job3.next_in_strand.should == true
208
+ end
209
+ end
210
+
211
+ it "should set multiple jobs as next_in_strand at once if needed" do
212
+ change_setting(Delayed::Settings, :num_strands, ->(strand_name) {
213
+ case strand_name
214
+ when "njobs"; 2
215
+ else nil
216
+ end
217
+ }) do
218
+ job1 = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs"])
219
+ job1.reload
220
+ job1.next_in_strand.should == true
221
+ job2 = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs"])
222
+ job2.reload
223
+ job2.next_in_strand.should == true
224
+
225
+ job3 = Delayed::Job.enqueue(SimpleJob.new, n_strand: ["njobs"])
226
+ job3.reload
227
+ job3.next_in_strand.should == false
228
+
229
+ # manually unset next_in_strand
230
+ Delayed::Job.where(:id => job2).update_all(:next_in_strand => false)
231
+ job2.reload
232
+ job2.next_in_strand.should == false
233
+
234
+ run_job(job1) # should update both jobs
235
+
236
+ job3.reload
237
+ job3.next_in_strand.should == true
238
+
239
+ job2.reload
240
+ job2.next_in_strand.should == true
241
+
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Delayed::CLI do
4
+ describe '#parse_cli_options!' do
5
+ it 'correctly parses the --config option' do
6
+ cli = described_class.new(%w{run --config /path/to/some/file.yml})
7
+ options = cli.parse_cli_options!
8
+ expect(options).to include config_file: '/path/to/some/file.yml'
9
+ end
10
+ end
11
+
12
+ describe '#run' do
13
+ before do
14
+ expect(Delayed::Settings).to receive(:worker_config).and_return({})
15
+ end
16
+
17
+ it 'prints help when no command is given' do
18
+ cli = described_class.new([])
19
+ expect(cli).to receive(:puts).with(/Usage/)
20
+ cli.run
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Delayed::Daemon do
4
+ let(:pid_folder) { "/test/pid/folder" }
5
+ let(:pid) { 9999 }
6
+ let(:subject) { described_class.new(pid_folder) }
7
+
8
+ before do
9
+ allow(subject).to receive(:pid).and_return(pid)
10
+ end
11
+
12
+ describe '#stop' do
13
+ it 'prints status if not running' do
14
+ expect(subject).to receive(:status).with(print: false, pid: pid).and_return(false)
15
+ expect(subject).to receive(:status).with(no_args)
16
+ expect(Process).to receive(:kill).never
17
+ subject.stop
18
+ end
19
+
20
+ it 'prints status if draining' do
21
+ expect(subject).to receive(:status).with(print: false, pid: pid).and_return(:draining)
22
+ expect(subject).to receive(:status).with(no_args)
23
+ expect(Process).to receive(:kill).never
24
+ subject.stop
25
+ end
26
+
27
+ it 'sends INT by default' do
28
+ expect(subject).to receive(:status).with(print: false, pid: pid).and_return(:running)
29
+ expect(subject).to receive(:puts).with(/Stopping pool/)
30
+ expect(Process).to receive(:kill).with('INT', pid)
31
+ expect(subject).to receive(:wait).with(false)
32
+ subject.stop
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+ require 'delayed/server'
3
+
4
+ RSpec.describe Delayed::Server, sinatra: true do
5
+ include Rack::Test::Methods
6
+
7
+ def app
8
+ described_class.new
9
+ end
10
+
11
+ def parsed_body
12
+ @parsed_body ||= JSON.parse(last_response.body)
13
+ end
14
+
15
+ before :all do
16
+ Delayed.select_backend(Delayed::Backend::ActiveRecord::Job)
17
+ end
18
+
19
+ after :all do
20
+ Delayed.send(:remove_const, :Job)
21
+ end
22
+
23
+ describe "get '/running'" do
24
+ before do
25
+ 3.times do |i|
26
+ Delayed::Job.create!({
27
+ run_at: Time.now,
28
+ locked_at: Time.now,
29
+ locked_by: "dummy-runner-#{i}:${$$}",
30
+ })
31
+ end
32
+ get '/running'
33
+ end
34
+
35
+ it 'must return a json object with the running job data in an array', aggregate_failures: true do
36
+ expect(last_response).to be_ok
37
+ expect(parsed_body['data']).to be_an Array
38
+ expect(parsed_body['data'].size).to eq 3
39
+ end
40
+ end
41
+
42
+ describe "get '/jobs'" do
43
+ let!(:job_1) { Delayed::Job.enqueue(SimpleJob.new, strand: 'strand-1') }
44
+ let!(:job_2) { Delayed::Job.enqueue(SimpleJob.new, strand: 'strand-2') }
45
+ let!(:job_3) { Delayed::Job.enqueue(SimpleJob.new, strand: 'strand-3') }
46
+
47
+ context 'with the flavor param set to id' do
48
+ before do
49
+ get "/jobs?flavor=id&search_term=#{job_2.id}"
50
+ end
51
+
52
+ it 'must only return the job with the id specified in the search_term param' do
53
+ jobs = parsed_body['data']
54
+ job_ids = jobs.map{ |j| j['id'] }
55
+ expect(job_ids).to eq [job_2.id]
56
+ end
57
+
58
+ it 'must set recordsFiltered in the response to 1' do
59
+ expect(parsed_body['recordsFiltered']).to eq 1
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Delayed::Settings do
4
+ let(:configfile) {<<-YAML
5
+ default:
6
+ workers:
7
+ - queue: myqueue
8
+ workers: 2
9
+ - queue: secondqueue
10
+ max_priority: 7
11
+ max_attempts: 1
12
+ YAML
13
+ }
14
+
15
+ describe '.worker_config' do
16
+ it 'merges each worker config with the top-level config' do
17
+ expect(File).to receive(:read).with("fname").and_return(configfile)
18
+ config = described_class.worker_config("fname")
19
+ expect(config[:workers]).to eq([
20
+ {'queue' => 'myqueue', 'workers' => 2, 'max_attempts' => 1},
21
+ {'queue' => 'secondqueue', 'max_priority' => 7, 'max_attempts' => 1},
22
+ ])
23
+ end
24
+ end
25
+
26
+ describe '.apply_worker_config!' do
27
+ it 'applies global settings from the given config' do
28
+ expect(described_class).to receive(:last_ditch_logfile=).with(true)
29
+ described_class.apply_worker_config!('last_ditch_logfile' => true)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Delayed::WorkQueue::InProcess do
4
+ before :all do
5
+ Delayed.select_backend(Delayed::Backend::ActiveRecord::Job)
6
+ end
7
+
8
+ after :all do
9
+ Delayed.send(:remove_const, :Job)
10
+ end
11
+
12
+ after :each do
13
+ Delayed::Worker.lifecycle.reset!
14
+ end
15
+
16
+ let(:subject) { described_class.new }
17
+ let(:args) { ["worker_name", "queue_name", 1, 2] }
18
+
19
+ it 'triggers the lifecycle event around the pop' do
20
+ called = false
21
+ Delayed::Worker.lifecycle.around(:work_queue_pop) do |queue, &cb|
22
+ expect(queue).to eq(subject)
23
+ expect(Delayed::Job).to receive(:get_and_lock_next_available).with(*args).and_return(:job)
24
+ called = true
25
+ cb.call(queue)
26
+ end
27
+ job = subject.get_and_lock_next_available(*args)
28
+ expect(job).to eq(:job)
29
+ expect(called).to eq(true)
30
+ end
31
+ end
@@ -0,0 +1,159 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Delayed::WorkQueue::ParentProcess do
4
+ before :all do
5
+ Delayed.select_backend(Delayed::Backend::ActiveRecord::Job)
6
+ end
7
+
8
+ after :all do
9
+ Delayed.send(:remove_const, :Job)
10
+ end
11
+
12
+ after :each do
13
+ Delayed::Worker.lifecycle.reset!
14
+ end
15
+
16
+ let(:subject) { described_class.new }
17
+
18
+ it 'generates a server listening on a valid unix socket' do
19
+ server = subject.server
20
+ expect(server).to be_a(Delayed::WorkQueue::ParentProcess::Server)
21
+ expect(server.listen_socket.local_address.unix?).to be(true)
22
+ expect { server.listen_socket.accept_nonblock }.to raise_error(IO::WaitReadable)
23
+ end
24
+
25
+ it 'generates a client connected to the server unix socket' do
26
+ server = subject.server
27
+ client = subject.client
28
+ expect(client).to be_a(Delayed::WorkQueue::ParentProcess::Client)
29
+ expect(client.addrinfo.unix?).to be(true)
30
+ expect(client.addrinfo.unix_path).to eq(server.listen_socket.local_address.unix_path)
31
+ end
32
+
33
+ describe Delayed::WorkQueue::ParentProcess::Client do
34
+ let(:subject) { described_class.new(addrinfo) }
35
+ let(:addrinfo) { double('Addrinfo') }
36
+ let(:connection) { double('Socket') }
37
+ let(:args) { ["worker_name", "queue_name", 1, 2] }
38
+ let(:job) { Delayed::Job.new(locked_by: "worker_name") }
39
+
40
+ it 'marshals the given arguments to the server and returns the response' do
41
+ expect(addrinfo).to receive(:connect).once.and_return(connection)
42
+ expect(Marshal).to receive(:dump).with(args, connection).ordered
43
+ expect(Marshal).to receive(:load).with(connection).and_return(job).ordered
44
+ response = subject.get_and_lock_next_available(*args)
45
+ expect(response).to eq(job)
46
+ end
47
+
48
+ it 'returns nil and then reconnects on socket error' do
49
+ expect(addrinfo).to receive(:connect).once.and_return(connection)
50
+ expect(Marshal).to receive(:dump).and_raise(SystemCallError.new("failure"))
51
+ response = subject.get_and_lock_next_available(*args)
52
+ expect(response).to be_nil
53
+
54
+ expect(addrinfo).to receive(:connect).once.and_return(connection)
55
+ expect(Marshal).to receive(:dump).with(args, connection)
56
+ expect(Marshal).to receive(:load).with(connection).and_return(job)
57
+ response = subject.get_and_lock_next_available(*args)
58
+ expect(response).to eq(job)
59
+ end
60
+
61
+ it 'errors if the response is not a locked job' do
62
+ expect(addrinfo).to receive(:connect).once.and_return(connection)
63
+ expect(Marshal).to receive(:dump).with(args, connection)
64
+ expect(Marshal).to receive(:load).with(connection).and_return(:not_a_job)
65
+ expect { subject.get_and_lock_next_available(*args) }.to raise_error(Delayed::WorkQueue::ParentProcess::ProtocolError)
66
+ end
67
+
68
+ it 'errors if the response is a job not locked by this worker' do
69
+ expect(addrinfo).to receive(:connect).once.and_return(connection)
70
+ expect(Marshal).to receive(:dump).with(args, connection)
71
+ job.locked_by = "somebody_else"
72
+ expect(Marshal).to receive(:load).with(connection).and_return(job)
73
+ expect { subject.get_and_lock_next_available(*args) }.to raise_error(Delayed::WorkQueue::ParentProcess::ProtocolError)
74
+ end
75
+ end
76
+
77
+ describe Delayed::WorkQueue::ParentProcess::Server do
78
+ let(:subject) { described_class.new(listen_socket) }
79
+ let(:listen_socket) { Socket.unix_server_socket(Delayed::WorkQueue::ParentProcess.generate_socket_path) }
80
+ let(:args) { [1,2,3] }
81
+ let(:job) { :a_job }
82
+
83
+ it 'accepts new clients' do
84
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
85
+ expect { subject.run_once }.to change(subject, :connected_clients).by(1)
86
+ end
87
+
88
+ it 'queries the queue on client request' do
89
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
90
+ subject.run_once
91
+
92
+ expect(Delayed::Job).to receive(:get_and_lock_next_available).with(*args).and_return(job)
93
+ Marshal.dump(args, client)
94
+ subject.run_once
95
+ expect(Marshal.load(client)).to eq(job)
96
+ end
97
+
98
+ it 'drops the client on i/o error' do
99
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
100
+ subject.run_once
101
+
102
+ Marshal.dump(args, client)
103
+
104
+ expect(Marshal).to receive(:load).and_raise(IOError.new("socket went away"))
105
+ expect { subject.run_once }.to change(subject, :connected_clients).by(-1)
106
+ end
107
+
108
+ it 'drops the client on timeout' do
109
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
110
+ subject.run_once
111
+
112
+ Marshal.dump(args, client)
113
+
114
+ expect(Marshal).to receive(:load).and_raise(Timeout::Error.new("socket timed out"))
115
+ expect(Timeout).to receive(:timeout).with(Delayed::Settings.parent_process_client_timeout).and_yield
116
+ expect { subject.run_once }.to change(subject, :connected_clients).by(-1)
117
+ end
118
+
119
+ it 'tracks when clients are idle' do
120
+ expect(subject.all_workers_idle?).to be(true)
121
+
122
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
123
+ subject.run_once
124
+ expect(subject.all_workers_idle?).to be(true)
125
+
126
+ expect(Delayed::Job).to receive(:get_and_lock_next_available).with(*args).and_return(job)
127
+ Marshal.dump(args, client)
128
+ subject.run_once
129
+ expect(subject.all_workers_idle?).to be(false)
130
+
131
+ expect(Delayed::Job).to receive(:get_and_lock_next_available).with(*args).and_return(nil)
132
+ Marshal.dump(args, client)
133
+ subject.run_once
134
+ expect(subject.all_workers_idle?).to be(true)
135
+ end
136
+
137
+ it 'triggers the lifecycle event around the pop' do
138
+ called = false
139
+ client = Socket.unix(subject.listen_socket.local_address.unix_path)
140
+ subject.run_once
141
+
142
+ Delayed::Worker.lifecycle.around(:work_queue_pop) do |queue, &cb|
143
+ expect(subject.all_workers_idle?).to be(true)
144
+ expect(queue).to eq(subject)
145
+ expect(Delayed::Job).to receive(:get_and_lock_next_available).with(*args).and_return(job)
146
+ called = true
147
+ res = cb.call(queue)
148
+ expect(subject.all_workers_idle?).to be(false)
149
+ res
150
+ end
151
+
152
+ Marshal.dump(args, client)
153
+ subject.run_once
154
+
155
+ expect(Marshal.load(client)).to eq(job)
156
+ expect(called).to eq(true)
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,16 @@
1
+ require_relative "../spec_helper"
2
+
3
+ module Delayed
4
+ describe Worker do
5
+ describe "#perform" do
6
+ it "fires off an error callback when a job raises an exception" do
7
+ fired = false
8
+ Worker.lifecycle.before(:error) {|worker, exception| fired = true}
9
+ worker = Worker.new
10
+ job = double(:last_error= => nil, attempts: 1, reschedule: nil)
11
+ worker.perform(job)
12
+ expect(fired).to be_truthy
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path=>"../../"
4
+
5
+ gem "rails", "~> 3.2.19"
6
+
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path=>"../../"
4
+
5
+ gem "rails", "~> 4.0.10"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path=>"../../"
4
+
5
+ gem "rails", "~> 4.1.6"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec :path=>"../../"
4
+
5
+ gem "rails", "~> 4.2.5"
@@ -0,0 +1,7 @@
1
+ class AddStoryTable < ActiveRecord::Migration
2
+ def change
3
+ create_table :stories do |table|
4
+ table.string :text
5
+ end
6
+ end
7
+ end