superbolt 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e5ba86e1d694023338deda0d39210951edb45c5b
4
+ data.tar.gz: 76775a315705a55b7a0e149fc787957800cbc56f
5
+ SHA512:
6
+ metadata.gz: ee50367c87dcf66d69221f20b06aa3d533ffe45b27ddd038d5de41ac00af2c8609ea11c2d54e09cf80c5bacb649dd7877c4bc4043408ad6ec0932bc8688df568
7
+ data.tar.gz: 07dd4657ad96cf381b7f6e8f107d0c106ae983c274faf7d8324ad4dcb958b593b6e020b1b064607e667b452ad22218271bff451c5b2066698b9c19ec92fb9db4
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in superbolt.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 SocialChorus
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # Superbolt
2
+
3
+ Superbolt is an easy intra-app communication system for sending messages
4
+ between applications. It is backed by RabbitMQ and under the covers it
5
+ uses both the Bunny gem and the AMQP gem.
6
+
7
+ #### Why not just use those gems?
8
+
9
+ RabbitMQ is hard. Even though it is a messaging queue, by name, a
10
+ developer can't pick up one of these great gems and treat the queue
11
+ like a queue. There is great ceremony in the process. In fact, it is
12
+ quite easy to pass in the wrong queue arguments, or leave a connection
13
+ open.
14
+
15
+ Superbolt takes the ceremony away and let's developers focus on what is
16
+ important: reading and sending messages.
17
+
18
+ #### How does it make it easier?
19
+
20
+ Superbolt has two main components: a queue and an app.
21
+
22
+ The queue object acts like a queue. The queue can push, pop and peek.
23
+ The queue is able to spit out all or a subset of the messages on the
24
+ queue. It can clear itself. In short it makes inline operations doable.
25
+
26
+ The app on the other hand is an EventMachine process that takes over the
27
+ thread. It continually reads from a single queue until it recieves a
28
+ signal or message shutting it down. It is smart; it handles exceptions in message
29
+ processing by moving those problem messages to a related error queue. It also
30
+ listens on a separate quit queue for a graceful shutdown. A graceful
31
+ shutdown means no messages are lost.
32
+
33
+ #### Simple because there are less RabbitMQ capabilities
34
+
35
+ Superbolt makes intra-app communication easier via reducing
36
+ the types of things that can be done in RabbitMQ. While Superbolt was made to
37
+ address typical messaging patterns like RPC and work queues, it cuts out
38
+ RabbitMQ features like exclusive binding, and uses the library itself,
39
+ and conventions to make sure the right application gets the right
40
+ message. If all the features of RabbitMQ are needed, then using
41
+ Superbolt is not appropriate ... hop along.
42
+
43
+ #### Conventions
44
+
45
+ While Superbolt makes it possible to listen on any queue name
46
+ as an application, or interact with any queue name as an enumerable-ish queue, it
47
+ gets its real power from conventions.
48
+
49
+ Messages are JSON. Developers can do it differently, but the gains of
50
+ ease will be lost.
51
+
52
+ By convention each application has a name. The name is used along with
53
+ the application environment in order to communicate. The environment
54
+ means that test and development messages don't get lumped together. The
55
+ application name means that each application has to employ some
56
+ filtering to figure out what end process should handle the messages.
57
+
58
+ Per application filtering, is made very easy by Superbolt's message conventions.
59
+ A Superbolt message has three keys:
60
+
61
+ 1. origin: the origin application
62
+ 2. event: some identifier/sort key for the message type, will be 'default' by
63
+ default.
64
+ 3. arguments: additional data to be passed on to the handling process
65
+
66
+ Developer's can create a message without having to think to much about
67
+ these concerns:
68
+
69
+ Superbolt.app_name = 'me'
70
+ Superbolt.message.to('over_there').send!({just: 'do it!'})
71
+
72
+ Superbolt.queue('over_there').pop
73
+ => {
74
+ origin: 'me',
75
+ event: 'default',
76
+ arguments: {
77
+ just: 'do it!'
78
+ }
79
+ }
80
+
81
+ ## Usage (more)
82
+
83
+ ###Configuring:
84
+
85
+ Superbolt.config = {
86
+ app_name: 'my_app', # no default
87
+ env: 'staging', # looks to env for information
88
+ connection_params: {
89
+ # can use anything RabbitMQ speaks here
90
+ host: 'my-rabbitmq-provider.com'
91
+ } # defaults to what is set in the ENV or localhost
92
+ }
93
+
94
+ #### Connection Configuration
95
+
96
+ Out of the box Superbolt will look to the ENV to see if there is a
97
+ connection key, RABBITMQ\_URL. Developers can customize the connection
98
+ key that is used. Actual connection params that RabbitMQ Bunny/AMQP use
99
+ can also be passed in as above. If a connection key is not found in the
100
+ ENV, localhost will be used. If the application uses these typical
101
+ conventions, then no connection configuration is required.
102
+
103
+ #### App Name
104
+
105
+ The application name/identifier is an important default to setup in
106
+ order to get all the goodness of related to messaging and its
107
+ conventions.
108
+
109
+ If no app name is set up, a littlem or work is required to send a
110
+ message:
111
+
112
+ Superbolt.message
113
+ .to('over_there')
114
+ .from('me')
115
+ .send!({just: 'do it!'})
116
+
117
+ Without the #from call, the message will be sent without an origin. In
118
+ our experience it is typical to process messages differently depending
119
+ on the sender. Of course, that information can be encoded into the event
120
+ name, but it is pretty easy to configure an application name so event
121
+ filtering is easier for the consuming application.
122
+
123
+ Superbolt.app_name = 'my_great_app'
124
+ # - or -
125
+ Superbolt.config = {
126
+ app_name = 'my_great_app'
127
+ }
128
+
129
+ ### Sending messages
130
+
131
+ Superbolt doesn't want to developers worrying about exchanges,
132
+ durability or connection ceremony. Developers should be able to just
133
+ send a message. The ease of that sending depends on whether developers
134
+ are sticking with the Superbolt conventions or not.
135
+
136
+ #### The easiest way to message
137
+
138
+ The easiest way is to use the Superbolt level helper for sending a
139
+ message:
140
+
141
+ Superbolt.message
142
+ .re('dorothy')
143
+ .to('wicked_witch')
144
+ .send!('On yellow brick road; has friends!')
145
+
146
+ This message can be received on a queue
147
+ 'wicked\_witch\_staging', given that the environment is
148
+ 'staging'. When it is popped off it will look like this:
149
+
150
+ {
151
+ origin: 'my_configured_app_name',
152
+ event: 'dorothy',
153
+ arguments: 'On yellow brick road; has friends!'
154
+ }
155
+
156
+ #### A more customizable messaging experience
157
+
158
+ Messages can also be sent via Superbolt::Queue objects. In this case the
159
+ message can be anything and the queue name is exactly what is passed in.
160
+
161
+ queue = Superbolt::Queue.new('dorothy')
162
+ queue.push({demand: 'Surrender!'})
163
+ queue.pop
164
+ => {
165
+ 'demand' => 'Surrender!'
166
+ }
167
+
168
+ ### Reading messages
169
+
170
+ Messages can be read inline or via a standalone app.
171
+
172
+ #### Reading Inline
173
+
174
+ Reading messages inline is easy and to the point. The Superbolt::Queue
175
+ object tries to act queue-like instead of like a hard to use external
176
+ service.
177
+
178
+ Popping messages off the queue will remove the message from the queue
179
+ immediately. If something goes wrong with eth message processing, it is
180
+ the responsibility of the consuming application to figure out what to
181
+ do.
182
+
183
+ message = queue.pop # This removes the message permanently
184
+
185
+ Messages can be read in non-destructive ways as well.
186
+
187
+ message = queue.peek
188
+ # Ponder or process message.
189
+ # It is still hanging out at the top of the queue.
190
+ # It can be deleted with a pop,
191
+ # provided another consumer hasn't already deleted it!
192
+
193
+ Because of asynchronicity issues with job processing across several apps
194
+ or workers, the best usage for non-destructive inline reads is debugging
195
+ and information gathering.
196
+
197
+ queue.all # return all the messages on the queue
198
+
199
+ # remove messages meeting the block criteria
200
+ queue.delete {|m| m['level'] == 'not_important' }
201
+
202
+ # peek at messages in a certain range
203
+ queue.slice(2, 4)
204
+
205
+ # get a certain message, non-destructively
206
+ queue[3]
207
+
208
+ #### Reading via a Standalone App
209
+
210
+ Reading messages inline is useful, but in general an application wants
211
+ to read continuously for the latest and greatest intra-app
212
+ communications.
213
+
214
+ Superbolt::App.new('dorothy_inbox').run do |message, logger|
215
+ HomewardBound.new(message).perform
216
+ end
217
+
218
+ Exceptions raised in the processing block will not exit
219
+ the Superbolt app. Errors will be logged and the message will be put on
220
+ an error queue with information about the exception raised. Those
221
+ messages can be seen by accessing the related error queue:
222
+
223
+ error_queue = Superbolt::App.new('dorothy_inbox').error_queue
224
+ error_queue.all
225
+
226
+ The error queue is a Superbolt::Queue and methods like #pop, #delete,
227
+ and #[] are available to gather more data about the exceptions being
228
+ raised.
229
+
230
+ The app can be shutdown gracefully by sending a quit message to a
231
+ special queue:
232
+
233
+ quit_queue = Superbolt::App.new('dorothy_inbox').quit_queue
234
+ quit_queue.push(message: 'for a deploy')
235
+
236
+ ## Installation
237
+
238
+ Add this line to your application's Gemfile:
239
+
240
+ gem 'superbolt'
241
+
242
+ And then execute:
243
+
244
+ $ bundle
245
+
246
+ Or install it yourself as:
247
+
248
+ $ gem install superbolt
249
+
250
+ ## TODOs:
251
+
252
+ * Easy filtering/delegation of messages to classes
253
+ * Failed messages are put on another queue so that the app is not
254
+ in a failure loop.
255
+ * In code YARD stye documentation
256
+ * CodeClimate
257
+ * TravisCI continuout integration
258
+
259
+
260
+ ## Contributing
261
+
262
+ 1. Fork it
263
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
264
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
265
+ 4. Push to the branch (`git push origin my-new-feature`)
266
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,18 @@
1
+ module Superbolt
2
+ module Adapter
3
+ class AMQP < Base
4
+ def socket
5
+ @socket ||= ::AMQP.connect(config.connection_params)
6
+ end
7
+
8
+ def channel
9
+ @channel ||= ::AMQP::Channel.new(socket)
10
+ end
11
+
12
+ def close(&block)
13
+ socket.close(&block)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,28 @@
1
+ module Superbolt
2
+ module Adapter
3
+ class Base
4
+ attr_reader :config
5
+
6
+ def initialize(config=nil)
7
+ @config = config || Superbolt.config
8
+ end
9
+
10
+ delegate :closed?, :open, :open?,
11
+ to: :socket
12
+
13
+ def close
14
+ response = socket.close
15
+ @socket = nil
16
+ @channel = nil
17
+ response
18
+ end
19
+
20
+ delegate :queues, :acknowledge, :reject, :queue,
21
+ to: :channel
22
+
23
+ def exchange
24
+ channel.default_exchange
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ module Superbolt
2
+ module Adapter
3
+ class Bunny < Base
4
+ def socket
5
+ return @socket if @socket
6
+ @socket = ::Bunny.new(config.connection_params)
7
+ @socket.start
8
+ @socket
9
+ end
10
+
11
+ def channel
12
+ @channel ||= socket.create_channel
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,73 @@
1
+ module Superbolt
2
+ class App
3
+ attr_reader :config, :env
4
+ attr_accessor :logger
5
+
6
+ def initialize(name, options)
7
+ @name = name
8
+ @env = options[:env]
9
+ @logger = options[:logger] || Logger.new($stdout)
10
+ @config = options[:config] || Superbolt.config
11
+ end
12
+
13
+ def name
14
+ env ? "#{@name}_#{env}" : @name
15
+ end
16
+
17
+ def connection
18
+ @connection ||= Connection::App.new(name, config)
19
+ end
20
+
21
+ delegate :close, :closing, :exclusive?, :durable?, :auto_delete?,
22
+ :writer, :channel, :q,
23
+ to: :connection
24
+
25
+ def queue
26
+ connection.q
27
+ end
28
+
29
+ def quit_subscriber_queue
30
+ connection.qq
31
+ end
32
+
33
+ def quit_queue
34
+ Queue.new("#{connection.name}.quit", connection.config)
35
+ end
36
+
37
+ def error_queue
38
+ Queue.new("#{connection.name}.error", connection.config)
39
+ end
40
+
41
+ def run(&block)
42
+ EventMachine.run do
43
+ queue.subscribe(ack: false) do |metadata, payload|
44
+ message = IncomingMessage.new(metadata, payload, channel)
45
+ processor = Processor.new(message, logger, &block)
46
+ unless processor.perform
47
+ on_error(message.parse, processor.exception)
48
+ end
49
+ end
50
+
51
+ quit_subscriber_queue.subscribe do |message|
52
+ quit(message)
53
+ end
54
+ end
55
+ end
56
+
57
+ def on_error(message, error)
58
+ error_message = message.merge({error: {
59
+ class: error.class,
60
+ message: error.message,
61
+ backtrace: error.backtrace
62
+ }})
63
+ error_queue.push(error_message)
64
+ end
65
+
66
+ def quit(message='no message given')
67
+ logger.info "EXITING Superbolt App listening on queue #{name}: #{message}"
68
+ close {
69
+ EventMachine.stop { exit }
70
+ }
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,30 @@
1
+ module Superbolt
2
+ class Config
3
+ attr_reader :options
4
+
5
+ def initialize(options={})
6
+ @options = options
7
+ end
8
+
9
+ def connection_params
10
+ env_params || default
11
+ end
12
+
13
+ def env_connection_key
14
+ options[:connection_key] || 'RABBITMQ_URL'
15
+ end
16
+
17
+ def env_params
18
+ ENV[env_connection_key]
19
+ end
20
+
21
+ def default
22
+ options[:connection_params] || {host: '127.0.0.1'}
23
+ end
24
+
25
+ def ==(other)
26
+ other.connection_params == connection_params &&
27
+ other.env_connection_key == env_connection_key
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ module Superbolt
2
+ module Connection
3
+ class App < Base
4
+ def connection
5
+ @connection ||= Adapter::AMQP.new(config)
6
+ end
7
+
8
+ def close(&block)
9
+ connection.close(&block)
10
+ @connection = nil
11
+ @q = nil
12
+ @qq = nil
13
+ end
14
+
15
+ def qq
16
+ @qq ||= connection.queue("#{name}.quit", self.class.default_options)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,40 @@
1
+ module Superbolt
2
+ module Connection
3
+ class Base
4
+ attr_reader :name, :config
5
+
6
+ def initialize(name, config=nil)
7
+ @name = name
8
+ @config = config || Superbolt.config
9
+ end
10
+
11
+ def connection
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def close
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def q
20
+ @q ||= connection.queue(name, self.class.default_options)
21
+ end
22
+
23
+ delegate :exclusive?, :durable?, :auto_delete?,
24
+ to: :q
25
+
26
+ def channel
27
+ connection.channel
28
+ end
29
+
30
+ def self.default_options
31
+ {
32
+ :auto_delete => false,
33
+ :durable => true,
34
+ :exclusive => false
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,25 @@
1
+ module Superbolt
2
+ module Connection
3
+ class Queue < Base
4
+ def connection
5
+ @connection ||= Adapter::Bunny.new(config)
6
+ end
7
+
8
+ def close
9
+ connection.close
10
+ @connection = nil
11
+ @q = nil
12
+ end
13
+
14
+ def closing(&block)
15
+ response = block.call
16
+ close
17
+ response
18
+ end
19
+
20
+ def writer
21
+ connection.exchange
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ module Superbolt
2
+ def self.config=(options)
3
+ @config = Config.new(options)
4
+ end
5
+
6
+ def self.config
7
+ @config ||= Config.new
8
+ end
9
+
10
+ def self.queue(name)
11
+ Superbolt::Queue.new(name, config)
12
+ end
13
+
14
+ class << self
15
+ attr_writer :env
16
+ attr_accessor :app_name
17
+ end
18
+
19
+ def self.env
20
+ @env || 'development'
21
+ end
22
+
23
+ def self.message(args={})
24
+ Superbolt::Messenger.new(args)
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Superbolt
2
+ class IncomingMessage
3
+ attr_reader :payload, :tag, :channel
4
+
5
+ def initialize(delivery_info, payload, channel)
6
+ @payload = payload
7
+ @tag = delivery_info.delivery_tag
8
+ @channel = channel
9
+ end
10
+
11
+ def parse
12
+ JSON.parse(payload)
13
+ rescue JSON::ParserError
14
+ payload
15
+ end
16
+
17
+ def reject
18
+ channel.reject(tag)
19
+ end
20
+
21
+ def ack
22
+ channel.acknowledge(tag)
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,66 @@
1
+ module Superbolt
2
+ class Messenger
3
+ attr_accessor :origin, :name, :event, :arguments, :env
4
+
5
+ def initialize(options={})
6
+ @name = options.delete(:to)
7
+ @origin = options.delete(:from) || Superbolt.app_name
8
+ @event = options.delete(:event) || self.class.defaultevent
9
+ @env = Superbolt.env
10
+ @arguments = options
11
+ end
12
+
13
+ def message
14
+ {
15
+ origin: origin,
16
+ event: event,
17
+ arguments: arguments
18
+ }
19
+ end
20
+
21
+ # chainer methods
22
+ #
23
+ def to(val)
24
+ self.name = val
25
+ self
26
+ end
27
+
28
+ def from(val)
29
+ self.origin = val
30
+ self
31
+ end
32
+
33
+ def re(val)
34
+ self.event = val
35
+ self
36
+ end
37
+
38
+ def data(val)
39
+ self.arguments = val
40
+ self
41
+ end
42
+
43
+ # alias
44
+ # -------
45
+
46
+ def send!(args=nil)
47
+ self.arguments = args if args
48
+ queue.push(message)
49
+ end
50
+
51
+ def queue
52
+ unless name
53
+ raise "no destination app name defined, please pass one in"
54
+ end
55
+ Queue.new(destination_name)
56
+ end
57
+
58
+ def destination_name
59
+ "#{name}_#{env}"
60
+ end
61
+
62
+ def self.defaultevent
63
+ 'default'
64
+ end
65
+ end
66
+ end