Skip to content

Commit

Permalink
First release.
Browse files Browse the repository at this point in the history
  • Loading branch information
Phillip Koebbe committed Jun 16, 2016
1 parent 6fb5d1d commit 8e624c4
Show file tree
Hide file tree
Showing 24 changed files with 1,182 additions and 1 deletion.
16 changes: 16 additions & 0 deletions .gitignore
@@ -0,0 +1,16 @@
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
*.bundle
*.so
*.o
*.a
mkmf.log
vendor/bundle
.ruby-version
6 changes: 6 additions & 0 deletions .travis.yml
@@ -0,0 +1,6 @@
language: ruby
rvm:
- 2.2.0
before_install: gem install bundler -v 1.11.2
install: bundle install --path vendor/bundle
script: bundle exec rspec
3 changes: 3 additions & 0 deletions Gemfile
@@ -0,0 +1,3 @@
source 'https://rubygems.org'

gemspec
21 changes: 21 additions & 0 deletions LICENSE.txt
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2016 Fidelity Life Association

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
136 changes: 135 additions & 1 deletion README.md
@@ -1 +1,135 @@
bunny_burrow
# BunnyBurrow

BunnyBurrow is a simple approach to RPC over RabbitMQ via the Bunny gem.
A 'server' application listens on one ore more queues on a topic exchange
while a 'client' publishes to a single queue on the same exchange.
BunnyBurrow will do all of the dirty work of establishing a connection to
RabbitMQ, opening the channel, ensuring the exchange and queue(s) exist,
and sending the payloads back and forth. All you have to do is decide what
to do on each end. BunnyBurrow even cleans up after itself so there aren't
a bunch of queues and connections left laying around after they are no
longer needed.

Why 'burrow'?

burrow (noun)
1. a hole or tunnel dug by a small animal, especially a rabbit, as a dwelling.
synonyms: hole, tunnel, warren, dugout

burrow (verb)
1. (of an animal) make a hole or tunnel, especially to use as a dwelling.
synonyms: tunnel, dig (out), excavate, grub, mine, bore, channel

The idea that _a_ burrow is a tunnel (or channel) and _to_ burrow is to
make a tunnel (or channel) seemed to fit very nicely with what the gem does.
Well, that and 'bunny_rpc' was already taken ;)

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'bunny_burrow'
```

And then execute:

$ bundle

Or install it yourself as:

$ gem install bunny_burrow

## Usage

There are templates you can use to get started quickly (or just see how
BunnyBurrow is used) in the `templates` directory of the repository. But a
very simple server can be made with

```ruby
require 'bunny_burrow'

rpc_server = BunnyBurrow::Server.new do |server|
server.rabbitmq_url = 'amqp://user:pass@server[:port]/vhost'
server.rabbitmq_exchange = 'bunny_exchange'
server.logger = Logger.new(STDOUT)
end

rpc_server.subscribe('some.routing.key') do |payload, response|
response[:data] = do_something_with(payload)

# return the response
response
end

# can subscribe to multiple queues
rpc_server.subscribe('some.other.routing.key') do |payload, response|
response[:data] = do_something_else_with(payload)

# return the response
response
end

# tell the server to keep the process alive so it can receive messages
rpc_server.wait

# at some later point, stop waiting and close connections
rpc_server.shutdown
```

A client is equally as easy to implement:

```ruby
require 'bunny_burrow'

rpc_client = BunnyBurrow::Client.new do |client|
client.rabbitmq_url = 'amqp://user:pass@server[:port]/vhost'
client.rabbitmq_exchange = 'bunny_exchange'
client.logger = Logger.new(STDOUT)
end

payload = { question: 'the thing you want' }

# the rpc_client will wait for a response from the server
# (this _is_ RPC, after all ;) )
result = rpc_client.publish(payload, 'some.routing.key')
puts result

