chore-core 1.8.2 → 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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +173 -150
  4. data/chore-core.gemspec +3 -3
  5. data/lib/chore.rb +31 -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 +18 -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 +13 -19
  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 +14 -15
  21. data/lib/chore/strategies/consumer/single_consumer_strategy.rb +5 -5
  22. data/lib/chore/strategies/consumer/threaded_consumer_strategy.rb +9 -7
  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/strategies/worker/single_worker_strategy.rb +35 -13
  33. data/lib/chore/unit_of_work.rb +10 -1
  34. data/lib/chore/util.rb +5 -1
  35. data/lib/chore/version.rb +3 -3
  36. data/lib/chore/worker.rb +32 -3
  37. data/spec/chore/cli_spec.rb +2 -2
  38. data/spec/chore/consumer_spec.rb +1 -5
  39. data/spec/chore/duplicate_detector_spec.rb +17 -5
  40. data/spec/chore/fetcher_spec.rb +0 -11
  41. data/spec/chore/manager_spec.rb +7 -0
  42. data/spec/chore/queues/filesystem/filesystem_consumer_spec.rb +74 -16
  43. data/spec/chore/queues/sqs/consumer_spec.rb +117 -78
  44. data/spec/chore/queues/sqs/publisher_spec.rb +49 -60
  45. data/spec/chore/queues/sqs_spec.rb +32 -41
  46. data/spec/chore/strategies/consumer/batcher_spec.rb +50 -0
  47. data/spec/chore/strategies/consumer/single_consumer_strategy_spec.rb +3 -3
  48. data/spec/chore/strategies/consumer/threaded_consumer_strategy_spec.rb +7 -6
  49. data/spec/chore/strategies/consumer/throttled_consumer_strategy_spec.rb +165 -0
  50. data/spec/chore/strategies/worker/forked_worker_strategy_spec.rb +17 -2
  51. data/spec/chore/strategies/worker/helpers/ipc_spec.rb +127 -0
  52. data/spec/chore/strategies/worker/helpers/preforked_worker_spec.rb +236 -0
  53. data/spec/chore/strategies/worker/helpers/work_distributor_spec.rb +131 -0
  54. data/spec/chore/strategies/worker/helpers/worker_info_spec.rb +14 -0
  55. data/spec/chore/strategies/worker/helpers/worker_killer_spec.rb +97 -0
  56. data/spec/chore/strategies/worker/helpers/worker_manager_spec.rb +304 -0
  57. data/spec/chore/strategies/worker/preforked_worker_strategy_spec.rb +183 -0
  58. data/spec/chore/strategies/worker/single_worker_strategy_spec.rb +25 -0
  59. data/spec/chore/worker_spec.rb +82 -14
  60. data/spec/spec_helper.rb +1 -1
  61. data/spec/support/queues/sqs/fake_objects.rb +18 -0
  62. metadata +39 -15
@@ -1,53 +1,44 @@
1
1
  require 'spec_helper'
2
2
 
3
- module Chore
4
- describe Queues::SQS do
5
- context "when managing queues" do
6
- let(:fake_sqs) {double(Object)}
7
- let(:fake_queue_collection) {double(Object)}
8
- let(:queue_name) {"test"}
9
- let(:queue_url) {"http://amazon.sqs.url/queues/#{queue_name}"}
10
- let(:fake_queue) {double(Object)}
11
-
12
- before(:each) do
13
- allow(AWS::SQS).to receive(:new).and_return(fake_sqs)
14
- allow(Chore).to receive(:prefixed_queue_names).and_return([queue_name])
15
- allow(fake_queue).to receive(:delete)
3
+ describe Chore::Queues::SQS do
4
+ include_context 'fake objects'
5
+
6
+ context "when managing queues" do
7
+ before(:each) do
8
+ allow(Aws::SQS::Client).to receive(:new).and_return(sqs)
9
+ allow(sqs).to receive(:create_queue).and_return(queue)
10
+ allow(sqs).to receive(:delete_queue).and_return(Struct.new(nil))
11
+ allow(queue).to receive(:delete).and_return(sqs.delete_queue(queue))
12
+ allow(Chore).to receive(:prefixed_queue_names).and_return([queue_name])
13
+ allow(queue).to receive(:delete)
14
+ end
16
15
 
