cottontail 0.1.5 → 2.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,3 +1,3 @@
1
- module Cottontail
2
- VERSION = "0.1.5"
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