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.
@@ -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