worker_roulette 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- #Caveat Emptor: There is a race condition here, but it not serious;
6
- #the count may be incremented again by another process before the sender
7
- #is added to the job_queue. This is not a big deal bc it just means that
8
- #the sender's queue will be processed one slot behind it's rightful place.
9
- #This does not effect work_order ordering.
10
- @redis_pool.with do |redis|
11
- redis.incr(COUNTER_KEY) do |count|
12
- @count = count ||= 1
13
- redis.multi
14
- redis.zadd(job_board_key, @count, @sender)
15
- redis.rpush(sender_key, Oj.dump(work_order))
16
- redis.publish(@channel, WorkerRoulette::JOB_NOTIFICATIONS)
17
- redis.exec.callback &callback
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
- end
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
- @client_pool.with do |redis|
13
- get_sender_for_next_job(redis) do |sender_results|
14
- @sender = (sender_results || []).first.to_s
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
- private
48
- def get_sender_for_next_job(redis, &callback)
49
- redis.zrange(job_board_key, 0, 0).callback &callback
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
@@ -1,3 +1,3 @@
1
1
  module WorkerRoulette
2
- VERSION = "0.0.10"
2
+ VERSION = "0.0.11"
3
3
  end
@@ -4,6 +4,7 @@ require 'redis'
4
4
  require 'hiredis'
5
5
  require 'em-hiredis'
6
6
  require 'connection_pool'
7
+ require "digest/sha1"
7
8
 
8
9
  Dir[File.join(File.dirname(__FILE__),'worker_roulette','**','*.rb')].sort.each { |file| require file.gsub(".rb", "")}
9
10
 
@@ -3,48 +3,94 @@ require 'benchmark'
3
3
  require 'eventmachine'
4
4
 
5
5
  REDIS_CONNECTION_POOL_SIZE = 100
6
- ITERATIONS = 10_000
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
- ITERATIONS.times do |iteration|
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
- ITERATIONS.times do |iteration|
24
- sender = 'sender_' + iteration.to_s
25
- tradesman = WorkerRoulette.tradesman
26
- tradesman.work_orders!
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 for tradesmans to enqueue_work_order and read #{ITERATIONS} large work_orders via pubsub" do # ~1800 work_orders / second round trip
35
- ITERATIONS.times do |iteration|
36
- p = -> do
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.foreman(sender)
68
+ foreman = WorkerRoulette.a_foreman(sender)
39
69
  foreman.enqueue_work_order(work_order)
40
70
  end
41
- tradesman = WorkerRoulette.tradesman
42
- tradesman.wait_for_work_orders(p) {|m| m; tradesman.unsubscribe}
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 == [1, 1, 0]
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
- subject.enqueue_work_order(work_orders.first) do
67
- subject.enqueue_work_order(work_orders.last) do
68
- redis.zrange(subject.job_board_key, 0, -1, with_scores: true).should == [[sender.to_s, work_orders.length.to_f]]
69
- done
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 post the sender_id and work_orders transactionally" do
75
- EM::Hiredis::Client.any_instance.should_receive(:multi).and_call_original
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
- subject.enqueue_work_order(work_orders.first) do
80
+ first_foreman.enqueue_work_order(work_orders.first) do
84
81
  redis.get(subject.counter_key).should == "1"
85
- subject.enqueue_work_order(work_orders.last) do
86
- redis.get(subject.counter_key).should == "2"
87
- done
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 = ->(a,b) {good_subscribed = true}
168
- bad_publish = ->(a,b) {bad_subscribed = true}
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
- subject.wait_for_work_orders do |redis_work_orders, message, channel|
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 patch"
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
@@ -3,6 +3,8 @@ require 'evented-spec'
3
3
  require 'simplecov'
4
4
  require 'simplecov-rcov'
5
5
  require 'rspec'
6
+ require 'pry'
7
+
6
8
  class SimpleCov::Formatter::MergedFormatter
7
9
  def format(result)
8
10
  SimpleCov::Formatter::HTMLFormatter.new.format(result)
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.10
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-17 00:00:00.000000000 Z
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