worker_roulette 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +62 -30
- data/lib/worker_roulette/foreman.rb +13 -7
- data/lib/worker_roulette/tradesman.rb +14 -8
- data/lib/worker_roulette/version.rb +1 -1
- data/lib/worker_roulette.rb +4 -4
- data/spec/integration/worker_roulette_spec.rb +23 -2
- metadata +1 -1
data/README.md
CHANGED
@@ -4,36 +4,68 @@ WorkerRoulette is designed to allow large numbers of unique devices, processes,
|
|
4
4
|
|
5
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.
|
6
6
|
|
7
|
-
## Usage
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
7
|
+
## General Usage
|
8
|
+
```ruby
|
9
|
+
size_of_connection_pool = 100
|
10
|
+
redis_config = {host: 'localhost', timeout: 5, db: 1}
|
11
|
+
|
12
|
+
#Start it up
|
13
|
+
WorkerRoulette.start(size_of_connection_pool, redis_config)
|
14
|
+
|
15
|
+
#Enqueue some work
|
16
|
+
sender_id = :shady
|
17
|
+
foreman = WorkerRoulette.foreman(sender_id)
|
18
|
+
foreman.enqueue_work_order(['hello', 'foreman'])
|
19
|
+
|
20
|
+
#Pull it off
|
21
|
+
tradesman = WorkerRoulette.tradesman
|
22
|
+
messages = tradesman.work_orders! #drain the queue of the next available sender
|
23
|
+
messages.first # => ['hello', 'foreman']
|
24
|
+
|
25
|
+
#Enqueue some more from someone else
|
26
|
+
other_sender_id = :the_real_slim_shady
|
27
|
+
other_foreman = WorkerRoulette.foreman(other_sender_id)
|
28
|
+
other_foreman.enqueue_work_order({'can you get me' => 'the number nine?'})
|
29
|
+
|
30
|
+
#Have the same worker pull that off
|
31
|
+
messages = tradesman.work_orders! #drain the queue of the next available sender
|
32
|
+
messages.first # => {'can you get me' => 'the number nine?'}
|
33
|
+
|
34
|
+
#Have your workers wait for work to come in
|
35
|
+
on_subscribe_callback = -> do
|
36
|
+
puts "Huzzah! We're listening!"
|
37
|
+
foreman.enqueue_work_order('will I see you later?')
|
38
|
+
foreman.enqueue_work_order('can you give me back my dime?')
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
#And they will pull it off as it comes, as long as it comes
|
43
|
+
#(This is a blocking operation, so it is best in Threads or EventMachine.next_tick)
|
44
|
+
tradesman.wait_for_work_orders(on_subscribe_callback) do |messages| #drain the queue of the next available sender
|
45
|
+
messages # => ['will I see you later', 'can you give me back my dime?']
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
## Channels
|
50
|
+
You can also namespace your work orders over a channel, in case you have several sorts of competing consumers who should not step on each other's toes:
|
51
|
+
```ruby
|
52
|
+
tradesman = WorkerRoulette.tradesman('good_channel')
|
53
|
+
tradesman.should_receive(:work_orders!).and_call_original
|
54
|
+
|
55
|
+
good_foreman = WorkerRoulette.foreman('foreman', 'good_channel')
|
56
|
+
bad_foreman = WorkerRoulette.foreman('foreman', 'bad_channel')
|
57
|
+
|
58
|
+
publish = -> do
|
59
|
+
good_foreman.enqueue_work_order('some old fashion work')
|
60
|
+
bad_foreman.enqueue_work_order('evil biddings you should not carry out') #channels let us ignore his evil orders
|
61
|
+
end
|
62
|
+
|
63
|
+
tradesman.wait_for_work_orders(publish) do |work|
|
64
|
+
work.to_s.should match("some old fashion work") #only got the work from the good foreman
|
65
|
+
tradesman.unsubscribe
|
66
|
+
end
|
67
|
+
|
68
|
+
```
|
37
69
|
|
38
70
|
##Caveat Emptor
|
39
71
|
While WorkerRoulette does promise to keep the messages of each consumer processed in order by competing consumers, it does NOT guarantee the order in which the queues themselves will be processed. In general, work is processed in a FIFO order, but for performance reasons this has been left a loose FIFO. For example, if Abdul enqueue_work_orders some ordered messages ('1', '2', and '3') and then so do Mark and Wanda, Mark's messages may be processed first, then it would likely be Abdul's, and then Wanda's. However, even though Mark jumped the line, Abdul's messages will still be processed the order he enqueue_work_orderd them ('1', '2', then '3').
|
@@ -3,17 +3,23 @@ module WorkerRoulette
|
|
3
3
|
attr_reader :sender
|
4
4
|
COUNTER_KEY = 'counter_key'
|
5
5
|
|
6
|
-
def initialize(sender, redis_pool)
|
7
|
-
@sender
|
6
|
+
def initialize(sender, redis_pool, namespace = nil)
|
7
|
+
@sender = sender
|
8
|
+
@namespace = namespace
|
8
9
|
@redis_pool = redis_pool
|
10
|
+
@channel = namespace || WorkerRoulette::JOB_NOTIFICATIONS
|
9
11
|
end
|
10
12
|
|
11
13
|
def job_board_key
|
12
|
-
WorkerRoulette::JOB_BOARD
|
14
|
+
@job_board_key ||= "#{@namespace + ':' if @namespace}#{WorkerRoulette::JOB_BOARD}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def sender_key
|
18
|
+
@sender_key ||= "#{@namespace + ':' if @namespace}#{@sender}"
|
13
19
|
end
|
14
20
|
|
15
21
|
def counter_key
|
16
|
-
COUNTER_KEY
|
22
|
+
@counter_key ||= "#{@namespace + ':' if @namespace}#{COUNTER_KEY}"
|
17
23
|
end
|
18
24
|
|
19
25
|
def enqueue_work_order_without_headers(work_order)
|
@@ -25,9 +31,9 @@ module WorkerRoulette
|
|
25
31
|
@redis_pool.with({}) do |redis|
|
26
32
|
@count = redis.incr(COUNTER_KEY)
|
27
33
|
redis.multi do
|
28
|
-
redis.zadd(
|
29
|
-
redis.rpush(
|
30
|
-
redis.publish(
|
34
|
+
redis.zadd(job_board_key, @count, @sender)
|
35
|
+
redis.rpush(sender_key, Oj.dump(work_order))
|
36
|
+
redis.publish(@channel, WorkerRoulette::JOB_NOTIFICATIONS)
|
31
37
|
end
|
32
38
|
end
|
33
39
|
end
|
@@ -1,18 +1,24 @@
|
|
1
1
|
module WorkerRoulette
|
2
2
|
class Tradesman
|
3
3
|
attr_reader :sender
|
4
|
-
def initialize(client_pool, pubsub_pool)
|
4
|
+
def initialize(client_pool, pubsub_pool, namespace = nil)
|
5
5
|
@client_pool = client_pool
|
6
6
|
@pubsub_pool = pubsub_pool
|
7
|
+
@namespace = namespace
|
8
|
+
@channel = namespace || WorkerRoulette::JOB_NOTIFICATIONS
|
7
9
|
end
|
8
10
|
|
9
11
|
def job_board_key
|
10
|
-
WorkerRoulette::JOB_BOARD
|
12
|
+
@job_board_key ||= "#{@namespace + ':' if @namespace}#{WorkerRoulette::JOB_BOARD}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def sender_key
|
16
|
+
@sender_key ||= "#{@namespace + ':' if @namespace}#{@sender}"
|
11
17
|
end
|
12
18
|
|
13
19
|
def wait_for_work_orders(on_subscribe_callback = nil, &block)
|
14
20
|
@pubsub_pool.with({}) do |redis|
|
15
|
-
redis.subscribe(
|
21
|
+
redis.subscribe(@channel) do |on|
|
16
22
|
on.subscribe {on_subscribe_callback.call if on_subscribe_callback}
|
17
23
|
on.message {block.call(work_orders!) if block}
|
18
24
|
end
|
@@ -23,21 +29,21 @@ module WorkerRoulette
|
|
23
29
|
@client_pool.with({}) do |redis|
|
24
30
|
get_sender_for_next_job(redis)
|
25
31
|
results = redis.multi do
|
26
|
-
redis.lrange(
|
27
|
-
redis.del(
|
28
|
-
redis.zrem(
|
32
|
+
redis.lrange(sender_key, 0, -1)
|
33
|
+
redis.del(sender_key)
|
34
|
+
redis.zrem(job_board_key, sender_key)
|
29
35
|
end
|
30
36
|
((results || []).first || []).map {|work_order| Oj.load(work_order)}
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
34
40
|
def unsubscribe
|
35
|
-
@pubsub_pool.with({}) {|redis| redis.unsubscribe}
|
41
|
+
@pubsub_pool.with({}) {|redis| redis.unsubscribe(@channel)}
|
36
42
|
end
|
37
43
|
|
38
44
|
private
|
39
45
|
def get_sender_for_next_job(redis)
|
40
|
-
@sender = (redis.zrange(
|
46
|
+
@sender = (redis.zrange(job_board_key, 0, 0) || []).first.to_s
|
41
47
|
end
|
42
48
|
end
|
43
49
|
end
|
data/lib/worker_roulette.rb
CHANGED
@@ -22,14 +22,14 @@ module WorkerRoulette
|
|
22
22
|
@pubsub_connection_pool = connection_pool.new(@pool_config) {Redis.new(@redis_config)}
|
23
23
|
end
|
24
24
|
|
25
|
-
def self.foreman(sender)
|
25
|
+
def self.foreman(sender, channel = nil)
|
26
26
|
raise "WorkerRoulette not Started" unless @foreman_connection_pool
|
27
|
-
Foreman.new(sender, @foreman_connection_pool)
|
27
|
+
Foreman.new(sender, @foreman_connection_pool, channel)
|
28
28
|
end
|
29
29
|
|
30
|
-
def self.tradesman
|
30
|
+
def self.tradesman(channel = nil)
|
31
31
|
raise "WorkerRoulette not Started" unless @tradesman_connection_pool
|
32
|
-
Tradesman.new(@tradesman_connection_pool, @pubsub_connection_pool)
|
32
|
+
Tradesman.new(@tradesman_connection_pool, @pubsub_connection_pool, channel)
|
33
33
|
end
|
34
34
|
|
35
35
|
def self.tradesman_connection_pool
|
@@ -137,15 +137,36 @@ describe WorkerRoulette do
|
|
137
137
|
subject.work_orders!
|
138
138
|
end
|
139
139
|
|
140
|
-
it "should get the work_orders from the next
|
140
|
+
it "should get the work_orders from the next queue when a new job is ready" do
|
141
141
|
subject.work_orders!
|
142
142
|
subject.should_receive(:work_orders!).and_call_original
|
143
|
-
|
143
|
+
|
144
|
+
publisher = -> {puts :HOO.to_s; foreman.enqueue_work_order(work_orders); subject.unsubscribe}
|
145
|
+
|
144
146
|
subject.wait_for_work_orders(publisher) do |redis_work_orders|
|
145
147
|
redis_work_orders.should == [work_orders_with_headers]
|
146
148
|
end
|
147
149
|
end
|
148
150
|
|
151
|
+
it "should publish and subscribe on custom channels" do
|
152
|
+
tradesman = WorkerRoulette.tradesman('good_channel')
|
153
|
+
tradesman.should_receive(:work_orders!).and_call_original
|
154
|
+
|
155
|
+
good_foreman = WorkerRoulette.foreman('foreman', 'good_channel')
|
156
|
+
bad_foreman = WorkerRoulette.foreman('foreman', 'bad_channel')
|
157
|
+
|
158
|
+
|
159
|
+
publish = -> do
|
160
|
+
good_foreman.enqueue_work_order('some old fashion work')
|
161
|
+
bad_foreman.enqueue_work_order('evil biddings you should not carry out')
|
162
|
+
tradesman.unsubscribe
|
163
|
+
end
|
164
|
+
|
165
|
+
tradesman.wait_for_work_orders(publish) do |work|
|
166
|
+
work.to_s.should match("some old fashion work")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
149
170
|
it "should checkout a readlock for a queue and put it back when its done processing; lock should expire after 5 minutes?"
|
150
171
|
it "should eves drop on the job board"
|
151
172
|
|