17
- allow(fake_queue_collection).to receive(:[]).and_return(fake_queue)
18
- allow(fake_queue_collection).to receive(:create)
19
- allow(fake_queue_collection).to receive(:url_for).with(queue_name).and_return(queue_url)
16
+ it 'should create queues that are defined in its internal job name list' do
17
+ #Only one job defined in the spec suite
18
+ expect(sqs).to receive(:create_queue).with(queue_name: queue_name)
19
+ Chore::Queues::SQS.create_queues!
20
+ end
20
21
 
21
- allow(fake_sqs).to receive(:queues).and_return(fake_queue_collection)
22
- end
22
+ it 'should delete queues that are defined in its internal job name list' do
23
+ #Only one job defined in the spec suite
24
+ expect(sqs).to receive(:delete_queue).with(queue_url: sqs.get_queue_url.queue_url)
25
+ Chore::Queues::SQS.delete_queues!
26
+ end
23
27
 
24
- it 'should create queues that are defined in its internal job name list' do
25
- #Only one job defined in the spec suite
26
- expect(fake_queue_collection).to receive(:create)
27
- Chore::Queues::SQS.create_queues!
28
+ context 'and checking for existing queues' do
29
+ it 'checks for existing queues' do
30
+ expect(described_class).to receive(:existing_queues).and_return([])
31
+ Chore::Queues::SQS.create_queues!(true)
28
32
  end
29
33
 
30
- it 'should delete queues that are defined in its internal job name list' do
31
- #Only one job defined in the spec suite
32
- expect(fake_queue).to receive(:delete)
33
- Chore::Queues::SQS.delete_queues!
34
+ it 'raises an error if a queue does exist' do
35
+ allow(described_class).to receive(:existing_queues).and_return([queue_name])
36
+ expect{Chore::Queues::SQS.create_queues!(true)}.to raise_error(RuntimeError)
34
37
  end
35
38
 
36
- context 'and checking for existing queues' do
37
- it 'checks for existing queues' do
38
- expect(described_class).to receive(:existing_queues).and_return([])
39
- Chore::Queues::SQS.create_queues!(true)
40
- end
41
-
42
- it 'raises an error if a queue does exist' do
43
- allow(described_class).to receive(:existing_queues).and_return([queue_name])
44
- expect{Chore::Queues::SQS.create_queues!(true)}.to raise_error(RuntimeError)
45
- end
46
-
47
- it 'does not raise an error if a queue does not exist' do
48
- allow(described_class).to receive(:existing_queues).and_return([])
49
- expect{Chore::Queues::SQS.create_queues!(true)}.not_to raise_error
50
- end
39
+ it 'does not raise an error if a queue does not exist' do
40
+ allow(described_class).to receive(:existing_queues).and_return([])
41
+ expect{Chore::Queues::SQS.create_queues!(true)}.not_to raise_error
51
42
  end
52
43
  end
53
44
  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
@@ -11,13 +11,13 @@ describe Chore::Strategy::SingleConsumerStrategy do
11
11
  fetcher.stub(:manager) { manager }
12
12
  Chore.config.stub(:queues).and_return(test_queues)
13
13
  Chore.config.stub(:consumer).and_return(consumer)
14
-
14
+
15
15
  end
16
16
 
17
17
  it "should consume and then assign a message" do
18
18
  consumer.should_receive(:new).with(test_queues.first).and_return(consumer)
19
- consumer.should_receive(:consume).and_yield(1, 'test-queue', 60, "test", 0)
20
- manager.should_receive(:assign).with(Chore::UnitOfWork.new(1, 'test-queue', 60, "test", 0, consumer))
19
+ consumer.should_receive(:consume).and_yield(1, nil, 'test-queue', 60, "test", 0)
20
+ manager.should_receive(:assign).with(Chore::UnitOfWork.new(1, nil, 'test-queue', 60, "test", 0, consumer))
21
21
  strategy.fetch
22
22
  end
