banter 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,84 @@
1
+ require 'rubygems'
2
+ require 'celluloid/autostart'
3
+ require 'celluloid/io'
4
+ require 'awesome_print'
5
+ require 'active_support/all'
6
+ require 'optparse'
7
+ require 'fileutils'
8
+
9
+ require_relative './client_queue_listener'
10
+
11
+ # In order to use the server, the caller must be able to bring in the necessary
12
+ # classes for require before instantiating the instance.
13
+
14
+ module Banter
15
+ module Server
16
+
17
+ class SubscriberServer
18
+ include Celluloid
19
+
20
+ def initialize(subscribers)
21
+ @workers = subscribers.map { |subscriber| create_queue_listeners(subscriber) }
22
+ end
23
+
24
+ def start
25
+ @workers.each do |worker|
26
+ puts "Starting worker: #{worker.worker_class.name}"
27
+ worker.start
28
+ end
29
+
30
+ thread = Thread.current
31
+ interrupts = ["HUP", "INT", "QUIT", "ABRT", "TERM"]
32
+ interrupts.each do |signal_name|
33
+ Signal.trap(signal_name) {
34
+ puts "Processing #{signal_name}"
35
+ thread.run
36
+ }
37
+ end
38
+
39
+ Thread.stop
40
+
41
+ ::Banter::Logger.new.log_service("all_services", :warn, "Starting shutdown of all services")
42
+
43
+ @workers.each do |worker|
44
+ ::Banter::Logger.new.log_service("all_services", :warn, "Tearing down worker: #{worker.worker_class.name}")
45
+ begin
46
+ STDOUT.puts "Tearing down subscriber for #{worker.worker_class.name}"
47
+ worker.shutdown
48
+ rescue => e
49
+ ::Banter::Logger.new.log_service("all_services", :warn, "#{worker.worker_class.name} - did not tear down correctly. Error - #{e.message}")
50
+ end
51
+ end
52
+ ensure
53
+ Banter::CLI.instance.remove_pid
54
+ end
55
+
56
+ private
57
+
58
+ def warn(message)
59
+ old_behavior = ActiveSupport::Deprecation.behavior
60
+ ActiveSupport::Deprecation.behavior = [:stderr, :log]
61
+ ActiveSupport::Deprecation.warn(message)
62
+ ensure
63
+ ActiveSupport::Deprecation.behavior = old_behavior
64
+ end
65
+
66
+ def create_queue_listeners(subscriber)
67
+ routing_key = subscriber.subscribed_key
68
+ subscribed_queue_name = subscriber.subscribed_queue
69
+
70
+ if routing_key.blank?
71
+ raise ArgumentError.new("Routing key must be provided in #{subscriber.name} using `subscribe_to routing_key`")
72
+ end
73
+
74
+ if subscribed_queue_name.blank?
75
+ raise ArgumentError.new("Queue Name must be provided in #{subscriber.name} using `subscribe_to routing_key, on: queue_name`")
76
+ end
77
+
78
+ STDOUT.puts "Setting up listener for request_key: #{routing_key} and queue:#{subscribed_queue_name}"
79
+ ClientQueueListener.new(subscriber, routing_key, subscribed_queue_name)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
@@ -0,0 +1,100 @@
1
+ require 'rubygems'
2
+ require 'active_support/core_ext'
3
+
4
+ # This class provders the worker for executing the subscriber. It receives a message from rabbitmq and
5
+ # creates an instance of the subscriber to execute with the message and context
6
+
7
+ module Banter
8
+ class Subscriber
9
+ @@registered_subscribers = []
10
+
11
+ class_attribute :payload_validators, :error_handlers, :subscribed_key, :subscribed_queue
12
+ attr_accessor :delivery_routing_data, :delivery_properties, :context
13
+
14
+ def self.inherited(klass)
15
+ @@registered_subscribers << klass
16
+ end
17
+
18
+ # Specify the routing key that the subscriber class should listen to.
19
+ # @param [String] routing_key_name The routing key to subscribe to. Must be characters only separated by periods (.)
20
+ # @param [Hash] options Allowed option is :on to optionally specify a queue. If not provided, queue name is generated from the routing key
21
+ def self.subscribe_to(routing_key_name, options = {})
22
+ options.assert_valid_keys(:on)
23
+ unless validate_routing_key_name(routing_key_name)
24
+ raise ArgumentError.new("#{routing_key_name} is not supported. Only lower case characters separated by periods are allowed.")
25
+ end
26
+ self.subscribed_key = routing_key_name
27
+ self.subscribed_queue = generated_queue_name(routing_key_name, options[:on])
28
+ end
29
+
30
+ # Sets the validator for payload
31
+ #
32
+ # @param validator The validator to use for validating the payload.
33
+ # Returns false if the payload is not valid.
34
+ # Proc must accept a payload as an argument.
35
+ def self.validates_payload_with(*validators)
36
+ self.payload_validators ||= []
37
+ self.payload_validators += validators
38
+ end
39
+
40
+ # Sets an error handler for the class
41
+ def self.handle_errors_with(handler)
42
+ error_handler = handler
43
+ end
44
+
45
+ # @param [Hash] context Context from invocation
46
+ # @param [Object] delivery_routing_data Contains routing information like originator and routing key
47
+ # @param [Object] delivery_properties
48
+ def initialize(delivery_routing_data, delivery_properties, context)
49
+ @delivery_routing_data = delivery_routing_data
50
+ @delivery_properties = delivery_properties
51
+ @context = context
52
+ end
53
+
54
+ # Performs validation if validates_payload_with is defined and then calls the perform method
55
+ # @param [Object] payload Payload of the message
56
+ def perform!(payload)
57
+ if !valid_payload?(payload)
58
+ Banter.logger.error("Payload validation failed for #{self.class.name}")
59
+ raise ::Banter::PayloadValidationError.new("Invalid Payload for #{self.class.name}")
60
+ end
61
+
62
+ perform(context, payload)
63
+ end
64
+
65
+ # Actual subscribers need to implement perform method. This is the method where the message is actually processed.
66
+ # @param [Object] payload Payload of the message
67
+ def perform(payload)
68
+ raise "Need implementation for your worker."
69
+ end
70
+
71
+ # @return [String] The original routing key with which the current message was published
72
+ def routing_key
73
+ delivery_routing_data[:routing_key]
74
+ end
75
+
76
+ # Iterates over all the payload validators and returns false if any of them are false
77
+ # @param [Object] payload The payload/arguments of the message
78
+ # @return [Boolen] Should return true or false value - If no validators are specified, then returns true
79
+ def valid_payload?(payload)
80
+ return true unless payload_validators.present?
81
+
82
+ payload_validators.inject(true) { |is_valid, validator|
83
+ is_valid && (validator.respond_to?(:call) ? validator.call(payload) : send(validator, payload))
84
+ }
85
+ end
86
+
87
+ private
88
+
89
+ def self.validate_routing_key_name(key)
90
+ return true if key.blank?
91
+ key.match(/\A([a-z]+\.?)*([a-z]+)\Z/).present?
92
+ end
93
+
94
+ def self.generated_queue_name(routing_key, queue_name)
95
+ return queue_name if queue_name.present?
96
+ [Banter::Configuration.application_name.to_s.gsub(/[^\w\_]/, ''), routing_key.gsub(".", '_')].reject(&:blank?).join('_')
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,3 @@
1
+ module Banter
2
+ VERSION = "0.4.0"
3
+ end
@@ -0,0 +1,145 @@
1
+ require 'spec_helper'
2
+ require 'banter/cli'
3
+
4
+ describe Banter::CLI do
5
+ let(:cli) { Banter::CLI.instance }
6
+ after(:each) do
7
+ cli.instance_variable_set(:@subscribers, nil)
8
+ cli.instance_variable_set(:@pidfile, nil)
9
+ cli.instance_variable_set(:@require_path, nil)
10
+ end
11
+
12
+ describe '#parse' do
13
+ let(:options) { [] }
14
+ before { cli.parse(options) }
15
+
16
+ context "pidfile" do
17
+ it "works when nothing is passed" do
18
+ expect(cli.pidfile).to be_nil
19
+ end
20
+
21
+ context "passed in as -P" do
22
+ let(:options) { [ "-P", "mypidfile.pid"]}
23
+ it "parses" do
24
+ expect(cli.pidfile).to eq('mypidfile.pid')
25
+ end
26
+ end
27
+
28
+ context "passed in as --pidfile" do
29
+ let(:options) { [ "--pidfile", "mypidfile.pid"]}
30
+ it "parses" do
31
+ expect(cli.pidfile).to eq('mypidfile.pid')
32
+ end
33
+ end
34
+ end
35
+
36
+ context "only" do
37
+ it "works when nothing is passed" do
38
+ expect(cli.subscribers).to be_nil
39
+ end
40
+
41
+ context "passed in as -o" do
42
+ let(:options) { [ "-o", "Foobar,Doobar"]}
43
+ it "parses" do
44
+ expect(cli.subscribers).to eq([ 'Foobar', 'Doobar'])
45
+ end
46
+ end
47
+
48
+ context "passed in as --only" do
49
+ let(:options) { [ "--only", "Foobar,Doobar"]}
50
+ it "parses" do
51
+ expect(cli.subscribers).to eq([ 'Foobar', 'Doobar'])
52
+ end
53
+ end
54
+ end
55
+
56
+ context "require" do
57
+ it "works when nothing is passed" do
58
+ expect(cli.require_path).to eq(".")
59
+ end
60
+
61
+ context "passed in as -r" do
62
+ let(:options) { [ "-r", "/path/to/my/file"]}
63
+ it "parses" do
64
+ expect(cli.require_path).to eq("/path/to/my/file")
65
+ end
66
+ end
67
+
68
+ context "passed in as --require" do
69
+ let(:options) { [ "--require", "/path/to/my/file"]}
70
+ it "parses" do
71
+ expect(cli.require_path).to eq("/path/to/my/file")
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ describe '#run' do
78
+ describe "high-level steps to be completed" do
79
+ before {
80
+ allow(cli).to receive(:load_environment)
81
+ allow(cli).to receive(:write_pidfile)
82
+ allow(cli).to receive(:load_subscribers)
83
+ cli.run
84
+ }
85
+
86
+ it "runs" do
87
+ expect(cli).to have_received(:load_environment)
88
+ expect(cli).to have_received(:write_pidfile)
89
+ expect(cli).to have_received(:load_subscribers)
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '#write_pidfile' do
95
+ before {
96
+ cli.pidfile = pidfile
97
+ allow(File).to receive(:open)
98
+ cli.send(:write_pidfile)
99
+ }
100
+ context "no pidfile passed" do
101
+ let(:pidfile) { nil }
102
+ it "doesn't create a pidfile" do
103
+ expect(File).to_not have_received(:open)
104
+ end
105
+ end
106
+
107
+ context "pidfile path is supplied" do
108
+ let(:pidfile) { "/path/to/pidfile" }
109
+ it 'creates file' do
110
+ expect(File).to have_received(:open).with(pidfile, 'w')
111
+ end
112
+ end
113
+ end
114
+
115
+ describe '#load_subscribers' do
116
+ let(:subscribers) { nil }
117
+ let(:subscriber_server) { double('subscriber_server')}
118
+ before {
119
+ cli.subscribers = subscribers
120
+ allow(subscriber_server).to receive(:start)
121
+ allow(Banter::Server::SubscriberServer).to receive(:new).and_return(subscriber_server)
122
+ cli.send(:load_subscribers)
123
+ }
124
+
125
+ context "No subscribers passed through CLI" do
126
+ let(:all_subscribers) { Banter::Subscriber.class_variable_get(:@@registered_subscribers) }
127
+ it "loads all subscribers" do
128
+ expect(all_subscribers).to include(MyTestSubscriber1)
129
+ expect(all_subscribers).to include(MyTestSubscriber2)
130
+ expect(all_subscribers).to include(MyTestSubscriber3)
131
+ expect(Banter::Server::SubscriberServer).to have_received(:new).with(all_subscribers)
132
+ expect(subscriber_server).to have_received(:start)
133
+ end
134
+ end
135
+
136
+ context "Subscribers passed through CLI" do
137
+ let(:subscribers) { ['MyTestSubscriber1', 'MyTestSubscriber2'] }
138
+ it "loads all subscribers" do
139
+ expect(Banter::Server::SubscriberServer).to have_received(:new).with([MyTestSubscriber1, MyTestSubscriber2])
140
+ expect(subscriber_server).to have_received(:start)
141
+ end
142
+ end
143
+ end
144
+
145
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ describe Banter::Server::ClientQueueListener do
4
+ describe "#message_received" do
5
+
6
+ subject { -> { queue_listener.message_received( {}, {routing_key: routing_key}, {context: context, payload: payload}) } }
7
+ let(:queue_listener) { Banter::Server::ClientQueueListener.new(Klass, routing_key, 'queue') }
8
+ let(:routing_key) { 'some.test.key' }
9
+ let(:context) { {} }
10
+ let(:payload) { {hello: 'moto'} }
11
+ let(:context_before){}
12
+ let(:validators){ [] }
13
+
14
+ before do
15
+ context_before
16
+ Object.const_set :Klass, Class.new(Banter::Subscriber)
17
+ validators.each do |validator|
18
+ Klass.validates_payload_with validator
19
+ end
20
+
21
+ klass_instance
22
+
23
+ allow(Klass).to receive(:new).and_return(klass_instance)
24
+ subject.call unless respond_to? :no_call
25
+ end
26
+
27
+ after do
28
+ Object.send :remove_const, :Klass
29
+ end
30
+
31
+ context "Normal execution" do
32
+ let!(:klass_instance){
33
+ instance = Klass.new({},{}, {})
34
+ allow(instance).to receive(:perform)
35
+ instance
36
+ }
37
+
38
+ context "test exception" do
39
+ let(:no_call) { true }
40
+
41
+ it { should_not raise_exception }
42
+ end
43
+
44
+ context "call" do
45
+ it { expect(klass_instance).to have_received(:perform).with({}, payload) }
46
+ end
47
+ end
48
+
49
+ context "Exception during execution" do
50
+ let!(:klass_instance) {
51
+ instance = Klass.new({},{}, {})
52
+ allow(instance).to receive(:perform){
53
+ raise StandardError.new("test")
54
+ }
55
+ instance
56
+ }
57
+ let(:no_call) { true }
58
+
59
+ it { should_not raise_exception }
60
+ end
61
+
62
+ context "Payload error" do
63
+ let!(:klass_instance) {
64
+ instance = Klass.new({},{}, {})
65
+ allow(instance).to receive(:perform)
66
+ instance
67
+ }
68
+
69
+ let!(:validators) { [lambda{ |payload| false }] }
70
+
71
+ let(:context_before){ allow(Airbrake).to receive(:notify)}
72
+
73
+ it {expect(Airbrake).to have_received(:notify)}
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,161 @@
1
+ require 'spec_helper'
2
+
3
+ describe Banter::Subscriber do
4
+ before(:each) { Object.const_set :Klass, Class.new(Banter::Subscriber) }
5
+ after(:each) { Object.send :remove_const, :Klass }
6
+
7
+ describe "#routing_key" do
8
+ subject { Banter::Subscriber.new(routing_data, properties, {}).routing_key }
9
+
10
+ let(:properties) { {} }
11
+
12
+ context "Routing key is 'honest.test.route'" do
13
+ let(:routing_data) { {routing_key: "honest.test.route"} }
14
+
15
+ it { should eq "honest.test.route" }
16
+ end
17
+ end
18
+
19
+ describe "self.validates_payload_with" do
20
+ before do
21
+ validators.each do |validator|
22
+ Klass.validates_payload_with validator
23
+ end
24
+ end
25
+
26
+ context "One validator" do
27
+ let(:validators){ [lambda{|payload| true}] }
28
+ it {
29
+ expect(Klass.new({},{}, {}).payload_validators.length).to eq 1 }
30
+ end
31
+
32
+ context "Two validators" do
33
+ let(:validators){ [lambda{|payload| true}, lambda{|payload| true}] }
34
+ it { expect(Klass.new({},{}, {}).payload_validators.length).to eq 2 }
35
+ end
36
+ end
37
+
38
+ describe '#valid_payload?' do
39
+ context
40
+ subject { Klass.new({},{}, {}).valid_payload?({}) }
41
+
42
+ before do
43
+ validators.each do |validator|
44
+ Klass.validates_payload_with validator
45
+ end
46
+ end
47
+
48
+ context "Two validators returning true" do
49
+ let(:validators){ [lambda{|payload| true}, lambda{|payload| true}] }
50
+ it { should be true }
51
+ end
52
+
53
+ context "One validator returning true one returning false" do
54
+ let(:validators){ [lambda{|payload| false}, lambda{|payload| true}] }
55
+ it { should be false }
56
+ end
57
+
58
+ context "No validators" do
59
+ let(:validators){ [] }
60
+ it { should be true }
61
+ end
62
+
63
+ context "validators as symbol" do
64
+ context "method returns true" do
65
+ before {
66
+ Klass.class_variable_set(:@@foo_bar_called, false)
67
+ Klass.send :define_method, :foo_bar do |payload|
68
+ Klass.class_variable_set(:@@foo_bar_called, true)
69
+ true
70
+ end
71
+ }
72
+ let(:validators) { [lambda{|payload| true}, :foo_bar] }
73
+ it {
74
+ should be true
75
+ expect(Klass.class_variable_get(:@@foo_bar_called)).to be true
76
+ }
77
+
78
+ end
79
+
80
+ context "method returns false" do
81
+ before {
82
+ Klass.class_variable_set(:@@foo_bar_called, false)
83
+ Klass.send :define_method, :foo_bar do |payload|
84
+ Klass.class_variable_set(:@@foo_bar_called, true)
85
+ false
86
+ end
87
+ }
88
+ let(:validators) { [lambda{|payload| true}, :foo_bar] }
89
+ it {
90
+ should be false
91
+ expect(Klass.class_variable_get(:@@foo_bar_called)).to be true
92
+ }
93
+
94
+ end
95
+
96
+ context "proc returns false" do
97
+ before {
98
+ Klass.class_variable_set(:@@foo_bar_called, false)
99
+ Klass.send :define_method, :foo_bar do |payload|
100
+ Klass.class_variable_set(:@@foo_bar_called, true)
101
+ true
102
+ end
103
+ }
104
+ let(:validators) { [lambda{|payload| false}, :foo_bar] }
105
+ it "method shouldn't be called" do
106
+ should be false
107
+ expect(Klass.class_variable_get(:@@foo_bar_called)).to be false
108
+ end
109
+
110
+ end
111
+ end
112
+ end
113
+
114
+ describe "#validate_routing_key_name" do
115
+ it { expect(Klass.send(:validate_routing_key_name, 'abcdef')).to eq(true) }
116
+ it { expect(Klass.send(:validate_routing_key_name, '')).to eq(true) }
117
+ it { expect(Klass.send(:validate_routing_key_name, 'abcdef.abcdef')).to eq(true) }
118
+ it { expect(Klass.send(:validate_routing_key_name, 'abcdef.abcdef.asd')).to eq(true) }
119
+ it { expect(Klass.send(:validate_routing_key_name, 'abcdef.abcdef/')).to eq(false) }
120
+ it { expect(Klass.send(:validate_routing_key_name, 'abcdef.abcdef.a123')).to eq(false) }
121
+ it { expect(Klass.send(:validate_routing_key_name, 'abcdef.')).to eq(false) }
122
+ it { expect(Klass.send(:validate_routing_key_name, 'abcAf')).to eq(false) }
123
+ end
124
+
125
+ describe "#generated_queue_name" do
126
+ before(:each) { allow(Banter::Configuration).to receive(:application_name).and_return("app_name")}
127
+
128
+ it { expect(Klass.send(:generated_queue_name, 'abcdef', 'queue_name')).to eq('queue_name') }
129
+ it { expect(Klass.send(:generated_queue_name, 'abcdef', nil)).to eq('app_name_abcdef') }
130
+ it { expect(Klass.send(:generated_queue_name, 'abcdef.abcd', nil)).to eq('app_name_abcdef_abcd') }
131
+ it { expect(Klass.send(:generated_queue_name, 'a.b.c', nil)).to eq('app_name_a_b_c') }
132
+ end
133
+
134
+ describe '.subscribe_to' do
135
+ before(:each) { allow(Banter::Configuration).to receive(:application_name).and_return("app_name")}
136
+ context 'routing_key without queue_name' do
137
+ before { Klass.subscribe_to 'a.b.c' }
138
+ it 'sets the routing key and queue name' do
139
+ expect(Klass.subscribed_key).to eq('a.b.c')
140
+ expect(Klass.subscribed_queue).to eq('app_name_a_b_c')
141
+ end
142
+ end
143
+
144
+ context 'routing_key with queue_name' do
145
+ before { Klass.subscribe_to 'a.b.c', on: 'foo_bar' }
146
+ it 'sets the routing key and queue name' do
147
+ expect(Klass.subscribed_key).to eq('a.b.c')
148
+ expect(Klass.subscribed_queue).to eq('foo_bar')
149
+ end
150
+ end
151
+
152
+ context 'invalid routing key' do
153
+ before { }
154
+ it 'sets the routing key and queue name' do
155
+ expect {
156
+ Klass.subscribe_to 'a.b.c.', on: 'foo_bar'
157
+ }.to raise_error(ArgumentError)
158
+ end
159
+ end
160
+ end
161
+ end