amqp-subscribe-many 0.1.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/.travis.yml +3 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +40 -0
- data/LICENSE +20 -0
- data/Makefile +19 -0
- data/README.md +4 -0
- data/Rakefile +68 -0
- data/amqp-subscribe-many.gemspec +76 -0
- data/lib/messaging.rb +4 -0
- data/lib/messaging/client.rb +148 -0
- data/lib/messaging/configuration.rb +56 -0
- data/lib/messaging/consumer.rb +123 -0
- data/lib/messaging/producer.rb +59 -0
- data/script/config.yml +6 -0
- data/script/run.rb +86 -0
- data/test/client_test.rb +55 -0
- data/test/configuration_test.rb +75 -0
- data/test/consumer_test.rb +1 -0
- data/test/test_helper.rb +16 -0
- metadata +146 -0
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
amq-client (0.9.4)
|
5
|
+
amq-protocol (>= 0.9.4)
|
6
|
+
eventmachine
|
7
|
+
amq-protocol (0.9.4)
|
8
|
+
amqp (0.9.7)
|
9
|
+
amq-client (~> 0.9.4)
|
10
|
+
amq-protocol (>= 0.9.4)
|
11
|
+
eventmachine
|
12
|
+
eventmachine (0.12.10)
|
13
|
+
git (1.2.5)
|
14
|
+
jeweler (1.8.4)
|
15
|
+
bundler (~> 1.0)
|
16
|
+
git (>= 1.2.5)
|
17
|
+
rake
|
18
|
+
rdoc
|
19
|
+
json (1.7.3)
|
20
|
+
metaclass (0.0.1)
|
21
|
+
minitest (3.2.0)
|
22
|
+
mocha (0.12.0)
|
23
|
+
metaclass (~> 0.0.1)
|
24
|
+
rake (0.9.2.2)
|
25
|
+
rdoc (3.12)
|
26
|
+
json (~> 1.4)
|
27
|
+
redcarpet (2.1.1)
|
28
|
+
yard (0.8.2.1)
|
29
|
+
|
30
|
+
PLATFORMS
|
31
|
+
ruby
|
32
|
+
|
33
|
+
DEPENDENCIES
|
34
|
+
amqp (>= 0.9.6)
|
35
|
+
bundler
|
36
|
+
jeweler
|
37
|
+
minitest (>= 3.0)
|
38
|
+
mocha
|
39
|
+
redcarpet
|
40
|
+
yard
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) Brendan Hay
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Makefile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
#
|
2
|
+
# Bundler
|
3
|
+
#
|
4
|
+
|
5
|
+
require "rubygems"
|
6
|
+
require "bundler"
|
7
|
+
|
8
|
+
begin
|
9
|
+
Bundler.setup(:default)
|
10
|
+
rescue Bundler::BundlerError => ex
|
11
|
+
$stderr.puts ex.message
|
12
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
13
|
+
exit ex.status_code
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
#
|
18
|
+
# Tests
|
19
|
+
#
|
20
|
+
|
21
|
+
require "rake"
|
22
|
+
require "rake/testtask"
|
23
|
+
|
24
|
+
Rake::TestTask.new do |t|
|
25
|
+
t.libs.concat ["lib", "test"]
|
26
|
+
t.test_files = FileList["test/*_test.rb"]
|
27
|
+
t.verbose = true
|
28
|
+
end
|
29
|
+
|
30
|
+
task :default => :test
|
31
|
+
|
32
|
+
|
33
|
+
#
|
34
|
+
# Console
|
35
|
+
#
|
36
|
+
|
37
|
+
task :console do
|
38
|
+
sh "irb -rubygems -I lib -r messaging.rb"
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
#
|
43
|
+
# Gemify
|
44
|
+
#
|
45
|
+
|
46
|
+
require "jeweler"
|
47
|
+
|
48
|
+
Jeweler::RubygemsDotOrgTasks.new
|
49
|
+
|
50
|
+
Jeweler::Tasks.new do |gem|
|
51
|
+
gem.name = "amqp-subscribe-many"
|
52
|
+
gem.version = "0.1.1"
|
53
|
+
gem.homepage = "http://github.com/brendanhay/amqp-subscribe-many"
|
54
|
+
gem.license = "BSD"
|
55
|
+
gem.summary = "'Publish-one, subscribe-many' pattern implementation"
|
56
|
+
gem.description = gem.summary
|
57
|
+
gem.email = "brendan@soundcloud.com"
|
58
|
+
gem.authors = ["brendanhay"]
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
#
|
63
|
+
# Docs
|
64
|
+
#
|
65
|
+
|
66
|
+
require "yard"
|
67
|
+
|
68
|
+
YARD::Rake::YardocTask.new
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{amqp-subscribe-many}
|
8
|
+
s.version = "0.1.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = [%q{brendanhay}]
|
12
|
+
s.date = %q{2012-07-10}
|
13
|
+
s.description = %q{'Publish-one, subscribe-many' pattern implementation}
|
14
|
+
s.email = %q{brendan@soundcloud.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".travis.yml",
|
21
|
+
"Gemfile",
|
22
|
+
"Gemfile.lock",
|
23
|
+
"LICENSE",
|
24
|
+
"Makefile",
|
25
|
+
"README.md",
|
26
|
+
"Rakefile",
|
27
|
+
"amqp-subscribe-many.gemspec",
|
28
|
+
"lib/messaging.rb",
|
29
|
+
"lib/messaging/client.rb",
|
30
|
+
"lib/messaging/configuration.rb",
|
31
|
+
"lib/messaging/consumer.rb",
|
32
|
+
"lib/messaging/producer.rb",
|
33
|
+
"script/config.yml",
|
34
|
+
"script/run.rb",
|
35
|
+
"test/client_test.rb",
|
36
|
+
"test/configuration_test.rb",
|
37
|
+
"test/consumer_test.rb",
|
38
|
+
"test/test_helper.rb"
|
39
|
+
]
|
40
|
+
s.homepage = %q{http://github.com/brendanhay/amqp-subscribe-many}
|
41
|
+
s.licenses = [%q{BSD}]
|
42
|
+
s.require_paths = [%q{lib}]
|
43
|
+
s.rubygems_version = %q{1.8.7}
|
44
|
+
s.summary = %q{'Publish-one, subscribe-many' pattern implementation}
|
45
|
+
|
46
|
+
if s.respond_to? :specification_version then
|
47
|
+
s.specification_version = 3
|
48
|
+
|
49
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
50
|
+
s.add_runtime_dependency(%q<amqp>, [">= 0.9.6"])
|
51
|
+
s.add_development_dependency(%q<bundler>, [">= 0"])
|
52
|
+
s.add_development_dependency(%q<minitest>, [">= 3.0"])
|
53
|
+
s.add_development_dependency(%q<mocha>, [">= 0"])
|
54
|
+
s.add_development_dependency(%q<jeweler>, [">= 0"])
|
55
|
+
s.add_development_dependency(%q<redcarpet>, [">= 0"])
|
56
|
+
s.add_development_dependency(%q<yard>, [">= 0"])
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<amqp>, [">= 0.9.6"])
|
59
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
60
|
+
s.add_dependency(%q<minitest>, [">= 3.0"])
|
61
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
62
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
63
|
+
s.add_dependency(%q<redcarpet>, [">= 0"])
|
64
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
65
|
+
end
|
66
|
+
else
|
67
|
+
s.add_dependency(%q<amqp>, [">= 0.9.6"])
|
68
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
69
|
+
s.add_dependency(%q<minitest>, [">= 3.0"])
|
70
|
+
s.add_dependency(%q<mocha>, [">= 0"])
|
71
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
72
|
+
s.add_dependency(%q<redcarpet>, [">= 0"])
|
73
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
data/lib/messaging.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require "amqp"
|
2
|
+
|
3
|
+
module Messaging
|
4
|
+
|
5
|
+
#
|
6
|
+
# Raised when unrecoverable connection and channel errors are encountered.
|
7
|
+
#
|
8
|
+
class MessagingError < StandardError; end
|
9
|
+
|
10
|
+
#
|
11
|
+
# Provides methods and constants required to establish an AMQP
|
12
|
+
# connection and channel with failure handling and recovery.
|
13
|
+
# @see http://www.rabbitmq.com/amqp-0-9-1-reference.html#constants
|
14
|
+
# For a list of error codes that will cause an exception to be raised
|
15
|
+
# rather than invoking automatic recovery.
|
16
|
+
#
|
17
|
+
module Client
|
18
|
+
|
19
|
+
# Create an AMQP::Connection with auto-reconnect and error handling.
|
20
|
+
#
|
21
|
+
# @param uri [String] The AMQP URI to connect to.
|
22
|
+
# @param delay [Integer, nil] Time to delay between reconnection attempts.
|
23
|
+
# @return [AMQP::Connection]
|
24
|
+
# @api public
|
25
|
+
def open_connection(uri, delay = nil)
|
26
|
+
delay ||= config.reconnect_delay
|
27
|
+
options = AMQP::Client.parse_connection_uri(uri)
|
28
|
+
|
29
|
+
AMQP.connect(options) do |connection, open_ok|
|
30
|
+
# Handle TCP connection errors
|
31
|
+
connection.on_tcp_connection_loss do |conn, settings|
|
32
|
+
log.error("Connection to #{uri.inspect} lost, reconnecting")
|
33
|
+
|
34
|
+
conn.periodically_reconnect(delay)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Handle general errors
|
38
|
+
connection.on_error do |conn, error|
|
39
|
+
log.error("Connection to #{uri.inspect} lost, reconnecting")
|
40
|
+
|
41
|
+
if (402..540).include?(error.reply_code)
|
42
|
+
raise(MessagingError, "Channel exception: #{error.reply_text.inspect}")
|
43
|
+
end
|
44
|
+
|
45
|
+
conn.periodically_reconnect(delay)
|
46
|
+
end
|
47
|
+
|
48
|
+
log.debug("Connection to #{uri.inspect} started")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Open an AMQP::Channel with auto-recovery and error handling.
|
53
|
+
#
|
54
|
+
# @param connection [AMQP::Connection]
|
55
|
+
# @param prefetch [Integer, nil]
|
56
|
+
# @return [AMQP::Channel]
|
57
|
+
# @api public
|
58
|
+
def open_channel(connection, prefetch = nil)
|
59
|
+
AMQP::Channel.new(connection) do |channel, open_ok|
|
60
|
+
channel.auto_recovery = true
|
61
|
+
channel.prefetch(prefetch) if prefetch
|
62
|
+
|
63
|
+
channel.on_error do |ch, error|
|
64
|
+
log.error("Channel error #{error.reply_text.inspect}, recovering")
|
65
|
+
|
66
|
+
# Raise erroneous channel calls/conditions
|
67
|
+
# rather than endlessly retrying
|
68
|
+
if (403..406).include?(error.reply_code)
|
69
|
+
raise(MessagingError, "Channel exception: #{error.reply_text.inspect}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
log.debug("Channel #{channel.id} created")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Declare an exchange on the specified channel.
|
78
|
+
#
|
79
|
+
# @param channel [AMQP::Channel]
|
80
|
+
# @param name [String]
|
81
|
+
# @param type [String]
|
82
|
+
# @param options [Hash]
|
83
|
+
# @return [AMQP::Exchange]
|
84
|
+
# @api public
|
85
|
+
def declare_exchange(channel, name, type, options = {})
|
86
|
+
exchange =
|
87
|
+
# Check if default options need to be supplied to a non-default delcaration
|
88
|
+
if default_exchange?(name)
|
89
|
+
channel.default_exchange
|
90
|
+
else
|
91
|
+
channel.send(type, name, options)
|
92
|
+
end
|
93
|
+
|
94
|
+
log.debug("Exchange #{exchange.name.inspect} declared")
|
95
|
+
|
96
|
+
exchange
|
97
|
+
end
|
98
|
+
|
99
|
+
# Declare and bind a queue to the specified exchange via the
|
100
|
+
# supplied routing key.
|
101
|
+
#
|
102
|
+
# @param channel [AMQP::Channel]
|
103
|
+
# @param exchange [AMQP::Exchange]
|
104
|
+
# @param name [String]
|
105
|
+
# @param key [String]
|
106
|
+
# @param options [Hash]
|
107
|
+
# @return [AMQP::Queue]
|
108
|
+
# @api public
|
109
|
+
def declare_queue(channel, exchange, name, key, options = {})
|
110
|
+
channel.queue(name, options) do |queue|
|
111
|
+
# Check if additional bindings are needed
|
112
|
+
unless default_exchange?(exchange.name)
|
113
|
+
queue.bind(exchange, { :routing_key => key })
|
114
|
+
end
|
115
|
+
|
116
|
+
log.debug("Queue #{queue.name.inspect} bound to #{exchange.name.inspect}")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
|
122
|
+
# @return [#info, #debug, #error]
|
123
|
+
# @api protected
|
124
|
+
def log
|
125
|
+
config.logger
|
126
|
+
end
|
127
|
+
|
128
|
+
# @return [Messaging::Configuration]
|
129
|
+
# @api protected
|
130
|
+
def config
|
131
|
+
Configuration.instance
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
# @param name [String]
|
137
|
+
# @return [Boolean]
|
138
|
+
# @api private
|
139
|
+
def default_exchange?(name)
|
140
|
+
["amq.direct",
|
141
|
+
"amq.fanout",
|
142
|
+
"amq.topic",
|
143
|
+
"amqp.headers",
|
144
|
+
"amqp.match"].include?(name)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "singleton"
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
module Messaging
|
5
|
+
|
6
|
+
# Global configuration for producer and consumer mixins.
|
7
|
+
class Configuration
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
# @yieldparam [Messaging::Configuration] config
|
11
|
+
# @api public
|
12
|
+
def self.setup(&block)
|
13
|
+
yield(Configuration.instance)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @!attribute [r] publish_to
|
17
|
+
# @return [String]
|
18
|
+
attr_accessor :publish_to
|
19
|
+
|
20
|
+
# @!attribute [r] consume_from
|
21
|
+
# @return [Array<String>]
|
22
|
+
attr_accessor :consume_from
|
23
|
+
|
24
|
+
# @!attribute [r] prefetch
|
25
|
+
# @return [Integer]
|
26
|
+
attr_accessor :prefetch
|
27
|
+
|
28
|
+
# @!attribute [r] exchange_options
|
29
|
+
# @return [Hash]
|
30
|
+
attr_accessor :exchange_options
|
31
|
+
|
32
|
+
# @!attribute [r] queue_options
|
33
|
+
# @return [Hash]
|
34
|
+
attr_accessor :queue_options
|
35
|
+
|
36
|
+
# @!attribute [r] reconnect_delay
|
37
|
+
# @return [Integer]
|
38
|
+
attr_accessor :reconnect_delay
|
39
|
+
|
40
|
+
# @!attribute [r] logger
|
41
|
+
# @return [#info, #debug, #error]
|
42
|
+
attr_accessor :logger
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def initialize
|
46
|
+
@publish_to = "amqp://guest:guest@localhost:5672"
|
47
|
+
@consume_from = [publish_to]
|
48
|
+
@prefetch = 1
|
49
|
+
@exchange_options = { :auto_delete => false, :durable => true }
|
50
|
+
@queue_options = exchange_options
|
51
|
+
@reconnect_delay = 5
|
52
|
+
@logger = Logger.new(STDOUT)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Messaging
|
2
|
+
|
3
|
+
module Consumer
|
4
|
+
include Client
|
5
|
+
|
6
|
+
# DSL methods which are used to extend the target when
|
7
|
+
# {Messaging::Consumer} is included into a class.
|
8
|
+
module Extensions
|
9
|
+
|
10
|
+
# Subscribe to a queue which will invoke {Messaging::Consumer#on_message}
|
11
|
+
# upon receiving a message.
|
12
|
+
#
|
13
|
+
# @param exchange [String]
|
14
|
+
# @param type [String]
|
15
|
+
# @param queue [String]
|
16
|
+
# @param key [String]
|
17
|
+
# @return [Array<Array(String, String, String, String)>]
|
18
|
+
# @api public
|
19
|
+
def subscribe(exchange, type, queue, key)
|
20
|
+
subscriptions << [exchange, type, queue, key]
|
21
|
+
end
|
22
|
+
|
23
|
+
# A list of subscriptions intended for internal use.
|
24
|
+
#
|
25
|
+
# @return [Array<Array(String, String, String, String)>]
|
26
|
+
# @api private
|
27
|
+
def subscriptions
|
28
|
+
@subscriptions ||= []
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.included(base)
|
34
|
+
base.send(:extend, Extensions)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Messaging::Consumer]
|
38
|
+
# @api public
|
39
|
+
def consume
|
40
|
+
unless consumer_channels
|
41
|
+
@consumer_channels ||= consumer_connections.map do |conn|
|
42
|
+
open_channel(conn, config.prefetch)
|
43
|
+
end
|
44
|
+
|
45
|
+
subscriptions.each { |args| subscribe(*args) }
|
46
|
+
end
|
47
|
+
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
# Subscribe to a queue which will invoke the supplied block when
|
52
|
+
# a message is received.
|
53
|
+
# Additionally declaring a binding to the specified exchange/key pair.
|
54
|
+
#
|
55
|
+
# @param exchange [String]
|
56
|
+
# @param type [String]
|
57
|
+
# @param queue [String]
|
58
|
+
# @param key [String]
|
59
|
+
# @return [Messaging::Consumer]
|
60
|
+
# @api public
|
61
|
+
def subscribe(exchange, type, queue, key)
|
62
|
+
consumer_channels.each do |channel|
|
63
|
+
ex = declare_exchange(channel, exchange, type, config.exchange_options)
|
64
|
+
q = declare_queue(channel, ex, queue, key, config.queue_options)
|
65
|
+
|
66
|
+
q.subscribe(:ack => true) do |meta, payload|
|
67
|
+
log.debug("Receieved message on channel #{meta.channel.id} from queue #{queue.inspect}")
|
68
|
+
|
69
|
+
# If this raises an exception, the connection
|
70
|
+
# will be closed, and the message requeued by the broker.
|
71
|
+
on_message(meta, payload)
|
72
|
+
|
73
|
+
meta.ack
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
# @raise [NotImplementedError]
|
81
|
+
# @api protected
|
82
|
+
def on_message(meta, payload)
|
83
|
+
raise NotImplementedError
|
84
|
+
end
|
85
|
+
|
86
|
+
# Close all consumer_channels and then disconnect all the consumer_connections.
|
87
|
+
#
|
88
|
+
# @return []
|
89
|
+
# @api public
|
90
|
+
def disconnect
|
91
|
+
consumer_channels.each do |chan|
|
92
|
+
chan.close
|
93
|
+
end
|
94
|
+
|
95
|
+
consumer_connections.each do |conn|
|
96
|
+
conn.disconnect
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# @return [Array<AMQP::Connection>]
|
103
|
+
# @api private
|
104
|
+
def consumer_connections
|
105
|
+
@consumer_connections ||= config.consume_from.map do |uri|
|
106
|
+
open_connection(uri)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# @return [Array<AMQP::Channel>]
|
111
|
+
# @api private
|
112
|
+
def consumer_channels
|
113
|
+
@consumer_channels
|
114
|
+
end
|
115
|
+
|
116
|
+
# @return [Array<Array(String, String, String, String)>]
|
117
|
+
# @api private
|
118
|
+
def subscriptions
|
119
|
+
self.class.subscriptions
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Messaging
|
2
|
+
|
3
|
+
module Producer
|
4
|
+
include Client
|
5
|
+
|
6
|
+
# Publish a payload to the specified exchange/key pair.
|
7
|
+
#
|
8
|
+
# @param exchange [String]
|
9
|
+
# @param type [String]
|
10
|
+
# @param key [String]
|
11
|
+
# @param payload [Object]
|
12
|
+
# @return [Messaging::Producer]
|
13
|
+
# @api public
|
14
|
+
def publish(exchange, type, key, payload)
|
15
|
+
ex = producer_exchanges[exchange] ||=
|
16
|
+
declare_exchange(producer_channel, exchange, type, config.exchange_options)
|
17
|
+
|
18
|
+
log.debug("Publishing to exchange #{exchange.inspect} via #{key.inspect}")
|
19
|
+
|
20
|
+
ex.publish(payload, {
|
21
|
+
:exchange => exchange,
|
22
|
+
:routing_key => key
|
23
|
+
})
|
24
|
+
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# Close the channel and then disconnect the connection.
|
29
|
+
#
|
30
|
+
# @return []
|
31
|
+
# @api public
|
32
|
+
def disconnect
|
33
|
+
producer_channel.close do |close_ok|
|
34
|
+
producer_connection.disconnect
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# @return [Hash(String, AMQP::Exchange)]
|
41
|
+
# @api private
|
42
|
+
def producer_exchanges
|
43
|
+
@producer_exchanges ||= {}
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [AMQP::Connection]
|
47
|
+
# @api private
|
48
|
+
def producer_connection
|
49
|
+
@producer_connection ||= open_connection(config.publish_to)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [AMQP::Channel]
|
53
|
+
# @api private
|
54
|
+
def producer_channel
|
55
|
+
@producer_channel ||= open_channel(producer_connection)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
data/script/config.yml
ADDED
data/script/run.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "bundler"
|
5
|
+
|
6
|
+
Bundler.setup(:default)
|
7
|
+
|
8
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
9
|
+
|
10
|
+
require "messaging"
|
11
|
+
|
12
|
+
EXCHANGE = "exchange"
|
13
|
+
TYPE = "direct"
|
14
|
+
QUEUE = "queue"
|
15
|
+
KEY = "key"
|
16
|
+
|
17
|
+
# Load the config
|
18
|
+
yml = YAML::load_file(File.dirname(__FILE__) + "/config.yml")
|
19
|
+
|
20
|
+
# Setup configuration
|
21
|
+
Messaging::Configuration.setup do |config|
|
22
|
+
config.publish_to = yml[:publish_to]
|
23
|
+
config.consume_from = yml[:consume_from]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Consume example
|
27
|
+
class ConsumerProcessor
|
28
|
+
include Messaging::Consumer
|
29
|
+
|
30
|
+
subscribe(EXCHANGE, TYPE, QUEUE, KEY)
|
31
|
+
|
32
|
+
def on_message(meta, payload)
|
33
|
+
log.info("ConsumeProcessor channel #{meta.channel.id} received payload #{payload.inspect}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Publish example
|
38
|
+
class ProducerProcessor
|
39
|
+
include Messaging::Producer
|
40
|
+
end
|
41
|
+
|
42
|
+
# Consume + publish example
|
43
|
+
class DuplexProcessor
|
44
|
+
include Messaging::Producer
|
45
|
+
include Messaging::Consumer
|
46
|
+
|
47
|
+
subscribe(EXCHANGE, TYPE, QUEUE, KEY)
|
48
|
+
|
49
|
+
def on_message(meta, payload)
|
50
|
+
log.info("DuplexProcessor channel #{meta.channel.id} received payload #{payload.inspect}")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
EM.run do
|
55
|
+
|
56
|
+
# Instantiate the processors
|
57
|
+
consumer = ConsumerProcessor.new
|
58
|
+
producer = ProducerProcessor.new
|
59
|
+
duplex = DuplexProcessor.new
|
60
|
+
|
61
|
+
# Start the consumers
|
62
|
+
consumer.consume
|
63
|
+
duplex.consume
|
64
|
+
|
65
|
+
# Create a handle to the publish timer, to cancel later
|
66
|
+
timer = EM.add_periodic_timer(0.5) do
|
67
|
+
producer.publish(EXCHANGE, TYPE, KEY, "a_producer_payload")
|
68
|
+
duplex.publish(EXCHANGE, TYPE, KEY, "a_duplex_payload")
|
69
|
+
end
|
70
|
+
|
71
|
+
# Handle Ctrl-C interrupt
|
72
|
+
trap("INT") do
|
73
|
+
puts "Stopping..."
|
74
|
+
|
75
|
+
# Cancel the publisher timer
|
76
|
+
EM.cancel_timer(timer)
|
77
|
+
|
78
|
+
# Disconnect the producer/consumer connections
|
79
|
+
consumer.disconnect
|
80
|
+
producer.disconnect
|
81
|
+
duplex.disconnect
|
82
|
+
|
83
|
+
# Shutdown the EM loop
|
84
|
+
EM.add_timer(1) { EM.stop }
|
85
|
+
end
|
86
|
+
end
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class DummyClient
|
4
|
+
include Messaging::Client
|
5
|
+
end
|
6
|
+
|
7
|
+
class ClientTest < MiniTest::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
@client = DummyClient.new
|
10
|
+
@uri = "amqp://guest:guest@localhost:5672"
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_open_connection_adds_retry_handlers
|
14
|
+
delay = 3
|
15
|
+
|
16
|
+
# Connection yield param
|
17
|
+
conn = mock()
|
18
|
+
|
19
|
+
# on_tcp_connection_loss handler sets periodically reconnect
|
20
|
+
on_tcp_loss = mock()
|
21
|
+
on_tcp_loss.expects(:periodically_reconnect).with(delay)
|
22
|
+
on_tcp_loss.expects(:to_ary)
|
23
|
+
|
24
|
+
# A retryable/recoverable error code
|
25
|
+
error = mock()
|
26
|
+
error.expects(:reply_code).returns(1)
|
27
|
+
|
28
|
+
# on_error handler sets periodically reconnect
|
29
|
+
on_error = mock()
|
30
|
+
on_error.expects(:periodically_reconnect).with(delay)
|
31
|
+
|
32
|
+
conn.expects(:on_tcp_connection_loss).yields(on_tcp_loss)
|
33
|
+
conn.expects(:on_error).yields(on_error, error)
|
34
|
+
conn.expects(:to_ary)
|
35
|
+
|
36
|
+
AMQP.stubs(:connect).yields(conn)
|
37
|
+
|
38
|
+
@client.open_connection(@uri, delay)
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_open_channel_adds_recovery_handlers
|
42
|
+
prefetch = 16
|
43
|
+
|
44
|
+
# Channel yield param
|
45
|
+
chan = mock()
|
46
|
+
chan.expects(:on_error)
|
47
|
+
chan.expects(:auto_recovery=).with(true)
|
48
|
+
chan.expects(:prefetch).with(prefetch)
|
49
|
+
chan.expects(:id)
|
50
|
+
|
51
|
+
AMQP::Channel.stubs(:new).yields(chan, {})
|
52
|
+
|
53
|
+
@client.open_channel(mock(), prefetch)
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class ConfigurationTest < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
Messaging::Configuration.instance_variable_set(
|
6
|
+
"@__instance__",
|
7
|
+
Messaging::Configuration.send(:new)
|
8
|
+
)
|
9
|
+
|
10
|
+
@config = Messaging::Configuration
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_publish_to
|
14
|
+
assert(@config.instance.publish_to)
|
15
|
+
|
16
|
+
expected = "ballsacks"
|
17
|
+
@config.setup { |c| c.publish_to = expected }
|
18
|
+
|
19
|
+
assert_equal(expected, @config.instance.publish_to)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_consume_from
|
23
|
+
assert(@config.instance.consume_from.length > 0)
|
24
|
+
|
25
|
+
expected = "nutsacks"
|
26
|
+
@config.setup { |c| c.consume_from = expected }
|
27
|
+
|
28
|
+
assert_equal(expected, @config.instance.consume_from)
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_prefetch
|
32
|
+
assert(@config.instance.prefetch > 0)
|
33
|
+
|
34
|
+
expected = 7
|
35
|
+
@config.setup { |c| c.prefetch = expected }
|
36
|
+
|
37
|
+
assert_equal(expected, @config.instance.prefetch)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_exchange_options
|
41
|
+
assert(@config.instance.exchange_options)
|
42
|
+
|
43
|
+
expected = { :nonsense => true }
|
44
|
+
@config.setup { |c| c.exchange_options = expected }
|
45
|
+
|
46
|
+
assert_equal(expected, @config.instance.exchange_options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_queue_options
|
50
|
+
assert(@config.instance.queue_options)
|
51
|
+
|
52
|
+
expected = { :high_five => "ok!" }
|
53
|
+
@config.setup { |c| c.queue_options = expected }
|
54
|
+
|
55
|
+
assert_equal(expected, @config.instance.queue_options)
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_reconnect_delay
|
59
|
+
assert(@config.instance.reconnect_delay > 0)
|
60
|
+
|
61
|
+
expected = 12
|
62
|
+
@config.setup { |c| c.reconnect_delay = expected }
|
63
|
+
|
64
|
+
assert_equal(expected, @config.instance.reconnect_delay)
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_logger
|
68
|
+
assert(@config.instance.logger)
|
69
|
+
|
70
|
+
expected = "ballsacks"
|
71
|
+
@config.setup { |c| c.logger = expected }
|
72
|
+
|
73
|
+
assert_equal(expected, @config.instance.logger)
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative "test_helper"
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "minitest/autorun"
|
2
|
+
require "mocha"
|
3
|
+
require "messaging"
|
4
|
+
require "logger"
|
5
|
+
|
6
|
+
class MiniTestSetup < MiniTest::Unit
|
7
|
+
def _run_suites(suites, type)
|
8
|
+
Messaging::Configuration.setup do |config|
|
9
|
+
config.logger.level = Logger::Severity::UNKNOWN
|
10
|
+
end
|
11
|
+
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
MiniTest::Unit.runner = MiniTestSetup.new
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: amqp-subscribe-many
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- brendanhay
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-10 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: amqp
|
16
|
+
requirement: &70180070452160 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.9.6
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70180070452160
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: bundler
|
27
|
+
requirement: &70180070451500 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70180070451500
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: minitest
|
38
|
+
requirement: &70180070450860 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '3.0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70180070450860
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: mocha
|
49
|
+
requirement: &70180070450260 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70180070450260
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: jeweler
|
60
|
+
requirement: &70180070449620 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70180070449620
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: redcarpet
|
71
|
+
requirement: &70180070449020 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70180070449020
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: yard
|
82
|
+
requirement: &70180070448440 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70180070448440
|
91
|
+
description: ! '''Publish-one, subscribe-many'' pattern implementation'
|
92
|
+
email: brendan@soundcloud.com
|
93
|
+
executables: []
|
94
|
+
extensions: []
|
95
|
+
extra_rdoc_files:
|
96
|
+
- LICENSE
|
97
|
+
- README.md
|
98
|
+
files:
|
99
|
+
- .travis.yml
|
100
|
+
- Gemfile
|
101
|
+
- Gemfile.lock
|
102
|
+
- LICENSE
|
103
|
+
- Makefile
|
104
|
+
- README.md
|
105
|
+
- Rakefile
|
106
|
+
- amqp-subscribe-many.gemspec
|
107
|
+
- lib/messaging.rb
|
108
|
+
- lib/messaging/client.rb
|
109
|
+
- lib/messaging/configuration.rb
|
110
|
+
- lib/messaging/consumer.rb
|
111
|
+
- lib/messaging/producer.rb
|
112
|
+
- script/config.yml
|
113
|
+
- script/run.rb
|
114
|
+
- test/client_test.rb
|
115
|
+
- test/configuration_test.rb
|
116
|
+
- test/consumer_test.rb
|
117
|
+
- test/test_helper.rb
|
118
|
+
homepage: http://github.com/brendanhay/amqp-subscribe-many
|
119
|
+
licenses:
|
120
|
+
- BSD
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ! '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
segments:
|
132
|
+
- 0
|
133
|
+
hash: 235469886921977757
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
none: false
|
136
|
+
requirements:
|
137
|
+
- - ! '>='
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
requirements: []
|
141
|
+
rubyforge_project:
|
142
|
+
rubygems_version: 1.8.7
|
143
|
+
signing_key:
|
144
|
+
specification_version: 3
|
145
|
+
summary: ! '''Publish-one, subscribe-many'' pattern implementation'
|
146
|
+
test_files: []
|