# some time later, close connections
rpc_client.shutdown
```

Using the templates is also easy:

```
$ cd /path/to/development/work
$ git init my_rpc_server
$ cd my_rpc_server
$ cp /path/to/bunny_burrow/repo/templates/Gemfile .
$ cp -R /path/to/bunny_burrow/repo/templates/server/* .
$ grep -r your_project . | cut -d : -f 1 | xargs sed -i '' 's/your_project/my_rpc_server/'
$ grep -r YourProject . | cut -d : -f 1 | xargs sed -i '' 's/YourProject/MyRPCServer/'
$ mv lib/your_project lib/my_rpc_server
```

Edit the Gemfile as appropriate, then

```
$ bundle install [--path vendor/bundle]
$ git add .
$ git commit -m 'Initial commit.'
```

## Development

After checking out the repo, run `bundle install [--path vendor/bundle]` to install dependencies.
Then, run `bundle exec rspec` to run the tests.

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/vericity/bunny_burrow.

## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

6 changes: 6 additions & 0 deletions Rakefile
@@ -0,0 +1,6 @@
require "bundler/gem_tasks"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)

task :default => :spec
27 changes: 27 additions & 0 deletions bunny_burrow.gemspec
@@ -0,0 +1,27 @@
# coding: utf-8
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'bunny_burrow/version'

Gem::Specification.new do |spec|
spec.name = 'bunny_burrow'
spec.version = BunnyBurrow::VERSION
spec.authors = ['Fidelity Life Association']

spec.summary = 'RPC over RabbitMQ based on Bunny.'
spec.description = spec.summary
spec.license = 'MIT'

spec.files = ['lib/bunny_burrow.rb']
spec.require_paths = ['lib']

spec.add_development_dependency 'bundler', '~> 1.11'
spec.add_development_dependency 'pry'
spec.add_development_dependency 'pry-nav'
spec.add_development_dependency 'rake', '~> 10.0'
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'simplecov', '~> 0.11.2'
spec.add_development_dependency 'simplecov-rcov', '~> 0.2.3'

spec.add_runtime_dependency 'bunny', '~> 2.3.1'
end
6 changes: 6 additions & 0 deletions lib/bunny_burrow.rb
@@ -0,0 +1,6 @@
require 'bunny_burrow/version'
require 'bunny_burrow/client'
require 'bunny_burrow/server'

module BunnyBurrow
end
74 changes: 74 additions & 0 deletions lib/bunny_burrow/base.rb
@@ -0,0 +1,74 @@
require 'bunny'
require 'json'
require 'thread'

module BunnyBurrow
STATUS_OK = 'ok'
STATUS_CLIENT_ERROR = 'client_error'
STATUS_SERVER_ERROR = 'server_error'

class Base
attr_accessor :rabbitmq_url, :rabbitmq_exchange, :logger, :log_prefix
attr_writer :timeout, :log_request, :log_response

def initialize
yield self if block_given?
end

def timeout
@timeout ||= 60
end

def log_request?
@log_request ||= false
end

def log_response?
@log_response ||= false
end

def shutdown
log 'Shutting down'
channel.close
connection.close
end

private

def connection
unless @connection
@connection = Bunny.new(rabbitmq_url)
@connection.start
end

@connection
end

def channel
@channel ||= connection.create_channel
end

def default_exchange
@default_exchange ||= channel.default_exchange
end

def topic_exchange
@topic_exchange ||= channel.topic(rabbitmq_exchange, durable: true)
end

def lock
@lock ||= Mutex.new
end

def condition
@condition ||= ConditionVariable.new
end

def log(message, level: :info)
prefix = log_prefix || 'BunnyBurrow'
message = "#{prefix}: #{message}"
logger.send(level, message) if logger
end
end
end

53 changes: 53 additions & 0 deletions lib/bunny_burrow/client.rb
@@ -0,0 +1,53 @@
require_relative 'base'

module BunnyBurrow
class Client < Base
def publish(payload, routing_key)
result = nil

details = {
routing_key: routing_key,
reply_to: reply_to
}
details[:request] = payload if log_request?
log "Publishing #{details}"

begin
options = {
routing_key: routing_key,
reply_to: reply_to.name,
persistence: false
}

topic_exchange.publish(payload.to_json, options)

Timeout.timeout(timeout) do
reply_to.subscribe do |_, _, payload|
details[:response] = payload if log_response?
log "Receiving #{details}"
result = payload
lock.synchronize { condition.signal }
end

lock.synchronize { condition.wait(lock) }
end
rescue Timeout::Error => e
log "Timeout #{details}", level: :error
result = e.message
end

result
rescue => e
log e.message, level: :error
end

private

def reply_to
# when creating a queue, a blank name indicates we want the AMPQ broker
# to generate a unique name for us. Also note that this queue will be on
# the default exchange
@reply_to ||= channel.queue('', exclusive: true, auto_delete: true)
end
end
end

0 comments on commit 8e624c4

Please sign in to comment.