fairway 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/README.markdown +138 -78
- data/lib/fairway/config.rb +8 -15
- data/lib/fairway/connection.rb +10 -2
- data/lib/fairway/{queue_reader.rb → queue.rb} +11 -2
- data/lib/fairway/scripts.rb +17 -2
- data/lib/fairway/sidekiq/{non_blocking_fetch.rb → basic_fetch.rb} +5 -2
- data/lib/fairway/sidekiq/{queue_fetch.rb → fairway_fetch.rb} +14 -7
- data/lib/fairway/sidekiq/fetch.rb +59 -0
- data/lib/fairway/sidekiq.rb +10 -16
- data/lib/fairway/version.rb +1 -1
- data/lib/fairway.rb +7 -2
- data/spec/lib/fairway/connection_spec.rb +10 -0
- data/spec/lib/fairway/fetch_spec.rb +96 -0
- data/spec/lib/fairway/{queue_reader_spec.rb → queue_spec.rb} +46 -31
- data/spec/lib/fairway/scripts_spec.rb +21 -2
- data/spec/lib/fairway/sidekiq/{non_blocking_fetch_spec.rb → basic_fetch_spec.rb} +12 -2
- data/spec/lib/fairway/sidekiq/{queue_fetch_spec.rb → fairway_fetch_spec.rb} +19 -8
- data/spec/spec_helper.rb +1 -1
- metadata +14 -19
- data/lib/fairway/sidekiq/composite_fetch.rb +0 -37
- data/lib/fairway/sidekiq/fetcher.rb +0 -37
- data/lib/fairway/sidekiq/fetcher_factory.rb +0 -23
- data/redis/fairway_register_queue.lua +0 -5
- data/spec/lib/fairway/sidekiq/composite_fetch_spec.rb +0 -50
- data/spec/lib/fairway/sidekiq/fetcher_spec.rb +0 -31
data/Gemfile.lock
CHANGED
data/README.markdown
CHANGED
@@ -1,123 +1,183 @@
|
|
1
|
-
# Fairway
|
1
|
+
# Fairway - a fair way to queue messages in multi-user systems.
|
2
2
|
|
3
|
-
##
|
3
|
+
## Installation
|
4
4
|
|
5
|
-
|
6
|
-
* Publish messages for services who are listening
|
7
|
-
* adds messages to queues which services have registered
|
8
|
-
* keeps track of which facets have messages queued
|
9
|
-
* allows messages to be pulled off of queues in round-robin by facet
|
5
|
+
Install the gem:
|
10
6
|
|
11
|
-
|
7
|
+
gem install fairway
|
12
8
|
|
13
|
-
|
14
|
-
added to the queue, the backbone will begin pushing matching messages onto the queue.
|
9
|
+
Then make sure you `bundle install`.
|
15
10
|
|
16
|
-
|
17
|
-
queues up a ton of messages, messages for other users may be delayed.
|
11
|
+
## Configuration
|
18
12
|
|
19
|
-
|
20
|
-
|
13
|
+
Fairway.configure do |config|
|
14
|
+
config.redis = { host: "localhost", port: 6379 }
|
15
|
+
config.namespace = "fairway"
|
16
|
+
|
17
|
+
config.facet do |message|
|
18
|
+
message[:user]
|
19
|
+
end
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
config.register_queue("all_messages")
|
22
|
+
end
|
23
|
+
|
24
|
+
## What's a facet?
|
26
25
|
|
27
|
-
|
26
|
+
In many queuing systems, if a single user manages to queue up a lot of messages/jobs at once,
|
27
|
+
everyone else in the system has to wait. Facets are a way of splitting up a single queue by
|
28
|
+
user (or any other criteria) to ensure fair processing of each facet's jobs.
|
28
29
|
|
29
|
-
|
30
|
+
When pulling off a faceted queue, facets are processed in a round-robin fashion, so you'll pull
|
31
|
+
off one message for each facet which contains messages before doubling back and pulling
|
32
|
+
additional messages from a given facet.
|
30
33
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
You can define how to facet your messages during configuration:
|
35
|
+
|
36
|
+
Fairway.configure do |config|
|
37
|
+
config.facet do |message|
|
38
|
+
message[:user]
|
39
|
+
end
|
40
|
+
end
|
37
41
|
|
38
|
-
|
42
|
+
Now, any message delivered by fairway, will use the `user` key of the message to determine
|
43
|
+
which facet to use.
|
39
44
|
|
40
|
-
|
45
|
+
You could also just have a queue for each user, but at scale, this can get crazy and many
|
46
|
+
queuing systems don't perform well with thousands of queues.
|
41
47
|
|
42
|
-
|
48
|
+
## Queuing messages
|
43
49
|
|
44
|
-
|
50
|
+
In order to queue messages, you need to register a queue. You can register multiple queues,
|
51
|
+
and each queue will receive delivered messages.
|
45
52
|
|
46
|
-
|
53
|
+
Registering a queue is part of your fairway configuration:
|
47
54
|
|
48
|
-
|
55
|
+
Fairway.configure do |config|
|
56
|
+
config.register_queue("myqueue")
|
57
|
+
config.register_queue("yourqueue")
|
58
|
+
end
|
49
59
|
|
50
|
-
|
51
|
-
|
52
|
-
config.namespace = "letsdrive"
|
60
|
+
After configuring your queues, just create a fairway connection,
|
61
|
+
and it'll handle persisting your queues in Redis:
|
53
62
|
|
54
|
-
|
55
|
-
message[:user_id]
|
56
|
-
end
|
63
|
+
connection = Fairway::Connection.new
|
57
64
|
|
58
|
-
|
59
|
-
"#{message[:user_id]}:#{message[:type]}"
|
60
|
-
end
|
61
|
-
end
|
65
|
+
## Delivering messages
|
62
66
|
|
63
|
-
|
67
|
+
To add messages to your queues, you deliver them:
|
64
68
|
|
65
|
-
|
66
|
-
|
67
|
-
config.namespace = nil
|
69
|
+
connection = Fairway::Connection.new
|
70
|
+
connection.deliver(type: "invite_friends", user: "bob", friends: ["nancy", "john"])
|
68
71
|
|
69
|
-
|
70
|
-
|
71
|
-
end
|
72
|
+
Now, any registered queues will receive this message, faceted if you've defined
|
73
|
+
a facet in your configuration.
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
-
|
75
|
+
## Consuming messages from a queue
|
76
|
+
|
77
|
+
Once you have messages on a queue, you can pull them off and process them:
|
78
|
+
|
79
|
+
connection = Fairway::Connection.new
|
80
|
+
queue = Fairway::Queue.new(connection, "myqueue")
|
81
|
+
message = queue.pull
|
82
|
+
|
83
|
+
Behind the scenes, fairway uses a round-robin strategy to ensure equal weighting of
|
84
|
+
any facets which contain messages.
|
85
|
+
|
86
|
+
If there are no messages in any facets, `queue.pull` will return `nil`.
|
87
|
+
|
88
|
+
## Channeling messages
|
89
|
+
|
90
|
+
In many cases, you don't want all messages delivered to every queue. You'd like
|
91
|
+
to filter which messages a queue receives.
|
92
|
+
|
93
|
+
You can accomplish this with message channels. By default, all messages use the `default`
|
94
|
+
channel. You can customize this by creating a `Fairway::ChanneledConnection` and
|
95
|
+
a block which defines the channel for a given message:
|
96
|
+
|
97
|
+
conn = Fairway::Connection.new
|
98
|
+
conn = Fairway::ChanneledConnection.new(conn) do |message|
|
99
|
+
message[:type]
|
76
100
|
end
|
77
101
|
|
78
|
-
|
102
|
+
You can also register queues for a channel:
|
79
103
|
|
80
|
-
|
104
|
+
Fairway.configure do |config|
|
105
|
+
config.register_queue("invite_queue", "invite_friends")
|
106
|
+
end
|
107
|
+
|
108
|
+
Now, your queue will only receive messages which have the channel `invite_friends`.
|
81
109
|
|
82
|
-
|
110
|
+
If you'd like to receive messages with channels that match a pattern:
|
83
111
|
|
84
|
-
|
112
|
+
Fairway.configure do |config|
|
113
|
+
config.register_queue("invite_queue", "invite_.*")
|
114
|
+
end
|
85
115
|
|
86
|
-
|
87
|
-
|
116
|
+
Now, messages from the channels `invite_friends`, `invite_pets`, `invite_parents` will
|
117
|
+
be delivered to the `invite_queue`.
|
88
118
|
|
89
|
-
|
119
|
+
## Subscribing to messages
|
90
120
|
|
91
|
-
|
121
|
+
To listen for messages without the overhead of queuing them, you can subscribe:
|
92
122
|
|
93
|
-
|
123
|
+
connection = Fairway::Connection.new
|
94
124
|
|
95
|
-
|
96
|
-
|
97
|
-
puts "[#{channel}] #{message}"
|
98
|
-
end
|
125
|
+
connection.subscribe do |message|
|
126
|
+
# Do something with each message
|
99
127
|
end
|
100
128
|
|
101
|
-
If you'
|
129
|
+
If you'd like to only receive some messages, you can subscribe to just a particular channel:
|
102
130
|
|
103
|
-
|
131
|
+
connection = Fairway::Connection.new
|
104
132
|
|
105
|
-
*
|
106
|
-
|
133
|
+
connection.subscribe("invite_*") do |message|
|
134
|
+
# Do something with each message which
|
135
|
+
# has a channel matching "invite_*"
|
136
|
+
end
|
107
137
|
|
108
|
-
|
138
|
+
## Fairway and Sidekiq
|
109
139
|
|
110
|
-
|
140
|
+
Fairway isn't meant to be a robust system for processing queued messages/jobs. To more reliably
|
141
|
+
process queued messages, we've integrated with [Sidekiq](http://sidekiq.org/).
|
111
142
|
|
112
|
-
|
143
|
+
require 'fairway/sidekiq'
|
113
144
|
|
114
|
-
|
145
|
+
connection = Fairway::Connection.new
|
146
|
+
queues = Fairway::Queue.new(connection, "myqueue", "yourqueue")
|
115
147
|
|
116
|
-
|
117
|
-
from
|
148
|
+
Sidekiq.options[:fetch] = Fairway::Sidekiq::Fetch.new do |fetch|
|
149
|
+
fetch.from :sidekiq, 2
|
150
|
+
fetch.from queues, 1 do |queue, message|
|
151
|
+
# translate message to normalized Sidekiq job, if needed
|
152
|
+
{ "queue" => "fairway",
|
153
|
+
"class" => "FairwayMessageJob",
|
154
|
+
"args" => [message],
|
155
|
+
"retry" => true }
|
156
|
+
end
|
157
|
+
end
|
118
158
|
|
119
|
-
|
159
|
+
`fetch.from :sidekiq, 2` will fetch from sidekiq queues you have defined through the
|
160
|
+
normal sidekiq configuration.
|
120
161
|
|
121
|
-
|
162
|
+
`fetch.from queues, 1` will pull messages from your fairway queue, and allow you to translate
|
163
|
+
them into standard sidekiq jobs.
|
164
|
+
|
165
|
+
The second parameters are fetch weights, so in the above example, we'll look for jobs first from
|
166
|
+
your normal sidekiq queues twice as often as your fairway queues.
|
167
|
+
|
168
|
+
## Queue structure
|
169
|
+
|
170
|
+
TODO: low level description of what's going on? performance?
|
171
|
+
|
172
|
+
## LUA scripting
|
173
|
+
|
174
|
+
Fairway uses [LUA scripting](http://redis.io/commands/eval) heavily. This is for a few reasons:
|
175
|
+
|
176
|
+
* There is complex logic that can't be expressed in normal redis commands.
|
177
|
+
* Each script contains many redis commands and it's important that these
|
178
|
+
commands are processed atomically.
|
179
|
+
* Since the script is run inside of redis, once the script has started,
|
180
|
+
there's very low latency for each redis command. So, the script executes
|
181
|
+
much faster than if we made each call independantly over the network.
|
122
182
|
|
123
|
-
|
183
|
+
This means your must be using a Redis version `>= 2.6.0`
|
data/lib/fairway/config.rb
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
module Fairway
|
2
2
|
class Config
|
3
3
|
attr_accessor :namespace
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :defined_queues
|
5
5
|
|
6
6
|
DEFAULT_FACET = "default"
|
7
7
|
|
8
8
|
QueueDefinition = Struct.new(:name, :channel)
|
9
9
|
|
10
10
|
def initialize
|
11
|
-
@redis_options
|
12
|
-
@namespace
|
13
|
-
@facet
|
14
|
-
@
|
11
|
+
@redis_options = {}
|
12
|
+
@namespace = nil
|
13
|
+
@facet = lambda { |message| DEFAULT_FACET }
|
14
|
+
@defined_queues = []
|
15
|
+
|
15
16
|
yield self if block_given?
|
16
17
|
end
|
17
18
|
|
@@ -24,7 +25,7 @@ module Fairway
|
|
24
25
|
end
|
25
26
|
|
26
27
|
def register_queue(name, channel = Connection::DEFAULT_CHANNEL)
|
27
|
-
@
|
28
|
+
@defined_queues << QueueDefinition.new(name, channel)
|
28
29
|
end
|
29
30
|
|
30
31
|
def redis=(options)
|
@@ -36,19 +37,11 @@ module Fairway
|
|
36
37
|
end
|
37
38
|
|
38
39
|
def scripts
|
39
|
-
@scripts ||= Scripts.new(raw_redis,
|
40
|
+
@scripts ||= Scripts.new(raw_redis, @namespace)
|
40
41
|
end
|
41
42
|
|
42
43
|
private
|
43
44
|
|
44
|
-
def scripts_namespace
|
45
|
-
if @namespace.blank?
|
46
|
-
""
|
47
|
-
else
|
48
|
-
"#{@namespace}:"
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
45
|
def raw_redis
|
53
46
|
@raw_redis ||= Redis.new(@redis_options.merge(hiredis: true))
|
54
47
|
end
|
data/lib/fairway/connection.rb
CHANGED
@@ -7,8 +7,16 @@ module Fairway
|
|
7
7
|
def initialize(config = Fairway.config)
|
8
8
|
@config = config
|
9
9
|
|
10
|
-
@config.
|
11
|
-
scripts.
|
10
|
+
@config.defined_queues.each do |queue|
|
11
|
+
scripts.register_queue(queue.name, queue.channel)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def queues
|
16
|
+
@queues ||= begin
|
17
|
+
scripts.registered_queues.map do |name, _|
|
18
|
+
Queue.new(self, name)
|
19
|
+
end
|
12
20
|
end
|
13
21
|
end
|
14
22
|
|
@@ -1,7 +1,9 @@
|
|
1
1
|
module Fairway
|
2
|
-
class
|
2
|
+
class Queue
|
3
|
+
attr_reader :connection, :queue_names
|
4
|
+
|
3
5
|
def initialize(connection, *queue_names)
|
4
|
-
@connection
|
6
|
+
@connection = connection
|
5
7
|
@queue_names = [queue_names].flatten!
|
6
8
|
end
|
7
9
|
|
@@ -12,5 +14,12 @@ module Fairway
|
|
12
14
|
def pull
|
13
15
|
@connection.scripts.fairway_pull(@queue_names)
|
14
16
|
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
other.respond_to?(:connection) &&
|
20
|
+
other.respond_to?(:queue_names) &&
|
21
|
+
connection == other.connection &&
|
22
|
+
queue_names == other.queue_names
|
23
|
+
end
|
15
24
|
end
|
16
25
|
end
|
data/lib/fairway/scripts.rb
CHANGED
@@ -12,9 +12,17 @@ module Fairway
|
|
12
12
|
@namespace = namespace
|
13
13
|
end
|
14
14
|
|
15
|
+
def register_queue(name, channel)
|
16
|
+
@redis.hset(registered_queues_key, name, channel)
|
17
|
+
end
|
18
|
+
|
19
|
+
def registered_queues
|
20
|
+
@redis.hgetall(registered_queues_key)
|
21
|
+
end
|
22
|
+
|
15
23
|
def method_missing(method_name, *args)
|
16
24
|
loaded = false
|
17
|
-
@redis.evalsha(script_sha(method_name), [
|
25
|
+
@redis.evalsha(script_sha(method_name), [namespace], args)
|
18
26
|
rescue Redis::CommandError => ex
|
19
27
|
if ex.message.include?("NOSCRIPT") && !loaded
|
20
28
|
@redis.script(:load, script_source(method_name))
|
@@ -27,6 +35,14 @@ module Fairway
|
|
27
35
|
|
28
36
|
private
|
29
37
|
|
38
|
+
def registered_queues_key
|
39
|
+
"#{namespace}registered_queues"
|
40
|
+
end
|
41
|
+
|
42
|
+
def namespace
|
43
|
+
@namespace.blank? ? "" : "#{@namespace}:"
|
44
|
+
end
|
45
|
+
|
30
46
|
def script_sha(name)
|
31
47
|
self.class.script_shas[name] ||= Digest::SHA1.hexdigest(script_source(name))
|
32
48
|
end
|
@@ -38,6 +54,5 @@ module Fairway
|
|
38
54
|
def script_path(name)
|
39
55
|
Pathname.new(__FILE__).dirname.join("../../redis/#{name}.lua")
|
40
56
|
end
|
41
|
-
|
42
57
|
end
|
43
58
|
end
|
@@ -1,13 +1,15 @@
|
|
1
1
|
module Fairway
|
2
2
|
module Sidekiq
|
3
|
-
class
|
3
|
+
class BasicFetch < ::Sidekiq::BasicFetch
|
4
4
|
attr_reader :queues
|
5
5
|
|
6
6
|
def initialize(options)
|
7
7
|
@queues = options[:queues].map { |q| "queue:#{q}" }
|
8
8
|
end
|
9
9
|
|
10
|
-
def retrieve_work
|
10
|
+
def retrieve_work(options = {})
|
11
|
+
options = { blocking: true }.merge(options)
|
12
|
+
|
11
13
|
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work #{queues_cmd}"
|
12
14
|
|
13
15
|
work = ::Sidekiq.redis do |conn|
|
@@ -32,6 +34,7 @@ module Fairway
|
|
32
34
|
work = UnitOfWork.new(*work)
|
33
35
|
else
|
34
36
|
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got nil"
|
37
|
+
sleep 1 if options[:blocking]
|
35
38
|
end
|
36
39
|
|
37
40
|
work
|
@@ -1,18 +1,20 @@
|
|
1
|
-
require "sidekiq/fetch"
|
2
|
-
|
3
1
|
module Fairway
|
4
2
|
module Sidekiq
|
5
|
-
class
|
6
|
-
|
7
|
-
|
3
|
+
class FairwayFetch < ::Sidekiq::BasicFetch
|
4
|
+
attr_reader :queues
|
5
|
+
|
6
|
+
def initialize(queues, &block)
|
7
|
+
@queues = queues
|
8
8
|
@message_to_job = block if block_given?
|
9
9
|
end
|
10
10
|
|
11
|
-
def retrieve_work
|
11
|
+
def retrieve_work(options = {})
|
12
|
+
options = { blocking: true }.merge(options)
|
13
|
+
|
12
14
|
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work"
|
13
15
|
unit_of_work = nil
|
14
16
|
|
15
|
-
fairway_queue, work = @
|
17
|
+
fairway_queue, work = @queues.pull
|
16
18
|
|
17
19
|
if work
|
18
20
|
decoded_work = JSON.parse(work)
|
@@ -29,10 +31,15 @@ module Fairway
|
|
29
31
|
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got work"
|
30
32
|
else
|
31
33
|
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got nil"
|
34
|
+
sleep 1 if options[:blocking]
|
32
35
|
end
|
33
36
|
|
34
37
|
unit_of_work
|
35
38
|
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
other.respond_to?(:queues) && queues == other.queues
|
42
|
+
end
|
36
43
|
end
|
37
44
|
end
|
38
45
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Fairway
|
2
|
+
module Sidekiq
|
3
|
+
class Fetch
|
4
|
+
class Fetches
|
5
|
+
attr_reader :list
|
6
|
+
|
7
|
+
def from(queue, weight = 1, &block)
|
8
|
+
if queue == :sidekiq
|
9
|
+
queue = BasicFetch.new(::Sidekiq.options)
|
10
|
+
else
|
11
|
+
queue = FairwayFetch.new(queue, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
weight.times do
|
15
|
+
list << queue
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def list
|
20
|
+
@list ||= []
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(&block)
|
25
|
+
yield(@fetches = Fetches.new)
|
26
|
+
end
|
27
|
+
|
28
|
+
def new(options)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetches
|
33
|
+
@fetches.list
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch_order
|
37
|
+
fetches.shuffle.uniq
|
38
|
+
end
|
39
|
+
|
40
|
+
def retrieve_work
|
41
|
+
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work"
|
42
|
+
|
43
|
+
fetch_order.each do |fetch|
|
44
|
+
work = fetch.retrieve_work(blocking: false)
|
45
|
+
|
46
|
+
if work
|
47
|
+
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got work"
|
48
|
+
return work
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got nil"
|
53
|
+
sleep 1
|
54
|
+
|
55
|
+
return nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/fairway/sidekiq.rb
CHANGED
@@ -1,20 +1,14 @@
|
|
1
1
|
require "sidekiq"
|
2
|
-
require "sidekiq/
|
2
|
+
require "sidekiq/fetch"
|
3
3
|
|
4
|
-
require "fairway/sidekiq/
|
5
|
-
require "fairway/sidekiq/
|
6
|
-
require "fairway/sidekiq/
|
7
|
-
require "fairway/sidekiq/non_blocking_fetch"
|
8
|
-
require "fairway/sidekiq/queue_fetch"
|
4
|
+
require "fairway/sidekiq/fetch"
|
5
|
+
require "fairway/sidekiq/basic_fetch"
|
6
|
+
require "fairway/sidekiq/fairway_fetch"
|
9
7
|
|
10
|
-
# conn
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# queue_fetch = Fairway::Sidekiq::QueueFetch.new(queue_reader) do |message|
|
14
|
-
# # Transform message into a sidekiq job
|
15
|
-
# message
|
16
|
-
# end
|
8
|
+
# conn = Fairway::Connection.new
|
9
|
+
# queues = Fairway::Queue.new(conn, "queue1", "queue2")
|
17
10
|
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
11
|
+
# Sidekiq.options[:fetch] = Fairway::Sidekiq::Fetch.new do |fetch|
|
12
|
+
# fetch.from :sidekiq, 1
|
13
|
+
# fetch.from queues, 1
|
14
|
+
# end
|
data/lib/fairway/version.rb
CHANGED
data/lib/fairway.rb
CHANGED
@@ -9,7 +9,7 @@ require "fairway/config"
|
|
9
9
|
require "fairway/scripts"
|
10
10
|
require "fairway/channeled_connection"
|
11
11
|
require "fairway/connection"
|
12
|
-
require "fairway/
|
12
|
+
require "fairway/queue"
|
13
13
|
|
14
14
|
module Fairway
|
15
15
|
def self.config
|
@@ -17,6 +17,11 @@ module Fairway
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.configure
|
20
|
-
yield(config)
|
20
|
+
yield(config)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.reconfigure
|
24
|
+
@config = Config.new
|
25
|
+
yield(config)
|
21
26
|
end
|
22
27
|
end
|
@@ -23,6 +23,16 @@ module Fairway
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
+
describe "#queues" do
|
27
|
+
it "returns a Queue for every currently registered queue" do
|
28
|
+
Fairway.config.redis.hset("registered_queues", "name", "channel")
|
29
|
+
|
30
|
+
connection.queues.should == [
|
31
|
+
Queue.new(connection, "name")
|
32
|
+
]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
26
36
|
describe "#deliver" do
|
27
37
|
it "publishes message over the message topic channel" do
|
28
38
|
redis = Redis.new
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Fairway::Sidekiq
|
4
|
+
describe Fetch do
|
5
|
+
describe "#initialize" do
|
6
|
+
it "accepts a block to define of fetches with priority" do
|
7
|
+
fetch = Fetch.new do |fetch|
|
8
|
+
fetch.from :fetchA, 10
|
9
|
+
fetch.from :fetchB, 1
|
10
|
+
end
|
11
|
+
|
12
|
+
fetchA = FairwayFetch.new(:fetchA)
|
13
|
+
fetchB = FairwayFetch.new(:fetchB)
|
14
|
+
|
15
|
+
fetch.fetches.should == [Array.new(10, fetchA), fetchB].flatten
|
16
|
+
end
|
17
|
+
|
18
|
+
it "instantiates a BasicFetch if you fetch from the keyword :sidekiq" do
|
19
|
+
fetch = Fetch.new do |fetch|
|
20
|
+
fetch.from :sidekiq, 1
|
21
|
+
end
|
22
|
+
|
23
|
+
fetch.fetches.length.should == 1
|
24
|
+
fetch.fetches.first.should be_instance_of(BasicFetch)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "instantiates a FairwayFetch if you fetch from a queue object" do
|
28
|
+
queue = Fairway::Queue.new(Fairway::Connection.new, "fairway")
|
29
|
+
|
30
|
+
fetch = Fetch.new do |fetch|
|
31
|
+
fetch.from queue, 1
|
32
|
+
end
|
33
|
+
|
34
|
+
fetch.fetches.length.should == 1
|
35
|
+
fetch.fetches.first.should be_instance_of(FairwayFetch)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#new" do
|
40
|
+
it "returns itself to match Sidekiq fetch API" do
|
41
|
+
fetch = Fetch.new do |fetch|
|
42
|
+
fetch.from :fetchA, 1
|
43
|
+
end
|
44
|
+
|
45
|
+
fetch.new({}).should == fetch
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#fetch_order" do
|
50
|
+
let(:fetch) { Fetch.new { |f| f.from :fetchA, 10; f.from :fetchB, 1 } }
|
51
|
+
|
52
|
+
it "should shuffle and uniq fetches" do
|
53
|
+
fetch.fetches.should_receive(:shuffle).and_return(fetch.fetches)
|
54
|
+
fetch.fetch_order
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should unique fetches list" do
|
58
|
+
fetch.fetches.length.should == 11
|
59
|
+
fetch.fetch_order.length.should == 2
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#retrieve_work" do
|
64
|
+
let(:work) { mock(:work) }
|
65
|
+
let(:fetchA) { mock(:fetch) }
|
66
|
+
let(:fetchB) { mock(:fetch) }
|
67
|
+
let(:fetch) { Fetch.new { |f| f.from :fetchA, 10; f.from :fetchB, 1 } }
|
68
|
+
|
69
|
+
before do
|
70
|
+
fetch.stub(fetch_order: [fetchA, fetchB], sleep: nil)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "returns work from the first fetch who has work" do
|
74
|
+
fetchA.stub(retrieve_work: work)
|
75
|
+
fetchB.should_not_receive(:retrieve_work)
|
76
|
+
|
77
|
+
fetch.retrieve_work.should == work
|
78
|
+
end
|
79
|
+
|
80
|
+
it "attempts to retrieve work from each fetch in a non blocking fashion" do
|
81
|
+
fetchA.should_receive(:retrieve_work).with(blocking: false)
|
82
|
+
fetchB.should_receive(:retrieve_work).with(blocking: false)
|
83
|
+
fetch.retrieve_work.should be_nil
|
84
|
+
end
|
85
|
+
|
86
|
+
it "sleeps if no work is found" do
|
87
|
+
fetch.should_receive(:sleep).with(1)
|
88
|
+
|
89
|
+
fetchA.stub(retrieve_work: nil)
|
90
|
+
fetchB.stub(retrieve_work: nil)
|
91
|
+
|
92
|
+
fetch.retrieve_work
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
module Fairway
|
4
|
-
describe
|
4
|
+
describe Queue do
|
5
5
|
let(:connection) do
|
6
6
|
c = Connection.new(Fairway.config)
|
7
7
|
ChanneledConnection.new(c) do |message|
|
@@ -12,31 +12,31 @@ module Fairway
|
|
12
12
|
|
13
13
|
describe "#initialize" do
|
14
14
|
it "requires a Connection and queue names" do
|
15
|
-
lambda {
|
15
|
+
lambda { Queue.new }.should raise_error(ArgumentError)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
19
|
describe "#length" do
|
20
|
-
let(:
|
20
|
+
let(:queue) { Queue.new(connection, "myqueue") }
|
21
21
|
|
22
22
|
before do
|
23
23
|
Fairway.config.register_queue("myqueue", "event:helloworld")
|
24
24
|
end
|
25
25
|
|
26
26
|
it "returns the number of queued messages across facets" do
|
27
|
-
|
27
|
+
queue.length.should == 0
|
28
28
|
|
29
29
|
connection.deliver(message.merge(facet: 1, message: 1))
|
30
30
|
connection.deliver(message.merge(facet: 1, message: 2))
|
31
31
|
connection.deliver(message.merge(facet: 2, message: 3))
|
32
32
|
|
33
|
-
|
33
|
+
queue.length.should == 3
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
queue.pull
|
36
|
+
queue.pull
|
37
|
+
queue.pull
|
38
38
|
|
39
|
-
|
39
|
+
queue.length.should == 0
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
@@ -49,9 +49,9 @@ module Fairway
|
|
49
49
|
connection.deliver(message1 = message.merge(message: 1))
|
50
50
|
connection.deliver(message2 = message.merge(message: 2))
|
51
51
|
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
queue = Queue.new(connection, "myqueue")
|
53
|
+
queue.pull.should == ["myqueue", message1.to_json]
|
54
|
+
queue.pull.should == ["myqueue", message2.to_json]
|
55
55
|
end
|
56
56
|
|
57
57
|
it "pulls from facets of the queue in a round-robin nature" do
|
@@ -59,27 +59,27 @@ module Fairway
|
|
59
59
|
connection.deliver(message2 = message.merge(facet: 1, message: 2))
|
60
60
|
connection.deliver(message3 = message.merge(facet: 2, message: 3))
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
62
|
+
queue = Queue.new(connection, "myqueue")
|
63
|
+
queue.pull.should == ["myqueue", message1.to_json]
|
64
|
+
queue.pull.should == ["myqueue", message3.to_json]
|
65
|
+
queue.pull.should == ["myqueue", message2.to_json]
|
66
66
|
end
|
67
67
|
|
68
68
|
it "removes facet from active list if it becomes empty" do
|
69
69
|
connection.deliver(message)
|
70
70
|
|
71
71
|
Fairway.config.redis.smembers("myqueue:active_facets").should == ["1"]
|
72
|
-
|
73
|
-
|
72
|
+
queue = Queue.new(connection, "myqueue")
|
73
|
+
queue.pull
|
74
74
|
Fairway.config.redis.smembers("myqueue:active_facets").should be_empty
|
75
75
|
end
|
76
76
|
|
77
77
|
it "returns nil if there are no messages to retrieve" do
|
78
78
|
connection.deliver(message)
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
|
80
|
+
queue = Queue.new(connection, "myqueue")
|
81
|
+
queue.pull.should == ["myqueue", message.to_json]
|
82
|
+
queue.pull.should be_nil
|
83
83
|
end
|
84
84
|
|
85
85
|
context "pulling from multiple queues" do
|
@@ -92,14 +92,14 @@ module Fairway
|
|
92
92
|
connection.deliver(message1 = message.merge(topic: "event:1"))
|
93
93
|
connection.deliver(message2 = message.merge(topic: "event:2"))
|
94
94
|
|
95
|
-
|
96
|
-
|
97
|
-
|
95
|
+
queue = Queue.new(connection, "myqueue2", "myqueue1")
|
96
|
+
queue.pull.should == ["myqueue2", message2.to_json]
|
97
|
+
queue.pull.should == ["myqueue1", message1.to_json]
|
98
98
|
end
|
99
99
|
|
100
100
|
it "returns nil if no queues have messages" do
|
101
|
-
|
102
|
-
|
101
|
+
queue = Queue.new(connection, "myqueue2", "myqueue1")
|
102
|
+
queue.pull.should be_nil
|
103
103
|
end
|
104
104
|
|
105
105
|
it "pulls from facets of the queue in a round-robin nature" do
|
@@ -108,13 +108,28 @@ module Fairway
|
|
108
108
|
connection.deliver(message3 = message.merge(facet: 2, topic: "event:1"))
|
109
109
|
connection.deliver(message4 = message.merge(facet: 1, topic: "event:2"))
|
110
110
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
111
|
+
queue = Queue.new(connection, "myqueue2", "myqueue1")
|
112
|
+
queue.pull.should == ["myqueue2", message4.to_json]
|
113
|
+
queue.pull.should == ["myqueue1", message1.to_json]
|
114
|
+
queue.pull.should == ["myqueue1", message3.to_json]
|
115
|
+
queue.pull.should == ["myqueue1", message2.to_json]
|
116
116
|
end
|
117
117
|
end
|
118
118
|
end
|
119
|
+
|
120
|
+
describe "equality" do
|
121
|
+
it "should equal queues with same connection and queue names" do
|
122
|
+
Queue.new(connection, "a", "b", "c").should == Queue.new(connection, "a", "b", "c")
|
123
|
+
end
|
124
|
+
|
125
|
+
it "doesn't equal queues with different connection" do
|
126
|
+
new_conn = Connection.new(Fairway.config)
|
127
|
+
Queue.new(connection, "a", "b", "c").should_not == Queue.new(new_conn, "a", "b", "c")
|
128
|
+
end
|
129
|
+
|
130
|
+
it "doesn't equal queues with different queues" do
|
131
|
+
Queue.new(connection, "a", "b", "c").should_not == Queue.new(connection, "a", "b")
|
132
|
+
end
|
133
|
+
end
|
119
134
|
end
|
120
135
|
end
|
@@ -10,17 +10,36 @@ module Fairway
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
+
describe "#register_queue" do
|
14
|
+
let(:scripts) { Scripts.new(Redis.new, "foo") }
|
15
|
+
|
16
|
+
it "adds the queue and channel to the hash of registered queues" do
|
17
|
+
scripts.register_queue("name", "channel")
|
18
|
+
Redis.new.hgetall("foo:registered_queues").should == { "name" => "channel" }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#registered_queue" do
|
23
|
+
let(:scripts) { Scripts.new(Redis.new, "foo") }
|
24
|
+
|
25
|
+
it "returns hash of all registered queues and their channels" do
|
26
|
+
Redis.new.hset("foo:registered_queues", "first", "channel1")
|
27
|
+
Redis.new.hset("foo:registered_queues", "second", "channel2")
|
28
|
+
scripts.registered_queues.should == { "first" => "channel1", "second" => "channel2" }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
13
32
|
describe "#method_missing" do
|
14
33
|
let(:scripts) { Scripts.new(Redis.new, "foo") }
|
15
34
|
|
16
35
|
it "runs the script" do
|
17
|
-
scripts.
|
36
|
+
scripts.fairway_pull("namespace", "name")
|
18
37
|
end
|
19
38
|
|
20
39
|
context "when the script does not exist" do
|
21
40
|
it "loads the script" do
|
22
41
|
Redis.new.script(:flush)
|
23
|
-
scripts.
|
42
|
+
scripts.fairway_pull("namespace", "name")
|
24
43
|
end
|
25
44
|
end
|
26
45
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
module Fairway::Sidekiq
|
4
|
-
describe
|
4
|
+
describe BasicFetch do
|
5
5
|
let(:queues) { [:critical, :critical, :default] }
|
6
|
-
let(:fetch) {
|
6
|
+
let(:fetch) { BasicFetch.new(queues: queues) }
|
7
7
|
|
8
8
|
it "accepts options with a list of queues and their weights" do
|
9
9
|
fetch.queues.should == ["queue:critical", "queue:critical", "queue:default"]
|
@@ -26,6 +26,16 @@ module Fairway::Sidekiq
|
|
26
26
|
unit_of_work.queue_name.should == "critical"
|
27
27
|
unit_of_work.message.should == "critical"
|
28
28
|
end
|
29
|
+
|
30
|
+
it "sleeps if no work is found" do
|
31
|
+
fetch.should_receive(:sleep).with(1)
|
32
|
+
fetch.retrieve_work
|
33
|
+
end
|
34
|
+
|
35
|
+
it "doesn't sleep if blocking option is false" do
|
36
|
+
fetch.should_not_receive(:sleep)
|
37
|
+
fetch.retrieve_work(blocking: false)
|
38
|
+
end
|
29
39
|
end
|
30
40
|
end
|
31
41
|
end
|
@@ -2,14 +2,13 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
module Fairway
|
4
4
|
module Sidekiq
|
5
|
-
describe
|
6
|
-
let(:
|
5
|
+
describe FairwayFetch do
|
6
|
+
let(:queue) { Queue.new(Connection.new, "fairway") }
|
7
7
|
let(:work) { { queue: "golf_events", type: "swing", name: "putt" }.to_json }
|
8
|
+
let(:fetch) { FairwayFetch.new(queue) }
|
8
9
|
|
9
|
-
it "requests work from the queue
|
10
|
-
|
11
|
-
|
12
|
-
reader.stub(pull: ["fairway", work])
|
10
|
+
it "requests work from the queue queue" do
|
11
|
+
queue.stub(pull: ["fairway", work])
|
13
12
|
|
14
13
|
unit_of_work = fetch.retrieve_work
|
15
14
|
unit_of_work.queue_name.should == "golf_events"
|
@@ -17,19 +16,31 @@ module Fairway
|
|
17
16
|
end
|
18
17
|
|
19
18
|
it "allows transforming of the message into a job" do
|
20
|
-
fetch =
|
19
|
+
fetch = FairwayFetch.new(queue) do |fairway_queue, message|
|
21
20
|
{
|
22
21
|
"queue" => "my_#{message["queue"]}",
|
23
22
|
"class" => "GolfEventJob"
|
24
23
|
}
|
25
24
|
end
|
26
25
|
|
27
|
-
|
26
|
+
queue.stub(pull: ["fairway", work])
|
28
27
|
|
29
28
|
unit_of_work = fetch.retrieve_work
|
30
29
|
unit_of_work.queue_name.should == "my_golf_events"
|
31
30
|
unit_of_work.message.should == { "queue" => "my_golf_events", "class" => "GolfEventJob" }.to_json
|
32
31
|
end
|
32
|
+
|
33
|
+
it "sleeps if no work is found" do
|
34
|
+
fetch.should_receive(:sleep).with(1)
|
35
|
+
queue.stub(pull: nil)
|
36
|
+
fetch.retrieve_work
|
37
|
+
end
|
38
|
+
|
39
|
+
it "doesn't sleep if blocking option is false" do
|
40
|
+
fetch.should_not_receive(:sleep)
|
41
|
+
queue.stub(pull: nil)
|
42
|
+
fetch.retrieve_work(blocking: false)
|
43
|
+
end
|
33
44
|
end
|
34
45
|
end
|
35
46
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -22,7 +22,7 @@ Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f}
|
|
22
22
|
|
23
23
|
RSpec.configure do |config|
|
24
24
|
config.before(:each) do
|
25
|
-
Fairway.
|
25
|
+
Fairway.reconfigure do |config|
|
26
26
|
config.namespace = "test:fairway"
|
27
27
|
config.facet { |message| message[:facet] }
|
28
28
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fairway
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
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: 2013-02-
|
12
|
+
date: 2013-02-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -94,27 +94,23 @@ files:
|
|
94
94
|
- lib/fairway/channeled_connection.rb
|
95
95
|
- lib/fairway/config.rb
|
96
96
|
- lib/fairway/connection.rb
|
97
|
-
- lib/fairway/
|
97
|
+
- lib/fairway/queue.rb
|
98
98
|
- lib/fairway/scripts.rb
|
99
99
|
- lib/fairway/sidekiq.rb
|
100
|
-
- lib/fairway/sidekiq/
|
101
|
-
- lib/fairway/sidekiq/
|
102
|
-
- lib/fairway/sidekiq/
|
103
|
-
- lib/fairway/sidekiq/non_blocking_fetch.rb
|
104
|
-
- lib/fairway/sidekiq/queue_fetch.rb
|
100
|
+
- lib/fairway/sidekiq/basic_fetch.rb
|
101
|
+
- lib/fairway/sidekiq/fairway_fetch.rb
|
102
|
+
- lib/fairway/sidekiq/fetch.rb
|
105
103
|
- lib/fairway/version.rb
|
106
104
|
- redis/fairway_deliver.lua
|
107
105
|
- redis/fairway_pull.lua
|
108
|
-
- redis/fairway_register_queue.lua
|
109
106
|
- spec/lib/fairway/channeled_connection_spec.rb
|
110
107
|
- spec/lib/fairway/config_spec.rb
|
111
108
|
- spec/lib/fairway/connection_spec.rb
|
112
|
-
- spec/lib/fairway/
|
109
|
+
- spec/lib/fairway/fetch_spec.rb
|
110
|
+
- spec/lib/fairway/queue_spec.rb
|
113
111
|
- spec/lib/fairway/scripts_spec.rb
|
114
|
-
- spec/lib/fairway/sidekiq/
|
115
|
-
- spec/lib/fairway/sidekiq/
|
116
|
-
- spec/lib/fairway/sidekiq/non_blocking_fetch_spec.rb
|
117
|
-
- spec/lib/fairway/sidekiq/queue_fetch_spec.rb
|
112
|
+
- spec/lib/fairway/sidekiq/basic_fetch_spec.rb
|
113
|
+
- spec/lib/fairway/sidekiq/fairway_fetch_spec.rb
|
118
114
|
- spec/lib/fairway/subscription_spec.rb
|
119
115
|
- spec/spec_helper.rb
|
120
116
|
homepage: https://github.com/customerio/fairway
|
@@ -145,11 +141,10 @@ test_files:
|
|
145
141
|
- spec/lib/fairway/channeled_connection_spec.rb
|
146
142
|
- spec/lib/fairway/config_spec.rb
|
147
143
|
- spec/lib/fairway/connection_spec.rb
|
148
|
-
- spec/lib/fairway/
|
144
|
+
- spec/lib/fairway/fetch_spec.rb
|
145
|
+
- spec/lib/fairway/queue_spec.rb
|
149
146
|
- spec/lib/fairway/scripts_spec.rb
|
150
|
-
- spec/lib/fairway/sidekiq/
|
151
|
-
- spec/lib/fairway/sidekiq/
|
152
|
-
- spec/lib/fairway/sidekiq/non_blocking_fetch_spec.rb
|
153
|
-
- spec/lib/fairway/sidekiq/queue_fetch_spec.rb
|
147
|
+
- spec/lib/fairway/sidekiq/basic_fetch_spec.rb
|
148
|
+
- spec/lib/fairway/sidekiq/fairway_fetch_spec.rb
|
154
149
|
- spec/lib/fairway/subscription_spec.rb
|
155
150
|
- spec/spec_helper.rb
|
@@ -1,37 +0,0 @@
|
|
1
|
-
module Fairway
|
2
|
-
module Sidekiq
|
3
|
-
class CompositeFetch
|
4
|
-
attr_reader :fetches
|
5
|
-
|
6
|
-
def initialize(fetches)
|
7
|
-
@fetches = []
|
8
|
-
|
9
|
-
fetches.each do |fetch, weight|
|
10
|
-
[weight.to_i, 1].max.times do
|
11
|
-
@fetches << fetch
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def fetch_order
|
17
|
-
fetches.shuffle.uniq
|
18
|
-
end
|
19
|
-
|
20
|
-
def retrieve_work
|
21
|
-
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work"
|
22
|
-
|
23
|
-
fetch_order.each do |fetch|
|
24
|
-
work = fetch.retrieve_work
|
25
|
-
|
26
|
-
if work
|
27
|
-
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got work"
|
28
|
-
return work
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got nil"
|
33
|
-
return nil
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
@@ -1,37 +0,0 @@
|
|
1
|
-
module Fairway
|
2
|
-
module Sidekiq
|
3
|
-
class Fetcher < ::Sidekiq::Fetcher
|
4
|
-
attr_reader :mgr, :strategy
|
5
|
-
|
6
|
-
def initialize(mgr, fetch)
|
7
|
-
@mgr = mgr
|
8
|
-
@strategy = fetch
|
9
|
-
end
|
10
|
-
|
11
|
-
def done!
|
12
|
-
@done = true
|
13
|
-
end
|
14
|
-
|
15
|
-
def fetch
|
16
|
-
watchdog('Fetcher#fetch died') do
|
17
|
-
return if @done
|
18
|
-
|
19
|
-
begin
|
20
|
-
work = @strategy.retrieve_work
|
21
|
-
|
22
|
-
if work
|
23
|
-
@mgr.async.assign(work)
|
24
|
-
else
|
25
|
-
after(TIMEOUT) { fetch }
|
26
|
-
end
|
27
|
-
rescue => ex
|
28
|
-
logger.error("Error fetching message: #{ex}")
|
29
|
-
logger.error(ex.backtrace.first)
|
30
|
-
sleep(TIMEOUT)
|
31
|
-
after(0) { fetch }
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
module Fairway
|
2
|
-
module Sidekiq
|
3
|
-
class FetcherFactory
|
4
|
-
def initialize(fetch)
|
5
|
-
@fetch = fetch
|
6
|
-
end
|
7
|
-
|
8
|
-
def done!
|
9
|
-
@fetcher.done!
|
10
|
-
end
|
11
|
-
|
12
|
-
def strategy
|
13
|
-
# This is only used for ::Sidekiq::BasicFetch.bulk_requeue
|
14
|
-
# which is the same for us.
|
15
|
-
::Sidekiq::BasicFetch
|
16
|
-
end
|
17
|
-
|
18
|
-
def new(mgr, options)
|
19
|
-
@fetcher = Fetcher.new(mgr, @fetch)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,50 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
module Fairway::Sidekiq
|
4
|
-
describe CompositeFetch do
|
5
|
-
describe "#initialize" do
|
6
|
-
it "accepts a hash of fetches with priority" do
|
7
|
-
fetcher = CompositeFetch.new(fetcherA: 10, fetcherB: 1)
|
8
|
-
fetcher.fetches.should == [Array.new(10, :fetcherA), :fetcherB].flatten
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
describe "#fetch_order" do
|
13
|
-
let(:fetcher) { CompositeFetch.new(fetcherA: 10, fetcherB: 1) }
|
14
|
-
|
15
|
-
it "should shuffle and uniq fetches" do
|
16
|
-
fetcher.fetches.should_receive(:shuffle).and_return(fetcher.fetches)
|
17
|
-
fetcher.fetch_order
|
18
|
-
end
|
19
|
-
|
20
|
-
it "should unique fetches list" do
|
21
|
-
fetcher.fetches.length.should == 11
|
22
|
-
fetcher.fetch_order.length.should == 2
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
describe "#retrieve_work" do
|
27
|
-
let(:work) { mock(:work) }
|
28
|
-
let(:fetcherA) { mock(:fetcher) }
|
29
|
-
let(:fetcherB) { mock(:fetcher) }
|
30
|
-
let(:fetcher) { CompositeFetch.new(fetcherA => 10, fetcherB => 1) }
|
31
|
-
|
32
|
-
before do
|
33
|
-
fetcher.stub(fetch_order: [fetcherA, fetcherB])
|
34
|
-
end
|
35
|
-
|
36
|
-
it "returns work from the first fetcher who has work" do
|
37
|
-
fetcherA.stub(retrieve_work: work)
|
38
|
-
fetcherB.should_not_receive(:retrieve_work)
|
39
|
-
|
40
|
-
fetcher.retrieve_work.should == work
|
41
|
-
end
|
42
|
-
|
43
|
-
it "attempts to retrieve work from each fetcher if no work is found" do
|
44
|
-
fetcherA.should_receive(:retrieve_work)
|
45
|
-
fetcherB.should_receive(:retrieve_work)
|
46
|
-
fetcher.retrieve_work.should be_nil
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
module Fairway::Sidekiq
|
4
|
-
describe Fetcher do
|
5
|
-
let(:manager) { mock(:manager) }
|
6
|
-
let(:fetch) { mock(:fetch) }
|
7
|
-
|
8
|
-
it "accepts a manager and a fetch strategy" do
|
9
|
-
fetcher = Fetcher.new(manager, fetch)
|
10
|
-
fetcher.mgr.should == manager
|
11
|
-
fetcher.strategy.should == fetch
|
12
|
-
end
|
13
|
-
|
14
|
-
describe "#fetch" do
|
15
|
-
let(:fetcher) { Fetcher.new(manager, fetch) }
|
16
|
-
|
17
|
-
it "retrieves work from fetch strategy" do
|
18
|
-
fetch.should_receive(:retrieve_work)
|
19
|
-
fetcher.fetch
|
20
|
-
end
|
21
|
-
|
22
|
-
it "tells manager to assign work if work is fetched" do
|
23
|
-
work = mock(:work)
|
24
|
-
fetch.stub(retrieve_work: work)
|
25
|
-
manager.stub(async: manager)
|
26
|
-
manager.should_receive(:assign).with(work)
|
27
|
-
fetcher.fetch
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|