worker_roulette 0.0.10 → 0.0.11
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.
- data/lib/worker_roulette/a_foreman.rb +32 -14
- data/lib/worker_roulette/a_tradesman.rb +27 -21
- data/lib/worker_roulette/lua.rb +31 -0
- data/lib/worker_roulette/version.rb +1 -1
- data/lib/worker_roulette.rb +1 -0
- data/spec/benchmark/perf_test.rb +70 -24
- data/spec/integration/evented_worker_roulette_spec.rb +65 -35
- data/spec/spec_helper.rb +2 -0
- metadata +3 -2
@@ -2,21 +2,39 @@ require_relative './foreman'
|
|
2
2
|
module WorkerRoulette
|
3
3
|
class AForeman < Foreman
|
4
4
|
def enqueue_work_order_without_headers(work_order, &callback)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
5
|
+
Lua.call(self.class.lua_enqueue_work_orders, [COUNTER_KEY, job_board_key, sender_key, @channel],
|
6
|
+
[@sender, Oj.dump(work_order), WorkerRoulette::JOB_NOTIFICATIONS], &callback)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def self.lua_enqueue_work_orders
|
11
|
+
<<-HERE
|
12
|
+
local counter_key = KEYS[1]
|
13
|
+
local job_board_key = KEYS[2]
|
14
|
+
local sender_key = KEYS[3]
|
15
|
+
local channel = KEYS[4]
|
16
|
+
|
17
|
+
local sender = ARGV[1]
|
18
|
+
local work_order = ARGV[2]
|
19
|
+
local job_notification = ARGV[3]
|
20
|
+
|
21
|
+
local function enqueue_work_orders(sender, work_order, job_notification)
|
22
|
+
local result = sender .. ' updated'
|
23
|
+
local sender_on_job_board = redis.call('ZSCORE', job_board_key, sender_key)
|
24
|
+
|
25
|
+
if (sender_on_job_board == false) then
|
26
|
+
local count = redis.call('INCR', counter_key)
|
27
|
+
local job_added = redis.call('ZADD',job_board_key, count, sender_key)
|
28
|
+
result = sender .. ' added'
|
29
|
+
end
|
30
|
+
|
31
|
+
local work_added = redis.call('RPUSH',sender_key, work_order)
|
32
|
+
local job_board_update = redis.call('PUBLISH', channel, job_notification)
|
33
|
+
return result
|
18
34
|
end
|
19
|
-
|
35
|
+
|
36
|
+
return enqueue_work_orders(sender, work_order, job_notification)
|
37
|
+
HERE
|
20
38
|
end
|
21
39
|
end
|
22
40
|
end
|
@@ -9,27 +9,12 @@ module WorkerRoulette
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def work_orders!(&callback)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
redis.multi
|
16
|
-
redis.lrange(sender_key, 0, -1)
|
17
|
-
redis.del(sender_key)
|
18
|
-
redis.zrem(job_board_key, sender_key)
|
19
|
-
redis.exec do |work_orders|
|
20
|
-
callback.call ((work_orders || []).first || []).map {|work_order| Oj.load(work_order)} if callback
|
21
|
-
end
|
22
|
-
end
|
12
|
+
Lua.call(self.class.lua_drain_work_orders, [job_board_key, nil], [@namespace]) do |results|
|
13
|
+
@sender = (results.first || '').split(':').first
|
14
|
+
callback.call (results[1] || []).map {|work_order| Oj.load(work_order)} if callback
|
23
15
|
end
|
24
16
|
end
|
25
17
|
|
26
|
-
def get_lock(redis, sender, timeout, on_failure = nil, &on_success)
|
27
|
-
@lock = EM::Hiredis::Lock.new(redis, sender, timeout)
|
28
|
-
@lock.callback &on_success
|
29
|
-
@lock.errback &(on_failure || proc {})
|
30
|
-
@lock
|
31
|
-
end
|
32
|
-
|
33
18
|
def unsubscribe(&callback)
|
34
19
|
deferable = @redis_pubsub.unsubscribe(@channel)
|
35
20
|
deferable.callback do
|
@@ -44,9 +29,30 @@ module WorkerRoulette
|
|
44
29
|
end
|
45
30
|
end
|
46
31
|
|
47
|
-
|
48
|
-
def
|
49
|
-
|
32
|
+
private
|
33
|
+
def self.lua_drain_work_orders
|
34
|
+
<<-HERE
|
35
|
+
local job_board_key = KEYS[1]
|
36
|
+
local empty = KEYS[2]
|
37
|
+
local namespace = ARGV[1]
|
38
|
+
|
39
|
+
local function drain_work_orders(job_board_key, namespace)
|
40
|
+
local sender_key = redis.call('ZRANGE', job_board_key, 0, 0)[1]
|
41
|
+
|
42
|
+
if sender_key == false then
|
43
|
+
return {}
|
44
|
+
end
|
45
|
+
|
46
|
+
local results = {}
|
47
|
+
results[1] = sender_key
|
48
|
+
results[2] = redis.call('LRANGE', sender_key, 0, -1)
|
49
|
+
results[3] = redis.call('DEL', sender_key)
|
50
|
+
results[4] = redis.call('ZREM', job_board_key, sender_key)
|
51
|
+
return results
|
52
|
+
end
|
53
|
+
|
54
|
+
return drain_work_orders(job_board_key, namespace)
|
55
|
+
HERE
|
50
56
|
end
|
51
57
|
end
|
52
58
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module WorkerRoulette
|
2
|
+
module Lua
|
3
|
+
@cache = Hash.new
|
4
|
+
|
5
|
+
def self.call(lua_script, keys_accessed = [], args = [], &callback)
|
6
|
+
WorkerRoulette.tradesman_connection_pool.with do |redis|
|
7
|
+
results = redis.evalsha(sha(lua_script), keys_accessed.length, *keys_accessed, *args)
|
8
|
+
results.callback &callback
|
9
|
+
results.errback {self.eval(redis, lua_script, keys_accessed, args, &callback)}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.sha(lua_script)
|
14
|
+
@cache[lua_script] ||= Digest::SHA1.hexdigest(lua_script)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.cache
|
18
|
+
@cache.dup
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.clear_cache!
|
22
|
+
@cache = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.eval(redis, lua_script, keys_accessed, args, &callback)
|
26
|
+
results = redis.eval(lua_script, keys_accessed.size, *keys_accessed, *args)
|
27
|
+
results.callback &callback
|
28
|
+
results.errback {|err_msg| raise EM::Hiredis::RedisError.new(err_msg)}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/worker_roulette.rb
CHANGED
data/spec/benchmark/perf_test.rb
CHANGED
@@ -3,48 +3,94 @@ require 'benchmark'
|
|
3
3
|
require 'eventmachine'
|
4
4
|
|
5
5
|
REDIS_CONNECTION_POOL_SIZE = 100
|
6
|
-
ITERATIONS =
|
6
|
+
ITERATIONS = 100_000
|
7
7
|
|
8
8
|
work_order = {'ding dong' => "hello_foreman_" * 100}
|
9
9
|
|
10
|
-
WorkerRoulette.start(size: REDIS_CONNECTION_POOL_SIZE)#{driver: :synchrony}
|
11
|
-
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
10
|
+
# WorkerRoulette.start(size: REDIS_CONNECTION_POOL_SIZE, evented: true)#{driver: :synchrony}
|
11
|
+
# WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
12
12
|
|
13
|
-
puts "Redis Connection Pool Size: #{REDIS_CONNECTION_POOL_SIZE}"
|
13
|
+
# puts "Redis Connection Pool Size: #{REDIS_CONNECTION_POOL_SIZE}"
|
14
|
+
|
15
|
+
# Benchmark.bmbm do |x|
|
16
|
+
# x.report "Time to insert and read #{ITERATIONS} large work_orders" do # ~2500 work_orders / second round trip; 50-50 read-write time; CPU and IO bound
|
17
|
+
# ITERATIONS.times do |iteration|
|
18
|
+
# sender = 'sender_' + iteration.to_s
|
19
|
+
# foreman = WorkerRoulette.foreman(sender)
|
20
|
+
# foreman.enqueue_work_order(work_order)
|
21
|
+
# end
|
22
|
+
|
23
|
+
# ITERATIONS.times do |iteration|
|
24
|
+
# sender = 'sender_' + iteration.to_s
|
25
|
+
# tradesman = WorkerRoulette.tradesman
|
26
|
+
# tradesman.work_orders!
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
|
31
|
+
EM::Hiredis.reconnect_timeout = 0.01
|
14
32
|
|
15
33
|
Benchmark.bmbm do |x|
|
16
|
-
x.report "Time to insert and read #{ITERATIONS} large work_orders" do # ~2500 work_orders / second round trip; 50-50 read-write time; CPU and IO bound
|
17
|
-
|
18
|
-
sender = 'sender_' + iteration.to_s
|
19
|
-
foreman = WorkerRoulette.foreman(sender)
|
20
|
-
foreman.enqueue_work_order(work_order)
|
21
|
-
end
|
34
|
+
x.report "Time to evently insert and read #{ITERATIONS} large work_orders" do # ~2500 work_orders / second round trip; 50-50 read-write time; CPU and IO bound
|
35
|
+
EM.run do
|
22
36
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
tradesman.
|
37
|
+
WorkerRoulette.start(evented: true)#{driver: :synchrony}
|
38
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
39
|
+
@total = 0
|
40
|
+
@tradesman = WorkerRoulette.a_tradesman
|
41
|
+
|
42
|
+
ITERATIONS.times do |iteration|
|
43
|
+
sender = 'sender_' + iteration.to_s
|
44
|
+
foreman = WorkerRoulette.a_foreman(sender)
|
45
|
+
foreman.enqueue_work_order(work_order) do
|
46
|
+
@tradesman.work_orders! do
|
47
|
+
@total += 1
|
48
|
+
EM.stop if @total == (ITERATIONS - 1)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
27
52
|
end
|
28
53
|
end
|
29
54
|
end
|
30
55
|
|
31
|
-
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
32
|
-
|
33
56
|
Benchmark.bmbm do |x|
|
34
|
-
x.report "Time
|
35
|
-
|
36
|
-
|
57
|
+
x.report "Time to evently pubsub insert and read #{ITERATIONS} large work_orders" do # ~2500 work_orders / second round trip; 50-50 read-write time; CPU and IO bound
|
58
|
+
EM.run do
|
59
|
+
@processed = 0
|
60
|
+
@total = 0
|
61
|
+
WorkerRoulette.start(evented: true)#{driver: :synchrony}
|
62
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
63
|
+
@total = 0
|
64
|
+
@tradesman = WorkerRoulette.a_tradesman
|
65
|
+
on_subscribe = ->(*args) do
|
66
|
+
ITERATIONS.times do |iteration|
|
37
67
|
sender = 'sender_' + iteration.to_s
|
38
|
-
foreman = WorkerRoulette.
|
68
|
+
foreman = WorkerRoulette.a_foreman(sender)
|
39
69
|
foreman.enqueue_work_order(work_order)
|
40
70
|
end
|
41
|
-
|
42
|
-
tradesman.wait_for_work_orders(
|
71
|
+
end
|
72
|
+
@tradesman.wait_for_work_orders(on_subscribe) {@processed += 1; EM.stop if @processed == (ITERATIONS - 1)}
|
43
73
|
end
|
44
74
|
end
|
45
75
|
end
|
46
76
|
|
47
|
-
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
77
|
+
# WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
78
|
+
|
79
|
+
# Benchmark.bmbm do |x|
|
80
|
+
# x.report "Time for tradesmans to enqueue_work_order and read #{ITERATIONS} large work_orders via pubsub" do # ~1800 work_orders / second round trip
|
81
|
+
# ITERATIONS.times do |iteration|
|
82
|
+
# p = -> do
|
83
|
+
# sender = 'sender_' + iteration.to_s
|
84
|
+
# foreman = WorkerRoulette.foreman(sender)
|
85
|
+
# foreman.enqueue_work_order(work_order)
|
86
|
+
# end
|
87
|
+
# tradesman = WorkerRoulette.tradesman
|
88
|
+
# tradesman.wait_for_work_orders(p) {|m| m; tradesman.unsubscribe}
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
|
93
|
+
# WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
48
94
|
|
49
95
|
# EM.run do
|
50
96
|
# EM.add_timer(6) {puts "em off";EM.stop}
|
@@ -63,4 +109,4 @@ WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
|
63
109
|
# end
|
64
110
|
|
65
111
|
# puts @end - @start
|
66
|
-
# WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
112
|
+
# WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
@@ -23,9 +23,9 @@ describe WorkerRoulette do
|
|
23
23
|
it "should enqueue work" do
|
24
24
|
called = false
|
25
25
|
foreman = WorkerRoulette.a_foreman('foreman')
|
26
|
-
foreman.enqueue_work_order('some old fashion work') do |redis_response|
|
26
|
+
foreman.enqueue_work_order('some old fashion work') do |redis_response, stuff|
|
27
27
|
called = true
|
28
|
-
redis_response.should ==
|
28
|
+
redis_response.should == 'foreman added'
|
29
29
|
end
|
30
30
|
done(0.1) {called.should == true}
|
31
31
|
end
|
@@ -63,28 +63,28 @@ describe WorkerRoulette do
|
|
63
63
|
end
|
64
64
|
|
65
65
|
it "should post the sender's id to the job board with an order number" do
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
66
|
+
first_foreman = WorkerRoulette.a_foreman('first_foreman')
|
67
|
+
first_foreman.enqueue_work_order('foo') do
|
68
|
+
subject.enqueue_work_order(work_orders.first) do
|
69
|
+
subject.enqueue_work_order(work_orders.last) do
|
70
|
+
redis.zrange(subject.job_board_key, 0, -1, with_scores: true).should == [["first_foreman", 1.0], ["katie_80", 2.0]]
|
71
|
+
done
|
72
|
+
end
|
70
73
|
end
|
71
74
|
end
|
72
75
|
end
|
73
76
|
|
74
|
-
it "should
|
75
|
-
|
76
|
-
subject.enqueue_work_order(work_orders.first) do
|
77
|
-
done
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
it "should generate sequential order numbers" do
|
77
|
+
it "should generate a monotically increasing score for senders not on the job board, but not for senders already there" do
|
78
|
+
first_foreman = WorkerRoulette.a_foreman('first_foreman')
|
82
79
|
redis.get(subject.counter_key).should == nil
|
83
|
-
|
80
|
+
first_foreman.enqueue_work_order(work_orders.first) do
|
84
81
|
redis.get(subject.counter_key).should == "1"
|
85
|
-
|
86
|
-
redis.get(subject.counter_key).should == "
|
87
|
-
|
82
|
+
first_foreman.enqueue_work_order(work_orders.last) do
|
83
|
+
redis.get(subject.counter_key).should == "1"
|
84
|
+
subject.enqueue_work_order(work_orders.first) do
|
85
|
+
redis.get(subject.counter_key).should == "2"
|
86
|
+
done
|
87
|
+
end
|
88
88
|
end
|
89
89
|
end
|
90
90
|
end
|
@@ -100,13 +100,52 @@ describe WorkerRoulette do
|
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
103
|
+
context Lua do
|
104
|
+
before do
|
105
|
+
Lua.clear_cache!
|
106
|
+
redis.script(:flush)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should load and call a lua script" do
|
110
|
+
lua_script = 'return redis.call("SET", KEYS[1], ARGV[1])'
|
111
|
+
Lua.call(lua_script, ['foo'], ['daddy']) do |result|
|
112
|
+
Lua.cache.keys.first.should == lua_script
|
113
|
+
Lua.cache.values.first.should == Digest::SHA1.hexdigest(lua_script)
|
114
|
+
result.should == "OK"
|
115
|
+
done
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should send a sha instead of a script once the script has been cached" do
|
120
|
+
lua_script = 'return KEYS'
|
121
|
+
Lua.should_receive(:eval).and_call_original
|
122
|
+
|
123
|
+
Lua.call(lua_script) do |result|
|
124
|
+
|
125
|
+
Lua.should_not_receive(:eval)
|
126
|
+
Lua.call(lua_script) do |result|
|
127
|
+
result.should == []
|
128
|
+
done
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should raise an error to the caller if the script fails in redis" do
|
134
|
+
lua_script = 'this is junk'
|
135
|
+
# Lua.call(lua_script)
|
136
|
+
# rspec cannot test this bc of the callbacks, but if you have doubts,
|
137
|
+
# uncomment the line above and watch it fail
|
138
|
+
done
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
103
142
|
context Tradesman do
|
104
143
|
let(:foreman) {WorkerRoulette.a_foreman(sender)}
|
105
144
|
let(:subject) {WorkerRoulette.a_tradesman}
|
106
145
|
|
107
146
|
it "should be working on behalf of a sender" do
|
108
147
|
foreman.enqueue_work_order(work_orders) do
|
109
|
-
subject.work_orders! do
|
148
|
+
subject.work_orders! do |r|
|
110
149
|
subject.sender.should == sender
|
111
150
|
done
|
112
151
|
end
|
@@ -137,21 +176,16 @@ describe WorkerRoulette do
|
|
137
176
|
end
|
138
177
|
end
|
139
178
|
|
140
|
-
it "should get the sender and work_order list transactionally" do
|
141
|
-
EM::Hiredis::Client.any_instance.should_receive(:multi).and_call_original
|
142
|
-
subject.work_orders! {done}
|
143
|
-
end
|
144
|
-
|
145
179
|
it "should get the work_orders from the next queue when a new job is ready" do
|
146
180
|
subject.should_receive(:work_orders!).and_call_original
|
181
|
+
publish = proc {foreman.enqueue_work_order(work_orders)}
|
147
182
|
|
148
|
-
subject.wait_for_work_orders do |redis_work_orders, message, channel|
|
183
|
+
subject.wait_for_work_orders(publish) do |redis_work_orders, message, channel|
|
149
184
|
subject.sender.should == "katie_80"
|
150
185
|
redis_work_orders.should == [work_orders_with_headers]
|
151
186
|
done
|
152
187
|
end
|
153
188
|
|
154
|
-
foreman.enqueue_work_order(work_orders)
|
155
189
|
end
|
156
190
|
|
157
191
|
it "should publish and subscribe on custom channels" do
|
@@ -164,8 +198,8 @@ describe WorkerRoulette do
|
|
164
198
|
good_foreman = WorkerRoulette.a_foreman('foreman', 'good_channel')
|
165
199
|
bad_foreman = WorkerRoulette.a_foreman('foreman', 'bad_channel')
|
166
200
|
|
167
|
-
good_publish =
|
168
|
-
bad_publish =
|
201
|
+
good_publish = proc {good_foreman.enqueue_work_order('some old fashion work')}
|
202
|
+
bad_publish = proc {bad_foreman.enqueue_work_order('evil biddings you should not carry out')}
|
169
203
|
|
170
204
|
tradesman.should_receive(:work_orders!).and_call_original
|
171
205
|
evil_tradesman.should_receive(:work_orders!).and_call_original
|
@@ -175,32 +209,28 @@ describe WorkerRoulette do
|
|
175
209
|
tradesman.wait_for_work_orders(good_publish) do |good_work|
|
176
210
|
good_work.to_s.should match("old fashion")
|
177
211
|
good_work.to_s.should_not match("evil")
|
178
|
-
good_subscribed.should == true
|
179
212
|
end
|
180
213
|
|
181
214
|
evil_tradesman.wait_for_work_orders(bad_publish) do |bad_work|
|
182
215
|
bad_work.to_s.should_not match("old fashion")
|
183
216
|
bad_work.to_s.should match("evil")
|
184
|
-
bad_subscribed.should == true
|
185
217
|
end
|
186
218
|
|
187
|
-
good_foreman.enqueue_work_order('some old fashion work')
|
188
|
-
bad_foreman.enqueue_work_order('evil biddings you should not carry out')
|
189
219
|
done(0.2)
|
190
220
|
end
|
191
221
|
|
192
222
|
it "should unsubscribe from the job board" do
|
193
|
-
|
223
|
+
publish = proc {foreman.enqueue_work_order(work_orders)}
|
224
|
+
subject.wait_for_work_orders(publish) do |redis_work_orders, message, channel|
|
194
225
|
subject.unsubscribe {done}
|
195
226
|
end
|
196
227
|
EM::Hiredis::PubsubClient.any_instance.should_receive(:close_connection).and_call_original
|
197
|
-
foreman.enqueue_work_order(work_orders)
|
198
228
|
end
|
199
229
|
|
200
230
|
it "should periodically (random time between 10 and 15 seconds?) poll the job board for new work, in case it missed a notification"
|
201
231
|
it "should not delete the messages from the queue until they have been processed succcesfully"
|
202
232
|
it "should not cache the sender our counter keys"
|
203
|
-
it "should pull off more than one
|
233
|
+
it "should pull off work orders for more than one sender"
|
204
234
|
end
|
205
235
|
|
206
236
|
context "Read Lock" do
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: worker_roulette
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.11
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-02-
|
12
|
+
date: 2014-02-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: oj
|
@@ -253,6 +253,7 @@ files:
|
|
253
253
|
- lib/worker_roulette/a_foreman.rb
|
254
254
|
- lib/worker_roulette/a_tradesman.rb
|
255
255
|
- lib/worker_roulette/foreman.rb
|
256
|
+
- lib/worker_roulette/lua.rb
|
256
257
|
- lib/worker_roulette/tradesman.rb
|
257
258
|
- lib/worker_roulette/version.rb
|
258
259
|
- spec/benchmark/perf_test.rb
|