fairway 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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