fairway 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|