superbolt 0.1.0

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