chore-core 1.8.2 → 3.2.3
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 +5 -5
- data/README.md +6 -0
- data/chore-core.gemspec +1 -0
- data/lib/chore.rb +11 -5
- data/lib/chore/cli.rb +21 -2
- data/lib/chore/consumer.rb +15 -5
- data/lib/chore/fetcher.rb +12 -7
- data/lib/chore/hooks.rb +2 -1
- data/lib/chore/job.rb +17 -0
- data/lib/chore/manager.rb +18 -2
- data/lib/chore/queues/filesystem/consumer.rb +116 -59
- data/lib/chore/queues/filesystem/filesystem_queue.rb +19 -0
- data/lib/chore/queues/filesystem/publisher.rb +12 -18
- data/lib/chore/queues/sqs/consumer.rb +6 -21
- data/lib/chore/strategies/consumer/batcher.rb +8 -9
- data/lib/chore/strategies/consumer/threaded_consumer_strategy.rb +3 -1
- data/lib/chore/strategies/consumer/throttled_consumer_strategy.rb +121 -0
- data/lib/chore/strategies/worker/forked_worker_strategy.rb +5 -6
- data/lib/chore/strategies/worker/helpers/ipc.rb +88 -0
- data/lib/chore/strategies/worker/helpers/preforked_worker.rb +163 -0
- data/lib/chore/strategies/worker/helpers/work_distributor.rb +65 -0
- data/lib/chore/strategies/worker/helpers/worker_info.rb +13 -0
- data/lib/chore/strategies/worker/helpers/worker_killer.rb +40 -0
- data/lib/chore/strategies/worker/helpers/worker_manager.rb +183 -0
- data/lib/chore/strategies/worker/preforked_worker_strategy.rb +150 -0
- data/lib/chore/strategies/worker/single_worker_strategy.rb +35 -13
- data/lib/chore/unit_of_work.rb +8 -0
- data/lib/chore/util.rb +5 -1
- data/lib/chore/version.rb +3 -3
- data/lib/chore/worker.rb +29 -0
- data/spec/chore/cli_spec.rb +2 -2
- data/spec/chore/consumer_spec.rb +0 -4
- data/spec/chore/duplicate_detector_spec.rb +17 -5
- data/spec/chore/fetcher_spec.rb +0 -11
- data/spec/chore/manager_spec.rb +7 -0
- data/spec/chore/queues/filesystem/filesystem_consumer_spec.rb +71 -11
- data/spec/chore/queues/sqs/consumer_spec.rb +1 -3
- data/spec/chore/strategies/consumer/batcher_spec.rb +50 -0
- data/spec/chore/strategies/consumer/threaded_consumer_strategy_spec.rb +1 -0
- data/spec/chore/strategies/consumer/throttled_consumer_strategy_spec.rb +165 -0
- data/spec/chore/strategies/worker/forked_worker_strategy_spec.rb +16 -1
- data/spec/chore/strategies/worker/helpers/ipc_spec.rb +127 -0
- data/spec/chore/strategies/worker/helpers/preforked_worker_spec.rb +236 -0
- data/spec/chore/strategies/worker/helpers/work_distributor_spec.rb +131 -0
- data/spec/chore/strategies/worker/helpers/worker_info_spec.rb +14 -0
- data/spec/chore/strategies/worker/helpers/worker_killer_spec.rb +97 -0
- data/spec/chore/strategies/worker/helpers/worker_manager_spec.rb +304 -0
- data/spec/chore/strategies/worker/preforked_worker_strategy_spec.rb +183 -0
- data/spec/chore/strategies/worker/single_worker_strategy_spec.rb +25 -0
- data/spec/chore/worker_spec.rb +69 -1
- metadata +33 -5
@@ -34,9 +34,7 @@ describe Chore::Queues::SQS::Consumer do
|
|
34
34
|
|
35
35
|
expect(AWS::SQS).to receive(:new).with(
|
36
36
|
:access_key_id => 'key',
|
37
|
-
:secret_access_key => 'secret'
|
38
|
-
:logger => Chore.logger,
|
39
|
-
:log_level => :debug
|
37
|
+
:secret_access_key => 'secret'
|
40
38
|
).and_return(sqs)
|
41
39
|
consumer.consume
|
42
40
|
end
|
@@ -90,4 +90,54 @@ describe Chore::Strategy::Batcher do
|
|
90
90
|
subject.batch.should == ['test']
|
91
91
|
end
|
92
92
|
end
|
93
|
+
|
94
|
+
describe 'schedule' do
|
95
|
+
let(:timeout) { 5 }
|
96
|
+
let(:batch) { [] }
|
97
|
+
|
98
|
+
before(:each) do
|
99
|
+
Thread.stub(:new) do |&block|
|
100
|
+
# Stop the batcher on the next iteration
|
101
|
+
subject.stub(:sleep) { subject.stop }
|
102
|
+
|
103
|
+
# Run the scheduling thread
|
104
|
+
block.call(timeout)
|
105
|
+
end
|
106
|
+
|
107
|
+
subject.batch = batch.dup
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'with no items' do
|
111
|
+
it 'should not invoke the callback' do
|
112
|
+
callback.should_not_receive(:call)
|
113
|
+
subject.schedule(timeout)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'with new items' do
|
118
|
+
let(:batch) do
|
119
|
+
[
|
120
|
+
Chore::UnitOfWork.new.tap {|work| work.created_at = Time.now - 2}
|
121
|
+
]
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should not invoke the callback' do
|
125
|
+
callback.should_not_receive(:call).with(batch)
|
126
|
+
subject.schedule(timeout)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'with old items' do
|
131
|
+
let(:batch) do
|
132
|
+
[
|
133
|
+
Chore::UnitOfWork.new.tap {|work| work.created_at = Time.now - 6}
|
134
|
+
]
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'should invoke the callback' do
|
138
|
+
callback.should_receive(:call).with(batch)
|
139
|
+
subject.schedule(timeout)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
93
143
|
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestConsumer < Chore::Consumer
|
4
|
+
def initialize(queue_name, opts={})
|
5
|
+
end
|
6
|
+
|
7
|
+
def consume
|
8
|
+
# just something that looks like an SQS message
|
9
|
+
msg = OpenStruct.new( :id => 1, :body => "test" )
|
10
|
+
yield msg if block_given?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class NoQueueConsumer < Chore::Consumer
|
15
|
+
def initialize(queue_name, opts={})
|
16
|
+
raise Chore::TerribleMistake
|
17
|
+
end
|
18
|
+
|
19
|
+
def consume
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe Chore::Strategy::ThrottledConsumerStrategy do
|
24
|
+
let(:fetcher) { double("fetcher") }
|
25
|
+
let(:manager) { double("manager") }
|
26
|
+
let(:thread) {double("thread")}
|
27
|
+
let(:consume_queue) { "TestQueue" }
|
28
|
+
let(:consumer) { TestConsumer }
|
29
|
+
let(:consumer_object) { consumer.new(consume_queue) }
|
30
|
+
let(:strategy) { Chore::Strategy::ThrottledConsumerStrategy.new(fetcher) }
|
31
|
+
let(:config) { double("config") }
|
32
|
+
let(:sized_queue) {double("sized_queue")}
|
33
|
+
let(:return_queue) {double("return_queue")}
|
34
|
+
let(:work) { double("work") }
|
35
|
+
let(:msg) { OpenStruct.new( :id => 1, :body => "test" ) }
|
36
|
+
|
37
|
+
|
38
|
+
before(:each) do
|
39
|
+
allow(fetcher).to receive(:consumers).and_return([consumer])
|
40
|
+
allow(fetcher).to receive(:manager).and_return(manager)
|
41
|
+
Chore.configure do |c|
|
42
|
+
c.queues = [consume_queue]
|
43
|
+
c.consumer = consumer
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context '#fetch' do
|
48
|
+
it 'should call consume, \'@number_of_consumers\' number of times' do
|
49
|
+
allow(strategy).to receive(:consume).and_return(thread)
|
50
|
+
allow(thread).to receive(:join).and_return(true)
|
51
|
+
allow(Chore).to receive(:config).and_return(config)
|
52
|
+
allow(config).to receive(:queues).and_return([consume_queue])
|
53
|
+
strategy.instance_variable_set(:@consumers_per_queue, 5)
|
54
|
+
expect(strategy).to receive(:consume).with(consume_queue).exactly(5).times
|
55
|
+
strategy.fetch
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context '#stop!' do
|
60
|
+
it 'should should stop itself, and every other consumer' do
|
61
|
+
allow(strategy).to receive(:running?).and_return(true)
|
62
|
+
strategy.instance_eval('@running = true')
|
63
|
+
strategy.instance_variable_set(:@consumers, [consumer_object])
|
64
|
+
expect(consumer_object).to receive(:stop)
|
65
|
+
strategy.stop!
|
66
|
+
expect(strategy.instance_variable_get(:@running)).to eq(false)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context '#provide_work' do
|
71
|
+
it 'should return upto n units of work' do
|
72
|
+
n = 2
|
73
|
+
strategy.instance_variable_set(:@queue, sized_queue)
|
74
|
+
allow(sized_queue).to receive(:size).and_return(10)
|
75
|
+
allow(sized_queue).to receive(:pop).and_return(work)
|
76
|
+
expect(sized_queue).to receive(:pop).exactly(n).times
|
77
|
+
res = strategy.provide_work(n)
|
78
|
+
expect(res.size).to eq(n)
|
79
|
+
expect(res).to be_a_kind_of(Array)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should return an empty array if no work is found in the queue' do
|
83
|
+
n = 2
|
84
|
+
strategy.instance_variable_set(:@queue, sized_queue)
|
85
|
+
allow(sized_queue).to receive(:size).and_return(0)
|
86
|
+
allow(sized_queue).to receive(:pop).and_return(work)
|
87
|
+
expect(sized_queue).to receive(:pop).exactly(0).times
|
88
|
+
res = strategy.provide_work(n)
|
89
|
+
expect(res.size).to eq(0)
|
90
|
+
expect(res).to be_a_kind_of(Array)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should return units of work from the return queue first' do
|
94
|
+
n = 2
|
95
|
+
strategy.instance_variable_set(:@return_queue, return_queue)
|
96
|
+
allow(return_queue).to receive(:empty?).and_return(false)
|
97
|
+
allow(return_queue).to receive(:size).and_return(10)
|
98
|
+
allow(return_queue).to receive(:pop).and_return(work)
|
99
|
+
expect(return_queue).to receive(:pop).exactly(n).times
|
100
|
+
res = strategy.provide_work(n)
|
101
|
+
expect(res.size).to eq(n)
|
102
|
+
expect(res).to be_a_kind_of(Array)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should return units of work from all queues if return queue is small' do
|
106
|
+
n = 2
|
107
|
+
|
108
|
+
strategy.instance_variable_set(:@return_queue, return_queue)
|
109
|
+
allow(return_queue).to receive(:empty?).and_return(false, true)
|
110
|
+
allow(return_queue).to receive(:size).and_return(1)
|
111
|
+
allow(return_queue).to receive(:pop).and_return(work)
|
112
|
+
expect(return_queue).to receive(:pop).once
|
113
|
+
|
114
|
+
strategy.instance_variable_set(:@queue, sized_queue)
|
115
|
+
allow(sized_queue).to receive(:size).and_return(1)
|
116
|
+
allow(sized_queue).to receive(:pop).and_return(work)
|
117
|
+
expect(sized_queue).to receive(:pop).once
|
118
|
+
|
119
|
+
res = strategy.provide_work(n)
|
120
|
+
expect(res.size).to eq(n)
|
121
|
+
expect(res).to be_a_kind_of(Array)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'return_work' do
|
126
|
+
it 'should add it to the internal return queue' do
|
127
|
+
strategy.instance_variable_set(:@return_queue, [])
|
128
|
+
strategy.send(:return_work, [work])
|
129
|
+
strategy.send(:return_work, [work])
|
130
|
+
return_queue = strategy.instance_variable_get(:@return_queue)
|
131
|
+
expect(return_queue).to eq([work, work])
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context '#consume' do
|
136
|
+
it 'should create a consumer object, add it to the list of consumers and start a consumer thread' do
|
137
|
+
allow(strategy).to receive(:start_consumer_thread).and_return(true)
|
138
|
+
expect(strategy).to receive(:start_consumer_thread)
|
139
|
+
strategy.send(:consume, consume_queue)
|
140
|
+
expect(strategy.instance_variable_get(:@consumers)).to be_a_kind_of(Array)
|
141
|
+
expect(strategy.instance_variable_get(:@consumers).first).to be_a_kind_of(TestConsumer)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
context '#start_consumer_thread' do
|
146
|
+
let(:thread) { double('thread') }
|
147
|
+
|
148
|
+
it 'should create a thread' do
|
149
|
+
allow(Thread).to receive(:new).and_return(thread)
|
150
|
+
res = strategy.send(:start_consumer_thread, consumer_object)
|
151
|
+
expect(res).to eq(thread)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context '#create_work_units' do
|
156
|
+
it 'should create an unit of work from what the consumer gets, and adds it to the internal queue' do
|
157
|
+
strategy.instance_variable_set(:@queue, [])
|
158
|
+
res = strategy.send(:create_work_units, consumer_object)
|
159
|
+
internal_queue = strategy.instance_variable_get(:@queue)
|
160
|
+
expect(internal_queue).to be_a_kind_of(Array)
|
161
|
+
expect(internal_queue.first).to be_a_kind_of(Chore::UnitOfWork)
|
162
|
+
expect(internal_queue.first.id).to eq(msg)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -8,11 +8,25 @@ describe Chore::Strategy::ForkedWorkerStrategy do
|
|
8
8
|
allow(strategy).to receive(:exit!)
|
9
9
|
strategy
|
10
10
|
end
|
11
|
+
let(:consumer) { double('consumer', :complete => nil, :reject => nil) }
|
11
12
|
let(:job_timeout) { 60 }
|
12
|
-
let(:job)
|
13
|
+
let(:job) do
|
14
|
+
Chore::UnitOfWork.new(
|
15
|
+
SecureRandom.uuid,
|
16
|
+
'test',
|
17
|
+
job_timeout,
|
18
|
+
Chore::Encoder::JsonEncoder.encode(TestJob.job_hash([1,2,"3"])),
|
19
|
+
0,
|
20
|
+
consumer
|
21
|
+
)
|
22
|
+
end
|
13
23
|
let!(:worker) { Chore::Worker.new(job) }
|
14
24
|
let(:pid) { Random.rand(2048) }
|
15
25
|
|
26
|
+
before(:each) do
|
27
|
+
allow(consumer).to receive(:duplicate_message?).and_return(false)
|
28
|
+
end
|
29
|
+
|
16
30
|
after(:each) do
|
17
31
|
allow(Process).to receive(:kill) { nil }
|
18
32
|
allow(Process).to receive(:wait) { pid }
|
@@ -164,6 +178,7 @@ describe Chore::Strategy::ForkedWorkerStrategy do
|
|
164
178
|
end
|
165
179
|
|
166
180
|
it 'should run the on_failure callback hook' do
|
181
|
+
allow(Chore).to receive(:run_hooks_for)
|
167
182
|
forker.assign(job)
|
168
183
|
expect(Chore).to receive(:run_hooks_for).with(:on_failure, anything, instance_of(Chore::TimeoutError))
|
169
184
|
sleep 2
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
describe Chore::Strategy::Ipc do
|
5
|
+
class DummyClass
|
6
|
+
include Chore::Strategy::Ipc
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
@dummy_instance = DummyClass.new
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:socket) { double('socket') }
|
14
|
+
let(:connection) { double('socket') }
|
15
|
+
let(:message) { "test message" }
|
16
|
+
let(:encoded_message) { "#{[Marshal.dump(message).size].pack('L>')}#{Marshal.dump(message)}" }
|
17
|
+
let(:socket_base) { './prefork_worker_sock-' }
|
18
|
+
let(:pid) { '1' }
|
19
|
+
let(:socket_file) { socket_base + pid }
|
20
|
+
|
21
|
+
describe '#create_master_socket' do
|
22
|
+
before(:each) do
|
23
|
+
allow(Process).to receive(:pid).and_return(pid)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'deletes socket file, if it already exists' do
|
27
|
+
allow(File).to receive(:exist?).with(socket_file).and_return(true)
|
28
|
+
allow(UNIXServer).to receive(:new).and_return(socket)
|
29
|
+
allow(socket).to receive(:setsockopt).and_return(true)
|
30
|
+
expect(File).to receive(:delete).with(socket_file)
|
31
|
+
@dummy_instance.create_master_socket
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should create and return a new UnixServer object' do
|
35
|
+
allow(File).to receive(:exist?).with(socket_file).and_return(false)
|
36
|
+
allow(UNIXServer).to receive(:new).and_return(socket)
|
37
|
+
allow(socket).to receive(:setsockopt).and_return(true)
|
38
|
+
expect(@dummy_instance.create_master_socket).to eq(socket)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should set the required socket options' do
|
42
|
+
allow(File).to receive(:exist?).with(socket_file).and_return(false)
|
43
|
+
allow(UNIXServer).to receive(:new).and_return(socket)
|
44
|
+
expect(socket).to receive(:setsockopt).with(:SOCKET, :REUSEADDR, true)
|
45
|
+
@dummy_instance.create_master_socket
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context '#child_connection' do
|
50
|
+
it 'should accept a connection and return the connection socket' do
|
51
|
+
expect(socket).to receive(:accept).and_return(connection)
|
52
|
+
expect(@dummy_instance.child_connection(socket)).to eq connection
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context '#send_msg' do
|
57
|
+
it 'should raise an exception if the message is empty' do
|
58
|
+
expect { @dummy_instance.send_msg(socket, nil) }.to raise_error('send_msg cannot send empty messages')
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should send a message with the predefined protocol (size of message + marshalled message)' do
|
62
|
+
expect(socket).to receive(:send).with(encoded_message, 0)
|
63
|
+
@dummy_instance.send_msg(socket, message)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context '#read_msg' do
|
68
|
+
before(:each) do
|
69
|
+
allow(IO).to receive(:select).with([socket], nil, nil, 0.5).and_return([[socket], [], []])
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should return nil if the message size is missing' do
|
73
|
+
allow(socket).to receive(:recv).and_return(nil)
|
74
|
+
expect(@dummy_instance.read_msg(socket)).to eq(nil)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should read a message with the predefined protocol (size of message + marshalled message)' do
|
78
|
+
allow(socket).to receive(:recv).and_return(encoded_message)
|
79
|
+
expect(@dummy_instance.read_msg(socket)).to eq(message)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should raise an exception if the connection was dropped' do
|
83
|
+
allow(socket).to receive(:recv).and_raise(Errno::ECONNRESET)
|
84
|
+
expect { @dummy_instance.read_msg(socket) }.to raise_error(Errno::ECONNRESET)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context '#add_worker_socket' do
|
89
|
+
it 'should create and return a new UnixSocket object' do
|
90
|
+
allow(UNIXSocket).to receive(:new).and_return(socket)
|
91
|
+
allow(socket).to receive(:setsockopt).and_return(true)
|
92
|
+
expect(@dummy_instance.add_worker_socket).to eq(socket)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'should set the required socket options' do
|
96
|
+
allow(UNIXSocket).to receive(:new).and_return(socket)
|
97
|
+
expect(socket).to receive(:setsockopt).with(:SOCKET, :REUSEADDR, true)
|
98
|
+
@dummy_instance.add_worker_socket
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context '#clear_ready' do
|
103
|
+
it 'should remove the ready signal from the the socket' do
|
104
|
+
expect(socket).to receive(:gets)
|
105
|
+
@dummy_instance.clear_ready(socket)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context '#signal_ready' do
|
110
|
+
it 'should set a ready signal on the socket' do
|
111
|
+
expect(socket).to receive(:puts).with('R')
|
112
|
+
@dummy_instance.signal_ready(socket)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context '#select_sockets' do
|
117
|
+
it 'should return a readable socket if one is found' do
|
118
|
+
allow(IO).to receive(:select).with([socket], nil, [socket], 0.5).and_return([[socket], [], []])
|
119
|
+
expect(@dummy_instance.select_sockets([socket], nil, 0.5)).to eq([[socket], [], []])
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should timeout and return no sockets if none are found within the timeout window' do
|
123
|
+
expect(@dummy_instance.select_sockets(nil, nil, 0.1)).to eq(nil)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Chore::Strategy::PreforkedWorker do
|
4
|
+
before(:each) do
|
5
|
+
allow_any_instance_of(Chore::Strategy::PreforkedWorker).to receive(:post_fork_setup)
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:preforkedworker) { Chore::Strategy::PreforkedWorker.new }
|
9
|
+
let(:socket) { double("socket") }
|
10
|
+
let(:work) { double("work") }
|
11
|
+
let(:consumer) { double("consumer") }
|
12
|
+
let(:worker) { double("worker") }
|
13
|
+
let(:config) { double("config") }
|
14
|
+
let(:queues) { double("queues") }
|
15
|
+
let(:consumer_object) { double("consumer_object") }
|
16
|
+
|
17
|
+
context '#start_worker' do
|
18
|
+
it 'should connect to the master to signal that it is ready, and process messages with the worker' do
|
19
|
+
allow(preforkedworker).to receive(:connect_to_master).and_return(socket)
|
20
|
+
expect(preforkedworker).to receive(:connect_to_master).with(socket)
|
21
|
+
allow(preforkedworker).to receive(:worker).and_return(true)
|
22
|
+
expect(preforkedworker).to receive(:worker).with(socket)
|
23
|
+
preforkedworker.start_worker(socket)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#worker' do
|
28
|
+
before(:each) do
|
29
|
+
preforkedworker.instance_variable_set(:@self_read, socket)
|
30
|
+
allow(preforkedworker).to receive(:select_sockets).and_return([[socket],nil,nil])
|
31
|
+
allow(preforkedworker).to receive(:read_msg).and_return(nil)
|
32
|
+
allow(preforkedworker).to receive(:is_orphan?).and_return(false)
|
33
|
+
allow(preforkedworker).to receive(:process_work).and_return(true)
|
34
|
+
allow(preforkedworker).to receive(:signal_ready).and_return(true)
|
35
|
+
allow(work).to receive(:queue_timeout).and_return(20*60)
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
it 'should not run while @running is false' do
|
40
|
+
allow(preforkedworker).to receive(:running?).and_return(false)
|
41
|
+
expect(preforkedworker).to receive(:select_sockets).exactly(0).times
|
42
|
+
begin
|
43
|
+
preforkedworker.send(:worker, nil)
|
44
|
+
rescue SystemExit=>e
|
45
|
+
expect(e.status).to eq(0)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should be able to handle timeouts on readable sockets' do
|
50
|
+
allow(preforkedworker).to receive(:running?).and_return(true, true, false)
|
51
|
+
allow(preforkedworker).to receive(:select_sockets).and_return(nil)
|
52
|
+
expect(preforkedworker).to receive(:select_sockets).exactly(2).times
|
53
|
+
begin
|
54
|
+
preforkedworker.send(:worker, nil)
|
55
|
+
rescue SystemExit=>e
|
56
|
+
expect(e.status).to eq(0)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should read a message if the connection is readable' do
|
61
|
+
allow(preforkedworker).to receive(:running?).and_return(true, false)
|
62
|
+
expect(preforkedworker).to receive(:read_msg).once
|
63
|
+
begin
|
64
|
+
preforkedworker.send(:worker, nil)
|
65
|
+
rescue SystemExit=>e
|
66
|
+
expect(e.status).to eq(0)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should check if the master is alive, and if not, it should end' do
|
71
|
+
allow(preforkedworker).to receive(:running).and_return(true)
|
72
|
+
allow(preforkedworker).to receive(:select_sockets).and_return([[socket],nil,nil])
|
73
|
+
allow(preforkedworker).to receive(:read_msg).and_return(nil)
|
74
|
+
allow(preforkedworker).to receive(:is_orphan?).and_return(true)
|
75
|
+
begin
|
76
|
+
preforkedworker.send(:worker, socket)
|
77
|
+
rescue SystemExit=>e
|
78
|
+
expect(e.status).to eq(0)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should process work if it is read from the master and signal ready' do
|
83
|
+
allow(preforkedworker).to receive(:running?).and_return(true, false)
|
84
|
+
allow(preforkedworker).to receive(:read_msg).and_return(work)
|
85
|
+
expect(preforkedworker).to receive(:process_work).once
|
86
|
+
expect(preforkedworker).to receive(:signal_ready).once
|
87
|
+
begin
|
88
|
+
preforkedworker.send(:worker, socket)
|
89
|
+
rescue SystemExit=>e
|
90
|
+
expect(e.status).to eq(0)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should exit the process if the connection to master is closed' do
|
95
|
+
allow(preforkedworker).to receive(:running?).and_return(true)
|
96
|
+
allow(preforkedworker).to receive(:read_msg).and_raise(Errno::ECONNRESET)
|
97
|
+
begin
|
98
|
+
preforkedworker.send(:worker, socket)
|
99
|
+
rescue SystemExit=>e
|
100
|
+
expect(e.status).to eq(0)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context '#connect_to_master' do
|
106
|
+
it 'should create a connection to the master, and send it its PID and a ready message' do
|
107
|
+
allow(preforkedworker).to receive(:child_connection).and_return(socket)
|
108
|
+
allow(preforkedworker).to receive(:send_msg).and_return(true)
|
109
|
+
allow(preforkedworker).to receive(:signal_ready).and_return(true)
|
110
|
+
|
111
|
+
expect(preforkedworker).to receive(:child_connection).once
|
112
|
+
expect(preforkedworker).to receive(:send_msg).once
|
113
|
+
expect(preforkedworker).to receive(:signal_ready).once
|
114
|
+
|
115
|
+
res = preforkedworker.send(:connect_to_master,socket)
|
116
|
+
|
117
|
+
expect(res).to eq(socket)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context '#post_fork_setup' do
|
122
|
+
before(:each) do
|
123
|
+
allow(preforkedworker).to receive(:procline).and_return(true)
|
124
|
+
allow(preforkedworker).to receive(:trap_signals)
|
125
|
+
allow_any_instance_of(Chore::Strategy::PreforkedWorker).to receive(:post_fork_setup).and_call_original
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should change the process name' do
|
129
|
+
expect(preforkedworker).to receive(:procline)
|
130
|
+
preforkedworker.send(:post_fork_setup)
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should trap new relevant signals' do
|
134
|
+
expect(preforkedworker).to receive(:trap_signals)
|
135
|
+
preforkedworker.send(:post_fork_setup)
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should run after_fork hooks" do
|
139
|
+
hook_called = false
|
140
|
+
Chore.add_hook(:after_fork) { hook_called = true }
|
141
|
+
preforkedworker.send(:post_fork_setup)
|
142
|
+
expect(hook_called).to be true
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context '#process_work' do
|
147
|
+
before(:each) do
|
148
|
+
allow(preforkedworker).to receive(:consumer).and_return(consumer)
|
149
|
+
allow(work).to receive(:queue_name).and_return("test_queue")
|
150
|
+
allow(work).to receive(:consumer=)
|
151
|
+
allow(work).to receive(:queue_timeout).and_return(10)
|
152
|
+
allow(Chore::Worker).to receive(:new).and_return(worker)
|
153
|
+
allow(worker).to receive(:start)
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'should fetch the consumer object associated with the queue' do
|
157
|
+
expect(preforkedworker).to receive(:consumer)
|
158
|
+
preforkedworker.send(:process_work, [work])
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'should create and start a worker object with the job sent to it' do
|
162
|
+
expect(worker).to receive(:start)
|
163
|
+
preforkedworker.send(:process_work, [work])
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'should timeout if the work runs for longer than the queue timeout' do
|
167
|
+
allow(work).to receive(:queue_timeout).and_return(1)
|
168
|
+
allow(worker).to receive(:start) { sleep 5 }
|
169
|
+
begin
|
170
|
+
preforkedworker.send(:process_work, [work])
|
171
|
+
rescue => e
|
172
|
+
expect(e.class).to eq(Timeout::Error)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context '#consumer' do
|
178
|
+
before(:each) do
|
179
|
+
preforkedworker.instance_variable_set(:@consumer_cache, {key_1: :value_1})
|
180
|
+
allow(Chore).to receive(:config).and_return(config)
|
181
|
+
allow(config).to receive(:queues).and_return(queues)
|
182
|
+
allow(queues).to receive(:size).and_return(2)
|
183
|
+
allow(config).to receive(:consumer).and_return(consumer)
|
184
|
+
allow(consumer).to receive(:new).and_return(:value_2)
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'should fetch a consumer object if it was created previously for this queue' do
|
188
|
+
preforkedworker.instance_variable_set(:@consumer_cache, {key_1: :value_1})
|
189
|
+
res = preforkedworker.send(:consumer,:key_1)
|
190
|
+
expect(res).to eq(:value_1)
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'should create and return a new consumer object if one does not exist for this queue' do
|
194
|
+
preforkedworker.instance_variable_set(:@consumer_cache, {})
|
195
|
+
expect(consumer).to receive(:new)
|
196
|
+
res = preforkedworker.send(:consumer,:key_2)
|
197
|
+
expect(res).to eq(:value_2)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
context '#trap_signals' do
|
202
|
+
it 'should reset signals' do
|
203
|
+
allow(Chore::Signal).to receive(:reset)
|
204
|
+
allow(Chore::Signal).to receive(:trap)
|
205
|
+
expect(Chore::Signal).to receive(:reset)
|
206
|
+
preforkedworker.send(:trap_signals)
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'should trap the signals passed to it' do
|
210
|
+
allow(Chore::Signal).to receive(:reset)
|
211
|
+
allow(Chore::Signal).to receive(:trap)
|
212
|
+
expect(Chore::Signal).to receive(:trap).with(:INT).once
|
213
|
+
expect(Chore::Signal).to receive(:trap).with(:QUIT).once
|
214
|
+
expect(Chore::Signal).to receive(:trap).with(:TERM).once
|
215
|
+
expect(Chore::Signal).to receive(:trap).with(:USR1).once
|
216
|
+
preforkedworker.send(:trap_signals)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
context '#is_orphan?' do
|
221
|
+
it 'should return true if the parent is dead' do
|
222
|
+
allow(Process).to receive(:ppid).and_return(10)
|
223
|
+
preforkedworker.instance_variable_set(:@manager_pid, 9)
|
224
|
+
res = preforkedworker.send(:is_orphan?)
|
225
|
+
expect(res).to eq(true)
|
226
|
+
end
|
227
|
+
|
228
|
+
it 'should return false if the parent is alive' do
|
229
|
+
allow(Process).to receive(:ppid).and_return(10)
|
230
|
+
preforkedworker.instance_variable_set(:@manager_pid, 10)
|
231
|
+
res = preforkedworker.send(:is_orphan?)
|
232
|
+
expect(res).to eq(false)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|