rabbit_rpc 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dfc96f46c3426bcc0d99bc1bb4e5915b21fbfe9b
4
+ data.tar.gz: a79352d3a4ddc718970493cc715d24e13c891ad0
5
+ SHA512:
6
+ metadata.gz: 9cb7b84171dd10688b417d5d28fc1bc8899785949b51f17b5961668f48268f1b7e309b2b33bf721f72ae1cadbb7b5848ef0eb0582b55bd4707804da6db4a05b1
7
+ data.tar.gz: 5a2e9cc981bbaef8ee781b205941afd721f2f682527229903118218a7de345b7e434a8a1cb59e81f1248ced3529c4c4163c411e72561f21184ca96fbed7a2ccd
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ .ruby-version
2
+ .ruby-gemset
3
+ *.gem
4
+ *.rbc
5
+ .bundle
6
+ .config
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in caerbannog.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Sohaib Bhatti
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,119 @@
1
+ # RabbitRPC
2
+
3
+ RabbitRPC helps in the rapid development of ruby services and their RPC
4
+ invocation over RabbitMQ in a service-oriented architecture.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'rabbit_rpc'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install rabbit_rpc
19
+
20
+ ## Getting Started
21
+
22
+ Rabbit-RPC can be broadly divided into two parts. EventMachine servers that
23
+ consume messages over RabbitMQ and an RPC invocation interface used for
24
+ producing messages.
25
+
26
+ ### Implementing Services(RabbitMQ Consumers)
27
+
28
+ Services can easily be defined via Rabbit-RPC.
29
+
30
+ ```ruby
31
+ class AuthorizationService
32
+ class << self
33
+
34
+ def auth(email, pass)
35
+ {
36
+ ok: true,
37
+ email: email,
38
+ pass: pass
39
+ }
40
+ end
41
+ end
42
+ end
43
+ ```
44
+
45
+ Once the services have been implemented and are loaded into loaded into
46
+ the global namespace, the service can then establish a connection with
47
+ RabbitMQ and start consuming messages.
48
+
49
+ ```ruby
50
+ RabbitRPC::Connection.new(QUEUE_NAME, QUEUE_ADDRESS).listen!
51
+ ```
52
+
53
+ Methods expected to send a response back to the RPC invocator,
54
+ will encode the response message and produce them on a callback queue.
55
+
56
+ Methods defined with the "one_way" prefix will not send a response back
57
+ to the RPC invocator.
58
+
59
+ ### RPC Invocation
60
+
61
+ RPC invocations, whether they're for services to communicate with one
62
+ another or for the user facing web server(Rails or Sinatra) to
63
+ communicate with the various services can be defined on an individual
64
+ basis. The reasoning behind this is that each service does not
65
+ necessarily need access to all other services and their corresponding
66
+ methods
67
+
68
+ By default, Rabbit-RPC expects a rabbit_rpc.yml file to present in the config
69
+ folder of the ruby app.This YAML file contains a definition of the names
70
+ of the services(queue names), their corresponding RabbitMQ URLs and
71
+ which method calls should exist.
72
+
73
+ ```yml
74
+ UserService:
75
+ address: amqp://localhost:5672
76
+ methods:
77
+ User: create, read, delete
78
+ Authorization: auth
79
+
80
+ EntertainmentService
81
+ address: amqp://localhost:5672
82
+ methods:
83
+ Movie: likes
84
+ Music: likes
85
+ ```
86
+
87
+ Rabbit-RPC relies on some conventions for it to work.
88
+ - The names of the services and their RabbitMQ queue names are the same.
89
+ - A service might be responsible for multiple functionaly. For the YAML
90
+ example above, the UserService is responsible for both the CRUD and
91
+ authentication of users. The service object is they key, and its
92
+ methods are provided by comma serperated values.
93
+ - Unless a method is defined with a "one_way" prefix, the RPC client
94
+ will wait for a a response in a synchronous fashion.
95
+
96
+ ```ruby
97
+ RabbitRPC::Config.initialize!
98
+ RabbitRPC::Client::UserService::Authorization.auth 'username', 'password'
99
+ => {"ok"=>true, "email"=>"username", "password"=>"password"}
100
+
101
+ RabbitRPC::Client::UserService::User.one_way_send_mail
102
+ => nil
103
+ ```
104
+
105
+ ## Example
106
+
107
+ A sample implementation of a service and RPC invocation can be seen [here.](https://github.com/sohaibbhatti/rabbit_rpc_example)
108
+
109
+ ## Contributing
110
+
111
+ As with all gems in their infancy. Rabbit-RPC is in a skeletal state. Some
112
+ of the code especially related to the Synchronous connection can be
113
+ optimized.
114
+
115
+ 1. Fork it
116
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
117
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
118
+ 4. Push to the branch (`git push origin my-new-feature`)
119
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/rabbit_rpc.rb ADDED
@@ -0,0 +1,17 @@
1
+ require "rabbit_rpc/version"
2
+ require "rabbit_rpc/logging"
3
+ require "rabbit_rpc/message"
4
+ require "rabbit_rpc/message_parser"
5
+ require "rabbit_rpc/config"
6
+ require "rabbit_rpc/client"
7
+ require "rabbit_rpc/synchronous_connection"
8
+ require "rabbit_rpc/request_handler"
9
+ require "rabbit_rpc/connection"
10
+
11
+ module RabbitRPC
12
+
13
+ def self.logger
14
+ RabbitRPC::Logging.logger
15
+ end
16
+
17
+ end
@@ -0,0 +1,5 @@
1
+ module RabbitRPC
2
+ # RPC methods and services are created in the RabbitRPC::Client namespace
3
+ # Refer to the Config.initialize! method
4
+ module Client; end
5
+ end
@@ -0,0 +1,87 @@
1
+ require 'yaml'
2
+ require 'active_support/inflector'
3
+
4
+ module RabbitRPC
5
+ class InvalidFormatError < StandardError; end
6
+
7
+ class Config
8
+ @@service_address = {}
9
+
10
+ class << self
11
+
12
+ def client_rpc_path
13
+ @@rpc_path ||= File.join 'config', 'rabbit_rpc.yml'
14
+ end
15
+
16
+ def client_rpc_path=(path)
17
+ @@rpc_path = path
18
+ end
19
+
20
+ # Traverses through the RPC YAML file and creates RPC invocations
21
+ # under the RabbitRPC::Client namespace.
22
+ #
23
+ # UserService:
24
+ # Authorization: auth
25
+ # Friend: list, delete
26
+ #
27
+ # RabbitRPC::Client::UserService::Friends.list
28
+ # TODO: Abstract this logic into a seperate class
29
+ def initialize!
30
+ YAML.load_file(client_rpc_path).each do |service_name, class_definitions|
31
+ unless class_definitions.is_a?(Hash) && class_definitions.keys.sort == %w[address methods]
32
+ raise InvalidFormatError, "Error parsing the structure of the RPC YAML"
33
+ end
34
+
35
+ mdule = Module.new
36
+ service_name = "#{service_name}".classify
37
+
38
+ ::RabbitRPC::Client.const_set service_name, mdule
39
+ class_definitions.each do |param, value|
40
+ if is_address?(param)
41
+ @@service_address ||= {}
42
+ @@service_address[service_name] = value
43
+ elsif is_method_declaration?(param)
44
+ populate_rpc(service_name, value)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def is_address?(key)
53
+ key == 'address'
54
+ end
55
+
56
+ def is_method_declaration?(key)
57
+ key == 'methods'
58
+ end
59
+
60
+ def populate_rpc(service_name, class_and_methods)
61
+ if @@service_address[service_name].nil?
62
+ raise InvalidFormatError, "The address declaration in the YAML file appears to be missing for #{service_name}"
63
+ end
64
+
65
+ class_and_methods.each do |klass_name, methods|
66
+ klass_name = klass_name.classify
67
+
68
+ klass = Class.new do
69
+ methods.gsub(/\s+/, "").split(',').each do |method_name|
70
+ define_singleton_method(method_name) do |*args|
71
+
72
+ RabbitRPC::SynchronousConnection.new(
73
+ service_name,
74
+ "#{service_name}.callback",
75
+ @@service_address[service_name]).publish!(RabbitRPC::Message.new("#{klass_name}Service.#{method_name}", *args))
76
+ end
77
+ end
78
+ end
79
+
80
+ "RabbitRPC::Client::#{service_name}".constantize.const_set(klass_name, klass)
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,80 @@
1
+ require 'amqp'
2
+
3
+ #TODO: RabbitMQ connection options
4
+ module RabbitRPC
5
+ class Connection
6
+ PREFETCH_DEFAULT = 5
7
+
8
+ include Logging
9
+
10
+ attr_reader :queue_name, :uri, :opts, :prefetch
11
+
12
+ def initialize(queue_name, uri, prefetch, opts = {})
13
+ @queue_name = queue_name
14
+ @uri = uri
15
+ @opts = opts
16
+ @prefetch = prefetch || PREFETCH_DEFAULT
17
+ end
18
+
19
+ def listen!
20
+ EventMachine.run do
21
+ close_connection_on_interrupt
22
+ subscribe_to_queue
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def subscribe_to_queue
29
+ queue.subscribe do |metadata, payload|
30
+
31
+ EM.defer do
32
+ request_handler = RequestHandler.new(payload)
33
+ response_message = request_handler.execute
34
+
35
+
36
+ unless request_handler.one_way
37
+
38
+ channel.default_exchange.publish(
39
+ response_message.to_msgpack,
40
+ routing_key: metadata.reply_to,
41
+ correlation_id: metadata.message_id,
42
+ mandatory: true
43
+ )
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+
50
+ def connect!
51
+ connection_params = ::AMQP::Client.parse_connection_uri @uri
52
+ connection_params.merge! @opts
53
+ logger.info 'Connecting to RabbitMQ'
54
+ @connection ||= ::AMQP.connect connection_params
55
+ end
56
+
57
+ def channel
58
+ logger.info 'Establishng connection with channel'
59
+ @channel ||= ::AMQP::Channel.new connect!, prefetch: @prefetch
60
+ end
61
+
62
+ # Private - Establish connection with a RabbitMQ queue.
63
+ # TODO: Queue options need to be provided
64
+ def queue
65
+ logger.info 'Connecting to queue'
66
+ @queue ||= channel.queue @queue_name
67
+ end
68
+
69
+ def close_connection_on_interrupt
70
+ %w[INT TERM].each do |interrupt_type|
71
+ Signal.trap(interrupt_type) do
72
+ logger.info 'Exiting'
73
+ @connection.close do
74
+ EventMachine.stop { exit }
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,32 @@
1
+ require 'logger'
2
+
3
+ module RabbitRPC
4
+
5
+ module Logging
6
+
7
+ def self.included(base)
8
+ base.send :include, Methods
9
+ base.extend Methods
10
+ end
11
+
12
+ module Methods
13
+ def logger
14
+ RabbitRPC::Logging.logger
15
+ end
16
+
17
+ # TODO: logger options
18
+ def log_exception(ex)
19
+ RabbitRPC::Logging.log_exception(ex)
20
+ end
21
+ end
22
+
23
+ def self.logger(target = $stdout)
24
+ @logger ||= Logger.new(target)
25
+ end
26
+
27
+ def self.log_exception(ex)
28
+ logger.error ('Message: ' + ex.message)
29
+ logger.error (['backtrace:'] + ex.backtrace).join("\n")
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ require 'msgpack'
2
+
3
+ module RabbitRPC
4
+ class Message
5
+
6
+ attr_reader :method_name, :args
7
+
8
+ def initialize(method_name, *args)
9
+ @method_name = method_name
10
+ @args = args
11
+ end
12
+
13
+ # Squeezes and serializes the RPC method name and arguments
14
+ #
15
+ # Returns the packed and serialized string
16
+ def pack
17
+ serialize(method: @method_name, args: @args)
18
+ end
19
+
20
+ # Unpacks a serialized message to a hash containing the method and
21
+ # its args. This method needs to be modified if a serializer other
22
+ # than MessagePack is to be used.
23
+ #
24
+ # Returns a Hash
25
+ def self.unpack(message)
26
+ MessagePack.unpack message
27
+ end
28
+
29
+ def self.generate_id
30
+ SecureRandom.uuid
31
+ end
32
+
33
+ private
34
+
35
+ # Private: Serialize the message. Currently using message pack. The
36
+ # implementation can be changed in order to use some other serializer.
37
+ #
38
+ # Returns the serialized String
39
+ def serialize(message)
40
+ message.to_msgpack
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ module RabbitRPC
2
+ class MessageParserException; end
3
+
4
+ class MessageParser
5
+ attr_reader :service_name, :method_name
6
+
7
+ # methods with the following prefix will not wait
8
+ # for a response
9
+ ONE_WAY_PREFIX = 'one_way'
10
+
11
+ def initialize(message)
12
+ @message = message
13
+ end
14
+
15
+ # Public: Extracts the Service name and method name
16
+ #
17
+ # Examples
18
+ #
19
+ # "UserService.create"
20
+ # # => "UserService", "create"
21
+ #
22
+ # Returns nothing
23
+ def parse
24
+ method = @message.is_a?(RabbitRPC::Message) ? @message.method_name : @message['method']
25
+ @service_name, @method_name = method.split('.')
26
+ end
27
+
28
+ # Public: Identifies whether a wait for a response is expected
29
+ #
30
+ # Returns a Boolean
31
+ def one_way?
32
+ parse if @method_name.nil?
33
+ @method_name.start_with?(ONE_WAY_PREFIX)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,37 @@
1
+ module RabbitRPC
2
+ class RequestHandler
3
+ include Logging
4
+
5
+ attr_reader :one_way
6
+
7
+ def initialize(serialized_message)
8
+ @message = Message.unpack serialized_message
9
+ end
10
+
11
+ def execute
12
+ parser = MessageParser.new(@message)
13
+ parser.parse
14
+
15
+ @one_way = parser.one_way?
16
+
17
+ logger.info "Received message #{@message}"
18
+
19
+ Kernel.const_get(parser.service_name).send(parser.method_name, *@message['args'] )
20
+ rescue ArgumentError => e
21
+ log_exception(e)
22
+ { ok: false, message: e.message }
23
+ rescue Exception => e
24
+ log_exception(e)
25
+ exception_response
26
+ end
27
+
28
+ private
29
+
30
+ # We do not want to return the actual exceptions back
31
+ # TODO: Perhaps have this user defined?
32
+ def exception_response
33
+ { ok: false, message: 'Error processing request' }
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,81 @@
1
+ require 'bunny'
2
+ require 'active_support/core_ext/object/try'
3
+
4
+ #TODO: Implement callback queue timeout
5
+ #TODO: RabbitMQ connection options
6
+ module RabbitRPC
7
+ # Connects to RabbitMQ for blocking RPC calls.
8
+ # Wait for a response when querying other services
9
+ class SynchronousConnection
10
+ DEFAULT_HEARTBEAT = 30
11
+
12
+ include Logging
13
+
14
+ # RabbitMQ related info
15
+ attr_reader :queue_name, :callback_queue_name, :rabbit_mq_url, :heartbeat
16
+
17
+ # Message unique identifier and resposne
18
+ attr_reader :message_id, :response
19
+
20
+ def initialize(queue_name, callback_queue_name, rabbit_mq_url, heart_beat = nil)
21
+ @queue_name = queue_name
22
+ @callback_queue_name = callback_queue_name
23
+ @rabbit_mq_url = rabbit_mq_url
24
+ @heartbeat = heart_beat || DEFAULT_HEARTBEAT
25
+
26
+ @message_id = Message.generate_id
27
+ end
28
+
29
+ def publish!(unpacked_message)
30
+ connect!
31
+
32
+ send_request(unpacked_message.pack)
33
+
34
+ if wait_for_response?(unpacked_message)
35
+ callback_queue.subscribe(block: true, ack: true) do |delivery_info, properties, payload|
36
+ if message_id == properties.try(:[],:correlation_id)
37
+ @channel.acknowledge(delivery_info.delivery_tag, false)
38
+ @response = Message.unpack payload
39
+
40
+ logger.info "Received message #{@response}"
41
+ delivery_info.consumer.cancel
42
+ end
43
+ end
44
+
45
+ return @response
46
+ end
47
+
48
+ return response
49
+ end
50
+
51
+ private
52
+
53
+ def connect!
54
+ @connection = Bunny.new(@rabbit_mq_url, heartbeat: @heartbeat).start
55
+ @channel = @connection.create_channel
56
+ end
57
+
58
+ def callback_queue
59
+ @channel.queue(@callback_queue_name, auto_delete: false)
60
+ end
61
+
62
+ def exchange
63
+ @exchange ||= @channel.default_exchange
64
+ end
65
+
66
+ def send_request(message)
67
+ exchange.publish(
68
+ message,
69
+ routing_key: @queue_name,
70
+ message_id: @message_id,
71
+ reply_to: @callback_queue_name,
72
+ auto_delete: false
73
+ )
74
+ end
75
+
76
+ def wait_for_response?(message)
77
+ !MessageParser.new(message).one_way?
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,3 @@
1
+ module RabbitRPC
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rabbit_rpc/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rabbit_rpc"
8
+ spec.version = RabbitRPC::VERSION
9
+ spec.authors = ["Sohaib Bhatti"]
10
+ spec.email = ["sohaibbbhatti@gmail.com"]
11
+ spec.description = %q{Framework for developing services and workers using RabbitMQ}
12
+ spec.summary = %q{Ruby RabbitMQ framework}
13
+ spec.homepage = "https://github.com/sohaibbhatti/rabbit_rpc"
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_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+
24
+ spec.add_dependency "msgpack", "~> 0.5.8"
25
+ spec.add_dependency "active_support", "~> 3.0.0"
26
+ spec.add_dependency "bunny", "~> 0.10.8"
27
+ spec.add_dependency "amqp", "~> 1.0.4"
28
+
29
+ spec.add_development_dependency "rspec", "~> 2.14.1"
30
+ spec.add_development_dependency "evented-spec"
31
+ end
@@ -0,0 +1,49 @@
1
+ require 'rabbit_rpc'
2
+
3
+ describe RabbitRPC::Config do
4
+ describe '.client_rpc_path' do
5
+ it 'assumes the file to be present in the config folder by default' do
6
+ RabbitRPC::Config.client_rpc_path.should == 'config/rabbit_rpc.yml'
7
+ end
8
+
9
+ it 'returns the configured value' do RabbitRPC::Config.client_rpc_path = 'foo/bar.yml'
10
+ RabbitRPC::Config.client_rpc_path.should == 'foo/bar.yml'
11
+ end
12
+ end
13
+
14
+ describe '.initialize!' do
15
+ context 'with a valid YAML file' do
16
+ before do
17
+ RabbitRPC::Config.client_rpc_path = 'spec/support/rpc.yaml'
18
+ RabbitRPC::Config.initialize!
19
+ end
20
+
21
+ it 'reads the client rpc file and initializes classes under the Client namespace' do
22
+ %w[create read delete].each do |meth|
23
+ RabbitRPC::Client::UserService::User.should respond_to meth
24
+ end
25
+
26
+ RabbitRPC::Client::UserService::Auth.should respond_to 'authorize'
27
+ end
28
+
29
+ it 'successfully encodes the messages with the proper arguments' do
30
+ # Do not wait for response
31
+ RabbitRPC::SynchronousConnection.any_instance.stub(:publish!).and_return(true)
32
+
33
+ RabbitRPC::Message.should_receive(:new).with('AuthService.authorize', 'omg', 'this', 'works'). \
34
+ and_call_original
35
+ RabbitRPC::Client::UserService::Auth.authorize 'omg', 'this', 'works'
36
+ end
37
+ end
38
+
39
+ it 'raises an exception if the address of the service is missing in the yml file' do
40
+ RabbitRPC::Config.client_rpc_path = 'spec/support/rpc_no_address.yaml'
41
+ expect { RabbitRPC::Config.initialize! }.to raise_error(RabbitRPC::InvalidFormatError)
42
+ end
43
+
44
+ it 'raises and exception if method definition is not decipherable' do
45
+ RabbitRPC::Config.client_rpc_path = 'spec/support/rpc_invalid_structure.yaml'
46
+ expect { RabbitRPC::Config.initialize! }.to raise_error(RabbitRPC::InvalidFormatError)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,71 @@
1
+ require 'rabbit_rpc'
2
+ require 'evented-spec'
3
+
4
+ describe RabbitRPC::Connection do
5
+ include EventedSpec::EMSpec
6
+ let(:connection) { RabbitRPC::Connection.new('UserService', 'amqp://localhost:5672', 5) }
7
+ let(:sync_connection) { RabbitRPC::SynchronousConnection.new('UserService', 'UserService.callback', 'amqp://localhost:5672') }
8
+ let(:exchange) { double 'exchange', publish: true }
9
+
10
+ it 'connects to the the specified rabbitMQ queue' do
11
+ em do
12
+ ::AMQP::Channel.any_instance.should_receive(:queue).with('UserService').and_call_original
13
+ connection.listen!
14
+ done(0.5)
15
+ end
16
+ end
17
+
18
+ it 'attempts to execute received messages' do
19
+ RabbitRPC::SynchronousConnection.new('UserService', 'UserService.callback', 'amqp://localhost:5672').publish!(RabbitRPC::Message.new('User.one_way_create'))
20
+
21
+ em do
22
+ RabbitRPC::RequestHandler.should_receive(:new).at_least(1).times.with(RabbitRPC::Message.new('User.one_way_create').pack).and_call_original
23
+ connection.listen!
24
+ done(0.5)
25
+ end
26
+ end
27
+
28
+ context 'when the client expects a response' do
29
+
30
+ it 'sends a response to the callback queue' do
31
+ RabbitRPC::Message.stub(:generate_id).and_return 'omg'
32
+ ::AMQP::Channel.any_instance.stub(:default_exchange).and_return exchange
33
+
34
+ send_message_expecting_response
35
+
36
+ em do
37
+ exchange.should_receive(:publish).with(anything(), routing_key: 'UserService.callback', correlation_id: 'omg', mandatory: true)
38
+ connection.listen!
39
+ done(0.5)
40
+ end
41
+ end
42
+ end
43
+
44
+ context 'when the client does not expect a response' do
45
+ it 'does not send a response to the callback queue' do
46
+ RabbitRPC::Message.stub(:generate_id).and_return 'omg'
47
+ ::AMQP::Channel.any_instance.stub(:default_exchange).and_return exchange
48
+
49
+ send_one_way_message
50
+
51
+ em do
52
+ exchange.should_not_receive(:publish).with(anything(), routing_key: 'UserService.callback', correlation_id: 'omg', mandatory: true)
53
+ connection.listen!
54
+ done(0.5)
55
+ end
56
+ end
57
+ end
58
+
59
+ # Disables the client blocking wait for receiving the message
60
+ # on the callback queue
61
+ def send_message_expecting_response
62
+ message = RabbitRPC::Message.new('User.create')
63
+ sync_connection.stub(:wait_for_response?).and_return(false)
64
+ sync_connection.publish! message
65
+ end
66
+
67
+ def send_one_way_message
68
+ message = RabbitRPC::Message.new('User.one_way_create')
69
+ sync_connection.publish! message
70
+ end
71
+ end
@@ -0,0 +1,14 @@
1
+ require 'rabbit_rpc'
2
+
3
+ describe RabbitRPC::Logging do
4
+ let(:random_class) do
5
+ class RandomClass
6
+ include RabbitRPC::Logging
7
+ end
8
+ end
9
+
10
+ it 'grants the class and its objects the ability to log' do
11
+ random_class.logger.should be_instance_of Logger
12
+ random_class.new.logger.should be_instance_of Logger
13
+ end
14
+ end
@@ -0,0 +1,50 @@
1
+ require 'rabbit_rpc'
2
+
3
+ describe RabbitRPC::MessageParser do
4
+ let(:message) { {'method' => 'UserService.create', 'args' => [1, 2, 3] } }
5
+ let(:one_way) { {'method' => 'AuthService.one_way_delete', 'args' => [1, 2, 3] } }
6
+
7
+ describe '#one_way?' do
8
+ it 'determines if the method begins with the one_way prefix' do
9
+ example_one = RabbitRPC::MessageParser.new(message)
10
+ example_one.parse
11
+ example_one.one_way?.should be_false
12
+
13
+ example_two = RabbitRPC::MessageParser.new(one_way)
14
+ example_two.parse
15
+ example_two.one_way?.should be_true
16
+ end
17
+
18
+ context 'parser has not executed' do
19
+ it 'determines if the method begins with the one_way prefix' do
20
+ RabbitRPC::MessageParser.new(message).one_way?.should be_false
21
+ RabbitRPC::MessageParser.new(one_way).one_way?.should be_true
22
+ end
23
+ end
24
+ end
25
+
26
+ describe '#parse' do
27
+ it 'correctly identifies the name of the Service' do
28
+ example_one = RabbitRPC::MessageParser.new(message)
29
+ example_one.parse
30
+ example_one.service_name.should == 'UserService'
31
+
32
+ example_two = RabbitRPC::MessageParser.new(one_way)
33
+ example_two.parse
34
+ example_two.service_name.should == 'AuthService'
35
+ end
36
+
37
+ it 'correctly identifies the name of the method' do
38
+ example_one = RabbitRPC::MessageParser.new(message)
39
+ example_one.parse
40
+ example_one.method_name.should == 'create'
41
+
42
+ example_two = RabbitRPC::MessageParser.new(one_way)
43
+ example_two.parse
44
+ example_two.method_name.should == 'one_way_delete'
45
+ end
46
+
47
+ # Test case for message class and hash
48
+ # Invalid format?
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ require 'rabbit_rpc'
2
+
3
+ describe RabbitRPC::Message do
4
+
5
+ describe '#pack' do
6
+ it 'succesfully shifts the RPC method and its arguments to a single datastructure' do
7
+ message = RabbitRPC::Message.new 'hello', 'argument_one', { optional: 'arguments' }
8
+ message.should_receive(:serialize).with(method: 'hello', args: ['argument_one', { optional: 'arguments' }])
9
+ message.pack
10
+ end
11
+
12
+ it 'succesfully handles the case of no arguments present' do
13
+ message = RabbitRPC::Message.new 'hello'
14
+ message.should_receive(:serialize).with(method: 'hello', args: [])
15
+ message.pack
16
+ end
17
+ end
18
+
19
+ describe '.unpack' do
20
+ it 'successfully converts the serialized message into a readable datastructure' do
21
+ message = RabbitRPC::Message.new 'hello', 'argument_one', { optional: 'arguments' }
22
+ RabbitRPC::Message.unpack(message.pack).should == {
23
+ 'method' => 'hello',
24
+ 'args' => ['argument_one', { 'optional' => 'arguments' }]
25
+ }
26
+ end
27
+ end
28
+
29
+ describe '.generate_id' do
30
+ it 'generates random values' do
31
+ RabbitRPC::Message.generate_id.should_not be_nil
32
+ RabbitRPC::Message.generate_id.should_not == RabbitRPC::Message.generate_id
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,45 @@
1
+ require 'rabbit_rpc'
2
+
3
+ describe RabbitRPC::RequestHandler do
4
+ let!(:sample_service) do
5
+ class UserService
6
+ def self.create
7
+ 'woot'
8
+ end
9
+ end
10
+ end
11
+ let(:message) { RabbitRPC::Message.new 'UserService.create' }
12
+ let(:one_way_message) { RabbitRPC::Message.new 'UserService.one_way_create' }
13
+ let(:invalid_message) { RabbitRPC::Message.new 'Usvice.create' }
14
+ let(:invalid_args_message) { RabbitRPC::Message.new 'UserService.create', 'args' }
15
+
16
+ describe '#execute' do
17
+ it 'succesfully executes the request method' do
18
+ RabbitRPC::RequestHandler.new(message.pack).execute.should == 'woot'
19
+ end
20
+
21
+ it 'identifies whether the method expects a response' do
22
+ handler = RabbitRPC::RequestHandler.new(message.pack)
23
+ handler.execute
24
+ handler.one_way.should be_false
25
+
26
+ handler = RabbitRPC::RequestHandler.new(one_way_message.pack)
27
+ handler.execute
28
+ handler.one_way.should be_true
29
+ end
30
+
31
+ it 'returns an exception message in the event of errors' do
32
+ RabbitRPC::RequestHandler.new(invalid_message.pack).execute.should == {
33
+ ok: false,
34
+ message: 'Error processing request'
35
+ }
36
+ end
37
+
38
+ it 'returns an error notifying an Argument error' do
39
+ RabbitRPC::RequestHandler.new(invalid_args_message.pack).execute.should == {
40
+ ok: false,
41
+ message: 'wrong number of arguments (1 for 0)'
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ UserService:
2
+ address: amqp:://localhost:5672
3
+ methods:
4
+ User: create, read, delete
5
+ Auth: authorize
@@ -0,0 +1,2 @@
1
+ Invalid: Format
2
+ Foo: Bar
@@ -0,0 +1,4 @@
1
+ UserService:
2
+ methods:
3
+ User: create, read, delete
4
+ Auth: authorize
@@ -0,0 +1,41 @@
1
+ require 'rabbit_rpc'
2
+
3
+ describe RabbitRPC::SynchronousConnection do
4
+ describe '#publish!' do
5
+ let(:connection) { RabbitRPC::SynchronousConnection.new('foo', 'bar', 'amqp:://localhost:5672') }
6
+ let(:exchange) { double 'exchange', publish: true }
7
+ let(:message) { RabbitRPC::Message.new 'User.create', 'bar' }
8
+ let(:one_way_message) { RabbitRPC::Message.new 'User.one_way_create', 'bar' }
9
+ let(:queue) { double 'queue', subscribe: true }
10
+
11
+ before do
12
+ connection.stub(:exchange).and_return exchange
13
+ connection.stub(:callback_queue).and_return queue
14
+ end
15
+
16
+ it 'sends a message to the relevant queue' do
17
+ exchange.should_receive(:publish).with(
18
+ message.pack,
19
+ routing_key: 'foo',
20
+ message_id: anything(),
21
+ reply_to: 'bar',
22
+ auto_delete: false
23
+ )
24
+ connection.publish! message
25
+ end
26
+
27
+ context 'when a response is expected' do
28
+ it 'subscribes to the callback queue to listen for a response' do
29
+ queue.should_receive :subscribe
30
+ connection.publish!(message)
31
+ end
32
+ end
33
+
34
+ context 'when no response is expected' do
35
+ it 'terminates' do
36
+ queue.should_not_receive :subscribe
37
+ connection.publish!(one_way_message)
38
+ end
39
+ end
40
+ end
41
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rabbit_rpc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Sohaib Bhatti
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: msgpack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 0.5.8
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.5.8
55
+ - !ruby/object:Gem::Dependency
56
+ name: active_support
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 3.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: bunny
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: 0.10.8
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: 0.10.8
83
+ - !ruby/object:Gem::Dependency
84
+ name: amqp
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: 1.0.4
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: 1.0.4
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: 2.14.1
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: 2.14.1
111
+ - !ruby/object:Gem::Dependency
112
+ name: evented-spec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Framework for developing services and workers using RabbitMQ
126
+ email:
127
+ - sohaibbbhatti@gmail.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - .gitignore
133
+ - Gemfile
134
+ - LICENSE.txt
135
+ - README.md
136
+ - Rakefile
137
+ - lib/rabbit_rpc.rb
138
+ - lib/rabbit_rpc/client.rb
139
+ - lib/rabbit_rpc/config.rb
140
+ - lib/rabbit_rpc/connection.rb
141
+ - lib/rabbit_rpc/logging.rb
142
+ - lib/rabbit_rpc/message.rb
143
+ - lib/rabbit_rpc/message_parser.rb
144
+ - lib/rabbit_rpc/request_handler.rb
145
+ - lib/rabbit_rpc/synchronous_connection.rb
146
+ - lib/rabbit_rpc/version.rb
147
+ - rabbit_rpc.gemspec
148
+ - spec/config_spec.rb
149
+ - spec/connection_spec.rb
150
+ - spec/logging_spec.rb
151
+ - spec/message_parser_spec.rb
152
+ - spec/message_spec.rb
153
+ - spec/request_handler_spec.rb
154
+ - spec/support/rpc.yaml
155
+ - spec/support/rpc_invalid_structure.yaml
156
+ - spec/support/rpc_no_address.yaml
157
+ - spec/synchronous_connection_spec.rb
158
+ homepage: https://github.com/sohaibbhatti/rabbit_rpc
159
+ licenses:
160
+ - MIT
161
+ metadata: {}
162
+ post_install_message:
163
+ rdoc_options: []
164
+ require_paths:
165
+ - lib
166
+ required_ruby_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - '>='
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ required_rubygems_version: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - '>='
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ requirements: []
177
+ rubyforge_project:
178
+ rubygems_version: 2.0.3
179
+ signing_key:
180
+ specification_version: 4
181
+ summary: Ruby RabbitMQ framework
182
+ test_files:
183
+ - spec/config_spec.rb
184
+ - spec/connection_spec.rb
185
+ - spec/logging_spec.rb
186
+ - spec/message_parser_spec.rb
187
+ - spec/message_spec.rb
188
+ - spec/request_handler_spec.rb
189
+ - spec/support/rpc.yaml
190
+ - spec/support/rpc_invalid_structure.yaml
191
+ - spec/support/rpc_no_address.yaml
192
+ - spec/synchronous_connection_spec.rb