chore-core 1.10.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +5 -13
  2. data/LICENSE.txt +1 -1
  3. data/README.md +172 -153
  4. data/chore-core.gemspec +3 -3
  5. data/lib/chore.rb +29 -5
  6. data/lib/chore/cli.rb +22 -4
  7. data/lib/chore/configuration.rb +1 -1
  8. data/lib/chore/consumer.rb +54 -12
  9. data/lib/chore/fetcher.rb +12 -7
  10. data/lib/chore/hooks.rb +2 -1
  11. data/lib/chore/job.rb +19 -0
  12. data/lib/chore/manager.rb +17 -2
  13. data/lib/chore/publisher.rb +18 -2
  14. data/lib/chore/queues/filesystem/consumer.rb +126 -64
  15. data/lib/chore/queues/filesystem/filesystem_queue.rb +19 -0
  16. data/lib/chore/queues/filesystem/publisher.rb +10 -16
  17. data/lib/chore/queues/sqs.rb +22 -13
  18. data/lib/chore/queues/sqs/consumer.rb +64 -51
  19. data/lib/chore/queues/sqs/publisher.rb +26 -17
  20. data/lib/chore/strategies/consumer/batcher.rb +6 -6
  21. data/lib/chore/strategies/consumer/single_consumer_strategy.rb +5 -5
  22. data/lib/chore/strategies/consumer/threaded_consumer_strategy.rb +7 -6
  23. data/lib/chore/strategies/consumer/throttled_consumer_strategy.rb +120 -0
  24. data/lib/chore/strategies/worker/forked_worker_strategy.rb +5 -6
  25. data/lib/chore/strategies/worker/helpers/ipc.rb +87 -0
  26. data/lib/chore/strategies/worker/helpers/preforked_worker.rb +163 -0
  27. data/lib/chore/strategies/worker/helpers/work_distributor.rb +65 -0
  28. data/lib/chore/strategies/worker/helpers/worker_info.rb +13 -0
  29. data/lib/chore/strategies/worker/helpers/worker_killer.rb +40 -0
  30. data/lib/chore/strategies/worker/helpers/worker_manager.rb +183 -0
  31. data/lib/chore/strategies/worker/preforked_worker_strategy.rb +150 -0
  32. data/lib/chore/unit_of_work.rb +2 -1
  33. data/lib/chore/util.rb +5 -1
  34. data/lib/chore/version.rb +2 -2
  35. data/lib/chore/worker.rb +30 -3
  36. data/spec/chore/cli_spec.rb +2 -2
  37. data/spec/chore/consumer_spec.rb +1 -5
  38. data/spec/chore/duplicate_detector_spec.rb +17 -5
  39. data/spec/chore/fetcher_spec.rb +0 -11
  40. data/spec/chore/manager_spec.rb +7 -0
  41. data/spec/chore/queues/filesystem/filesystem_consumer_spec.rb +74 -16
  42. data/spec/chore/queues/sqs/consumer_spec.rb +117 -78
  43. data/spec/chore/queues/sqs/publisher_spec.rb +49 -60
  44. data/spec/chore/queues/sqs_spec.rb +32 -41
  45. data/spec/chore/strategies/consumer/single_consumer_strategy_spec.rb +3 -3
  46. data/spec/chore/strategies/consumer/threaded_consumer_strategy_spec.rb +6 -6
  47. data/spec/chore/strategies/consumer/throttled_consumer_strategy_spec.rb +165 -0
  48. data/spec/chore/strategies/worker/forked_worker_strategy_spec.rb +6 -1
  49. data/spec/chore/strategies/worker/helpers/ipc_spec.rb +127 -0
  50. data/spec/chore/strategies/worker/helpers/preforked_worker_spec.rb +236 -0
  51. data/spec/chore/strategies/worker/helpers/work_distributor_spec.rb +131 -0
  52. data/spec/chore/strategies/worker/helpers/worker_info_spec.rb +14 -0
  53. data/spec/chore/strategies/worker/helpers/worker_killer_spec.rb +97 -0
  54. data/spec/chore/strategies/worker/helpers/worker_manager_spec.rb +304 -0
  55. data/spec/chore/strategies/worker/preforked_worker_strategy_spec.rb +183 -0
  56. data/spec/chore/strategies/worker/single_worker_strategy_spec.rb +1 -1
  57. data/spec/chore/worker_spec.rb +70 -15
  58. data/spec/spec_helper.rb +1 -1
  59. data/spec/support/queues/sqs/fake_objects.rb +18 -0
  60. metadata +53 -29
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chore::Strategy::WorkerInfo do
4
+ let(:pid) { 1 }
5
+ let(:worker_info) { Chore::Strategy::WorkerInfo.new(pid) }
6
+
7
+ context '#initialize' do
8
+ it 'should initialize the WorkerInfo with a pid' do
9
+ wi = Chore::Strategy::WorkerInfo.new(pid)
10
+ expect(wi.pid).to equal(pid)
11
+ expect(wi.socket).to equal(nil)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+ require 'get_process_mem'
3
+
4
+ describe Chore::Strategy::WorkerKiller do
5
+ let(:memory_limit) { 1024 }
6
+ let(:request_limit) { 100 }
7
+ let(:check_cycle) { 16 }
8
+ let(:worker_killer) { Chore::Strategy::WorkerKiller.new }
9
+ let(:process_mem_obj) { double('process_mem_obj', bytes: 10) }
10
+
11
+ context '#initialize' do
12
+ it 'should initialize the WorkerKiller correctly' do
13
+ allow(Chore.config).to receive(:memory_limit_bytes).and_return(memory_limit)
14
+ allow(Chore.config).to receive(:request_limit).and_return(request_limit)
15
+ allow(Chore.config).to receive(:check_cycle).and_return(check_cycle)
16
+
17
+ wk = Chore::Strategy::WorkerKiller.new
18
+ expect(wk.instance_variable_get(:@memory_limit)).to equal(memory_limit)
19
+ expect(wk.instance_variable_get(:@request_limit)).to equal(request_limit)
20
+ expect(wk.instance_variable_get(:@check_cycle)).to equal(check_cycle)
21
+ expect(wk.instance_variable_get(:@check_count)).to equal(0)
22
+ expect(wk.instance_variable_get(:@current_requests)).to equal(0)
23
+ end
24
+ end
25
+
26
+ context '#check_memory' do
27
+ before(:each) do
28
+ allow(GetProcessMem).to receive(:new).and_return(process_mem_obj)
29
+ worker_killer.instance_variable_set(:@memory_limit, memory_limit)
30
+ worker_killer.instance_variable_set(:@check_cycle, check_cycle)
31
+ end
32
+
33
+ it 'should return nil when memory_limit is nil' do
34
+ worker_killer.instance_variable_set(:@memory_limit, nil)
35
+ expect(worker_killer.check_memory).to eq(nil)
36
+ end
37
+
38
+ it 'should increment the check count by 1' do
39
+ worker_killer.instance_variable_set(:@check_count, 1)
40
+ worker_killer.check_memory
41
+ expect(worker_killer.instance_variable_get(:@check_count)).to eq(2)
42
+ end
43
+
44
+ context 'check_count equals check_cycle' do
45
+ before(:each) do
46
+ worker_killer.instance_variable_set(:@check_count, 15)
47
+ end
48
+
49
+ it 'should check memory' do
50
+ expect(process_mem_obj).to receive(:bytes)
51
+ worker_killer.check_memory
52
+ end
53
+
54
+ it 'should reset the check_count to zero' do
55
+ allow(process_mem_obj).to receive(:bytes).and_return(0)
56
+ worker_killer.check_memory
57
+ expect(worker_killer.instance_variable_get(:@check_count)).to equal(0)
58
+ end
59
+
60
+ it 'should exit if the process mem exceeds the memory_limit' do
61
+ allow(process_mem_obj).to receive(:bytes).and_return(2048)
62
+ begin
63
+ worker_killer.check_memory
64
+ rescue SystemExit=>e
65
+ expect(e.status).to eq(0)
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ context '#check_requests' do
72
+ before(:each) do
73
+ worker_killer.instance_variable_set(:@request_limit, request_limit)
74
+ worker_killer.instance_variable_set(:@current_requests, 0)
75
+ end
76
+
77
+ it 'should return nil when request_limit is nil' do
78
+ worker_killer.instance_variable_set(:@request_limit, nil)
79
+ expect(worker_killer.check_requests).to eq(nil)
80
+ end
81
+
82
+ it 'should increment current requests' do
83
+ worker_killer.check_requests
84
+ expect(worker_killer.instance_variable_get(:@current_requests)).to eq(1)
85
+ end
86
+
87
+ it 'should exit when current_requests exceeds request_limit' do
88
+ worker_killer.instance_variable_set(:@current_requests, request_limit - 1)
89
+
90
+ begin
91
+ worker_killer.check_requests
92
+ rescue SystemExit=>e
93
+ expect(e.status).to eq(0)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,304 @@
1
+ require 'spec_helper'
2
+ require 'chore/signal'
3
+ require 'chore/strategies/worker/helpers/worker_manager'
4
+
5
+ describe Chore::Strategy::WorkerManager do
6
+ let(:master_socket) { double('master_socket') }
7
+
8
+ let(:worker_manager) { Chore::Strategy::WorkerManager.new(master_socket) }
9
+
10
+
11
+ let(:worker_pid_1) { 1 }
12
+ let(:socket_1) { double('socket_1') }
13
+
14
+ let(:worker_pid_2) { 2 }
15
+ let(:socket_2) { double('socket_2') }
16
+
17
+ let(:worker_pid_3) { 3 }
18
+
19
+ let(:pid_sock_hash) { { worker_pid_1 => socket_1,
20
+ worker_pid_2 => socket_2 } }
21
+ let(:pid_sock_hash_2) { { worker_pid_1 => socket_1 } }
22
+
23
+ let(:pid_sock_hash_3) { { worker_pid_1 => socket_1,
24
+ worker_pid_2 => socket_2,
25
+ worker_pid_3 => nil } }
26
+
27
+ let(:config) { double("config") }
28
+ let(:worker) { double("worker") }
29
+
30
+ let(:worker_info_1) { double('worker_info_1') }
31
+ let(:worker_info_2) { double('worker_info_2') }
32
+
33
+ let(:pid_to_worker) { {
34
+ worker_pid_1 => worker_info_1,
35
+ worker_pid_2 => worker_info_2
36
+ } }
37
+ let(:socket_to_worker) { {
38
+ socket_1 => worker_info_1,
39
+ socket_2 => worker_info_2
40
+ } }
41
+
42
+ context '#include_ipc' do
43
+ it 'should include the Ipc module' do
44
+ expect(worker_manager.ipc_help).to eq(:available)
45
+ end
46
+ end
47
+
48
+ context '#create_and_attach_workers' do
49
+ before(:each) do
50
+ allow(worker_manager).to receive(:create_workers).and_yield(2)
51
+ allow(worker_manager).to receive(:attach_workers).and_return(true)
52
+ end
53
+
54
+ it 'should call to create replacement workers' do
55
+ expect(worker_manager).to receive(:create_workers)
56
+ worker_manager.create_and_attach_workers
57
+ end
58
+
59
+ it 'should create a map between new workers to new sockets' do
60
+ expect(worker_manager).to receive(:attach_workers).with(2)
61
+ worker_manager.create_and_attach_workers
62
+ end
63
+ end
64
+
65
+ context '#respawn_terminated_workers!' do
66
+ before(:each) do
67
+ allow(worker_manager).to receive(:create_and_attach_workers).and_return(true)
68
+ allow(worker_manager).to receive(:reap_workers).and_return(true)
69
+ end
70
+
71
+ it 'should reap all the terminated worker processes' do
72
+ expect(worker_manager).to receive(:reap_workers)
73
+ worker_manager.respawn_terminated_workers!
74
+ end
75
+
76
+ it 'should re-create and attach all the workers that died' do
77
+ expect(worker_manager).to receive(:create_and_attach_workers)
78
+ worker_manager.respawn_terminated_workers!
79
+ end
80
+ end
81
+
82
+ context '#stop_workers' do
83
+ let(:signal) { 'TERM' }
84
+ before(:each) do
85
+ allow(Process).to receive(:kill).and_return(nil)
86
+ allow(worker_manager).to receive(:reap_workers)
87
+ worker_manager.instance_variable_set(:@pid_to_worker, pid_sock_hash)
88
+ end
89
+
90
+ it 'should forward the signal received to each of the child processes' do
91
+ pid_sock_hash.each do |pid, sock|
92
+ expect(Process).to receive(:kill).with(signal, pid)
93
+ end
94
+ worker_manager.stop_workers(signal)
95
+ end
96
+
97
+ it 'should reap all terminated child processes' do
98
+ expect(worker_manager).to receive(:reap_workers)
99
+ worker_manager.stop_workers(signal)
100
+ end
101
+ end
102
+
103
+ context 'worker_sockets' do
104
+ it 'should return a list of socket assoicated with workers' do
105
+ worker_manager.instance_variable_set(:@socket_to_worker, socket_to_worker)
106
+ res = worker_manager.worker_sockets
107
+ expect(res).to eq([socket_1, socket_2])
108
+ end
109
+ end
110
+
111
+ context '#ready_workers' do
112
+ it 'should return a list of workers assoicated with given sockets' do
113
+ worker_manager.instance_variable_set(:@socket_to_worker, socket_to_worker)
114
+ allow(worker_info_1).to receive(:reset_start_time!)
115
+ res = worker_manager.ready_workers([socket_1])
116
+ expect(res).to eq([worker_info_1])
117
+ end
118
+
119
+ it 'should yield when a block is passed to it' do
120
+ worker_manager.instance_variable_set(:@socket_to_worker, socket_to_worker)
121
+ allow(worker_info_1).to receive(:reset_start_time!)
122
+ expect{ |b| worker_manager.ready_workers([socket_1], &b) }.to yield_control
123
+ end
124
+ end
125
+
126
+ context '#create_workers' do
127
+ before(:each) do
128
+ allow(worker_manager).to receive(:fork).and_yield
129
+ allow(worker_manager).to receive(:run_worker_instance)
130
+ allow(Chore).to receive(:config).and_return(config)
131
+ end
132
+
133
+ it 'should fork it running process till we have the right optimized number of workers and return the number of workers it created' do
134
+ allow(config).to receive(:num_workers).and_return(1)
135
+ expect(worker_manager).to receive(:fork).once
136
+ expect(worker_manager).to receive(:run_worker_instance).once
137
+ worker_manager.send(:create_workers)
138
+ end
139
+
140
+ it 'should raise an exception if an inconsistent number of workers are created' do
141
+ allow(config).to receive(:num_workers).and_return(0)
142
+ allow(worker_manager).to receive(:inconsistent_worker_number).and_return(true)
143
+ expect(worker_manager).to receive(:inconsistent_worker_number)
144
+ expect { worker_manager.send(:create_workers) }.to raise_error(RuntimeError)
145
+ end
146
+
147
+ it 'should call the block passed to it with the number of workers it created' do
148
+ allow(config).to receive(:num_workers).and_return(0)
149
+ allow(worker_manager).to receive(:inconsistent_worker_number).and_return(false)
150
+ expect { |b| worker_manager.send(:create_workers, &b) }.to yield_control.once
151
+ end
152
+ end
153
+
154
+ context '#inconsistent_worker_number' do
155
+ it 'should check if the worker numbers match the number configured' do
156
+ allow(Chore).to receive(:config).and_return(config)
157
+ allow(config).to receive(:num_workers).and_return(2)
158
+ worker_manager.instance_variable_set(:@pid_to_worker, pid_to_worker)
159
+ res_false = worker_manager.send(:inconsistent_worker_number)
160
+ allow(config).to receive(:num_workers).and_return(4)
161
+ res_true = worker_manager.send(:inconsistent_worker_number)
162
+ expect(res_true).to be(true)
163
+ expect(res_false).to be(false)
164
+ end
165
+ end
166
+
167
+ context '#run_worker_instance' do
168
+ before(:each) do
169
+ allow(Chore::Strategy::PreforkedWorker).to receive(:new).and_return(worker)
170
+ allow(worker).to receive(:start_worker).and_return(true)
171
+ allow(worker_manager).to receive(:exit).and_return(true)
172
+ end
173
+
174
+ it 'should create a PreforkedWorker object and start it' do
175
+ expect(Chore::Strategy::PreforkedWorker).to receive(:new).and_return(worker)
176
+ expect(worker).to receive(:start_worker)
177
+ worker_manager.send(:run_worker_instance)
178
+ end
179
+
180
+ it 'should ensure that the process exits when the PreforkedWorker completes' do
181
+ expect(worker_manager).to receive(:exit).with(true)
182
+ worker_manager.send(:run_worker_instance)
183
+ end
184
+ end
185
+
186
+ context '#attach_workers' do
187
+ before(:each) do
188
+ allow(worker_manager).to receive(:create_worker_sockets).and_return([socket_1, socket_2])
189
+ allow(worker_manager).to receive(:read_msg).and_return(worker_pid_1, worker_pid_2)
190
+ allow(worker_manager).to receive(:kill_unattached_workers).and_return(true)
191
+ worker_manager.instance_variable_set(:@pid_to_worker, pid_to_worker)
192
+ worker_manager.instance_variable_set(:@socket_to_worker, {})
193
+ allow(worker_info_1).to receive(:socket=)
194
+ allow(worker_info_2).to receive(:socket=)
195
+ allow(worker_manager).to receive(:select_sockets).and_return([[socket_1], [], []], [[socket_2], [], []])
196
+ end
197
+
198
+ it 'should add as many sockets as the number passed to it as a param' do
199
+ expect(worker_manager).to receive(:read_msg).twice
200
+ worker_manager.send(:attach_workers, 2)
201
+ end
202
+
203
+ it 'should select on each socket to make sure its readable' do
204
+ expect(worker_manager).to receive(:select_sockets).twice
205
+ worker_manager.send(:attach_workers, 2)
206
+ end
207
+
208
+ it 'should get the PID from each socket it creates and map that to the worker that it is connected to' do
209
+ worker_manager.send(:attach_workers, 2)
210
+ expect(worker_manager.instance_variable_get(:@socket_to_worker)).to eq(socket_to_worker)
211
+ end
212
+
213
+ it 'should kill any unattached workers' do
214
+ expect(worker_manager).to receive(:kill_unattached_workers)
215
+ worker_manager.send(:attach_workers, 2)
216
+ end
217
+
218
+ it 'should close sockets that failed to get a connection by timing out' do
219
+ allow(worker_manager).to receive(:select_sockets).and_return([[socket_1], [], []], [nil, nil, nil])
220
+ expect(socket_1).not_to receive(:close)
221
+ expect(socket_2).to receive(:close)
222
+ worker_manager.send(:attach_workers, 2)
223
+ end
224
+
225
+ it 'should close sockets that failed to get a connection by econnreset' do
226
+ allow(worker_manager).to receive(:select_sockets).with(socket_1, nil, 2).and_return([[socket_1], [], []])
227
+ allow(worker_manager).to receive(:select_sockets).with(socket_2, nil, 2).and_raise(Errno::ECONNRESET)
228
+ expect(socket_1).not_to receive(:close)
229
+ expect(socket_2).to receive(:close)
230
+ worker_manager.send(:attach_workers, 2)
231
+ end
232
+ end
233
+
234
+ context "#create_worker_sockets" do
235
+ it 'should return an array of sockets equal to the number passed to it' do
236
+ allow(worker_manager).to receive(:add_worker_socket).and_return(socket_1, socket_2, socket_2, socket_1)
237
+ num = 3
238
+ res = worker_manager.send(:create_worker_sockets, num)
239
+ expect(res.size).to eq(num)
240
+ end
241
+ end
242
+
243
+ context "#kill_unattached_workers" do
244
+ it 'should send a kill -9 to PIDs that do not have a socket attached to them' do
245
+ worker_manager.instance_variable_set(:@pid_to_worker, pid_to_worker)
246
+ allow(worker_info_1).to receive(:socket).and_return(socket_1)
247
+ allow(worker_info_2).to receive(:socket).and_return(nil)
248
+ allow(Process).to receive(:kill).and_return(true)
249
+ expect(Process).to receive(:kill).with('KILL', worker_pid_2).once
250
+ worker_manager.send(:kill_unattached_workers)
251
+ end
252
+ end
253
+
254
+ context "#reap_workers" do
255
+ before(:each) do
256
+ worker_manager.instance_variable_set(:@pid_to_worker, pid_to_worker)
257
+ allow(worker_info_1).to receive(:socket).and_return(socket_1)
258
+ allow(worker_info_1).to receive(:pid).and_return(worker_pid_1)
259
+ allow(worker_info_2).to receive(:socket).and_return(socket_2)
260
+ allow(worker_info_2).to receive(:pid).and_return(worker_pid_2)
261
+ end
262
+
263
+ it 'should run through each worker-pid map and delete the worker pids from the hash, if they were dead' do
264
+ allow(worker_manager).to receive(:reap_process).and_return(false, true)
265
+ allow(socket_2).to receive(:close)
266
+ expect(worker_manager.instance_variable_get(:@pid_to_worker).size).to eq(2)
267
+ worker_manager.send(:reap_workers)
268
+ expect(worker_manager.instance_variable_get(:@pid_to_worker).size).to eq(1)
269
+ end
270
+
271
+ it 'closes the socket associated with the reaped worker' do
272
+ allow(worker_manager).to receive(:reap_process).and_return(false, true)
273
+ expect(socket_2).to receive(:close)
274
+ worker_manager.send(:reap_workers)
275
+ end
276
+
277
+ it 'should not change the worker pids map, if all the childern are running' do
278
+ allow(worker_manager).to receive(:reap_process).and_return(false, false)
279
+ expect(worker_manager.instance_variable_get(:@pid_to_worker).size).to eq(2)
280
+ worker_manager.send(:reap_workers)
281
+ expect(worker_manager.instance_variable_get(:@pid_to_worker).size).to eq(2)
282
+ end
283
+ end
284
+
285
+ context "#reap_worker" do
286
+ it 'should return true if the pid was dead' do
287
+ allow(Process).to receive(:wait).and_return(worker_pid_1)
288
+ res = worker_manager.send(:reap_process, worker_pid_1)
289
+ expect(res).to eq(true)
290
+ end
291
+
292
+ it 'should return false if the pid was running' do
293
+ allow(Process).to receive(:wait).and_return(nil)
294
+ res = worker_manager.send(:reap_process, worker_pid_1)
295
+ expect(res).to eq(false)
296
+ end
297
+
298
+ it 'should return true if the pid was dead' do
299
+ allow(Process).to receive(:wait).and_raise(Errno::ECHILD)
300
+ res = worker_manager.send(:reap_process, worker_pid_1)
301
+ expect(res).to eq(true)
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,183 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chore::Strategy::PreForkedWorkerStrategy do
4
+ let(:manager) { double('manager') }
5
+ let(:socket) { double('socket') }
6
+ let(:pipe) { double('pipe') }
7
+ let(:worker) { double('worker') }
8
+ let(:worker_manager) { Chore::Strategy::WorkerManager.new(socket) }
9
+ let(:strategy) { Chore::Strategy::PreForkedWorkerStrategy.new(manager) }
10
+ let(:work_distributor) { Chore::Strategy::WorkDistributor }
11
+
12
+ before(:each) do
13
+ allow_any_instance_of(Chore::Strategy::PreForkedWorkerStrategy).to receive(:trap_signals).and_return(true)
14
+ strategy.instance_variable_set(:@worker_manager, worker_manager)
15
+ end
16
+
17
+ context '#start' do
18
+ it 'should create, attach workers and start the worker manager' do
19
+ allow(worker_manager).to receive(:create_and_attach_workers)
20
+ allow(strategy).to receive(:worker_assignment_thread).and_return(true)
21
+
22
+ expect(worker_manager).to receive(:create_and_attach_workers)
23
+ expect(strategy).to receive(:worker_assignment_thread)
24
+ strategy.start
25
+ end
26
+ end
27
+
28
+ context '#stop!' do
29
+ it 'should set the system\'s running state to false' do
30
+ strategy.instance_variable_set(:@running, true)
31
+ strategy.stop!
32
+ expect(strategy.instance_variable_get(:@running)).to eq false
33
+ end
34
+ end
35
+
36
+ context '#worker_assignment_thread' do
37
+ before(:each) do
38
+ allow(Thread).to receive(:new).and_return(true)
39
+ end
40
+
41
+ it 'create a new thread with the worker_assignment_loop' do
42
+ allow(strategy).to receive(:worker_assignment_loop).and_return(true)
43
+ expect(Thread).to receive(:new).once.and_yield
44
+ expect(strategy).to receive(:worker_assignment_loop)
45
+ expect(Process).to receive(:exit)
46
+ strategy.send(:worker_assignment_thread)
47
+ end
48
+
49
+ it 'rescues a \'TerribleMistake\' exception and performs a shutdown of chore' do
50
+ allow(strategy).to receive(:worker_assignment_loop).and_raise(Chore::TerribleMistake)
51
+ allow(manager).to receive(:shutdown!)
52
+ allow(Thread).to receive(:new).and_yield
53
+ expect(Process).to receive(:exit)
54
+ strategy.send(:worker_assignment_thread)
55
+ end
56
+ end
57
+
58
+ context '#worker_assignment_loop' do
59
+ before(:each) do
60
+ allow(socket).to receive(:eof?).and_return(false)
61
+
62
+ allow(strategy).to receive(:running?).and_return(true, false)
63
+ strategy.instance_variable_set(:@self_read, pipe)
64
+ allow(strategy).to receive(:select_sockets).and_return([[socket], nil, nil])
65
+ allow(strategy).to receive(:handle_self_pipe_signal).and_return(true)
66
+ allow(strategy).to receive(:fetch_and_assign_jobs).and_return(true)
67
+
68
+ allow(worker_manager).to receive(:worker_sockets).and_return([socket])
69
+ allow(worker_manager).to receive(:ready_workers).and_yield([worker])
70
+ allow(worker_manager).to receive(:destroy_expired!)
71
+
72
+ allow(work_distributor).to receive(:fetch_and_assign_jobs)
73
+ end
74
+
75
+ it 'should terminate when @running is set to false' do
76
+ allow(strategy).to receive(:running?).and_return(false)
77
+ expect(strategy).to receive(:select_sockets).exactly(0).times
78
+ strategy.send(:worker_assignment_loop)
79
+ end
80
+
81
+ it 'should get the worker_sockets' do
82
+ expect(worker_manager).to receive(:worker_sockets)
83
+ strategy.send(:worker_assignment_loop)
84
+ end
85
+
86
+ it 'should handle no sockets being ready' do
87
+ allow(strategy).to receive(:select_sockets).and_return(nil)
88
+ expect(strategy).to receive(:select_sockets).once
89
+ strategy.send(:worker_assignment_loop)
90
+ end
91
+
92
+ it 'should handle signals if alerted on a self pipe' do
93
+ allow(strategy).to receive(:select_sockets).and_return([[pipe], nil, nil])
94
+ expect(strategy).to receive(:handle_signal).once
95
+ strategy.send(:worker_assignment_loop)
96
+ end
97
+
98
+ it 'should check if sockets have hit EOF' do
99
+ allow(strategy).to receive(:handle_signal)
100
+ expect(socket).to receive(:eof?).once.and_return(false)
101
+ strategy.send(:worker_assignment_loop)
102
+ end
103
+
104
+ it 'should not assign jobs if sockets have hit EOF' do
105
+ allow(strategy).to receive(:handle_signal)
106
+ allow(socket).to receive(:eof?).and_return(true)
107
+ expect(work_distributor).to_not receive(:fetch_and_assign_jobs)
108
+ strategy.send(:worker_assignment_loop)
109
+ end
110
+
111
+ it 'should handle fetch and assign jobs when workers are ready' do
112
+ expect(work_distributor).to receive(:fetch_and_assign_jobs).with([worker], manager).once
113
+ strategy.send(:worker_assignment_loop)
114
+ end
115
+ end
116
+
117
+ context '#handle_signal' do
118
+ before(:each) do
119
+ strategy.instance_variable_set(:@self_read, pipe)
120
+ allow(pipe).to receive(:read_nonblock).and_return(nil)
121
+
122
+ allow(worker_manager).to receive(:respawn_terminated_workers!).and_return(true)
123
+ allow(worker_manager).to receive(:stop_workers).and_return(true)
124
+ allow(manager).to receive(:shutdown!).and_return(true)
125
+ end
126
+
127
+ it 'should respawn terminated workers in the event of a SIGCHLD' do
128
+ allow(pipe).to receive(:read_nonblock).and_return('1')
129
+ expect(worker_manager).to receive(:respawn_terminated_workers!).once
130
+
131
+ strategy.send(:handle_signal)
132
+ end
133
+
134
+ it 'should signal its children, and shutdown in the event of one of INT, QUIT or TERM signals' do
135
+ allow(pipe).to receive(:read_nonblock).and_return('2', '3', '4')
136
+ expect(worker_manager).to receive(:stop_workers).exactly(3).times
137
+ expect(manager).to receive(:shutdown!).exactly(3).times
138
+ 3.times do
139
+ strategy.send(:handle_signal)
140
+ end
141
+ end
142
+
143
+ it 'should propagte the signal it receives to its children' do
144
+ allow(pipe).to receive(:read_nonblock).and_return('3')
145
+ expect(worker_manager).to receive(:stop_workers).with(:QUIT)
146
+ strategy.send(:handle_signal)
147
+ end
148
+
149
+ it 'should reset logs on a USR1 signal' do
150
+ allow(pipe).to receive(:read_nonblock).and_return('5')
151
+ expect(Chore).to receive(:reopen_logs)
152
+ strategy.send(:handle_signal)
153
+ end
154
+
155
+ it 'should not preform any task when an unhandled signal is called' do
156
+ allow(pipe).to receive(:read_nonblock).and_return('9')
157
+ expect(worker_manager).to receive(:respawn_terminated_workers!).exactly(0).times
158
+ expect(worker_manager).to receive(:stop_workers).exactly(0).times
159
+ expect(manager).to receive(:shutdown!).exactly(0).times
160
+ strategy.send(:handle_signal)
161
+ end
162
+ end
163
+
164
+ context '#trap_signals' do
165
+ before(:each) do
166
+ allow_any_instance_of(Chore::Strategy::PreForkedWorkerStrategy).to receive(:trap_signals).and_call_original
167
+ end
168
+
169
+ let(:signals) { { '1' => 'QUIT' } }
170
+
171
+ it 'should reset signals' do
172
+ allow(Chore::Signal).to receive(:reset)
173
+ expect(Chore::Signal).to receive(:reset)
174
+ strategy.send(:trap_signals, {}, pipe)
175
+ end
176
+
177
+ it 'should trap the signals passed to it' do
178
+ allow(Chore::Signal).to receive(:reset)
179
+ expect(Chore::Signal).to receive(:trap).with('QUIT').once
180
+ strategy.send(:trap_signals, signals, pipe)
181
+ end
182
+ end
183
+ end