worker_roulette 0.1.7 → 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/README.md +31 -88
- data/lib/worker_roulette/foreman.rb +13 -14
- data/lib/worker_roulette/lua.rb +19 -15
- data/lib/worker_roulette/tradesman.rb +100 -72
- data/lib/worker_roulette/version.rb +1 -1
- data/lib/worker_roulette.rb +66 -73
- data/spec/benchmark/irb_demo_runner.rb +39 -0
- data/spec/benchmark/perf_test.rb +22 -26
- data/spec/integration/evented_worker_roulette_spec.rb +165 -186
- data/spec/integration/worker_roulette_spec.rb +123 -145
- data/spec/spec_helper.rb +13 -12
- data/spec/unit/evented_readlock_spec.rb +22 -21
- data/spec/unit/lua_spec.rb +12 -14
- data/spec/unit/readlock_spec.rb +10 -11
- data/worker_roulette.gemspec +1 -0
- metadata +18 -5
- data/lib/worker_roulette/a_tradesman.rb +0 -46
- data/spec/unit/worker_roulette_spec.rb +0 -14
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
MzJmOTEyZjdhMWMyYmIzODBiOWFmZTdmNDE5NGUzM2U4MDdlMGYxYg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MDNhZWRmYjVkOWQ1MGEwYjk3NmQ5Njg5YjViMzIwZWFkYzU5NjMxMA==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
MGNhMjYxYTkxY2UwZGJmMDZjNTc1MzY0N2Y4MTYzMWJkZTE5N2RiYjZkNjIz
|
10
|
+
MzQ4NmVlYTU5ZjU2MGZmNWVhYTg5NDRiNWRlZTBlNzMxZDk2ZWE4OTE1OTg2
|
11
|
+
Y2Q1OTBhMmVhN2IwNmQ3OGQ3ZDA1MTI4MDdmMzA1MWJkZTZkYzc=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
Y2I0ZGFkOTI2OGNjODA5M2NmMTQxM2Q2MTNhN2IzMTVlZDZkNWI1ZjU2Nzll
|
14
|
+
OTUxNjhkOTFjYzU4NzQ3OTU5OTMxYjI3OGZmY2NiMzlhNmM0NTMwYTg5ZGZj
|
15
|
+
ZGE5MjliZWI0MTE2MjAzMWM5MTY3ODhmMzhlYjYyMmNhZTdjYWM=
|
data/README.md
CHANGED
@@ -1,99 +1,48 @@
|
|
1
1
|
# WorkerRoulette
|
2
2
|
|
3
|
-
WorkerRoulette is designed to allow large numbers of unique devices, processes, users, or whatever communicate over individual channels without messing up the order of their messages. WorkerRoulette was created to solve two otherwise hard problems. First, other messaging solutions (I'm looking at you RabbitMQ) are not designed to handle very large numbers of queues (
|
3
|
+
WorkerRoulette is designed to allow large numbers of unique devices, processes, users, or whatever communicate over individual channels without messing up the order of their messages. WorkerRoulette was created to solve two otherwise hard problems. First, other messaging solutions (I'm looking at you RabbitMQ) are not designed to handle very large numbers of queues (millions); because WorkerRoulette is built on top of Redis, we have successfully tested it running with millions of queues. Second, other messaging systems assume one (or more) of three things: 1. Your message consumers know the routing key of messages they are interested in processing; 2. Your messages can wait so that only one consumer is processed at a time; 3. You love to complicated write code to put your messages back in order. Sometimes, none of these things is true and that is where WorkerRoulette comes in.
|
4
4
|
|
5
|
-
WorkerRoulette lets you have thousands of competing consumers (
|
5
|
+
WorkerRoulette lets you have thousands of competing consumers (distributed over as many machines as you'd like) processing ordered messages from millions of totally unknown message providers. It does all this and ensures that the messages sent from each message provider are processed in exactly the order it sent them.
|
6
6
|
|
7
|
-
##
|
7
|
+
## Use
|
8
8
|
```ruby
|
9
9
|
size_of_connection_pool = 100
|
10
10
|
|
11
11
|
#Start it up
|
12
|
-
#the config takes size for the connection pool size, evented to specify
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
sender_id = :shady
|
17
|
-
foreman = WorkerRoulette.a_foreman(sender_id)
|
18
|
-
|
19
|
-
foreman.enqueue_work_order(['hello', 'foreman']) do |msg|
|
20
|
-
puts "work enqueued #{msg}"
|
21
|
-
end
|
22
|
-
|
23
|
-
#Pull it off
|
24
|
-
tradesman = WorkerRoulette.a_tradesman
|
25
|
-
tradesman.work_orders! do |work_orders| #drain the queue of the next available sender
|
26
|
-
work_orders.first # => ['hello', 'foreman']
|
27
|
-
end
|
28
|
-
|
29
|
-
#Enqueue some more from someone else
|
30
|
-
other_sender_id = :the_real_slim_shady
|
31
|
-
other_foreman = WorkerRoulette.a_foreman(other_sender_id)
|
32
|
-
other_foreman.enqueue_work_order({'can you get me' => 'the number nine?'}) do |msg|
|
33
|
-
puts "work enqueued #{msg}"
|
34
|
-
end
|
35
|
-
|
36
|
-
#Have the same worker pull that off
|
37
|
-
tradesman.work_orders! do |work_orders| #drain the queue of the next available sender
|
38
|
-
work_orders.first # => {'can you get me' => 'the number nine?'}
|
39
|
-
end
|
40
|
-
|
41
|
-
#Have your workers wait for work to come in
|
42
|
-
on_subscribe_callback = -> do
|
43
|
-
puts "Huzzah! We're listening!"
|
44
|
-
foreman.enqueue_work_order('will I see you later?')
|
45
|
-
foreman.enqueue_work_order('can you give me back my dime?')
|
46
|
-
end
|
47
|
-
|
48
|
-
|
49
|
-
#And they will pull it off as it comes, as long as it comes
|
50
|
-
#NB: This is NOT a blocking operation, so no worries
|
51
|
-
tradesman.wait_for_work_orders(on_subscribe_callback) do |work_orders, message, channel| #drain the queue of the next available sender
|
52
|
-
work_orders # => ['will I see you later', 'can you give me back my dime?']
|
53
|
-
message # => 'new_job_ready'
|
54
|
-
channel # => '' #the name of the channel the message was published on, if one was used -- see below
|
55
|
-
end
|
56
|
-
```
|
57
|
-
|
58
|
-
## Synchronous Api
|
59
|
-
```ruby
|
60
|
-
size_of_connection_pool = 100
|
61
|
-
|
62
|
-
#Start it up
|
63
|
-
#the config takes size for the connection pool size, evented to specify which api to use, then the normal redis config
|
64
|
-
WorkerRoulette.start(size: size_of_connection_pool, evented: false, host: 'localhost', timeout: 5, db: 1)
|
12
|
+
#the config takes size for the connection pool size, evented to specify whether to use evented redis or not,
|
13
|
+
#polling_time dictates the number of seconds to wait before checking the job queue
|
14
|
+
#(use higher numbers for high traffic systems), then the normal redis config
|
15
|
+
WorkerRoulette.start(size: size_of_connection_pool, evented: false, host: 'localhost', timeout: 5, db: 1, polling_time: 2)
|
65
16
|
|
66
17
|
#Enqueue some work
|
67
18
|
sender_id = :shady
|
68
19
|
foreman = WorkerRoulette.foreman(sender_id)
|
69
|
-
foreman.enqueue_work_order(
|
20
|
+
foreman.enqueue_work_order('hello')
|
70
21
|
|
71
|
-
#Pull it off
|
22
|
+
#Pull it off (headers always include both the unique id of the sender)
|
72
23
|
tradesman = WorkerRoulette.tradesman
|
73
24
|
work_orders = tradesman.work_orders! #drain the queue of the next available sender
|
74
|
-
work_orders
|
25
|
+
work_orders # => {'headers' => {'sender' => :shady}, 'payload' => ['hello']}
|
75
26
|
|
76
27
|
#Enqueue some more from someone else
|
77
28
|
other_sender_id = :the_real_slim_shady
|
78
|
-
other_foreman = WorkerRoulette.foreman(other_sender_id)
|
79
|
-
other_foreman.enqueue_work_order({'can you get me' => 'the number nine?'})
|
29
|
+
other_foreman = WorkerRoulette.foreman(other_sender_id, 'some_namespace')
|
30
|
+
other_foreman.enqueue_work_order({'can you get me' => 'the number nine?'}, {'custom' => 'headers'})
|
80
31
|
|
81
32
|
#Have the same worker pull that off
|
82
33
|
work_orders = tradesman.work_orders! #drain the queue of the next available sender
|
83
|
-
work_orders
|
34
|
+
work_orders # => {'headers' => {'sender' => :the_real_slim_shady, 'custom' => 'headers'},
|
35
|
+
# 'payload' => [{'can you get me' => 'the number nine?'}]}
|
84
36
|
|
85
37
|
#Have your workers wait for work to come in
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
#
|
94
|
-
#NB: This IS a blocking operation
|
95
|
-
tradesman.wait_for_work_orders(on_subscribe_callback) do |work_orders| #drain the queue of the next available sender
|
96
|
-
work_orders # => ['will I see you later', 'can you give me back my dime?']
|
38
|
+
foreman.enqueue_work_order('will I see you later?')
|
39
|
+
foreman.enqueue_work_order('can you give me back my dime?')
|
40
|
+
|
41
|
+
#And they will pull it off as it comes, as long as it comes. If the job board is empty (ie there is now work to do),
|
42
|
+
#this method will poll redis every "polling_time" seconds until it finds work,
|
43
|
+
#at which point it will continue processing jobs until the job board is empty again
|
44
|
+
tradesman.wait_for_work_orders do |work_orders| #drain the queue of the next available sender
|
45
|
+
work_orders.first # => ['will I see you later', 'can you give me back my dime?']
|
97
46
|
end
|
98
47
|
```
|
99
48
|
|
@@ -106,33 +55,27 @@ tradesman.should_receive(:work_orders!).and_call_original
|
|
106
55
|
good_foreman = WorkerRoulette.foreman('foreman', 'good_channel')
|
107
56
|
bad_foreman = WorkerRoulette.foreman('foreman', 'bad_channel')
|
108
57
|
|
109
|
-
|
110
|
-
|
111
|
-
bad_foreman.enqueue_work_order('evil biddings you should not carry out')
|
112
|
-
end
|
58
|
+
good_foreman.enqueue_work_order('some old fashion work')
|
59
|
+
bad_foreman.enqueue_work_order('evil biddings you should not carry out')
|
113
60
|
|
114
|
-
tradesman.wait_for_work_orders
|
61
|
+
tradesman.wait_for_work_orders do |work|
|
115
62
|
work.to_s.should match("some old fashion work") #only got the work from the good foreman
|
116
63
|
work.to_s.should_not match("evil") #channels let us ignore the other's evil orders
|
117
|
-
tradesman.unsubscribe
|
118
64
|
end
|
119
65
|
```
|
120
66
|
|
121
67
|
## Performance
|
122
68
|
Running the performance tests on my laptop, the numbers break down like this:
|
123
|
-
###
|
124
|
-
-
|
125
|
-
- Manual:
|
69
|
+
### Evented
|
70
|
+
- Polling: ~11,500 read-write round-trips / second
|
71
|
+
- Manual: ~6,000 read-write round-trips / second
|
126
72
|
|
127
|
-
###
|
128
|
-
-
|
129
|
-
- Manual:
|
73
|
+
### Non-Evented
|
74
|
+
- Polling: ~10,000 read-write round-trips / second
|
75
|
+
- Manual: ~5,500 read-write round-trips / second
|
130
76
|
|
131
77
|
To run the perf tests yourself run `bundle exec spec:perf`
|
132
78
|
|
133
|
-
## Redis Pubsub and Polling
|
134
|
-
The `wait_for_work_orders` method works using Redis' pubsub mechanism. The advantage to this is that it is very fast and minimizes network traffic. The downside is that Redis' pubsub implementation is 'fire and forget', so any subscribers who are not listening at the moment the message is published will miss it. In order to compensate for this, WorkerRoulette's Async Api creates a backup timer (using EM.add_periodic_timer) that will poll redis every 20-25 seconds for new work. Since the timer is reset every time new work comes in, if you have an active publisher, the timer may never need to fire. It only serves a backup to make sure no work is left waiting in the queues because of network problems. Since there is no one polling mechanism that works for all situations in a synchrounous environment, this feature is only available through the Async Api.
|
135
|
-
|
136
79
|
## Redis Version
|
137
80
|
WorkerRoulette uses Redis' lua scripting feature to acheive such high throughput and therefore requires a version of Redis that supports lua scripting (>= Redis 2.6)
|
138
81
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module WorkerRoulette
|
2
2
|
class Foreman
|
3
|
-
attr_reader :sender
|
3
|
+
attr_reader :sender, :namespace, :channel
|
4
4
|
|
5
5
|
LUA_ENQUEUE_WORK_ORDERS = <<-HERE
|
6
6
|
local counter_key = KEYS[1]
|
@@ -16,25 +16,28 @@ module WorkerRoulette
|
|
16
16
|
local zadd = 'ZADD'
|
17
17
|
local rpush = 'RPUSH'
|
18
18
|
local publish = 'PUBLISH'
|
19
|
+
local zcard = 'ZCARD'
|
20
|
+
local del = 'DEL'
|
19
21
|
|
20
22
|
local function enqueue_work_orders(work_order, job_notification)
|
23
|
+
redis_call(rpush, sender_key, work_order)
|
24
|
+
|
25
|
+
-- called when a work from a new sender is added
|
21
26
|
if (redis_call(zscore, job_board_key, sender_key) == false) then
|
22
27
|
local count = redis_call(incr, counter_key)
|
23
28
|
redis_call(zadd, job_board_key, count, sender_key)
|
24
29
|
end
|
25
|
-
|
26
|
-
redis_call(rpush,sender_key, work_order)
|
27
|
-
redis_call(publish, channel, job_notification)
|
28
30
|
end
|
29
31
|
|
30
32
|
enqueue_work_orders(work_order, job_notification)
|
31
33
|
HERE
|
32
34
|
|
33
|
-
def initialize(
|
35
|
+
def initialize(redis_pool, sender, namespace = nil)
|
34
36
|
@sender = sender
|
35
37
|
@namespace = namespace
|
36
38
|
@redis_pool = redis_pool
|
37
39
|
@channel = namespace || WorkerRoulette::JOB_NOTIFICATIONS
|
40
|
+
@lua = Lua.new(@redis_pool)
|
38
41
|
end
|
39
42
|
|
40
43
|
def enqueue_work_order(work_order, headers = {}, &callback)
|
@@ -43,7 +46,7 @@ module WorkerRoulette
|
|
43
46
|
end
|
44
47
|
|
45
48
|
def enqueue_work_order_without_headers(work_order, &callback)
|
46
|
-
|
49
|
+
@lua.call(LUA_ENQUEUE_WORK_ORDERS, [counter_key, job_board_key, sender_key, @channel],
|
47
50
|
[WorkerRoulette.dump(work_order), WorkerRoulette::JOB_NOTIFICATIONS], &callback)
|
48
51
|
end
|
49
52
|
|
@@ -55,18 +58,14 @@ module WorkerRoulette
|
|
55
58
|
@counter_key ||= WorkerRoulette.counter_key(@namespace)
|
56
59
|
end
|
57
60
|
|
58
|
-
private
|
59
|
-
|
60
61
|
def sender_key
|
61
|
-
@sender_key
|
62
|
+
@sender_key ||= WorkerRoulette.sender_key(@sender, @namespace)
|
62
63
|
end
|
63
64
|
|
65
|
+
private
|
66
|
+
|
64
67
|
def default_headers
|
65
68
|
Hash['sender' => sender]
|
66
69
|
end
|
67
|
-
|
68
|
-
def self.lua_enqueue_work_orders
|
69
|
-
LUA_ENQUEUE_WORK_ORDERS
|
70
|
-
end
|
71
70
|
end
|
72
|
-
end
|
71
|
+
end
|
data/lib/worker_roulette/lua.rb
CHANGED
@@ -1,36 +1,40 @@
|
|
1
1
|
module WorkerRoulette
|
2
|
-
|
3
|
-
|
2
|
+
class Lua
|
3
|
+
Thread.main[:worker_roulette_lua_script_cache] = Hash.new
|
4
4
|
|
5
|
-
def
|
6
|
-
|
5
|
+
def initialize(connection_pool)
|
6
|
+
@connection_pool = connection_pool
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(lua_script, keys_accessed = [], args = [], &callback)
|
10
|
+
@connection_pool.with do |redis|
|
7
11
|
results = evalsha(redis, lua_script, keys_accessed, args, &callback)
|
8
12
|
end
|
9
13
|
end
|
10
14
|
|
11
|
-
def
|
12
|
-
|
15
|
+
def sha(lua_script)
|
16
|
+
Thread.main[:worker_roulette_lua_script_cache][lua_script] ||= Digest::SHA1.hexdigest(lua_script)
|
13
17
|
end
|
14
18
|
|
15
|
-
def
|
16
|
-
|
19
|
+
def cache
|
20
|
+
Thread.main[:worker_roulette_lua_script_cache].dup
|
17
21
|
end
|
18
22
|
|
19
|
-
def
|
20
|
-
|
23
|
+
def clear_cache!
|
24
|
+
Thread.main[:worker_roulette_lua_script_cache] = {}
|
21
25
|
end
|
22
26
|
|
23
|
-
def
|
24
|
-
results = redis.eval(lua_script, keys_accessed.
|
27
|
+
def eval(redis, lua_script, keys_accessed, args, &callback)
|
28
|
+
results = redis.eval(lua_script, keys_accessed.length, *keys_accessed, *args)
|
25
29
|
results.callback &callback if callback
|
26
30
|
results.errback {|err_msg| raise EM::Hiredis::RedisError.new(err_msg)}
|
27
31
|
end
|
28
32
|
|
29
|
-
def
|
33
|
+
def evalsha(redis, lua_script, keys_accessed, args, &callback)
|
30
34
|
if redis.class == EM::Hiredis::Client
|
31
35
|
results = redis.evalsha(sha(lua_script), keys_accessed.length, *keys_accessed, *args)
|
32
36
|
results.callback &callback if callback
|
33
|
-
results.errback {
|
37
|
+
results.errback {eval(redis, lua_script, keys_accessed, args, &callback)}
|
34
38
|
else
|
35
39
|
begin
|
36
40
|
results = redis.evalsha(sha(lua_script), keys_accessed, args)
|
@@ -43,4 +47,4 @@ module WorkerRoulette
|
|
43
47
|
results
|
44
48
|
end
|
45
49
|
end
|
46
|
-
end
|
50
|
+
end
|
@@ -1,98 +1,126 @@
|
|
1
|
+
require 'timers'
|
2
|
+
|
1
3
|
module WorkerRoulette
|
2
4
|
class Tradesman
|
3
|
-
attr_reader :last_sender
|
5
|
+
attr_reader :last_sender, :remaining_jobs, :timer
|
4
6
|
|
5
7
|
LUA_DRAIN_WORK_ORDERS = <<-HERE
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
if sender_key == empty_string then
|
28
|
-
sender_key = redis_call(zrange, job_board_key, 0, 0)[1] or empty_string
|
29
|
-
if sender_key == empty_string then
|
30
|
-
return {}
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
local lock_key = lock_key_prefix .. sender_key
|
35
|
-
local locked = (redis_call(get, lock_key) == lock_value)
|
36
|
-
|
37
|
-
if not locked then
|
38
|
-
local results = {sender_key, redis_call('LRANGE', sender_key, 0, -1)}
|
39
|
-
redis_call(del, sender_key)
|
40
|
-
redis_call('ZREM', job_board_key, sender_key)
|
41
|
-
redis_call(set, lock_key, lock_value, ex, 1, nx)
|
42
|
-
return results
|
43
|
-
else
|
44
|
-
local sender_index = redis_call(zrank, job_board_key, sender_key)
|
45
|
-
local next_index = sender_index + 1
|
46
|
-
local next_sender_key = redis_call(zrange, job_board_key, next_index, next_index)[1]
|
47
|
-
if next_sender_key then
|
48
|
-
return drain_work_orders(job_board_key, empty_string, next_sender_key)
|
49
|
-
else
|
50
|
-
return {}
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
8
|
+
local empty_string = ""
|
9
|
+
local job_board_key = KEYS[1]
|
10
|
+
local last_sender_key = KEYS[2] or empty_string
|
11
|
+
local sender_key = ARGV[1] or empty_string
|
12
|
+
local redis_call = redis.call
|
13
|
+
local lock_key_prefix = "L*:"
|
14
|
+
local lock_value = 1
|
15
|
+
local ex = "EX"
|
16
|
+
local nx = "NX"
|
17
|
+
local get = "GET"
|
18
|
+
local set = "SET"
|
19
|
+
local del = "DEL"
|
20
|
+
local lrange = "LRANGE"
|
21
|
+
local zrank = "ZRANK"
|
22
|
+
local zrange = "ZRANGE"
|
23
|
+
local zrem = "ZREM"
|
24
|
+
local zcard = 'ZCARD'
|
25
|
+
|
26
|
+
local function drain_work_orders(job_board_key, last_sender_key, sender_key)
|
54
27
|
|
55
|
-
|
56
|
-
|
28
|
+
--kill lock for last_sender_key
|
29
|
+
if last_sender_key ~= empty_string then
|
30
|
+
local last_sender_lock_key = lock_key_prefix .. last_sender_key
|
31
|
+
redis_call(del, last_sender_lock_key)
|
32
|
+
end
|
33
|
+
|
34
|
+
if sender_key == empty_string then
|
35
|
+
sender_key = redis_call(zrange, job_board_key, 0, 0)[1] or empty_string
|
57
36
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
@channel = namespace || WorkerRoulette::JOB_NOTIFICATIONS
|
37
|
+
-- return if job_board is empty
|
38
|
+
if sender_key == empty_string then
|
39
|
+
return {empty_string, {}, 0}
|
40
|
+
end
|
63
41
|
end
|
64
42
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
43
|
+
local lock_key = lock_key_prefix .. sender_key
|
44
|
+
local was_not_locked = redis_call(set, lock_key, lock_value, ex, 3, nx)
|
45
|
+
|
46
|
+
if was_not_locked then
|
47
|
+
local work_orders = redis_call(lrange, sender_key, 0, -1)
|
48
|
+
redis_call(del, sender_key)
|
49
|
+
|
50
|
+
redis_call(zrem, job_board_key, sender_key)
|
51
|
+
local remaining_jobs = redis_call(zcard, job_board_key) or 0
|
52
|
+
|
53
|
+
return {sender_key, work_orders, remaining_jobs}
|
54
|
+
else
|
55
|
+
local sender_index = redis_call(zrank, job_board_key, sender_key)
|
56
|
+
local next_index = sender_index + 1
|
57
|
+
local next_sender_key = redis_call(zrange, job_board_key, next_index, next_index)[1]
|
58
|
+
if next_sender_key then
|
59
|
+
return drain_work_orders(job_board_key, empty_string, next_sender_key)
|
60
|
+
else
|
61
|
+
-- return if job_board is empty
|
62
|
+
return {empty_string, {}, 0}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
return drain_work_orders(job_board_key, last_sender_key, empty_string)
|
68
|
+
HERE
|
69
|
+
|
70
|
+
def initialize(redis_pool, evented, namespace = nil, polling_time = WorkerRoulette::DEFAULT_POLLING_TIME)
|
71
|
+
@evented = evented
|
72
|
+
@polling_time = polling_time
|
73
|
+
@redis_pool = redis_pool
|
74
|
+
@namespace = namespace
|
75
|
+
@channel = namespace || WorkerRoulette::JOB_NOTIFICATIONS
|
76
|
+
@timer = Timers::Group.new
|
77
|
+
@lua = Lua.new(@redis_pool)
|
78
|
+
@remaining_jobs = 0
|
79
|
+
end
|
80
|
+
|
81
|
+
def wait_for_work_orders(&on_message_callback)
|
82
|
+
return unless on_message_callback
|
83
|
+
work_orders! do |work|
|
84
|
+
on_message_callback.call(work) if work.any?
|
85
|
+
if @evented
|
86
|
+
evented_drain_work_queue!(&on_message_callback)
|
87
|
+
else
|
88
|
+
non_evented_drain_work_queue!(&on_message_callback)
|
70
89
|
end
|
71
90
|
end
|
72
91
|
end
|
73
92
|
|
74
93
|
def work_orders!(&callback)
|
75
|
-
|
76
|
-
results
|
77
|
-
|
78
|
-
|
94
|
+
@lua.call(LUA_DRAIN_WORK_ORDERS, [job_board_key, @last_sender], [nil]) do |results|
|
95
|
+
sender_key = results[0]
|
96
|
+
work_orders = results[1]
|
97
|
+
@remaining_jobs = results[2]
|
98
|
+
@last_sender = sender_key.split(':').last
|
99
|
+
work = work_orders.map {|work_order| WorkerRoulette.load(work_order)}
|
79
100
|
callback.call work if callback
|
80
101
|
work
|
81
102
|
end
|
82
103
|
end
|
83
104
|
|
84
|
-
def unsubscribe
|
85
|
-
@pubsub_pool.with {|redis| redis.unsubscribe(@channel)}
|
86
|
-
end
|
87
|
-
|
88
105
|
def job_board_key
|
89
106
|
@job_board_key ||= WorkerRoulette.job_board_key(@namespace)
|
90
107
|
end
|
91
108
|
|
92
|
-
|
109
|
+
private
|
110
|
+
def evented_drain_work_queue!(&on_message_callback)
|
111
|
+
if remaining_jobs > 0
|
112
|
+
EM.next_tick {wait_for_work_orders(&on_message_callback)}
|
113
|
+
else
|
114
|
+
EM.add_timer(@polling_time) {wait_for_work_orders(&on_message_callback)}
|
115
|
+
end
|
116
|
+
end
|
93
117
|
|
94
|
-
def
|
95
|
-
|
118
|
+
def non_evented_drain_work_queue!(&on_message_callback)
|
119
|
+
if remaining_jobs > 0
|
120
|
+
wait_for_work_orders(&on_message_callback)
|
121
|
+
else
|
122
|
+
@timer.after(@polling_time) {wait_for_work_orders(&on_message_callback)}
|
123
|
+
end
|
96
124
|
end
|
97
125
|
end
|
98
126
|
end
|