cottontail 0.1.5 → 2.0.0.pre.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.
- checksums.yaml +7 -0
- data/.gitignore +9 -4
- data/.rubocop.yml +4 -0
- data/.travis.yml +15 -0
- data/Gemfile +12 -1
- data/LICENSE.txt +1 -1
- data/README.md +17 -17
- data/Rakefile +1 -1
- data/cottontail.gemspec +4 -1
- data/lib/cottontail.rb +6 -294
- data/lib/cottontail/configurable.rb +68 -0
- data/lib/cottontail/consumer.rb +215 -0
- data/lib/cottontail/consumer/collection.rb +28 -0
- data/lib/cottontail/consumer/entity.rb +45 -0
- data/lib/cottontail/consumer/launcher.rb +32 -0
- data/lib/cottontail/consumer/session.rb +27 -0
- data/lib/cottontail/version.rb +2 -2
- data/spec/cottontail/configurable_spec.rb +41 -0
- data/spec/cottontail/consumer/collection_spec.rb +95 -0
- data/spec/cottontail/consumer/entity_spec.rb +120 -0
- data/spec/integration/consumer_simple_spec.rb +41 -0
- data/spec/spec_helper.rb +42 -0
- data/spec/support/coverage.rb +16 -0
- data/spec/support/rabbitmq.rb +14 -0
- data/spec/support/test_consumer.rb +32 -0
- metadata +114 -55
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fffe7fed426e7be9c4107730b1ca60cf85d6e976
|
4
|
+
data.tar.gz: 93d238e96dafe0b22e3497e41ac9f8041a3b7401
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6cf61ebc80120a7a4351a11c2a86b541c38bb9c93b0af68d84cfcb184c069b2eb00e2fc9e711e9ff8014736ee15da54a7c3c83a9f490de36ce4e7cb546d9c5f5
|
7
|
+
data.tar.gz: 62be0c8eac7b7abc0ac355c8c8e1a2bedd9f3817810b9bb349783ae2c2b58ec56062a10d5b3a283513cf757ff556f4adc1021cd8052fae2e16c3f4a1f684e2ea
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
@@ -1,4 +1,15 @@
|
|
1
|
-
source
|
1
|
+
source 'http://rubygems.org'
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in cottontail.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
group :development, :test do
|
7
|
+
gem 'rubocop'
|
8
|
+
gem 'byebug'
|
9
|
+
|
10
|
+
gem 'rspec-core', '~> 3'
|
11
|
+
gem 'rspec-expectations', '~> 3'
|
12
|
+
|
13
|
+
gem 'simplecov', require: false, platform: :ruby_21
|
14
|
+
gem 'coveralls', require: false, platform: :ruby_21
|
15
|
+
end
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
This library is a wrapper around the popular AMQP bunny gem. It is inspired by Sinatra to easily consume messages on different routing keys.
|
4
4
|
|
5
|
+
[](https://badge.fury.io/rb/cottontail)
|
6
|
+
[](https://travis-ci.org/rudionrails/cottontail)
|
7
|
+
[](https://codeclimate.com/github/rudionrails/cottontail)
|
8
|
+
[](https://coveralls.io/github/rudionrails/cottontail?branch=master)
|
9
|
+
|
5
10
|
## Installation
|
6
11
|
|
7
12
|
System wide:
|
@@ -22,30 +27,25 @@ When using this gem, you should already be familiar with RabbitMQ and, idealy, B
|
|
22
27
|
|
23
28
|
```ruby
|
24
29
|
require 'cottontail'
|
25
|
-
require 'bunny'
|
26
|
-
|
27
|
-
class Worker < Cottontail::Base
|
28
|
-
# configure the Bunny client with the default connection (host: "localhost", port: 5672)
|
29
|
-
# and with logging.
|
30
|
-
client :logging => true
|
31
30
|
|
32
|
-
|
33
|
-
|
31
|
+
class Worker
|
32
|
+
include Cottontail::Consumer
|
34
33
|
|
35
|
-
|
36
|
-
|
34
|
+
session ENV['RABBITMQ_URL'] do |session, worker|
|
35
|
+
channel = session.create_channel
|
37
36
|
|
37
|
+
queue = channel.queue('', durable: true)
|
38
|
+
worker.subscribe(queue, exclusive: true, ack: false)
|
39
|
+
end
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
-
route "message.received" do
|
42
|
-
puts "This is the payload #{payload.inspect}"
|
41
|
+
consume do |delivery_info, properties, payload|
|
42
|
+
logger.info payload.inspect
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
-
# The following will start Cottontail right away. You need to be aware that it
|
46
|
+
# The following will start Cottontail right away. You need to be aware that it
|
47
47
|
# will enter the Bunny subscribe loop, so it will block the process.
|
48
|
-
Worker.
|
48
|
+
Worker.start
|
49
49
|
```
|
50
50
|
|
51
51
|
To run it, you may start it like the following code example. However, you should use a more sophisticated solution for daemonizing your processes in a production environment. See http://www.ruby-toolbox.com/categories/daemonizing for futher inspiration.
|
@@ -54,4 +54,4 @@ To run it, you may start it like the following code example. However, you should
|
|
54
54
|
ruby worker.rb &
|
55
55
|
```
|
56
56
|
|
57
|
-
Copyright © 2012 Rudolf Schmidt, released under the MIT license
|
57
|
+
Copyright © 2012-2015 Rudolf Schmidt, released under the MIT license
|
data/Rakefile
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
data/cottontail.gemspec
CHANGED
@@ -6,6 +6,7 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.name = "cottontail"
|
7
7
|
s.version = Cottontail::VERSION
|
8
8
|
s.authors = ["Rudolf Schmidt"]
|
9
|
+
s.license = 'MIT'
|
9
10
|
|
10
11
|
s.homepage = "http://github.com/rudionrails/cottontail"
|
11
12
|
s.summary = %q{Sinatra inspired wrapper around the AMQP Bunny gem}
|
@@ -18,5 +19,7 @@ Gem::Specification.new do |s|
|
|
18
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
20
|
s.require_paths = ["lib"]
|
20
21
|
|
21
|
-
s.add_runtime_dependency "bunny"
|
22
|
+
s.add_runtime_dependency "bunny", "~> 2"
|
23
|
+
s.add_runtime_dependency "yell", ">= 2", "<= 3"
|
24
|
+
s.add_runtime_dependency "activesupport", ">= 3", "<= 4"
|
22
25
|
end
|
data/lib/cottontail.rb
CHANGED
@@ -1,298 +1,10 @@
|
|
1
|
-
require '
|
2
|
-
require 'bunny'
|
1
|
+
require 'yell'
|
3
2
|
|
4
|
-
|
5
|
-
|
3
|
+
require File.dirname(__FILE__) + '/cottontail/configurable'
|
4
|
+
require File.dirname(__FILE__) + '/cottontail/consumer'
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
#
|
10
|
-
# last_error will be set once an exception was raise
|
11
|
-
attr_reader :last_error
|
6
|
+
module Cottontail #:nodoc:
|
7
|
+
include Cottontail::Configurable
|
12
8
|
|
13
|
-
|
14
|
-
# header [Qrack::Protocol::Header] The header of the message including size, properties, etc.
|
15
|
-
# payload [String] The message sent through RabbitMQ
|
16
|
-
# delivery_details [Hash] Includes the exchange, routing_key, etc
|
17
|
-
#
|
18
|
-
# This is the original message from the queue (not be confused with the payload sent via RabitMQ).
|
19
|
-
attr_reader :message
|
20
|
-
|
21
|
-
# Accessor to the message's payload
|
22
|
-
def payload; message[:payload]; end
|
23
|
-
|
24
|
-
# Accessor to the message's header
|
25
|
-
def header; message[:header]; end
|
26
|
-
|
27
|
-
# Accessor to the message's delivery details
|
28
|
-
def delivery_details; message[:delivery_details]; end
|
29
|
-
|
30
|
-
# Accessor to the delivery detail's routing key
|
31
|
-
def routing_key; delivery_details[:routing_key]; end
|
32
|
-
end
|
33
|
-
|
34
|
-
class Base
|
35
|
-
include Helpers
|
36
|
-
|
37
|
-
class << self
|
38
|
-
attr_reader :routes
|
39
|
-
|
40
|
-
# Conveniently set the client
|
41
|
-
def client( *args, &block )
|
42
|
-
set :client, args, &block
|
43
|
-
end
|
44
|
-
|
45
|
-
# Conveniently set the exchange
|
46
|
-
def exchange( *args, &block )
|
47
|
-
set :exchange, args, &block
|
48
|
-
end
|
49
|
-
|
50
|
-
# Conveniently set the queue
|
51
|
-
def queue( *args, &block )
|
52
|
-
set :queue, args, &block
|
53
|
-
end
|
54
|
-
|
55
|
-
# Set runtime configuration
|
56
|
-
#
|
57
|
-
# @example
|
58
|
-
# set :logger, Logger.new(STDOUT)
|
59
|
-
# set(:logger) { Logger.new(STDOUT) } # will be called on first usage
|
60
|
-
def set( key, value = nil, &block )
|
61
|
-
@settings[ key ] = block ? block : value
|
62
|
-
end
|
63
|
-
|
64
|
-
# Override the standard subscribe loop
|
65
|
-
#
|
66
|
-
# @example
|
67
|
-
# subscribe :ack => false do |message|
|
68
|
-
# puts "Received #{message.inspect}"
|
69
|
-
# route! message
|
70
|
-
# end
|
71
|
-
def subscribe( options = {}, &block )
|
72
|
-
set :subscribe, [ options, compile!("subscribe", &block) ]
|
73
|
-
end
|
74
|
-
|
75
|
-
# Defines routing on class level
|
76
|
-
#
|
77
|
-
# @example
|
78
|
-
# route "message.sent" do
|
79
|
-
# ... stuff to do ...
|
80
|
-
# end
|
81
|
-
def route( key, options = {}, &block )
|
82
|
-
@routes[key] = [ options, compile!("route_#{key}", &block) ]
|
83
|
-
end
|
84
|
-
|
85
|
-
# Define error handlers
|
86
|
-
#
|
87
|
-
# @example Generic route
|
88
|
-
# error do
|
89
|
-
# puts "an error occured"
|
90
|
-
# end
|
91
|
-
#
|
92
|
-
# @example Error on specific Exception
|
93
|
-
# error RouteNotFound do
|
94
|
-
# puts "Route not found for #{routing_key.inspect}"
|
95
|
-
# end
|
96
|
-
def error( *codes, &block )
|
97
|
-
codes << :default if codes.empty? # the default error handling
|
98
|
-
|
99
|
-
compiled = compile!("error_#{codes.join("_")}", &block)
|
100
|
-
codes.each { |c| @errors[c] = compiled }
|
101
|
-
end
|
102
|
-
|
103
|
-
# Route on class level (handy for testing)
|
104
|
-
#
|
105
|
-
# @example
|
106
|
-
# route! :payload => "some message", :routing_key => "v2.message.sent"
|
107
|
-
def route!( message )
|
108
|
-
new.route!( message )
|
109
|
-
end
|
110
|
-
|
111
|
-
|
112
|
-
# Retrieve the settings
|
113
|
-
#
|
114
|
-
# In case a block was given to a settings, it will be called upon first execution and set
|
115
|
-
# as the setting's value.
|
116
|
-
def settings( key )
|
117
|
-
if @settings[key].is_a? Proc
|
118
|
-
@settings[key] = @settings[key].call
|
119
|
-
end
|
120
|
-
|
121
|
-
@settings[key]
|
122
|
-
end
|
123
|
-
|
124
|
-
# Retrieve the error block for the passed Exception class
|
125
|
-
#
|
126
|
-
# If no class matches, the default will be returned in case it has been set (else nil).
|
127
|
-
def error_for( klass )
|
128
|
-
@errors[klass] || @errors[:default]
|
129
|
-
end
|
130
|
-
|
131
|
-
# convenience method to start the instance
|
132
|
-
def run; new.run; end
|
133
|
-
|
134
|
-
# Reset the class
|
135
|
-
def reset!
|
136
|
-
@settings = {}
|
137
|
-
|
138
|
-
@errors = {}
|
139
|
-
@routes = {}
|
140
|
-
|
141
|
-
# default logger
|
142
|
-
set :logger, Logger.new(STDOUT)
|
143
|
-
|
144
|
-
# retry settings
|
145
|
-
set :retries, true
|
146
|
-
set :delay_on_retry, 2
|
147
|
-
|
148
|
-
# default subscribe loop
|
149
|
-
set :subscribe, [{}, proc { |m| route! m }]
|
150
|
-
|
151
|
-
# default bunny options
|
152
|
-
client
|
153
|
-
exchange "default"
|
154
|
-
queue "default"
|
155
|
-
end
|
156
|
-
|
157
|
-
|
158
|
-
private
|
159
|
-
|
160
|
-
def inherited( subclass )
|
161
|
-
subclass.reset!
|
162
|
-
super
|
163
|
-
end
|
164
|
-
|
165
|
-
# compiles a given proc to an unbound method to be called later on a different binding
|
166
|
-
def compile!( name, &block )
|
167
|
-
define_method name, &block
|
168
|
-
method = instance_method name
|
169
|
-
remove_method name
|
170
|
-
|
171
|
-
block.arity == 0 ? proc { |a,p| method.bind(a).call } : proc { |a,*p| method.bind(a).call(*p) }
|
172
|
-
end
|
173
|
-
|
174
|
-
end
|
175
|
-
|
176
|
-
|
177
|
-
def initialize
|
178
|
-
reset!
|
179
|
-
end
|
180
|
-
|
181
|
-
# Starts the consumer service and enters the subscribe loop.
|
182
|
-
def run
|
183
|
-
# establish connection and bind routing keys
|
184
|
-
logger.debug "[Cottontail] Connecting to client: #{settings(:client).inspect}"
|
185
|
-
@client = Bunny.new( *settings(:client) )
|
186
|
-
@client.start
|
187
|
-
|
188
|
-
logger.debug "[Cottontail] Declaring exchange: #{settings(:exchange).inspect}"
|
189
|
-
exchange = @client.exchange( *settings(:exchange) )
|
190
|
-
|
191
|
-
logger.debug "[Cottontail] Declaring queue: #{settings(:queue).inspect}"
|
192
|
-
queue = @client.queue( *settings(:queue) )
|
193
|
-
|
194
|
-
routes.keys.each do |key|
|
195
|
-
logger.debug "[Cottontail] Binding #{key.inspect}"
|
196
|
-
queue.bind( exchange, :key => key )
|
197
|
-
end
|
198
|
-
|
199
|
-
# enter the subscribe loop
|
200
|
-
subscribe!( queue )
|
201
|
-
rescue Exception => e
|
202
|
-
@client.stop if @client
|
203
|
-
reset!
|
204
|
-
|
205
|
-
logger.error "#{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
|
206
|
-
|
207
|
-
# raise when no retries are defined
|
208
|
-
raise( e, caller ) unless settings(:retries)
|
209
|
-
|
210
|
-
logger.debug "[Cottontail] Going to retry in #{settings(:delay_on_retry)} seconds..."
|
211
|
-
sleep settings(:delay_on_retry) if settings(:delay_on_retry)
|
212
|
-
retry
|
213
|
-
end
|
214
|
-
|
215
|
-
# Performs the routing of the given AMQP message
|
216
|
-
#
|
217
|
-
# The method will raise an error if no route was found or an exception
|
218
|
-
# was raised within matched routing block.
|
219
|
-
def route!( message )
|
220
|
-
@message = message
|
221
|
-
|
222
|
-
options, block = routes[routing_key]
|
223
|
-
|
224
|
-
raise Cottontail::RouteNotFound.new(routing_key) if block.nil?
|
225
|
-
block.call(self)
|
226
|
-
end
|
227
|
-
|
228
|
-
|
229
|
-
private
|
230
|
-
|
231
|
-
# Retrieve routes
|
232
|
-
def routes; self.class.routes; end
|
233
|
-
|
234
|
-
# Retrieve settings
|
235
|
-
def settings(key); self.class.settings(key); end
|
236
|
-
|
237
|
-
# Conveniently access the logger
|
238
|
-
def logger; settings(:logger); end
|
239
|
-
|
240
|
-
# Reset the instance
|
241
|
-
def reset!
|
242
|
-
@client = nil
|
243
|
-
|
244
|
-
prepare_client_settings!
|
245
|
-
end
|
246
|
-
|
247
|
-
# Handles the subscribe loop on the queue.
|
248
|
-
def subscribe!( queue )
|
249
|
-
logger.debug "[Cottontail] Entering subscribe loop"
|
250
|
-
|
251
|
-
options, block = settings(:subscribe)
|
252
|
-
queue.subscribe( options ) do |m|
|
253
|
-
with_error_handling!( m, &block )
|
254
|
-
end
|
255
|
-
end
|
256
|
-
|
257
|
-
# Gracefully handles the given message.
|
258
|
-
#
|
259
|
-
# @param [Message] m The RabbitMQ message to be handled
|
260
|
-
def with_error_handling!( m, &block )
|
261
|
-
block.call(self, m)
|
262
|
-
rescue Exception => err
|
263
|
-
@last_error = err
|
264
|
-
|
265
|
-
if block = self.class.error_for(err.class)
|
266
|
-
block.call(self)
|
267
|
-
else
|
268
|
-
# if no defined exception handling block could be found, then re-raise
|
269
|
-
raise( err, caller )
|
270
|
-
end
|
271
|
-
ensure
|
272
|
-
@last_error = nil # unset error after handling
|
273
|
-
end
|
274
|
-
|
275
|
-
# The bunny gem itself is not able to handle multiple hosts - although multiple RabbitMQ instances may run in parralel.
|
276
|
-
#
|
277
|
-
# You may pass :hosts as option when settings the client in order to cycle through them in case a connection was lost.
|
278
|
-
def prepare_client_settings!
|
279
|
-
return {} unless options = settings(:client).first
|
280
|
-
|
281
|
-
if hosts = options[:hosts]
|
282
|
-
host, port = hosts.shift
|
283
|
-
hosts << [host, port]
|
284
|
-
|
285
|
-
options.merge!( :host => host, :port => port )
|
286
|
-
end
|
287
|
-
|
288
|
-
options
|
289
|
-
end
|
290
|
-
|
291
|
-
public
|
292
|
-
|
293
|
-
# === Perform the initial setup
|
294
|
-
reset!
|
295
|
-
|
296
|
-
end
|
9
|
+
set :logger, -> { Yell.new(:stdout, colors: true) }
|
297
10
|
end
|
298
|
-
|