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
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
require 'active_support/concern'
|
3
|
+
|
4
|
+
module Cottontail #:nodoc:
|
5
|
+
module Configurable #:nodoc:
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods #:nodoc:
|
9
|
+
delegate :set, :get, to: :config
|
10
|
+
|
11
|
+
def config
|
12
|
+
return @__config__ if defined?(@__config__)
|
13
|
+
|
14
|
+
@__config__ =
|
15
|
+
if respond_to?(:superclass) && superclass.respond_to?(:config)
|
16
|
+
superclass.config.inheritable_copy
|
17
|
+
else
|
18
|
+
# create a new "anonymous" class that will host the compiled
|
19
|
+
# reader methods
|
20
|
+
Class.new(Configuration).new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def configure
|
25
|
+
yield config
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def config
|
30
|
+
self.class.config
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
class Configuration #:nodoc:
|
36
|
+
def initialize
|
37
|
+
reset!
|
38
|
+
end
|
39
|
+
|
40
|
+
# Set a configuration option.
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# set :logger, Yell.new($stdout)
|
44
|
+
# set :logger, -> { Yell.new($stdout) }
|
45
|
+
def set(key, value = nil, &block)
|
46
|
+
@settings[key] = !block.nil? ? block : value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Get a configuration option. It will be evalued of the first time
|
50
|
+
# of calling.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# get :logger
|
54
|
+
def get(key)
|
55
|
+
if (value = @settings[key]).is_a?(Proc)
|
56
|
+
@settings[key] = value.call
|
57
|
+
end
|
58
|
+
|
59
|
+
@settings[key]
|
60
|
+
end
|
61
|
+
|
62
|
+
# @private
|
63
|
+
def reset!
|
64
|
+
@settings = {}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/configurable'
|
4
|
+
require File.dirname(__FILE__) + '/consumer/launcher'
|
5
|
+
require File.dirname(__FILE__) + '/consumer/session'
|
6
|
+
require File.dirname(__FILE__) + '/consumer/collection'
|
7
|
+
require File.dirname(__FILE__) + '/consumer/entity'
|
8
|
+
|
9
|
+
module Cottontail
|
10
|
+
# The Cottontail::Consumer is the module for receiving
|
11
|
+
# asynchronous AMQP messages.
|
12
|
+
#
|
13
|
+
# @example A basic worker
|
14
|
+
# class Worker
|
15
|
+
# include Cottontail::Consumer
|
16
|
+
#
|
17
|
+
# session ENV['RABBITMQ_URL'] do |worker, session|
|
18
|
+
# channel = session.create_channel
|
19
|
+
#
|
20
|
+
# queue = channel.queue('', durable: true)
|
21
|
+
# worker.subscribe(queue, exclusive: true, ack: false)
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# consume do |delivery_info, properties, payload|
|
25
|
+
# logger.info payload.inspect
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @example More custom worker
|
30
|
+
# class Worker
|
31
|
+
# include Cottontail::Consumer
|
32
|
+
#
|
33
|
+
# session ENV['RABBITMQ_URL'] do |worker, session|
|
34
|
+
# # You always need a separate channel
|
35
|
+
# channel = session.create_channel
|
36
|
+
#
|
37
|
+
# # Creates a `topic` exchange ('cottontail-exchange'), binds a
|
38
|
+
# # queue ('cottontail-queue') to it and listens to any possible
|
39
|
+
# # routing key ('#').
|
40
|
+
# exchange = channel.topic('cottontail-exchange')
|
41
|
+
# queue = channel.queue('cottontail-queue', durable: true)
|
42
|
+
# .bind(exchange, routing_key: '#')
|
43
|
+
#
|
44
|
+
# # Now you need to subscribe the worker instance to this queue.
|
45
|
+
# worker.subscribe(queue, exclusive: true, ack: false)
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# consume 'custom-route' do |delivery_info, properties, payload|
|
49
|
+
# logger.info "routing_key: 'custom-route' | #{payload.inspect}"
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# consume do |delivery_info, properties, payload|
|
53
|
+
# logger.info "any routing key | #{payload.inspect}"
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
module Consumer
|
57
|
+
extend ActiveSupport::Concern
|
58
|
+
|
59
|
+
included do
|
60
|
+
include Cottontail::Configurable
|
61
|
+
|
62
|
+
# default settings
|
63
|
+
set :consumables, -> { Cottontail::Consumer::Collection.new }
|
64
|
+
set :session, -> { [nil, -> {}] }
|
65
|
+
set :logger, -> { Cottontail.get(:logger) }
|
66
|
+
end
|
67
|
+
|
68
|
+
module ClassMethods #:nodoc:
|
69
|
+
# Set the Bunny session.
|
70
|
+
#
|
71
|
+
# You are required to setup a standard Bunny session as you would
|
72
|
+
# when using Bunny directly. This enables you to be configurable to
|
73
|
+
# the maximum extend.
|
74
|
+
#
|
75
|
+
# @example Simple Bunny::Session
|
76
|
+
# session ENV['RABBITMQ_URL'] do |session, worker|
|
77
|
+
# channel = session.create_channel
|
78
|
+
#
|
79
|
+
# queue = channel.queue('MyAwesomeQueue', durable: true)
|
80
|
+
# worker.subscribe(queue, exclusive: true, ack: false)
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# @example Subscribe to multiple queues
|
84
|
+
# session ENV['RABBITMQ_URL'] do |session, worker|
|
85
|
+
# channel = session.create_channel
|
86
|
+
#
|
87
|
+
# queue_a = channel.queue('queue_a', durable: true)
|
88
|
+
# worker.subscribe(queue_a, exclusive: true, ack: false)
|
89
|
+
#
|
90
|
+
# queue_b = channel.queue('queue_b', durable: true)
|
91
|
+
# worker.subscribe(queue_b, exclusive: true, ack: false)
|
92
|
+
# end
|
93
|
+
def session(options = nil, &block)
|
94
|
+
set :session, [options, block]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Method for consuming messages.
|
98
|
+
#
|
99
|
+
# When `:any` is provided as parameter, all messages will be routed to
|
100
|
+
# this block. This is the default.
|
101
|
+
#
|
102
|
+
# @example By routing key
|
103
|
+
# consume route: 'message.sent' do |delivery_info, properties, payload|
|
104
|
+
# # stuff to do
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# # you can also use a shortcut for this
|
108
|
+
# consume "message.sent" do |delivery_info, properties, payload|
|
109
|
+
# # stuff to do
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# @example By multiple routing keys
|
113
|
+
# consume route: ['message.sent', 'message.read'] do |delivery_info, properties, payload|
|
114
|
+
# # stuff to do
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# # you can also use a shortcut for this
|
118
|
+
# consume ["message.sent", "message.read"] do |delivery_info, properties, payload|
|
119
|
+
# # stuff to do
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# @example Scoped to a specific queue
|
123
|
+
# consume route: 'message.sent', queue: 'chats' do |delivery_info, properties, payload|
|
124
|
+
# # do stuff
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# @example By message type (not yet implemented)
|
128
|
+
# consume type: 'ChatMessage' do |delivery_info, properties, payload|
|
129
|
+
# # stuff to do
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# @example By multiple message types (not yet implemented)
|
133
|
+
# consume type: ['ChatMessage', 'PushMessage'] do |delivery_info, properties, payload|
|
134
|
+
# # stuff to do
|
135
|
+
# end
|
136
|
+
#
|
137
|
+
def consume(route = {}, options = {}, &block)
|
138
|
+
options =
|
139
|
+
if route.is_a?(Hash)
|
140
|
+
route
|
141
|
+
else
|
142
|
+
{ route: route }
|
143
|
+
end.merge(options)
|
144
|
+
|
145
|
+
get(:consumables).push(
|
146
|
+
Cottontail::Consumer::Entity.new(options, &block)
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Conveniently start the consumer
|
151
|
+
#
|
152
|
+
# @example Since setup
|
153
|
+
# class Worker
|
154
|
+
# include Cottontail::Consumer
|
155
|
+
#
|
156
|
+
# # ... configuration ...
|
157
|
+
# end
|
158
|
+
#
|
159
|
+
# Worker.start
|
160
|
+
def start(blocking = true)
|
161
|
+
new.start(blocking)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def initialize
|
166
|
+
@__launcher__ = Cottontail::Consumer::Launcher.new(self)
|
167
|
+
@__session__ = Cottontail::Consumer::Session.new(self)
|
168
|
+
|
169
|
+
logger.debug '[Cottontail] initialized'
|
170
|
+
end
|
171
|
+
|
172
|
+
def start(blocking = true)
|
173
|
+
logger.info '[Cottontail] starting up'
|
174
|
+
|
175
|
+
@__session__.start
|
176
|
+
@__launcher__.start if blocking
|
177
|
+
end
|
178
|
+
|
179
|
+
def stop
|
180
|
+
logger.info '[Cottontail] shutting down'
|
181
|
+
|
182
|
+
# @__launcher__.stop
|
183
|
+
@__session__.stop
|
184
|
+
end
|
185
|
+
|
186
|
+
# @private
|
187
|
+
def subscribe(queue, options)
|
188
|
+
queue.subscribe(options) do |delivery_info, properties, payload|
|
189
|
+
consume(delivery_info, properties, payload)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def consume(delivery_info, properties, payload)
|
196
|
+
consumable = consumables.find(delivery_info, properties, payload)
|
197
|
+
|
198
|
+
if consumable.nil?
|
199
|
+
logger.error '[Cottontail] Could not consume message'
|
200
|
+
else
|
201
|
+
consumable.exec(self, delivery_info, properties, payload)
|
202
|
+
end
|
203
|
+
rescue => exception
|
204
|
+
logger.error exception
|
205
|
+
end
|
206
|
+
|
207
|
+
def consumables
|
208
|
+
config.get(:consumables)
|
209
|
+
end
|
210
|
+
|
211
|
+
def logger
|
212
|
+
config.get(:logger)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Cottontail #:nodoc:
|
2
|
+
module Consumer #:nodoc:
|
3
|
+
# Holds the collection of entities
|
4
|
+
class Collection
|
5
|
+
def initialize
|
6
|
+
@items = []
|
7
|
+
end
|
8
|
+
|
9
|
+
# Pushes entity to the list and sorts it
|
10
|
+
def push(entity)
|
11
|
+
@items.push(entity).sort!
|
12
|
+
end
|
13
|
+
|
14
|
+
# {exchange: 'exchange', queue: 'queue', route: 'route'}
|
15
|
+
# {exchange: 'exchange', queue: 'queue', route: :any}
|
16
|
+
# {exchange: 'exchange', queue: :any, route: 'route'}
|
17
|
+
# {exchange: 'exchange', queue: :any, route: :any}
|
18
|
+
# {exchange: :any, queue: 'queue'}
|
19
|
+
# {exchange: :any, queue: :any}
|
20
|
+
def find(delivery_info, _properties = nil, _payload = nil)
|
21
|
+
@items
|
22
|
+
.select { |e| e.matches?(:exchange, delivery_info.exchange) }
|
23
|
+
.select { |e| e.matches?(:queue, delivery_info.consumer.queue.name) }
|
24
|
+
.find { |e| e.matches?(:route, delivery_info.routing_key) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Cottontail #:nodoc:
|
2
|
+
module Consumer #:nodoc:
|
3
|
+
class Entity #:nodoc:
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
VALID_KEYS = [:exchange, :queue, :route]
|
7
|
+
|
8
|
+
def initialize(options = {}, &block)
|
9
|
+
@options = options.keep_if { |k, _| VALID_KEYS.include?(k) }
|
10
|
+
@block = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def matches?(key, value)
|
14
|
+
[value, nil, :any].include?(@options[key])
|
15
|
+
end
|
16
|
+
|
17
|
+
def exec(object, *args)
|
18
|
+
object.instance_exec(*args, &@block)
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def <=>(other)
|
24
|
+
comparables <=> other.comparables
|
25
|
+
end
|
26
|
+
|
27
|
+
def comparables
|
28
|
+
VALID_KEYS.map { |key| Property.new(@options[key] || '') }
|
29
|
+
end
|
30
|
+
|
31
|
+
class Property < String #:nodoc:
|
32
|
+
protected
|
33
|
+
|
34
|
+
def <=>(other)
|
35
|
+
case
|
36
|
+
when length == 0 && other.length == 0 then 0
|
37
|
+
when length == 0 then 1
|
38
|
+
when other.length == 0 then -1
|
39
|
+
else super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Cottontail #:nodoc:
|
4
|
+
module Consumer #:nodoc:
|
5
|
+
class Launcher #:nodoc:
|
6
|
+
SIGNALS = [:QUIT, :TERM, :INT]
|
7
|
+
|
8
|
+
def initialize(consumer)
|
9
|
+
@consumer = consumer
|
10
|
+
@launcher = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
stop unless @launcher.nil?
|
15
|
+
|
16
|
+
SIGNALS.each do |signal|
|
17
|
+
Signal.trap(signal) { Thread.new { stop } }
|
18
|
+
end
|
19
|
+
|
20
|
+
@launcher = Thread.new { sleep }
|
21
|
+
@launcher.join
|
22
|
+
end
|
23
|
+
|
24
|
+
def stop
|
25
|
+
@consumer.stop if @consumer.respond_to?(:stop)
|
26
|
+
|
27
|
+
@launcher.kill if @launcher.respond_to?(:kill)
|
28
|
+
@launcher = nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
|
3
|
+
module Cottontail #:nodoc:
|
4
|
+
module Consumer #:nodoc:
|
5
|
+
class Session #:nodoc:
|
6
|
+
def initialize(consumer)
|
7
|
+
@consumer = consumer
|
8
|
+
@options, @block = @consumer.config.get(:session)
|
9
|
+
@session = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
stop unless @session.nil?
|
14
|
+
|
15
|
+
@session = Bunny.new(@options)
|
16
|
+
@session.start
|
17
|
+
|
18
|
+
@consumer.instance_exec(@consumer, @session, &@block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def stop
|
22
|
+
@session.stop if @session.respond_to?(:stop)
|
23
|
+
@session = nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/cottontail/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
module Cottontail
|
2
|
-
VERSION =
|
1
|
+
module Cottontail #:nodoc:
|
2
|
+
VERSION = '2.0.0.pre.1'
|
3
3
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Cottontail::Configurable do
|
4
|
+
class ConfigurationFactory #:nodoc:
|
5
|
+
include Cottontail::Configurable
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:config) { ConfigurationFactory.config }
|
9
|
+
before { config.reset! }
|
10
|
+
|
11
|
+
it 'responds to :get' do
|
12
|
+
expect(config).to respond_to(:get)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'responds to :set' do
|
16
|
+
expect(config).to respond_to(:set)
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'String' do
|
20
|
+
let(:value) { 'value' }
|
21
|
+
before { config.set(:key, value) }
|
22
|
+
|
23
|
+
it 'returns the correct value' do
|
24
|
+
expect(config.get(:key)).to eq(value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'Proc' do
|
29
|
+
let(:value) { -> { 'value' } }
|
30
|
+
before { config.set(:key, value) }
|
31
|
+
|
32
|
+
it 'returns the correct value' do
|
33
|
+
expect(config.get(:key)).to eq(value.call)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'calls only once' do
|
37
|
+
v = config.get(:key)
|
38
|
+
expect(config.get(:key)).to equal(v)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|