nexia_worker_roulette 0.1.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.agignore +2 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.simplecov +16 -0
- data/Gemfile +9 -0
- data/Guardfile +3 -0
- data/LICENSE.txt +0 -0
- data/README.md +114 -0
- data/Rakefile +32 -0
- data/lib/worker_roulette/foreman.rb +71 -0
- data/lib/worker_roulette/lua.rb +50 -0
- data/lib/worker_roulette/tradesman.rb +125 -0
- data/lib/worker_roulette/version.rb +3 -0
- data/lib/worker_roulette.rb +96 -0
- data/spec/benchmark/irb_demo_runner.rb +39 -0
- data/spec/benchmark/perf_test.rb +137 -0
- data/spec/helpers/.gitkeep +1 -0
- data/spec/integration/evented_worker_roulette_spec.rb +250 -0
- data/spec/integration/worker_roulette_spec.rb +172 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/unit/evented_readlock_spec.rb +108 -0
- data/spec/unit/lua_spec.rb +47 -0
- data/spec/unit/readlock_spec.rb +71 -0
- data/worker_roulette.gemspec +34 -0
- metadata +258 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'worker_roulette'
|
2
|
+
require 'benchmark'
|
3
|
+
require 'eventmachine'
|
4
|
+
|
5
|
+
REDIS_CONNECTION_POOL_SIZE = 100
|
6
|
+
ITERATIONS = 10_000
|
7
|
+
|
8
|
+
work_order = {'ding dong' => "hello_foreman_" * 100}
|
9
|
+
|
10
|
+
EM::Hiredis.reconnect_timeout = 0.01
|
11
|
+
|
12
|
+
puts "Redis Connection Pool Size: #{REDIS_CONNECTION_POOL_SIZE}"
|
13
|
+
|
14
|
+
times = Benchmark.bm do |x|
|
15
|
+
x.report "#{ITERATIONS} ASync Api Read/Writes" do
|
16
|
+
EM.run do
|
17
|
+
WorkerRoulette.start(evented: true)
|
18
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
19
|
+
@total = 0
|
20
|
+
@tradesman = WorkerRoulette.tradesman
|
21
|
+
|
22
|
+
ITERATIONS.times do |iteration|
|
23
|
+
sender = 'sender_' + iteration.to_s
|
24
|
+
foreman = WorkerRoulette.foreman(sender)
|
25
|
+
foreman.enqueue_work_order(work_order) do
|
26
|
+
@tradesman.work_orders! do
|
27
|
+
@total += 1
|
28
|
+
EM.stop if @total == (ITERATIONS - 1)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
puts "#{(ITERATIONS / times.first.real).to_i} ASync Api Read/Writes per second"
|
36
|
+
puts "#################"
|
37
|
+
puts
|
38
|
+
|
39
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
40
|
+
|
41
|
+
times = Benchmark.bm do |x|
|
42
|
+
x.report "#{ITERATIONS * 2} ASync Api Polling Read/Writes" do
|
43
|
+
EM.run do
|
44
|
+
@processed = 0
|
45
|
+
@total = 0
|
46
|
+
WorkerRoulette.start(evented: true)
|
47
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
48
|
+
@total = 0
|
49
|
+
@tradesman = WorkerRoulette.tradesman
|
50
|
+
ITERATIONS.times do |iteration|
|
51
|
+
@start ||= Time.now
|
52
|
+
sender = 'sender_' + iteration.to_s
|
53
|
+
foreman = WorkerRoulette.foreman(sender)
|
54
|
+
foreman.enqueue_work_order(work_order)
|
55
|
+
end
|
56
|
+
@tradesman.wait_for_work_orders {@processed += 1; ((@stop = Time.now) && EM.add_timer(1){EM.stop}) if @processed == (ITERATIONS - 1)}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
puts "#{ITERATIONS * 2 / (@stop - @start)} ASync Api Polling Read/Writes per second"
|
61
|
+
puts "#################"
|
62
|
+
puts
|
63
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
64
|
+
|
65
|
+
WorkerRoulette.start(size: REDIS_CONNECTION_POOL_SIZE, evented: false)
|
66
|
+
times = Benchmark.bm do |x|
|
67
|
+
puts x.class.name
|
68
|
+
x.report "#{ITERATIONS} Sync Api Writes" do
|
69
|
+
ITERATIONS.times do |iteration|
|
70
|
+
sender = 'sender_' + iteration.to_s
|
71
|
+
foreman = WorkerRoulette.foreman(sender)
|
72
|
+
foreman.enqueue_work_order(work_order)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
76
|
+
end
|
77
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
78
|
+
|
79
|
+
puts "#{(ITERATIONS / times.first.real).to_i} Sync Api Writes per second"
|
80
|
+
puts "#################"
|
81
|
+
puts
|
82
|
+
ITERATIONS.times do |iteration|
|
83
|
+
sender = 'sender_' + iteration.to_s
|
84
|
+
foreman = WorkerRoulette.foreman(sender)
|
85
|
+
foreman.enqueue_work_order(work_order)
|
86
|
+
end
|
87
|
+
|
88
|
+
times = Benchmark.bm do |x|
|
89
|
+
x.report "#{ITERATIONS} Sync Api Reads" do
|
90
|
+
ITERATIONS.times do |iteration|
|
91
|
+
sender = 'sender_' + iteration.to_s
|
92
|
+
tradesman = WorkerRoulette.tradesman
|
93
|
+
tradesman.work_orders!
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
puts "#{(ITERATIONS / times.first.real).to_i} Sync Api Reads per second"
|
98
|
+
puts "#################"
|
99
|
+
puts
|
100
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
101
|
+
|
102
|
+
times = Benchmark.bm do |x|
|
103
|
+
x.report "#{ITERATIONS} Sync Api Read/Writes" do
|
104
|
+
ITERATIONS.times do |iteration|
|
105
|
+
sender = 'sender_' + iteration.to_s
|
106
|
+
foreman = WorkerRoulette.foreman(sender)
|
107
|
+
foreman.enqueue_work_order(work_order)
|
108
|
+
end
|
109
|
+
|
110
|
+
ITERATIONS.times do |iteration|
|
111
|
+
sender = 'sender_' + iteration.to_s
|
112
|
+
tradesman = WorkerRoulette.tradesman
|
113
|
+
tradesman.work_orders!
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
puts "#{(ITERATIONS / times.first.real).to_i} Sync Api Read/Writes per second"
|
118
|
+
puts "#################"
|
119
|
+
puts
|
120
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
121
|
+
|
122
|
+
times = Benchmark.bm do |x|
|
123
|
+
x.report "#{ITERATIONS * 2} Sync Api Polling Read/Writes" do
|
124
|
+
WorkerRoulette.start(size: REDIS_CONNECTION_POOL_SIZE, evented: false)
|
125
|
+
ITERATIONS.times do |iteration|
|
126
|
+
sender = 'sender_' + iteration.to_s
|
127
|
+
foreman = WorkerRoulette.foreman(sender)
|
128
|
+
foreman.enqueue_work_order(work_order)
|
129
|
+
tradesman = WorkerRoulette.tradesman
|
130
|
+
tradesman.wait_for_work_orders {|m| m}
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
puts "#{(ITERATIONS * 2 / times.first.real).to_i} Sync Api Polling Read/Writes per second"
|
135
|
+
puts "#################"
|
136
|
+
puts
|
137
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
@@ -0,0 +1 @@
|
|
1
|
+
.gitkeep
|
@@ -0,0 +1,250 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
module WorkerRoulette
|
3
|
+
describe WorkerRoulette do
|
4
|
+
include EventedSpec::EMSpec
|
5
|
+
|
6
|
+
let(:sender) {'katie_80'}
|
7
|
+
let(:work_orders) {["hello", "foreman"]}
|
8
|
+
let(:default_headers) {Hash['headers' => {'sender' => sender}]}
|
9
|
+
let(:hello_work_order) {Hash['payload' => "hello"]}
|
10
|
+
let(:foreman_work_order) {Hash['payload' => "foreman"]}
|
11
|
+
let(:work_orders_with_headers) {default_headers.merge({'payload' => work_orders})}
|
12
|
+
let(:jsonized_work_orders_with_headers) {[WorkerRoulette.dump(work_orders_with_headers)]}
|
13
|
+
let(:worker_roulette) { WorkerRoulette.start(evented: true) }
|
14
|
+
let(:redis) {Redis.new(worker_roulette.redis_config)}
|
15
|
+
|
16
|
+
context "Evented Foreman" do
|
17
|
+
let(:subject) {worker_roulette.foreman(sender)}
|
18
|
+
|
19
|
+
it "enqueues work" do
|
20
|
+
called = false
|
21
|
+
foreman = worker_roulette.foreman('foreman')
|
22
|
+
foreman.enqueue_work_order('some old fashion work') do |redis_response, stuff|
|
23
|
+
called = true
|
24
|
+
end
|
25
|
+
done(0.1) { expect(called).to be_truthy }
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should enqueue_work_order two work_orders in the sender's slot in the job board" do
|
29
|
+
subject.enqueue_work_order(work_orders.first) do
|
30
|
+
subject.enqueue_work_order(work_orders.last) do
|
31
|
+
expected = work_orders.map { |m| WorkerRoulette.dump(default_headers.merge({'payload' => m})) }
|
32
|
+
expect(redis.lrange(sender, 0, -1)).to eq(expected)
|
33
|
+
done
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should enqueue_work_order an array of work_orders without headers in the sender's slot in the job board" do
|
39
|
+
subject.enqueue_work_order_without_headers(work_orders) do
|
40
|
+
expect(redis.lrange(sender, 0, -1)).to eq([WorkerRoulette.dump(work_orders)])
|
41
|
+
done
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should enqueue_work_order an array of work_orders with default headers in the sender's slot in the job board" do
|
46
|
+
subject.enqueue_work_order(work_orders) do
|
47
|
+
expect(redis.lrange(sender, 0, -1)).to eq(jsonized_work_orders_with_headers)
|
48
|
+
done
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should enqueue_work_order an array of work_orders with additional headers in the sender's slot in the job board" do
|
53
|
+
extra_headers = {'foo' => 'bars'}
|
54
|
+
subject.enqueue_work_order(work_orders, extra_headers) do
|
55
|
+
work_orders_with_headers['headers'].merge!(extra_headers)
|
56
|
+
expect(redis.lrange(sender, 0, -1)).to eq([WorkerRoulette.dump(work_orders_with_headers)])
|
57
|
+
done
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should post the sender's id to the job board with an order number" do
|
62
|
+
first_foreman = worker_roulette.foreman('first_foreman')
|
63
|
+
first_foreman.enqueue_work_order('foo') do
|
64
|
+
subject.enqueue_work_order(work_orders.first) do
|
65
|
+
subject.enqueue_work_order(work_orders.last) do
|
66
|
+
expect(redis.zrange(subject.job_board_key, 0, -1, with_scores: true)).to eq([["first_foreman", 1.0], ["katie_80", 2.0]])
|
67
|
+
done
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should generate a monotically increasing score for senders not on the job board, but not for senders already there" do
|
74
|
+
first_foreman = worker_roulette.foreman('first_foreman')
|
75
|
+
expect(redis.get(subject.counter_key)).to be_nil
|
76
|
+
first_foreman.enqueue_work_order(work_orders.first) do
|
77
|
+
expect(redis.get(subject.counter_key)).to eq("1")
|
78
|
+
first_foreman.enqueue_work_order(work_orders.last) do
|
79
|
+
expect(redis.get(subject.counter_key)).to eq("1")
|
80
|
+
subject.enqueue_work_order(work_orders.first) do
|
81
|
+
expect(redis.get(subject.counter_key)).to eq("2")
|
82
|
+
done
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "Evented Tradesman" do
|
90
|
+
let(:foreman) {worker_roulette.foreman(sender)}
|
91
|
+
let(:subject) {worker_roulette.tradesman(nil, 0.01) }
|
92
|
+
|
93
|
+
it "should be working on behalf of a sender" do
|
94
|
+
foreman.enqueue_work_order(work_orders) do
|
95
|
+
subject.work_orders! do |r|
|
96
|
+
expect(subject.last_sender).to eq(sender)
|
97
|
+
done
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should remove the lock from the last_sender's queue" do
|
103
|
+
most_recent_sender = 'most_recent_sender'
|
104
|
+
most_recent_foreman = worker_roulette.foreman(most_recent_sender)
|
105
|
+
other_foreman = worker_roulette.foreman('katie_80')
|
106
|
+
|
107
|
+
other_foreman.enqueue_work_order(work_orders) do
|
108
|
+
most_recent_foreman.enqueue_work_order(work_orders) do
|
109
|
+
expect(redis.keys("L*:*").length).to eq(0)
|
110
|
+
subject.work_orders! do
|
111
|
+
expect(redis.get("L*:katie_80")).to eq("1")
|
112
|
+
expect(redis.keys("L*:*").length).to eq(1)
|
113
|
+
subject.work_orders! do
|
114
|
+
expect(redis.keys("L*:*").length).to eq(1)
|
115
|
+
expect(redis.get("L*:most_recent_sender")).to eq("1")
|
116
|
+
subject.work_orders!
|
117
|
+
done(0.2) do
|
118
|
+
expect(redis.keys("L*:*").length).to eq(0)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should drain one set of work_orders from the sender's slot in the job board" do
|
127
|
+
foreman.enqueue_work_order(work_orders) do
|
128
|
+
subject.work_orders! do |r|
|
129
|
+
expect(r).to eq([work_orders_with_headers])
|
130
|
+
subject.work_orders! do |r| expect(r).to be_empty
|
131
|
+
subject.work_orders! {|r| expect(r).to be_empty; done} #does not throw an error if queue is alreay empty
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should take the oldest sender off the job board (FIFO)" do
|
138
|
+
foreman.enqueue_work_order(work_orders) do
|
139
|
+
oldest_sender = sender.to_s
|
140
|
+
most_recent_sender = 'most_recent_sender'
|
141
|
+
most_recent_foreman = worker_roulette.foreman(most_recent_sender)
|
142
|
+
most_recent_foreman.enqueue_work_order(work_orders) do
|
143
|
+
expect(redis.zrange(subject.job_board_key, 0, -1)).to eq([oldest_sender, most_recent_sender])
|
144
|
+
subject.work_orders! { expect(redis.zrange(subject.job_board_key, 0, -1)).to eq([most_recent_sender]); done }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should get the work_orders from the next queue when a new job is ready" do
|
150
|
+
#tradesman polls every so often, we care that it is called at least twice, but did not use
|
151
|
+
#the built in rspec syntax for that bc if the test ends while we're talking to redis, redis
|
152
|
+
#throws an Error. This way we ensure we call work_orders! at least twice and just stub the second
|
153
|
+
#call so as not to hurt redis' feelings.
|
154
|
+
|
155
|
+
expect(subject).to receive(:work_orders!).and_call_original
|
156
|
+
expect(subject).to receive(:work_orders!)
|
157
|
+
|
158
|
+
foreman.enqueue_work_order(work_orders) do
|
159
|
+
subject.wait_for_work_orders do |redis_work_orders|
|
160
|
+
expect(redis_work_orders).to eq([work_orders_with_headers])
|
161
|
+
expect(subject.last_sender).to match(/katie_80/)
|
162
|
+
done(0.1)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should publish and subscribe on custom channels" do
|
168
|
+
good_subscribed = false
|
169
|
+
bad_subscribed = false
|
170
|
+
|
171
|
+
tradesman = worker_roulette.tradesman('good_channel', 0.001)
|
172
|
+
evil_tradesman = worker_roulette.tradesman('bad_channel', 0.001)
|
173
|
+
|
174
|
+
good_foreman = worker_roulette.foreman('foreman', 'good_channel')
|
175
|
+
bad_foreman = worker_roulette.foreman('foreman', 'bad_channel')
|
176
|
+
|
177
|
+
#tradesman polls every so often, we care that it is called at least twice, but did not use
|
178
|
+
#the built in rspec syntax for that bc if the test ends while we're talking to redis, redis
|
179
|
+
#throws an Error. This way we ensure we call work_orders! at least twice and just stub the second
|
180
|
+
#call so as not to hurt redis' feelings.
|
181
|
+
expect(tradesman).to receive(:work_orders!).and_call_original
|
182
|
+
expect(tradesman).to receive(:work_orders!)
|
183
|
+
|
184
|
+
expect(evil_tradesman).to receive(:work_orders!).and_call_original
|
185
|
+
expect(evil_tradesman).to receive(:work_orders!)
|
186
|
+
|
187
|
+
good_foreman.enqueue_work_order('some old fashion work') do
|
188
|
+
bad_foreman.enqueue_work_order('evil biddings you should not carry out') do
|
189
|
+
|
190
|
+
tradesman.wait_for_work_orders do |good_work|
|
191
|
+
expect(good_work.to_s).to match("old fashion")
|
192
|
+
expect(good_work.to_s).not_to match("evil")
|
193
|
+
end
|
194
|
+
|
195
|
+
evil_tradesman.wait_for_work_orders do |bad_work|
|
196
|
+
expect(bad_work.to_s).not_to match("old fashion")
|
197
|
+
expect(bad_work.to_s).to match("evil")
|
198
|
+
end
|
199
|
+
done(0.1)
|
200
|
+
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should pull off work orders for more than one sender" do
|
206
|
+
tradesman = worker_roulette.tradesman('good_channel')
|
207
|
+
|
208
|
+
good_foreman = worker_roulette.foreman('good_foreman', 'good_channel')
|
209
|
+
lazy_foreman = worker_roulette.foreman('lazy_foreman', 'good_channel')
|
210
|
+
|
211
|
+
got_good = false
|
212
|
+
got_lazy = false
|
213
|
+
good_foreman.enqueue_work_order('do good work') do
|
214
|
+
tradesman.work_orders! do |r|
|
215
|
+
got_good = true
|
216
|
+
expect(r.first['payload']).to eq('do good work')
|
217
|
+
end
|
218
|
+
end
|
219
|
+
lazy_foreman.enqueue_work_order('just get it done') do
|
220
|
+
tradesman.work_orders! do |r|
|
221
|
+
got_lazy = true
|
222
|
+
expect(r.first['payload']).to eq('just get it done')
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
done(0.2) {expect(got_good && got_lazy).to eq(true)}
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
pending "should return a hash with a string in the payload if OJ cannot parse the json"
|
231
|
+
|
232
|
+
context "Failure" do
|
233
|
+
it "should not put the sender_id and work_orders back if processing fails bc new work_orders may have been processed while that process failed" do; done; end
|
234
|
+
end
|
235
|
+
|
236
|
+
context "Concurrent Access" do
|
237
|
+
it "should not leak connections"
|
238
|
+
|
239
|
+
it "should be fork() proof" do
|
240
|
+
@subject = worker_roulette.tradesman
|
241
|
+
@subject.work_orders! do
|
242
|
+
fork do
|
243
|
+
@subject.work_orders!
|
244
|
+
end
|
245
|
+
end
|
246
|
+
done(1)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
module WorkerRoulette
|
3
|
+
describe WorkerRoulette do
|
4
|
+
let(:sender) {'katie_80'}
|
5
|
+
let(:work_orders) {["hello", "foreman"]}
|
6
|
+
let(:default_headers) {Hash['headers' => {'sender' => sender}]}
|
7
|
+
let(:hello_work_order) {Hash['payload' => "hello"]}
|
8
|
+
let(:foreman_work_order) {Hash['payload' => "foreman"]}
|
9
|
+
let(:work_orders_with_headers) {default_headers.merge({'payload' => work_orders})}
|
10
|
+
let(:jsonized_work_orders_with_headers) {[WorkerRoulette.dump(work_orders_with_headers)]}
|
11
|
+
let(:worker_roulette) { WorkerRoulette.start }
|
12
|
+
|
13
|
+
let(:redis) {Redis.new(worker_roulette.redis_config)}
|
14
|
+
|
15
|
+
before do
|
16
|
+
redis.flushall
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should exist" do
|
20
|
+
expect(worker_roulette).to be_instance_of(WorkerRoulette)
|
21
|
+
end
|
22
|
+
|
23
|
+
context Foreman do
|
24
|
+
let(:subject) {worker_roulette.foreman(sender)}
|
25
|
+
|
26
|
+
it "should be working on behalf of a sender" do
|
27
|
+
expect(subject.sender).to eq(sender)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should enqueue_work_order two work_orders in the sender's work queue" do
|
31
|
+
subject.enqueue_work_order(work_orders.first) {}
|
32
|
+
subject.enqueue_work_order(work_orders.last) {}
|
33
|
+
expect(redis.lrange(sender, 0, -1)).to eq(work_orders.map {|m| WorkerRoulette.dump(default_headers.merge({'payload' => m})) })
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should enqueue_work_order an array of work_orders without headers in the sender's work queue" do
|
37
|
+
subject.enqueue_work_order_without_headers(work_orders)
|
38
|
+
expect(redis.lrange(sender, 0, -1)).to eq([WorkerRoulette.dump(work_orders)])
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should enqueue_work_order an array of work_orders with default headers in the sender's work queue" do
|
42
|
+
subject.enqueue_work_order(work_orders)
|
43
|
+
expect(redis.lrange(sender, 0, -1)).to eq(jsonized_work_orders_with_headers)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should enqueue_work_order an array of work_orders with additional headers in the sender's work queue" do
|
47
|
+
extra_headers = {'foo' => 'bars'}
|
48
|
+
subject.enqueue_work_order(work_orders, extra_headers)
|
49
|
+
work_orders_with_headers['headers'].merge!(extra_headers)
|
50
|
+
expect(redis.lrange(sender, 0, -1)).to eq([WorkerRoulette.dump(work_orders_with_headers)])
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should post the sender's id to the job board with an order number" do
|
54
|
+
subject.enqueue_work_order(work_orders.first)
|
55
|
+
worker_roulette.foreman('other_forman').enqueue_work_order(work_orders.last)
|
56
|
+
expect(redis.zrange(subject.job_board_key, 0, -1, with_scores: true)).to eq([[sender, 1.0], ["other_forman", 2.0]])
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should generate a monotically increasing score for senders not on the job board, but not for senders already there" do
|
60
|
+
other_forman = worker_roulette.foreman('other_forman')
|
61
|
+
expect(redis.get(subject.counter_key)).to be_nil
|
62
|
+
subject.enqueue_work_order(work_orders.first)
|
63
|
+
expect(redis.get(subject.counter_key)).to eq("1")
|
64
|
+
subject.enqueue_work_order(work_orders.last)
|
65
|
+
expect(redis.get(subject.counter_key)).to eq("1")
|
66
|
+
other_forman.enqueue_work_order(work_orders.last)
|
67
|
+
expect(redis.get(other_forman.counter_key)).to eq("2")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context Tradesman do
|
72
|
+
let(:foreman) { worker_roulette.foreman(sender) }
|
73
|
+
let(:tradesman) { worker_roulette.tradesman }
|
74
|
+
|
75
|
+
before do
|
76
|
+
foreman.enqueue_work_order(work_orders)
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'removing locks from queues' do
|
80
|
+
it "for the last_sender's queue" do
|
81
|
+
most_recent_sender = 'most_recent_sender'
|
82
|
+
most_recent_foreman = worker_roulette.foreman(most_recent_sender)
|
83
|
+
most_recent_foreman.enqueue_work_order(work_orders)
|
84
|
+
expect(redis.keys("L*:*").length).to eq(0)
|
85
|
+
tradesman.work_orders!
|
86
|
+
expect(redis.get("L*:katie_80")).to eq("1")
|
87
|
+
expect(redis.keys("L*:*").length).to eq(1)
|
88
|
+
tradesman.work_orders!
|
89
|
+
expect(redis.keys("L*:*").length).to eq(1)
|
90
|
+
expect(redis.get("L*:most_recent_sender")).to eq("1")
|
91
|
+
tradesman.work_orders!
|
92
|
+
expect(redis.keys("L*:*").length).to eq(0)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should have a last sender if it found messages" do
|
97
|
+
expect(tradesman.work_orders!.length).to eq(1)
|
98
|
+
expect(tradesman.last_sender).to eq(sender)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should not have a last sender if it found no messages" do
|
102
|
+
expect(tradesman.work_orders!.length).to eq(1)
|
103
|
+
expect(tradesman.work_orders!.length).to eq(0)
|
104
|
+
expect(tradesman.last_sender).to be_nil
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should drain one set of work_orders from the sender's work queue" do
|
108
|
+
expect(tradesman.work_orders!).to eq([work_orders_with_headers])
|
109
|
+
expect(tradesman.work_orders!).to be_empty
|
110
|
+
expect(tradesman.work_orders!).to be_empty #does not throw an error if queue is already empty
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should drain all the work_orders from the sender's work queue" do
|
114
|
+
foreman.enqueue_work_order(work_orders)
|
115
|
+
expect(tradesman.work_orders!).to eq([work_orders_with_headers, work_orders_with_headers])
|
116
|
+
expect(tradesman.work_orders!).to be_empty
|
117
|
+
expect(tradesman.work_orders!).to be_empty #does not throw an error if queue is already empty
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should take the oldest sender off the job board (FIFO)" do
|
121
|
+
oldest_sender = sender.to_s
|
122
|
+
most_recent_sender = 'most_recent_sender'
|
123
|
+
most_recent_foreman = worker_roulette.foreman(most_recent_sender)
|
124
|
+
most_recent_foreman.enqueue_work_order(work_orders)
|
125
|
+
expect(redis.zrange(tradesman.job_board_key, 0, -1)).to eq([oldest_sender, most_recent_sender])
|
126
|
+
tradesman.work_orders!
|
127
|
+
expect(redis.zrange(tradesman.job_board_key, 0, -1)).to eq([most_recent_sender])
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should get the work_orders from the next queue when a new job is ready, then poll for new work" do
|
131
|
+
tradesman.wait_for_work_orders do |redis_work_orders|
|
132
|
+
expect(redis_work_orders).to eq([work_orders_with_headers])
|
133
|
+
expect(tradesman.last_sender).to eq('katie_80')
|
134
|
+
allow(tradesman).to receive(:wait_for_work_orders)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should publish and subscribe on custom channels" do
|
139
|
+
tradesman = worker_roulette.tradesman('good_channel')
|
140
|
+
expect(tradesman).to receive(:work_orders!).and_call_original
|
141
|
+
|
142
|
+
good_foreman = worker_roulette.foreman('foreman', 'good_channel')
|
143
|
+
bad_foreman = worker_roulette.foreman('foreman', 'bad_channel')
|
144
|
+
|
145
|
+
good_foreman.enqueue_work_order('some old fashion work')
|
146
|
+
bad_foreman.enqueue_work_order('evil biddings you should not carry out')
|
147
|
+
|
148
|
+
tradesman.wait_for_work_orders do |work|
|
149
|
+
expect(work.to_s).to match("some old fashion work")
|
150
|
+
expect(work.to_s).not_to match("evil")
|
151
|
+
expect(tradesman.last_sender).to eq('foreman')
|
152
|
+
allow(tradesman).to receive(:wait_for_work_orders)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "Failure" do
|
157
|
+
it "should not put the sender_id and work_orders back if processing fails bc new work_orders may have been processed while that process failed" do; end
|
158
|
+
end
|
159
|
+
|
160
|
+
context "Concurrent Access" do
|
161
|
+
it "should pool its connections" do
|
162
|
+
Array.new(100) do
|
163
|
+
Thread.new {worker_roulette.tradesman_connection_pool.with {|pooled_redis| pooled_redis.get("foo")}}
|
164
|
+
end.each(&:join)
|
165
|
+
worker_roulette.tradesman_connection_pool.with do |pooled_redis|
|
166
|
+
expect(pooled_redis.info["connected_clients"].to_i).to be > (worker_roulette.pool_size)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module WorkerRoulette
|
2
|
+
require 'worker_roulette'
|
3
|
+
require 'evented-spec'
|
4
|
+
require 'rspec'
|
5
|
+
require 'pry'
|
6
|
+
|
7
|
+
require File.expand_path(File.join("..", "..", "lib", "worker_roulette.rb"), __FILE__)
|
8
|
+
|
9
|
+
Dir[File.join(File.dirname(__FILE__), 'helpers', '**/*.rb')].sort.each { |file| require file.gsub(".rb", "")}
|
10
|
+
|
11
|
+
EM::Hiredis.reconnect_timeout = 0.01
|
12
|
+
|
13
|
+
RSpec.configure do |c|
|
14
|
+
c.after(:each) do
|
15
|
+
Redis.new(WorkerRoulette.start.redis_config).flushdb
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module WorkerRoulette
|
3
|
+
describe "Evented Read Lock" do
|
4
|
+
include EventedSpec::EMSpec
|
5
|
+
|
6
|
+
let(:redis) {Redis.new(WorkerRoulette.start.redis_config)}
|
7
|
+
let(:sender) {'katie_80'}
|
8
|
+
let(:work_orders) {"hellot"}
|
9
|
+
let(:lock_key) {"L*:#{sender}"}
|
10
|
+
let(:default_headers) {Hash['headers' => {'sender' => sender}]}
|
11
|
+
let(:work_orders_with_headers) {default_headers.merge({'payload' => work_orders})}
|
12
|
+
let(:jsonized_work_orders_with_headers) {[WorkerRoulette.dump(work_orders_with_headers)]}
|
13
|
+
let(:worker_roulette) { WorkerRoulette.start(evented: true) }
|
14
|
+
let(:foreman) {worker_roulette.foreman(sender)}
|
15
|
+
let(:number_two) {worker_roulette.foreman('number_two')}
|
16
|
+
let(:subject) {worker_roulette.tradesman}
|
17
|
+
let(:subject_two) {worker_roulette.tradesman}
|
18
|
+
let(:lua) { Lua.new(worker_roulette.tradesman_connection_pool) }
|
19
|
+
|
20
|
+
em_before do
|
21
|
+
lua.clear_cache!
|
22
|
+
redis.script(:flush)
|
23
|
+
redis.flushdb
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should lock a queue when it reads from it" do
|
27
|
+
evented_readlock_preconditions do
|
28
|
+
expect(redis.get(lock_key)).not_to be_nil
|
29
|
+
done
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should set the lock to expire in 3 seconds" do
|
34
|
+
evented_readlock_preconditions do
|
35
|
+
expect(redis.ttl(lock_key)).to eq(3)
|
36
|
+
done
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should not read a locked queue" do
|
41
|
+
evented_readlock_preconditions do
|
42
|
+
foreman.enqueue_work_order(work_orders) do #locked
|
43
|
+
subject_two.work_orders! { |work| expect(work).to be_empty; done}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should read from the first available queue that is not locked" do
|
49
|
+
evented_readlock_preconditions do
|
50
|
+
foreman.enqueue_work_order(work_orders) do #locked
|
51
|
+
number_two.enqueue_work_order(work_orders) do #unlocked
|
52
|
+
subject_two.work_orders!{|work| expect(work.first['headers']['sender']).to eq('number_two'); done}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should release its last lock when it asks for its next work order from another sender" do
|
59
|
+
evented_readlock_preconditions do
|
60
|
+
number_two.enqueue_work_order(work_orders) do #unlocked
|
61
|
+
expect(subject.last_sender).to eq(sender)
|
62
|
+
subject.work_orders! do |work|
|
63
|
+
expect(work.first['headers']['sender']).to eq('number_two')
|
64
|
+
expect(redis.get(lock_key)).to be_nil
|
65
|
+
done
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should not release its lock when it asks for its next work order from the same sender" do
|
72
|
+
evented_readlock_preconditions do
|
73
|
+
foreman.enqueue_work_order(work_orders) do #locked
|
74
|
+
subject.work_orders! do |work|
|
75
|
+
expect(work).to eq([work_orders_with_headers])
|
76
|
+
expect(subject.last_sender).to eq(sender)
|
77
|
+
expect(redis.get(lock_key)).not_to be_nil
|
78
|
+
done
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should not take out another lock if there is no work to do" do
|
85
|
+
evented_readlock_preconditions do
|
86
|
+
foreman.enqueue_work_order(work_orders) do #locked
|
87
|
+
subject.work_orders! do |work_order|
|
88
|
+
expect(work_order).to eq([work_orders_with_headers])
|
89
|
+
subject.work_orders! do |work|
|
90
|
+
expect(work).to be_empty
|
91
|
+
expect(redis.get(lock_key)).to be_nil
|
92
|
+
done
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def evented_readlock_preconditions(&spec_block)
|
100
|
+
foreman.enqueue_work_order(work_orders) do
|
101
|
+
subject.work_orders! do |work|
|
102
|
+
expect(work).to eq([work_orders_with_headers])
|
103
|
+
spec_block.call
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|