23
23
  end
@@ -29,8 +29,8 @@ describe Chore::Strategy::ThreadedConsumerStrategy do
29
29
  before(:each) do
30
30
  fetcher.stub(:consumers) { [consumer] }
31
31
  fetcher.stub(:manager) { manager }
32
- Chore.configure do |c|
33
- c.queues = ['test']
32
+ Chore.configure do |c|
33
+ c.queues = ['test']
34
34
  c.consumer = consumer
35
35
  c.batch_size = batch_size
36
36
  end
@@ -40,7 +40,7 @@ describe Chore::Strategy::ThreadedConsumerStrategy do
40
40
  let(:batch_size) { 2 }
41
41
 
42
42
  it "should queue but not assign the message" do
43
- consumer.any_instance.should_receive(:consume).and_yield(1, 'test-queue', 60, "test", 0)
43
+ consumer.any_instance.should_receive(:consume).and_yield(1, nil, 'test-queue', 60, "test", 0)
44
44
  strategy.fetch
45
45
  strategy.batcher.batch.size.should == 1
46
46
 
@@ -51,6 +51,7 @@ describe Chore::Strategy::ThreadedConsumerStrategy do
51
51
  work.message.should == "test"
52
52
  work.previous_attempts.should == 0
53
53
  work.current_attempt.should == 1
54
+ work.created_at.should_not be_nil
54
55
  end
55
56
  end
56
57
 
@@ -59,7 +60,7 @@ describe Chore::Strategy::ThreadedConsumerStrategy do
59
60
 
60
61
  it "should assign the batch" do
61
62
  manager.should_receive(:assign)
62
- consumer.any_instance.should_receive(:consume).and_yield(1, 'test-queue', 60, "test", 0)
63
+ consumer.any_instance.should_receive(:consume).and_yield(1, nil, 'test-queue', 60, "test", 0)
63
64
  strategy.fetch
64
65
  strategy.batcher.batch.size.should == 0
65
66
  end
@@ -89,8 +90,8 @@ describe Chore::Strategy::ThreadedConsumerStrategy do
89
90
 
90
91
  before do
91
92
  fetcher.stub(:consumers) { [bad_consumer] }
92
- Chore.configure do |c|
93
- c.queues = ['test']
93
+ Chore.configure do |c|
94
+ c.queues = ['test']
94
95
  c.consumer = bad_consumer
95
96
  c.batch_size = batch_size
96
97
  c.threads_per_queue = 1
@@ -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,26 @@ 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) { Chore::UnitOfWork.new(SecureRandom.uuid, 'test', job_timeout, Chore::Encoder::JsonEncoder.encode(TestJob.job_hash([1,2,"3"])), 0) }
13
+ let(:job) do
14
+ Chore::UnitOfWork.new(
15
+ SecureRandom.uuid,
16
+ nil,
17
+ 'test',
18
+ job_timeout,
19
+ Chore::Encoder::JsonEncoder.encode(TestJob.job_hash([1,2,"3"])),
20
+ 0,
21
+ consumer
22
+ )
23
+ end
13
24
  let!(:worker) { Chore::Worker.new(job) }
14
25
  let(:pid) { Random.rand(2048) }
15
26
 
27
+ before(:each) do
28
+ allow(consumer).to receive(:duplicate_message?).and_return(false)
29
+ end
30
+
16
31
  after(:each) do
17
32
  allow(Process).to receive(:kill) { nil }
18
33
  allow(Process).to receive(:wait) { pid }
@@ -164,6 +179,7 @@ describe Chore::Strategy::ForkedWorkerStrategy do
164
179
  end
165
180
 
166
181
  it 'should run the on_failure callback hook' do
182
+ allow(Chore).to receive(:run_hooks_for)
167
183
  forker.assign(job)
168
184
  expect(Chore).to receive(:run_hooks_for).with(:on_failure, anything, instance_of(Chore::TimeoutError))
169
185
  sleep 2
@@ -281,4 +297,3 @@ describe Chore::Strategy::ForkedWorkerStrategy do
281
297
  end
282
298
  end
283
299
  end
284
-
@@ -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
+