chore-core 1.10.0 → 4.0.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 (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