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 CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fairway (0.0.3)
4
+ fairway (0.0.5)
5
5
  activesupport
6
6
  hiredis
7
7
  redis
data/README.markdown CHANGED
@@ -1,123 +1,183 @@
1
- # Fairway (redis backbone)
1
+ # Fairway - a fair way to queue messages in multi-user systems.
2
2
 
3
- ## Responsibilities
3
+ ## Installation
4
4
 
5
- * Allow consumption of messages from various sources
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
- ### Queue structure
7
+ gem install fairway
12
8
 
13
- Once a service registers a queue, with a regex for what messages should be
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
- In some cases, queuing systems can have problems in multi-user systems. If one user
17
- queues up a ton of messages, messages for other users may be delayed.
11
+ ## Configuration
18
12
 
19
- Driver solves that by allowing "facets" for each queue, which can be set to a facet
20
- for each user.
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
- Each facet is added to a list if it has messages waiting to be processed. This list
23
- is used to enforce a round-robin stategy for pulling messages off of the queue. This
24
- means we'll process one message for every facet which has messages queued, before
25
- looping back and processing additional messages.
21
+ config.register_queue("all_messages")
22
+ end
23
+
24
+ ## What's a facet?
26
25
 
27
- ### Redis LUA scripting
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
- Driver uses [LUA scripts](http://redis.io/commands/eval) inside of redis heavily. This is for a few reasons:
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
- * There is complex logic that can't be expressed in normal redis commands.
32
- * Each script contains many redis commands and it's important that these
33
- commands are processed atomically. A LUA script does that.
34
- * Since the script is run inside of redis, once the script has started,
35
- there's very low latency for each redis command. So, the script executes
36
- much faster than if we made each call independantly over the network.
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
- This means your Redis version must be `>= 2.6.0`
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
- ### Usage
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
- Add driver to your Gemfile
48
+ ## Queuing messages
43
49
 
44
- gem 'driver', git: 'git@github.com:customerio/driver.git'
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
- Make sure to `bundle install`.
53
+ Registering a queue is part of your fairway configuration:
47
54
 
48
- ##### Configure driver
55
+ Fairway.configure do |config|
56
+ config.register_queue("myqueue")
57
+ config.register_queue("yourqueue")
58
+ end
49
59
 
50
- Driver.configure do |config|
51
- config.redis = { host: "yourserver.com", port: 6379 }
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
- config.facet do |message|
55
- message[:user_id]
56
- end
63
+ connection = Fairway::Connection.new
57
64
 
58
- config.topic do |message|
59
- "#{message[:user_id]}:#{message[:type]}"
60
- end
61
- end
65
+ ## Delivering messages
62
66
 
63
- If you don't configure, it'll default to:
67
+ To add messages to your queues, you deliver them:
64
68
 
65
- Driver.configure do |config|
66
- config.redis = { host: "localhost", port: 6379 }
67
- config.namespace = nil
69
+ connection = Fairway::Connection.new
70
+ connection.deliver(type: "invite_friends", user: "bob", friends: ["nancy", "john"])
68
71
 
69
- config.facet do |message|
70
- message[:facet]
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
- config.topic do |message|
74
- message[:topic]
75
- end
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
- ##### Create an instance of driver
102
+ You can also register queues for a channel:
79
103
 
80
- driver = Driver::Client.new
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
- ##### Send messages
110
+ If you'd like to receive messages with channels that match a pattern:
83
111
 
84
- driver.deliver(facet: 1, type: :page, name: "http://customer.io/blog", referrer: "http://customer.io")
112
+ Fairway.configure do |config|
113
+ config.register_queue("invite_queue", "invite_.*")
114
+ end
85
115
 
86
- You can pass any hash of data you'd like. Using the default configuration, this message will have a topic
87
- of `page`, which can useful if you'd like to listen for, or process, messages.
116
+ Now, messages from the channels `invite_friends`, `invite_pets`, `invite_parents` will
117
+ be delivered to the `invite_queue`.
88
118
 
89
- ##### Listen for messages
119
+ ## Subscribing to messages
90
120
 
91
- If a message is sent in the middle of the forest, and no one is listening, was it ever really sent?
121
+ To listen for messages without the overhead of queuing them, you can subscribe:
92
122
 
93
- You can listen for messages that are delivered by driver by subscribing to message topics:
123
+ connection = Fairway::Connection.new
94
124
 
95
- driver.redis.psubscribe("page:*google*") do |on|
96
- on.pmessage do |pattern, channel, message|
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've configured your topic to be `"#{message[:type]}:#{message[:name]}`, this will listen for any page events with google in the name.
129
+ If you'd like to only receive some messages, you can subscribe to just a particular channel:
102
130
 
103
- Now, if you deliver a message, it'll be printed out on the console.
131
+ connection = Fairway::Connection.new
104
132
 
105
- *Note:* redis psubscribe is blocking. So, you'll need multiple console windows open.
106
- One to deliver the message, and one to listen for them.
133
+ connection.subscribe("invite_*") do |message|
134
+ # Do something with each message which
135
+ # has a channel matching "invite_*"
136
+ end
107
137
 
108
- ##### Create a queue
138
+ ## Fairway and Sidekiq
109
139
 
110
- Ok, so now you can listen to messages, but what if your listener dies and you miss all your important messages?
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
- Not to worry, you can tell driver to queue up any messages you want to know about.
143
+ require 'fairway/sidekiq'
113
144
 
114
- driver.register_queue("myqueue", ".*:page:.*google.*")
145
+ connection = Fairway::Connection.new
146
+ queues = Fairway::Queue.new(connection, "myqueue", "yourqueue")
115
147
 
116
- Now driver will deliver all page events with google in the name to the queue named `myqueue`. To retrieve messages
117
- from your queue:
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
- message = driver.pull("myqueue")
159
+ `fetch.from :sidekiq, 2` will fetch from sidekiq queues you have defined through the
160
+ normal sidekiq configuration.
120
161
 
121
- This will return a message or nil (if no messages are queued).
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
- *Note:* `pull` is facet aware, and will rotate through all facets with queued messages.
183
+ This means your must be using a Redis version `>= 2.6.0`
@@ -1,17 +1,18 @@
1
1
  module Fairway
2
2
  class Config
3
3
  attr_accessor :namespace
4
- attr_reader :queues
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 = nil
13
- @facet = lambda { |message| DEFAULT_FACET }
14
- @queues = []
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
- @queues << QueueDefinition.new(name, channel)
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, scripts_namespace)
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
@@ -7,8 +7,16 @@ module Fairway
7
7
  def initialize(config = Fairway.config)
