banter 0.4.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,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