superbolt 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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