8
8
  @config = config
9
9
 
10
- @config.queues.each do |queue|
11
- scripts.fairway_register_queue(queue.name, queue.channel)
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 QueueReader
2
+ class Queue
3
+ attr_reader :connection, :queue_names
4
+
3
5
  def initialize(connection, *queue_names)
4
- @connection = 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
@@ -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), [@namespace], args)
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 NonBlockingFetch < ::Sidekiq::BasicFetch
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 QueueFetch < ::Sidekiq::BasicFetch
6
- def initialize(queue_reader, &block)
7
- @queue_reader = queue_reader
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 = @queue_reader.pull
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
@@ -1,20 +1,14 @@
1
1
  require "sidekiq"
2
- require "sidekiq/manager"
2
+ require "sidekiq/fetch"
3
3
 
4
- require "fairway/sidekiq/composite_fetch"
5
- require "fairway/sidekiq/fetcher"
6
- require "fairway/sidekiq/fetcher_factory"
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 = Fairway::Connection.new
11
- # queue_reader = Fairway::QueueReader.new(conn, "fairway")
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
- # non_blocking_fetch = Fairway::Sidekiq::NonBlockingFetch.new(Sidekiq.options)
19
- # fetch = Fairway::Sidekiq::CompositeFetch.new(queue_fetch => 1, non_blocking_fetch => 1)
20
- # Sidekiq.options[:fetcher] = Fairway::Sidekiq::FetcherFactory.new(fetch)
11
+ # Sidekiq.options[:fetch] = Fairway::Sidekiq::Fetch.new do |fetch|
12
+ # fetch.from :sidekiq, 1
13
+ # fetch.from queues, 1
14
+ # end
@@ -1,3 +1,3 @@
1
1
  module Fairway
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.5"
3
3
  end
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/queue_reader"
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) if block_given?
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 QueueReader do
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 { QueueReader.new }.should raise_error(ArgumentError)
15
+ lambda { Queue.new }.should raise_error(ArgumentError)
16
16
  end
