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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- M2EzZmZkYTllZGYwMjA1MDI1MzVmZDU5ODk1ZGRiNzYzMjQzMjQ1OQ==
4
+ MzJmOTEyZjdhMWMyYmIzODBiOWFmZTdmNDE5NGUzM2U4MDdlMGYxYg==
5
5
  data.tar.gz: !binary |-
6
- NDZmNjhjMTJjZDJhMjJlNzI5MjE2MjllZjFkMGQ1ZDIxODExNTQ4NQ==
6
+ MDNhZWRmYjVkOWQ1MGEwYjk3NmQ5Njg5YjViMzIwZWFkYzU5NjMxMA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- NjAyNTBlZDUxNmM5M2Y1MGMwMGYyNzdkNDUzNDEzMGQxNTI0ODY4NTlhY2U1
10
- YjRiYjQ1MWI3YzMxNDdmZWM3ZjE1Y2ViZDg1M2FhYTA1ZjhmYzNhYWYwMjY2
11
- ZThkODM4ZjY0MmE5NzlmMTdlZTRiMzRhMzUwMWFjMGJkM2NlYTU=
9
+ MGNhMjYxYTkxY2UwZGJmMDZjNTc1MzY0N2Y4MTYzMWJkZTE5N2RiYjZkNjIz
10
+ MzQ4NmVlYTU5ZjU2MGZmNWVhYTg5NDRiNWRlZTBlNzMxZDk2ZWE4OTE1OTg2
11
+ Y2Q1OTBhMmVhN2IwNmQ3OGQ3ZDA1MTI4MDdmMzA1MWJkZTZkYzc=
12
12
  data.tar.gz: !binary |-
13
- ODUxMmQwNWNhOWI5NTBkMmQ2NzFlMzUzY2Q4NjZlZTYzZDE4ZmQ2NGZiODJk
14
- ZWNkMzc3ZmUxY2UzOTk4OTJjZjlmNDZkMjU4N2Y3ZGVjMmUzYzIxOTdmMzg2
15
- MDJlZjAzNGFiN2JjMGMzNWEwYWVlNmY3OTQ4MjE0NjliY2ViOTE=
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 (30,000+); 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.
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 (distrubted 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.
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
- ## Asynchronous Api (Evented)
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 which api to use, then the normal redis config
13
- WorkerRoulette.start(size: size_of_connection_pool, evented: true, host: 'localhost', timeout: 5, db: 1)
14
-
15
- #Enqueue some work
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(['hello', 'foreman'])
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.first # => ['hello', 'foreman']
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.first # => {'can you get me' => 'the number nine?'}
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
- on_subscribe_callback = -> do
87
- puts "Huzzah! We're listening!"
88
- foreman.enqueue_work_order('will I see you later?')
89
- foreman.enqueue_work_order('can you give me back my dime?')
90
- end
91
-
92
-
93
- #And they will pull it off as it comes, as long as it comes
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
- publish = -> do
110
- good_foreman.enqueue_work_order('some old fashion work')
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(publish) do |work|
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
- ### Async Api
124
- - Pubsub: ~5500 read-write round-trips / second
125
- - Manual: ~4500 read-write round-trips / second
69
+ ### Evented
70
+ - Polling: ~11,500 read-write round-trips / second
71
+ - Manual: ~6,000 read-write round-trips / second
126
72
 
127
- ### Sync Api
128
- - Pubsub: ~2700 read-write round-trips / second
129
- - Manual: ~2500 read-write round-trips / second
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(sender, redis_pool, namespace = nil)
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
- Lua.call(self.class.lua_enqueue_work_orders, [counter_key, job_board_key, sender_key, @channel],
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 = WorkerRoulette.sender_key(sender, @namespace)
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
@@ -1,36 +1,40 @@
1
1
  module WorkerRoulette
2
- module Lua
3
- @cache = Hash.new
2
+ class Lua
3
+ Thread.main[:worker_roulette_lua_script_cache] = Hash.new
4
4
 
5
- def self.call(lua_script, keys_accessed = [], args = [], &callback)
6
- WorkerRoulette.tradesman_connection_pool.with do |redis|
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 self.sha(lua_script)
12
- @cache[lua_script] ||= Digest::SHA1.hexdigest(lua_script)
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 self.cache
16
- @cache.dup
19
+ def cache
20
+ Thread.main[:worker_roulette_lua_script_cache].dup
17
21
  end
18
22
 
19
- def self.clear_cache!
20
- @cache = {}
23
+ def clear_cache!
24
+ Thread.main[:worker_roulette_lua_script_cache] = {}
21
25
  end
22
26
 
23
- def self.eval(redis, lua_script, keys_accessed, args, &callback)
24
- results = redis.eval(lua_script, keys_accessed.size, *keys_accessed, *args)
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 self.evalsha(redis, lua_script, keys_accessed, args, &callback)
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 {self.eval(redis, lua_script, keys_accessed, args, &callback)}
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
- local empty_string = ""
7
- local job_board_key = KEYS[1]
8
- local last_sender_key = KEYS[2] or empty_string
9
- local sender_key = ARGV[1] or empty_string
10
- local redis_call = redis.call
11
- local lock_key_prefix = "L*:"
12
- local lock_value = "L"
13
- local ex = "EX"
14
- local nx = "NX"
15
- local get = "GET"
16
- local set = "SET"
17
- local del = "DEL"
18
- local zrank = "ZRANK"
19
- local zrange = "ZRANGE"
20
-
21
- local function drain_work_orders(job_board_key, last_sender_key, sender_key)
22
- if last_sender_key ~= empty_string then
23
- local last_sender_lock_key = lock_key_prefix .. last_sender_key
24
- redis_call(del, last_sender_lock_key)
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
- return drain_work_orders(job_board_key, last_sender_key, empty_string)
56
- HERE
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
- def initialize(client_pool, pubsub_pool, namespace = nil)
59
- @client_pool = client_pool
60
- @pubsub_pool = pubsub_pool
61
- @namespace = namespace
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
- def wait_for_work_orders(on_subscribe_callback = nil, &block)
66
- @pubsub_pool.with do |redis|
67
- redis.subscribe(@channel) do |on|
68
- on.subscribe {on_subscribe_callback.call if on_subscribe_callback}
69
- on.message {block.call(work_orders! + work_orders!) if block}
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
- Lua.call(self.class.lua_drain_work_orders, [job_board_key, @last_sender], [nil]) do |results|
76
- results ||= []
77
- @last_sender = (results.first || '').split(':').first
78
- work = (results[1] || []).map {|work_order| WorkerRoulette.load(work_order)}
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
- private
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 self.lua_drain_work_orders
95
- LUA_DRAIN_WORK_ORDERS
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
@@ -1,3 +1,3 @@
1
1
  module WorkerRoulette
2
- VERSION = "0.1.7"
2
+ VERSION = "0.1.9"
3
3
  end