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.
@@ -0,0 +1,33 @@
1
+ module Superbolt
2
+ class Processor
3
+ attr_reader :message, :logger, :block
4
+ attr_accessor :start_time, :exception
5
+
6
+ def initialize(message, logger, &block)
7
+ @message = message
8
+ @logger = logger
9
+ @block = block
10
+ end
11
+
12
+ def perform
13
+ start!
14
+ block.call(message.parse, logger)
15
+ finish!
16
+ true
17
+ rescue Exception => e
18
+ self.exception = e
19
+ logger.error("#{e.message}\n#{e.backtrace}")
20
+ false
21
+ end
22
+
23
+ def start!
24
+ self.start_time = Time.now
25
+ logger.info "[#{start_time}] Processing message: #{message.parse}"
26
+ end
27
+
28
+ def finish!
29
+ end_time = Time.now
30
+ logger.info "[#{end_time}] Finished message: #{message.parse}\n in #{start_time - end_time} seconds"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,93 @@
1
+ module Superbolt
2
+ class Queue
3
+ attr_reader :name, :config
4
+
5
+ def initialize(name, config=nil)
6
+ @name = name
7
+ @config = config || Superbolt.config
8
+ end
9
+
10
+ def connection
11
+ @connection ||= Connection::Queue.new(name, config)
12
+ end
13
+
14
+ delegate :close, :closing, :exclusive?, :durable?, :auto_delete?,
15
+ :writer, :channel, :q,
16
+ to: :connection
17
+
18
+ def push(message)
19
+ closing do
20
+ writer.publish(message.to_json, routing_key: name)
21
+ end
22
+ end
23
+
24
+ def size
25
+ closing do
26
+ q.message_count
27
+ end
28
+ end
29
+
30
+ def clear
31
+ closing do
32
+ q.purge
33
+ end
34
+ end
35
+
36
+ # TODO: roll up some of these subscribe methods
37
+
38
+ def read
39
+ messages = []
40
+ closing do
41
+ q.subscribe(:ack => true) do |delivery_info, metadata, payload|
42
+ message = IncomingMessage.new(delivery_info, payload, channel)
43
+ messages << message
44
+ end
45
+ end
46
+ messages
47
+ end
48
+
49
+ def all
50
+ read.map(&:parse)
51
+ end
52
+
53
+ def peek
54
+ all.first
55
+ end
56
+
57
+ def pop
58
+ messages = []
59
+ closing do
60
+ popped = false
61
+ q.subscribe(:ack => false) do |delivery_info, metadata, message|
62
+ next if popped
63
+ messages << IncomingMessage.new(delivery_info, message, channel)
64
+ popped = true
65
+ end
66
+ end
67
+ message = messages.first
68
+ message && message.parse
69
+ end
70
+
71
+ delegate :slice, :[],
72
+ to: :all
73
+
74
+ def delete
75
+ messages = []
76
+ closing do
77
+ q.subscribe(:ack => true) do |delivery_info, metadata, payload|
78
+ message = IncomingMessage.new(delivery_info, payload, channel)
79
+ relevant = yield(message.parse)
80
+ if relevant
81
+ messages << message.parse
82
+ message.ack
83
+ end
84
+ end
85
+
86
+ # channel is closed by block before message ack can complete
87
+ # therefore we must sleep :(
88
+ sleep 0.02
89
+ end
90
+ messages
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,3 @@
1
+ module Superbolt
2
+ VERSION = "0.1.0"
3
+ end
data/lib/superbolt.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'json'
2
+
3
+ require 'bunny'
4
+ require 'amqp'
5
+ require 'active_support/core_ext/module/delegation'
6
+
7
+ require "superbolt/version"
8
+ require "superbolt/config"
9
+ require "superbolt/adapter/base"
10
+ require "superbolt/adapter/bunny"
11
+ require "superbolt/adapter/amqp"
12
+ require "superbolt/connection/base"
13
+ require "superbolt/connection/queue"
14
+ require "superbolt/connection/app"
15
+ require "superbolt/queue"
16
+ require "superbolt/incoming_message"
17
+ require "superbolt/app"
18
+ require "superbolt/processor"
19
+ require "superbolt/facade"
20
+ require "superbolt/messenger"
21
+
22
+ $stdout.sync = true
data/spec/app_spec.rb ADDED
@@ -0,0 +1,98 @@
1
+ require 'spec_helper'
2
+
3
+ describe Superbolt::App do
4
+ let(:env) { 'test' }
5
+ let(:name) { 'superbolt' }
6
+ let(:logger) { Logger.new('/dev/null') }
7
+ let(:app) {
8
+ Superbolt::App.new(name, {
9
+ env: env,
10
+ logger: logger
11
+ })
12
+ }
13
+ let(:queue) { Superbolt::Queue.new("#{name}_#{env}") }
14
+ let(:quit_queue) { Superbolt::Queue.new("#{name}_#{env}.quit") }
15
+ let(:error_queue) { Superbolt::Queue.new("#{name}_#{env}.error") }
16
+ let(:messages) { [] }
17
+
18
+ before do
19
+ queue.clear
20
+ quit_queue.clear
21
+ error_queue.clear
22
+ end
23
+
24
+ describe '#run' do
25
+ it "shuts down with any message to the quit queue" do
26
+ queue.push({please: 'stop'})
27
+
28
+ app.run do |arguments|
29
+ quit_queue.push({message: 'just because'})
30
+ end
31
+
32
+ queue.size.should == 0
33
+ quit_queue.size.should == 0
34
+ end
35
+
36
+ it 'passes messages to the block for processing' do
37
+ queue.push({first: 1})
38
+ queue.push({last: 2})
39
+
40
+ app.run do |message, logger|
41
+ messages << message
42
+ quit_queue.push({message: 'quit'}) if message['last']
43
+ end
44
+
45
+ messages.size.should == 2
46
+ messages.should == [
47
+ {'first' => 1},
48
+ {'last' => 2}
49
+ ]
50
+ end
51
+
52
+ it 'removes messages from the queue on successful completion' do
53
+ queue.push({first: 1})
54
+ queue.push({last: 2})
55
+
56
+ app.run do |message, logger|
57
+ messages << message
58
+ quit_queue.push({message: 'quit'}) if message['last']
59
+ end
60
+
61
+ queue.size.should == 0
62
+ end
63
+
64
+
65
+ it "passes a logger to the block" do
66
+ mock_logger = double
67
+ app.logger = mock_logger
68
+
69
+ message_received = false
70
+ mock_logger.stub(:info) do |m|
71
+ if m == {'write' => 'out'}
72
+ message_received = true
73
+ end
74
+ end
75
+
76
+ queue.push({write: 'out'})
77
+
78
+ app.run do |message, logger|
79
+ logger.info(message)
80
+ quit_queue.push({message: 'stop!'})
81
+ end
82
+
83
+ message_received.should be_true
84
+ end
85
+
86
+ it "moves the message to an error queue if an exception is raised" do
87
+ queue.push({oh: 'noes'})
88
+
89
+ app.run do |message|
90
+ quit_queue.push({message: 'halt thyself'})
91
+ raise "something went wrong"
92
+ end
93
+
94
+ queue.size.should == 0
95
+ error_queue.size.should == 1
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe Superbolt::Config do
4
+ let(:config) {
5
+ Superbolt::Config.new(options)
6
+ }
7
+
8
+ let(:options) {
9
+ {}
10
+ }
11
+
12
+ describe '#connection' do
13
+ context "environmental variables" do
14
+ context 'default behavior' do
15
+ let(:old_value) { ENV['RABBITMQ_URL'] }
16
+ let(:url) { 'http://cloudamqp-url.com' }
17
+
18
+ before do
19
+ old_value
20
+ ENV['RABBITMQ_URL'] = url
21
+ end
22
+
23
+ after do
24
+ ENV['RABBITMQ_URL'] = old_value
25
+ end
26
+
27
+ it "returns the RABBITMQ_URL" do
28
+ config.connection_params.should == url
29
+ end
30
+ end
31
+
32
+ context 'additional configuration passed in' do
33
+ let(:old_value) { ENV['SOMEOTHERURL'] }
34
+ let(:url) { 'http://someother-url.com' }
35
+ let(:options) {
36
+ {connection_key: 'SOMEOTHERURL'}
37
+ }
38
+
39
+ before do
40
+ old_value
41
+ ENV['SOMEOTHERURL'] = url
42
+ end
43
+
44
+ after do
45
+ ENV['SOMEOTHERURL'] = old_value
46
+ end
47
+
48
+ it "returns the url specified in the env" do
49
+ config.connection_params.should == url
50
+ end
51
+ end
52
+ end
53
+
54
+ context 'no environmental variables' do
55
+ context 'default' do
56
+ it "uses the default url" do
57
+ config.connection_params.should == {
58
+ :host => '127.0.0.1'
59
+ }
60
+ end
61
+ end
62
+
63
+ context 'connection params passed in' do
64
+ let(:options) {
65
+ {
66
+ connection_params: {
67
+ :host => 'hoo.com'
68
+ }
69
+ }
70
+ }
71
+
72
+ it "uses what it is given" do
73
+ config.connection_params.should == options[:connection_params]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe Superbolt::Adapter::Bunny do
4
+ let(:connection) { Superbolt::Adapter::Bunny.new }
5
+
6
+ it "has an underlying open connection via Bunny" do
7
+ connection.socket.should be_a Bunny::Session
8
+ connection.socket.should be_open
9
+ connection.should be_open
10
+ end
11
+
12
+ it "has a channel" do
13
+ connection.channel.should be_a Bunny::Channel
14
+ end
15
+
16
+ it "delegates queue creation to the channel" do
17
+ queue = connection.queue('changelica')
18
+ queue.should be_a Bunny::Queue
19
+ connection.queues.keys.should include('changelica')
20
+ end
21
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ describe Superbolt::Messenger do
4
+ let(:env) { 'test' }
5
+ let(:name) { 'my_friend_app' }
6
+ let(:queue) { Superbolt::Queue.new("#{name}_#{env}") }
7
+
8
+ before do
9
+ queue.clear
10
+ Superbolt.env = env
11
+ Superbolt.app_name = nil
12
+ end
13
+
14
+ let(:messenger) { Superbolt::Messenger.new }
15
+
16
+ describe 'queue generation' do
17
+ it "can be instantiated with a queue destination" do
18
+ m = Superbolt::Messenger.new(to: name)
19
+ m.name.should == name
20
+ m.queue.name.should == "#{name}_#{env}"
21
+ end
22
+
23
+ it "destination queue can be set via #from" do
24
+ messenger.to('activator')
25
+ messenger.queue.name.should == "activator_#{env}"
26
+ end
27
+
28
+ it "raises an error if the name is nil" do
29
+ expect {
30
+ messenger.queue
31
+ }.to raise_error
32
+ end
33
+ end
34
+
35
+ describe 'underlying message' do
36
+ let(:message) { messenger.message }
37
+
38
+ it "starts its life with no interesting values" do
39
+ message[:origin].should == nil
40
+ message[:event].should == 'default'
41
+ message[:arguments].should == {}
42
+ end
43
+
44
+ it "calls to #from, set the origin on the message" do
45
+ messenger.from('linkticounter')
46
+ message[:origin].should == 'linkticounter'
47
+ end
48
+
49
+ it "passes event data to the message" do
50
+ messenger.re('zap')
51
+ message[:event].should == 'zap'
52
+ end
53
+
54
+ it "passes data to the message" do
55
+ messenger.data({foo: 'bar'})
56
+ message[:arguments].should == {foo: 'bar'}
57
+ end
58
+ end
59
+
60
+ describe 'send!' do
61
+ before do
62
+ messenger
63
+ .to(name)
64
+ .from('me')
65
+ .re('love')
66
+ end
67
+
68
+ it "returns a hash that gets sent to the right queue" do
69
+ messenger.send!('none')
70
+ queue.pop.should == {
71
+ 'origin' => 'me',
72
+ 'event' => 'love',
73
+ 'arguments' => 'none'
74
+ }
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Superbolt::Queue' do
4
+ let(:name) { 'superbolt_test' }
5
+ let(:connection) { Superbolt::Connection.new }
6
+ let(:queue) { Superbolt::Queue.new(name) }
7
+ let(:messages) { [] }
8
+
9
+ before do
10
+ queue.clear
11
+ end
12
+
13
+ it "has the right name" do
14
+ queue.name.should == name
15
+ end
16
+
17
+ it "is setup with the right defaults" do
18
+ queue.exclusive?.should be_false
19
+ queue.durable?.should be_true
20
+ queue.auto_delete?.should be_false
21
+ end
22
+
23
+ describe 'queue/array operations' do
24
+ let(:message) { {hello: 'insomniacs'} }
25
+ let(:decoded) { {'hello' => 'insomniacs'} }
26
+
27
+ describe '#push' do
28
+ let(:bunny_queue) {connection.queue(name, Superbolt::Queue.default_options)}
29
+
30
+ it "writes to the queue" do
31
+ queue.push(message)
32
+ queue.size.should == 1
33
+ end
34
+ end
35
+
36
+ describe '#peek' do
37
+ it "returns the message but leaves it in the queue" do
38
+ queue.push(message)
39
+ queue.peek.should == decoded
40
+ queue.size.should == 1
41
+ end
42
+ end
43
+
44
+ describe '#pop' do
45
+ it "returns the message and deletes it from the queue" do
46
+ queue.push(message)
47
+ queue.pop.should == decoded
48
+ queue.size.should == 0
49
+ end
50
+ end
51
+
52
+ describe '#all' do
53
+ before do
54
+ queue.push(message)
55
+ queue.push(message)
56
+ queue.push(message)
57
+ end
58
+
59
+ it "returns all the messages on the queue" do
60
+ messages = queue.all
61
+ messages.size.should == 3
62
+ messages.uniq.should == [decoded]
63
+ end
64
+
65
+ it "does not consume the messages" do
66
+ queue.size.should == 3
67
+ end
68
+ end
69
+
70
+ describe '#slice(offset, n)' do
71
+ before do
72
+ (0..9).to_a.each do |i|
73
+ queue.push(message.merge(i: i))
74
+ end
75
+ end
76
+
77
+ it "returns a set of messages determined by the offset and the number requested" do
78
+ messages = queue.slice(1,3)
79
+ messages.size.should == 3
80
+ messages.map{|json| json['i']}.should == [1,2,3]
81
+ end
82
+
83
+ it "does not consume messages" do
84
+ queue.slice(1,3)
85
+ queue.size.should == 10
86
+ end
87
+ end
88
+
89
+ describe '#[i]' do
90
+ before do
91
+ (0..9).to_a.each do |i|
92
+ queue.push(message.merge(i: i))
93
+ end
94
+ end
95
+
96
+ it "return the message at the i-th position without removing it from the queue" do
97
+ json = queue[3]
98
+ json['i'].should == 3
99
+ end
100
+ end
101
+
102
+ describe '#delete(&block)' do
103
+ before do
104
+ (0..9).to_a.each do |i|
105
+ queue.push(message.merge(i: i))
106
+ end
107
+ end
108
+
109
+ it "returns all messages where the block is true" do
110
+ messages = queue.delete{|json| json['i'] > 2 && json['i'] != 6 && json['i'] < 8 }
111
+ messages.map{|json| json['i']}.should == [3,4,5,7]
112
+ end
113
+
114
+ it "removes those messages from the queue" do
115
+ queue.delete{|json| json['i'] > 2 && json['i'] != 6 && json['i'] < 8 }
116
+ queue.size.should == 6
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,8 @@
1
+ here = File.dirname(__FILE__)
2
+ require "#{here}/../lib/superbolt"
3
+ Dir["#{here}/support/**/*.rb"].each {|f| require f}
4
+
5
+ RSpec.configure do |config|
6
+ config.color = true
7
+ config.order = :rand
8
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Superbolt, 'the facade' do
4
+ describe '.config' do
5
+ after do
6
+ Superbolt.instance_eval do
7
+ @config = nil
8
+ end
9
+ end
10
+
11
+ it "stores the default" do
12
+ Superbolt.config.should == Superbolt::Config.new
13
+ end
14
+
15
+ it "can be customized" do
16
+ Superbolt.config = {
17
+ connection_key: 'SOME_RABBITMQ_URL'
18
+ }
19
+
20
+ Superbolt.config.env_connection_key.should == 'SOME_RABBITMQ_URL'
21
+ end
22
+ end
23
+
24
+ describe '.queue' do
25
+ it "creates a queue with the default config" do
26
+ queue = double('queue')
27
+ Superbolt::Queue.should_receive(:new)
28
+ .with('queue_name', Superbolt.config)
29
+ .and_return(queue)
30
+ Superbolt.queue('queue_name').should == queue
31
+ end
32
+ end
33
+
34
+ describe '.message' do
35
+ it "sends messages via the messenger system" do
36
+ queue = Superbolt.queue('activator_test')
37
+ queue.clear
38
+ Superbolt.env = 'test'
39
+ Superbolt.app_name = 'bossanova'
40
+
41
+ Superbolt.message
42
+ .to('activator')
43
+ .re('update')
44
+ .send!({class: 'Advocate'})
45
+
46
+ queue.pop.should == {
47
+ 'origin' => 'bossanova',
48
+ 'event' => 'update',
49
+ 'arguments' => {
50
+ 'class' => 'Advocate'
51
+ }
52
+ }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,7 @@
1
+ def queue_name
2
+ 'superbolt_test'
3
+ end
4
+
5
+ def clear_queue(name=queue_name)
6
+ #Bunny.
7
+ end
data/superbolt.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'superbolt/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "superbolt"
8
+ spec.version = Superbolt::VERSION
9
+ spec.authors = ["socialchorus"]
10
+ spec.email = ["developers@socialchorus.com"]
11
+ spec.description = %q{Superbolt is comprised of a standalone app, and a queue-like queue for sending messages between services and applications.}
12
+ spec.summary = %q{Superbolt is a gem that makes SOA intra-app communication easy, via RabbitMQ}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+ spec.add_dependency "amqp"
23
+ spec.add_dependency "bunny", "~> 0.9.0.rc1"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec"
28
+ end