17
17
  end
18
18
 
19
19
  describe "#length" do
20
- let(:reader) { QueueReader.new(connection, "myqueue") }
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
- reader.length.should == 0
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
- reader.length.should == 3
33
+ queue.length.should == 3
34
34
 
35
- reader.pull
36
- reader.pull
37
- reader.pull
35
+ queue.pull
36
+ queue.pull
37
+ queue.pull
38
38
 
39
- reader.length.should == 0
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
- reader = QueueReader.new(connection, "myqueue")
53
- reader.pull.should == ["myqueue", message1.to_json]
54
- reader.pull.should == ["myqueue", message2.to_json]
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
- reader = QueueReader.new(connection, "myqueue")
63
- reader.pull.should == ["myqueue", message1.to_json]
64
- reader.pull.should == ["myqueue", message3.to_json]
65
- reader.pull.should == ["myqueue", message2.to_json]
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
- reader = QueueReader.new(connection, "myqueue")
73
- reader.pull
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
- reader = QueueReader.new(connection, "myqueue")
81
- reader.pull.should == ["myqueue", message.to_json]
82
- reader.pull.should be_nil
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
- reader = QueueReader.new(connection, "myqueue2", "myqueue1")
96
- reader.pull.should == ["myqueue2", message2.to_json]
97
- reader.pull.should == ["myqueue1", message1.to_json]
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
- reader = QueueReader.new(connection, "myqueue2", "myqueue1")
102
- reader.pull.should be_nil
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
- reader = QueueReader.new(connection, "myqueue2", "myqueue1")
112
- reader.pull.should == ["myqueue2", message4.to_json]
113
- reader.pull.should == ["myqueue1", message1.to_json]
114
- reader.pull.should == ["myqueue1", message3.to_json]
115
- reader.pull.should == ["myqueue1", message2.to_json]
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.fairway_register_queue("namespace", "name", "topic")
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.fairway_register_queue("namespace", "name", "topic")
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 NonBlockingFetch do
4
+ describe BasicFetch do
5
5
  let(:queues) { [:critical, :critical, :default] }
6
- let(:fetch) { NonBlockingFetch.new(queues: queues) }
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 QueueFetch do
6
- let(:reader) { QueueReader.new(Connection.new, "fairway") }
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 reader" do
10
- fetch = QueueFetch.new(reader)
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 = QueueFetch.new(reader) do |fairway_queue, message|
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
- reader.stub(pull: ["fairway", work])
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.configure do |config|
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
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-15 00:00:00.000000000 Z
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/queue_reader.rb
97
+ - lib/fairway/queue.rb
98
98
  - lib/fairway/scripts.rb
99
99
  - lib/fairway/sidekiq.rb
100
- - lib/fairway/sidekiq/composite_fetch.rb
101
- - lib/fairway/sidekiq/fetcher.rb
102
- - lib/fairway/sidekiq/fetcher_factory.rb
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/queue_reader_spec.rb
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/composite_fetch_spec.rb
115
- - spec/lib/fairway/sidekiq/fetcher_spec.rb
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/queue_reader_spec.rb
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/composite_fetch_spec.rb
151
- - spec/lib/fairway/sidekiq/fetcher_spec.rb
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,5 +0,0 @@
1
- local hash = KEYS[1] .. 'registered_queues';
2
- local queue_name = ARGV[1];
3
- local queue_message = ARGV[2];
4
-
5
- redis.call('hset', hash, queue_name, queue_message);
@@ -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