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.
@@ -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