fairway 0.0.1
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/.rbenv-gemsets +1 -0
- data/.rspec +1 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +50 -0
- data/README.markdown +123 -0
- data/Rakefile +1 -0
- data/boot.rb +7 -0
- data/fairway.gemspec +24 -0
- data/lib/fairway/channeled_connection.rb +17 -0
- data/lib/fairway/config.rb +57 -0
- data/lib/fairway/connection.rb +28 -0
- data/lib/fairway/queue_reader.rb +12 -0
- data/lib/fairway/scripts.rb +43 -0
- data/lib/fairway/sidekiq/composite_fetch.rb +37 -0
- data/lib/fairway/sidekiq/fetcher.rb +37 -0
- data/lib/fairway/sidekiq/fetcher_factory.rb +23 -0
- data/lib/fairway/sidekiq/non_blocking_fetch.rb +64 -0
- data/lib/fairway/sidekiq/queue_fetch.rb +33 -0
- data/lib/fairway/sidekiq.rb +20 -0
- data/lib/fairway/version.rb +3 -0
- data/lib/fairway.rb +22 -0
- data/redis/fairway_deliver.lua +25 -0
- data/redis/fairway_pull.lua +21 -0
- data/redis/fairway_register_queue.lua +5 -0
- data/spec/lib/fairway/channeled_connection_spec.rb +75 -0
- data/spec/lib/fairway/config_spec.rb +55 -0
- data/spec/lib/fairway/connection_spec.rb +79 -0
- data/spec/lib/fairway/queue_reader_spec.rb +101 -0
- data/spec/lib/fairway/scripts_spec.rb +28 -0
- data/spec/lib/fairway/sidekiq/composite_fetch_spec.rb +50 -0
- data/spec/lib/fairway/sidekiq/fetcher_spec.rb +31 -0
- data/spec/lib/fairway/sidekiq/non_blocking_fetch_spec.rb +31 -0
- data/spec/lib/fairway/sidekiq/queue_fetch_spec.rb +35 -0
- data/spec/lib/fairway/subscription_spec.rb +16 -0
- data/spec/spec_helper.rb +35 -0
- metadata +155 -0
data/.rbenv-gemsets
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
fairway
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
fairway (0.0.1)
|
5
|
+
activesupport
|
6
|
+
hiredis
|
7
|
+
redis
|
8
|
+
redis-namespace
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: http://rubygems.org/
|
12
|
+
specs:
|
13
|
+
activesupport (3.2.11)
|
14
|
+
i18n (~> 0.6)
|
15
|
+
multi_json (~> 1.0)
|
16
|
+
celluloid (0.12.4)
|
17
|
+
facter (>= 1.6.12)
|
18
|
+
timers (>= 1.0.0)
|
19
|
+
connection_pool (1.0.0)
|
20
|
+
diff-lcs (1.1.3)
|
21
|
+
facter (1.6.17)
|
22
|
+
hiredis (0.4.5)
|
23
|
+
i18n (0.6.1)
|
24
|
+
multi_json (1.5.0)
|
25
|
+
redis (3.0.2)
|
26
|
+
redis-namespace (1.2.1)
|
27
|
+
redis (~> 3.0.0)
|
28
|
+
rspec (2.12.0)
|
29
|
+
rspec-core (~> 2.12.0)
|
30
|
+
rspec-expectations (~> 2.12.0)
|
31
|
+
rspec-mocks (~> 2.12.0)
|
32
|
+
rspec-core (2.12.2)
|
33
|
+
rspec-expectations (2.12.1)
|
34
|
+
diff-lcs (~> 1.1.3)
|
35
|
+
rspec-mocks (2.12.2)
|
36
|
+
sidekiq (2.7.2)
|
37
|
+
celluloid (~> 0.12.0)
|
38
|
+
connection_pool (~> 1.0)
|
39
|
+
multi_json (~> 1)
|
40
|
+
redis (~> 3)
|
41
|
+
redis-namespace
|
42
|
+
timers (1.1.0)
|
43
|
+
|
44
|
+
PLATFORMS
|
45
|
+
ruby
|
46
|
+
|
47
|
+
DEPENDENCIES
|
48
|
+
fairway!
|
49
|
+
rspec
|
50
|
+
sidekiq
|
data/README.markdown
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# Fairway (redis backbone)
|
2
|
+
|
3
|
+
## Responsibilities
|
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
|
10
|
+
|
11
|
+
### Queue structure
|
12
|
+
|
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.
|
15
|
+
|
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.
|
18
|
+
|
19
|
+
Driver solves that by allowing "facets" for each queue, which can be set to a facet
|
20
|
+
for each user.
|
21
|
+
|
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.
|
26
|
+
|
27
|
+
### Redis LUA scripting
|
28
|
+
|
29
|
+
Driver uses [LUA scripts](http://redis.io/commands/eval) inside of redis heavily. This is for a few reasons:
|
30
|
+
|
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.
|
37
|
+
|
38
|
+
This means your Redis version must be `>= 2.6.0`
|
39
|
+
|
40
|
+
### Usage
|
41
|
+
|
42
|
+
Add driver to your Gemfile
|
43
|
+
|
44
|
+
gem 'driver', git: 'git@github.com:customerio/driver.git'
|
45
|
+
|
46
|
+
Make sure to `bundle install`.
|
47
|
+
|
48
|
+
##### Configure driver
|
49
|
+
|
50
|
+
Driver.configure do |config|
|
51
|
+
config.redis = { host: "yourserver.com", port: 6379 }
|
52
|
+
config.namespace = "letsdrive"
|
53
|
+
|
54
|
+
config.facet do |message|
|
55
|
+
message[:user_id]
|
56
|
+
end
|
57
|
+
|
58
|
+
config.topic do |message|
|
59
|
+
"#{message[:user_id]}:#{message[:type]}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
If you don't configure, it'll default to:
|
64
|
+
|
65
|
+
Driver.configure do |config|
|
66
|
+
config.redis = { host: "localhost", port: 6379 }
|
67
|
+
config.namespace = nil
|
68
|
+
|
69
|
+
config.facet do |message|
|
70
|
+
message[:facet]
|
71
|
+
end
|
72
|
+
|
73
|
+
config.topic do |message|
|
74
|
+
message[:topic]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
##### Create an instance of driver
|
79
|
+
|
80
|
+
driver = Driver::Client.new
|
81
|
+
|
82
|
+
##### Send messages
|
83
|
+
|
84
|
+
driver.deliver(facet: 1, type: :page, name: "http://customer.io/blog", referrer: "http://customer.io")
|
85
|
+
|
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.
|
88
|
+
|
89
|
+
##### Listen for messages
|
90
|
+
|
91
|
+
If a message is sent in the middle of the forest, and no one is listening, was it ever really sent?
|
92
|
+
|
93
|
+
You can listen for messages that are delivered by driver by subscribing to message topics:
|
94
|
+
|
95
|
+
driver.redis.psubscribe("page:*google*") do |on|
|
96
|
+
on.pmessage do |pattern, channel, message|
|
97
|
+
puts "[#{channel}] #{message}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
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.
|
102
|
+
|
103
|
+
Now, if you deliver a message, it'll be printed out on the console.
|
104
|
+
|
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.
|
107
|
+
|
108
|
+
##### Create a queue
|
109
|
+
|
110
|
+
Ok, so now you can listen to messages, but what if your listener dies and you miss all your important messages?
|
111
|
+
|
112
|
+
Not to worry, you can tell driver to queue up any messages you want to know about.
|
113
|
+
|
114
|
+
driver.register_queue("myqueue", ".*:page:.*google.*")
|
115
|
+
|
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:
|
118
|
+
|
119
|
+
message = driver.pull("myqueue")
|
120
|
+
|
121
|
+
This will return a message or nil (if no messages are queued).
|
122
|
+
|
123
|
+
*Note:* `pull` is facet aware, and will rotate through all facets with queued messages.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/boot.rb
ADDED
data/fairway.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fairway/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "fairway"
|
8
|
+
gem.version = Fairway::VERSION
|
9
|
+
gem.authors = ["John Allison"]
|
10
|
+
gem.email = ["john@customer.io"]
|
11
|
+
gem.description = "A fair way to queue work in multi-user systems."
|
12
|
+
gem.summary = "A fair way to queue work in multi-user systems."
|
13
|
+
gem.homepage = "https://github.com/customerio/fairway"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency("activesupport")
|
21
|
+
gem.add_dependency("redis")
|
22
|
+
gem.add_dependency("hiredis")
|
23
|
+
gem.add_dependency("redis-namespace")
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Fairway
|
2
|
+
class ChanneledConnection
|
3
|
+
def initialize(connection, &block)
|
4
|
+
@connection = connection
|
5
|
+
@block = block
|
6
|
+
end
|
7
|
+
|
8
|
+
def deliver(message)
|
9
|
+
channel = @block.call(message)
|
10
|
+
@connection.deliver(message, channel)
|
11
|
+
end
|
12
|
+
|
13
|
+
def scripts
|
14
|
+
@connection.scripts
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Fairway
|
2
|
+
class Config
|
3
|
+
attr_accessor :namespace
|
4
|
+
attr_reader :queues
|
5
|
+
|
6
|
+
DEFAULT_FACET = "default"
|
7
|
+
|
8
|
+
QueueDefinition = Struct.new(:name, :channel)
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@redis_options = {}
|
12
|
+
@namespace = nil
|
13
|
+
@facet = lambda { |message| DEFAULT_FACET }
|
14
|
+
@queues = []
|
15
|
+
yield self if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
def facet(&block)
|
19
|
+
if block_given?
|
20
|
+
@facet = block
|
21
|
+
else
|
22
|
+
@facet
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def register_queue(name, channel = Connection::DEFAULT_CHANNEL)
|
27
|
+
@queues << QueueDefinition.new(name, channel)
|
28
|
+
end
|
29
|
+
|
30
|
+
def redis=(options)
|
31
|
+
@redis_options = options
|
32
|
+
end
|
33
|
+
|
34
|
+
def redis
|
35
|
+
@redis ||= Redis::Namespace.new(@namespace, redis: raw_redis)
|
36
|
+
end
|
37
|
+
|
38
|
+
def scripts
|
39
|
+
@scripts ||= Scripts.new(raw_redis, scripts_namespace)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def scripts_namespace
|
45
|
+
if @namespace.blank?
|
46
|
+
""
|
47
|
+
else
|
48
|
+
"#{@namespace}:"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def raw_redis
|
53
|
+
@raw_redis ||= Redis.new(@redis_options.merge(hiredis: true))
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "fairway/scripts"
|
2
|
+
|
3
|
+
module Fairway
|
4
|
+
class Connection
|
5
|
+
DEFAULT_CHANNEL = "default"
|
6
|
+
|
7
|
+
def initialize(config = Fairway.config)
|
8
|
+
@config = config
|
9
|
+
|
10
|
+
@config.queues.each do |queue|
|
11
|
+
scripts.fairway_register_queue(queue.name, queue.channel)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def deliver(message, channel = DEFAULT_CHANNEL)
|
16
|
+
scripts.fairway_deliver(
|
17
|
+
channel,
|
18
|
+
@config.facet.call(message),
|
19
|
+
message.to_json
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def scripts
|
24
|
+
@config.scripts
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "digest/sha1"
|
3
|
+
|
4
|
+
module Fairway
|
5
|
+
class Scripts
|
6
|
+
def self.script_shas
|
7
|
+
@script_shas ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(redis, namespace)
|
11
|
+
@redis = redis
|
12
|
+
@namespace = namespace
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(method_name, *args)
|
16
|
+
loaded = false
|
17
|
+
@redis.evalsha(script_sha(method_name), [@namespace], args)
|
18
|
+
rescue Redis::CommandError => ex
|
19
|
+
if ex.message.include?("NOSCRIPT") && !loaded
|
20
|
+
@redis.script(:load, script_source(method_name))
|
21
|
+
loaded = true
|
22
|
+
retry
|
23
|
+
else
|
24
|
+
raise
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def script_sha(name)
|
31
|
+
self.class.script_shas[name] ||= Digest::SHA1.hexdigest(script_source(name))
|
32
|
+
end
|
33
|
+
|
34
|
+
def script_source(name)
|
35
|
+
script_path(name).read
|
36
|
+
end
|
37
|
+
|
38
|
+
def script_path(name)
|
39
|
+
Pathname.new(__FILE__).dirname.join("../../redis/#{name}.lua")
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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
|
@@ -0,0 +1,37 @@
|
|
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
|
@@ -0,0 +1,23 @@
|
|
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
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Fairway
|
2
|
+
module Sidekiq
|
3
|
+
class NonBlockingFetch < ::Sidekiq::BasicFetch
|
4
|
+
attr_reader :queues
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@queues = options[:queues].map { |q| "queue:#{q}" }
|
8
|
+
end
|
9
|
+
|
10
|
+
def retrieve_work
|
11
|
+
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work #{queues_cmd}"
|
12
|
+
|
13
|
+
work = ::Sidekiq.redis do |conn|
|
14
|
+
script = <<-SCRIPT
|
15
|
+
-- take advantage of non-blocking scripts
|
16
|
+
for i = 1, #KEYS do
|
17
|
+
local work = redis.call('rpop', KEYS[i]);
|
18
|
+
|
19
|
+
if work then
|
20
|
+
return {KEYS[i], work};
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
return nil;
|
25
|
+
SCRIPT
|
26
|
+
|
27
|
+
conn.eval(script, queues_cmd.map{|q| "#{namespace}#{q}"})
|
28
|
+
end
|
29
|
+
|
30
|
+
if (work)
|
31
|
+
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got work"
|
32
|
+
work = UnitOfWork.new(*work)
|
33
|
+
else
|
34
|
+
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got nil"
|
35
|
+
end
|
36
|
+
|
37
|
+
work
|
38
|
+
end
|
39
|
+
|
40
|
+
def queues_cmd
|
41
|
+
@queues.shuffle.uniq
|
42
|
+
end
|
43
|
+
|
44
|
+
def namespace
|
45
|
+
@namespace ||= begin
|
46
|
+
namespaces = []
|
47
|
+
|
48
|
+
::Sidekiq.redis do |conn|
|
49
|
+
wrapper = conn
|
50
|
+
|
51
|
+
while defined?(wrapper.redis)
|
52
|
+
namespaces.unshift(wrapper.namespace)
|
53
|
+
wrapper = wrapper.redis
|
54
|
+
end
|
55
|
+
|
56
|
+
namespace = namespaces.join(":")
|
57
|
+
namespace += ":" unless namespace.blank?
|
58
|
+
namespace
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "sidekiq/fetch"
|
2
|
+
|
3
|
+
module Fairway
|
4
|
+
module Sidekiq
|
5
|
+
class QueueFetch < ::Sidekiq::BasicFetch
|
6
|
+
def initialize(queue_reader, &block)
|
7
|
+
@queue_reader = queue_reader
|
8
|
+
@message_to_job = block if block_given?
|
9
|
+
end
|
10
|
+
|
11
|
+
def retrieve_work
|
12
|
+
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work"
|
13
|
+
unit_of_work = nil
|
14
|
+
|
15
|
+
fairway_queue, work = @queue_reader.pull
|
16
|
+
|
17
|
+
if work
|
18
|
+
decoded_work = JSON.parse(work)
|
19
|
+
work = @message_to_job.call(fairway_queue, decoded_work).to_json if @message_to_job
|
20
|
+
unit_of_work = UnitOfWork.new(decoded_work["queue"], work)
|
21
|
+
end
|
22
|
+
|
23
|
+
if unit_of_work
|
24
|
+
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got work"
|
25
|
+
else
|
26
|
+
::Sidekiq.logger.debug "#{self.class.name}#retrieve_work got nil"
|
27
|
+
end
|
28
|
+
|
29
|
+
unit_of_work
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "sidekiq"
|
2
|
+
require "sidekiq/manager"
|
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"
|
9
|
+
|
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
|
17
|
+
#
|
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)
|
data/lib/fairway.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "fairway/version"
|
2
|
+
|
3
|
+
require "active_support/core_ext"
|
4
|
+
require "redis"
|
5
|
+
require "hiredis"
|
6
|
+
require "redis-namespace"
|
7
|
+
|
8
|
+
require "fairway/config"
|
9
|
+
require "fairway/scripts"
|
10
|
+
require "fairway/channeled_connection"
|
11
|
+
require "fairway/connection"
|
12
|
+
require "fairway/queue_reader"
|
13
|
+
|
14
|
+
module Fairway
|
15
|
+
def self.config
|
16
|
+
@config ||= Config.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configure
|
20
|
+
yield(config) if block_given?
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
local namespace = KEYS[1];
|
2
|
+
local topic = ARGV[1];
|
3
|
+
local facet = ARGV[2];
|
4
|
+
local message = ARGV[3];
|
5
|
+
|
6
|
+
local registered_queues_key = namespace .. 'registered_queues';
|
7
|
+
local registered_queues = redis.call('hgetall', registered_queues_key);
|
8
|
+
|
9
|
+
for i = 1, #registered_queues, 2 do
|
10
|
+
local queue_name = registered_queues[i];
|
11
|
+
local queue_message = registered_queues[i+1];
|
12
|
+
|
13
|
+
if string.find(topic, queue_message) then
|
14
|
+
local active_facets = namespace .. queue_name .. ':active_facets';
|
15
|
+
local facet_queue = namespace .. queue_name .. ':facet_queue';
|
16
|
+
|
17
|
+
redis.call('lpush', namespace .. queue_name .. ':' .. facet, message)
|
18
|
+
|
19
|
+
if redis.call('sadd', active_facets, facet) == 1 then
|
20
|
+
redis.call('lpush', facet_queue, facet);
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
redis.call('publish', namespace .. topic, message);
|
@@ -0,0 +1,21 @@
|
|
1
|
+
local namespace = KEYS[1];
|
2
|
+
|
3
|
+
for index, queue_name in ipairs(ARGV) do
|
4
|
+
local active_facets = namespace .. queue_name .. ':active_facets';
|
5
|
+
local facet_queue = namespace .. queue_name .. ':facet_queue';
|
6
|
+
|
7
|
+
local facet = redis.call('rpop', facet_queue);
|
8
|
+
|
9
|
+
if facet then
|
10
|
+
local message_queue = namespace .. queue_name .. ':' .. facet;
|
11
|
+
local message = redis.call('rpop', message_queue);
|
12
|
+
|
13
|
+
if redis.call('llen', message_queue) == 0 then
|
14
|
+
redis.call('srem', active_facets, facet);
|
15
|
+
else
|
16
|
+
redis.call('lpush', facet_queue, facet);
|
17
|
+
end
|
18
|
+
|
19
|
+
return {queue_name, message};
|
20
|
+
end
|
21
|
+
end
|