Initial commit

This commit is contained in:
2017-02-28 13:56:27 +00:00
commit 86d952da9a
22 changed files with 604 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
# rspec failure tracking
.rspec_status

2
.rspec Normal file
View File

@@ -0,0 +1,2 @@
--format documentation
--color

5
.rubocop.yml Normal file
View File

@@ -0,0 +1,5 @@
Documentation:
Enabled: false
Style/HashSyntax:
EnforcedStyle: ruby19_no_mixed_keys

5
.travis.yml Normal file
View File

@@ -0,0 +1,5 @@
sudo: false
language: ruby
rvm:
- 2.4.0
before_install: gem install bundler -v 1.14.3

4
Gemfile Normal file
View File

@@ -0,0 +1,4 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in bunnyrun.gemspec
gemspec

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# BunnyRun
Easy to use runtime for [Bunny](http://rubybunny.info/)-based AMQP consumers.
## Features
- Simple and powerful DSL for writing consumers and publishers.
- CLI tool for running consumers (`bunnyrun my_consumer.rb`).
- Logging mechanisms.
- Error reporting mechanisms.
## License
The gem is available as open source under the terms of
the [MIT License](http://opensource.org/licenses/MIT).

6
Rakefile Normal file
View File

@@ -0,0 +1,6 @@
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task default: :spec

14
bin/console Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env ruby
require "bundler/setup"
require "bunnyrun"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start(__FILE__)

8
bin/setup Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here

32
bunnyrun.gemspec Normal file
View File

@@ -0,0 +1,32 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'bunnyrun/version'
Gem::Specification.new do |spec|
spec.name = 'bunnyrun'
spec.version = BunnyRun::VERSION
spec.authors = ['Jim Myhrberg']
spec.email = ['contact@jimeh.me']
spec.summary = 'Easy to use runtime for bunny-based AMQP consumers.'
spec.description = 'Easy to use runtime for bunny-based AMQP consumers.'
spec.homepage = 'https://github.com/jimeh/bunnyrun'
spec.files = `git ls-files -z`.split("\x0").reject do |f|
f.match(%r{^(bin|test|spec|features)/})
end
spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']
spec.add_development_dependency 'bundler', '~> 1.14'
spec.add_development_dependency 'rake', '~> 10.0'
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'rubocop', '~> 0.47'
spec.add_development_dependency 'byebug'
spec.add_runtime_dependency 'bunny', '~> 2.6'
spec.add_runtime_dependency 'trollop', '~> 2.1.2'
end

5
exe/bunnyrun Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env ruby
$LOAD_PATH.unshift(File.expand_path('../../lib', File.realpath(__FILE__)))
require 'bunnyrun'
BunnyRun::CLI.run(ARGV)

1
lib/bunny_run.rb Normal file
View File

@@ -0,0 +1 @@
require 'bunnyrun'

21
lib/bunnyrun.rb Normal file
View File

@@ -0,0 +1,21 @@
require 'bunnyrun/consumer'
require 'bunnyrun/cli'
require 'bunnyrun/runner'
require 'bunnyrun/version'
module BunnyRun
class << self
def publish(exchange_name, payload, attrs = {}); end
def after_start(&block)
callbacks[:after_start] ||= []
callbacks[:after_start] << block
end
private
def callbacks
@callbacks ||= {}
end
end
end

30
lib/bunnyrun/cli.rb Normal file
View File

@@ -0,0 +1,30 @@
require 'bunnyrun/consumer'
require 'bunnyrun/options'
require 'bunnyrun/runner'
module BunnyRun
class CLI
attr_reader :options
def self.run(argv = [])
new.run(argv)
end
def run(argv = [])
options = Options.parse(argv)
require_files(options.paths)
consumers = Consumer.children
runner = Runner.new(options, consumers)
runner.run
end
private
def require_files(paths)
paths.each do |path|
require File.join(Dir.pwd, path)
end
end
end
end

128
lib/bunnyrun/consumer.rb Normal file
View File

@@ -0,0 +1,128 @@
require 'bunnyrun/message'
module BunnyRun
class Consumer
attr_reader :connection
attr_reader :publish_channel
attr_reader :default_prefetch
attr_reader :logger
class << self
def inherited(klass)
children << klass
end
def children
@children ||= []
end
def queue(name = nil, attrs = {})
return @queue if name.nil?
@queue = { name: name, attrs: attrs }
end
def exchange(name, attrs = {})
exchanges[name] = attrs
end
def bind(exchange_name, attrs = {})
bindings << [exchange_name, attrs]
end
def manual_ack(value = nil)
return @manual_ack || false if value.nil?
@manual_ack = value
end
def prefetch(count = nil)
return @prefetch if count.nil?
@prefetch = count
end
def exchanges
@exchanges ||= {}
end
def bindings
@bindings ||= []
end
end
def initialize(opts = {})
@connection = opts[:connection]
@publish_channel = opts[:publish_channel]
@default_prefetch = opts[:default_prefetch]
@logger = opts[:logger]
end
def channel
@channel ||= connection.create_channel
end
def start
perform_bindings
set_prefetch
subscribe
end
def subscribe
opts = { manual_ack: self.class.manual_ack }
queue.subscribe(opts) do |delivery_info, properties, payload|
message = Message.new(delivery_info, properties, payload)
perform(message)
end
end
def publish(exchange_name, payload, attrs = {})
exch = publish_exchange(exchange_name)
exch.publish(payload, attrs)
end
def queue
@queue ||= begin
opts = self.class.queue
channel.queue(opts[:name], opts[:attrs])
end
end
def exchange(name)
exchanges[name] ||= begin
return unless self.class.exchanges.key?(name)
attrs = self.class.exchanges[name]
channel.exchange(name, attrs)
end
end
def publish_exchange(name)
publish_exchanges[name] ||= begin
return unless self.class.exchanges.key?(name)
attrs = self.class.exchanges[name]
publish_channel.exchange(name, attrs)
end
end
private
def perform_bindings
self.class.bindings.each do |(exchange_name, attrs)|
exch = exchange(exchange_name)
queue.bind(exch, attrs)
end
end
def set_prefetch
count = self.class.prefetch || default_prefetch
channel.prefetch(count, true)
end
def exchanges
@exchanges ||= {}
end
def publish_exchanges
@publish_exchanges ||= {}
end
end
end

View File

@@ -0,0 +1,5 @@
class String
def undent
gsub(/^.{#{slice(/^ +/).length}}/, '')
end
end

49
lib/bunnyrun/message.rb Normal file
View File

@@ -0,0 +1,49 @@
module BunnyRun
class Message
attr_reader :delivery_info
attr_reader :properties
attr_reader :payload
def initialize(delivery_info, properties, payload)
@delivery_info = delivery_info
@properties = properties
@payload = payload
@acked = false
end
def channel
delivery_info.channel
end
def ack
channel.ack(delivery_tag)
@acked = true
end
def reject
channel.reject(delivery_tag, false)
@acked = true
end
def requeue
channel.reject(delivery_tag, true)
@acked = true
end
def manual_ack?
!delivery_info.consumer.no_ack
end
def routing_key
delivery_info.routing_key
end
def delivery_mode
properties.delivery_mode
end
def delivery_tag
delivery_info.delivery_tag
end
end
end

152
lib/bunnyrun/options.rb Normal file
View File

@@ -0,0 +1,152 @@
require 'trollop'
require 'bunnyrun/core_ext/string'
module BunnyRun
class Options
class << self
def parse(argv = [])
args = argv.clone
opts = parse_args(args)
opts[:paths] = args
validate_paths(opts)
opts.each_with_object(new) do |(key, value), memo|
memo.send("#{key}=", value) if memo.respond_to?("#{key}=")
end
end
private
def parse_args(args)
Trollop.with_standard_exception_handling(parser) do
parser.parse(args)
end
end
def validate_paths(opts)
parser.die('One or more paths to consumers required', nil) \
if opts[:paths].empty?
end
def parser
return @parser if @parser
defaults = new
@parser = Trollop::Parser.new do
banner <<-EOF.undent
Usage: bunnyrun [options] [path ...]
Options:
EOF
version "bunnyrun #{BunnyRun::VERSION}"
opt :url, 'Connection string ' \
'(example: "amqp://guest:guest@127.0.0.1:5672/vhost")',
short: 'U', type: :string, default: defaults.url
opt :host, 'Host',
short: 'H', type: :string, default: defaults.host
opt :port, 'Port',
short: 'P', type: :int, default: defaults.port
opt :ssl, 'Connect using SSL',
short: 's', type: :bool, default: defaults.ssl
opt :vhost, 'Virtual host',
short: 'V', type: :string, default: defaults.vhost
opt :user, 'Username',
short: 'u', type: :string, default: defaults.user
opt :pass, 'Password',
short: 'p', type: :string, default: defaults.pass
opt :prefetch, 'Default prefetch count',
short: :none, type: :int, default: defaults.prefetch
banner ''
opt :log_target, 'Log target, file path or STDOUT',
short: 't', type: :string, default: defaults.log_target
opt :log_level, 'Log level (debug, info, warn, error, fatal)',
short: 'l', type: :string, default: defaults.log_level
opt :bunny_log_target, 'Log target used by Bunny',
short: :none, type: :string, default: defaults.bunny_log_target
opt :bunny_log_level, 'Log level used by Bunny',
short: :none, type: :string, default: defaults.bunny_log_level
conflicts :url, :host
conflicts :url, :port
conflicts :url, :ssl
conflicts :url, :vhost
conflicts :url, :user
conflicts :url, :pass
banner ''
end
end
end
def url
@url ||= nil
end
attr_writer :url
def host
@host ||= '127.0.0.1'
end
attr_writer :host
def port
@port ||= 5672
end
attr_writer :port
def ssl
@ssl ||= false
end
attr_writer :ssl
def vhost
@vhost ||= '/'
end
attr_writer :vhost
def user
@user ||= 'guest'
end
attr_writer :user
def pass
@pass ||= 'guest'
end
attr_writer :pass
def prefetch
@prefetch ||= 1
end
attr_writer :prefetch
def log_target
@log_target ||= 'STDOUT'
end
attr_writer :log_target
def log_level
@log_level ||= 'info'
end
attr_writer :log_level
def bunny_log_target
@bunny_log_target ||= 'STDOUT'
end
attr_writer :bunny_log_target
def bunny_log_level
@bunny_log_level ||= 'warn'
end
attr_writer :bunny_log_level
def paths
@paths ||= []
end
attr_writer :paths
end
end

85
lib/bunnyrun/runner.rb Normal file
View File

@@ -0,0 +1,85 @@
require 'bunny'
require 'logger'
require 'bunnyrun/consumer'
module BunnyRun
class Runner
attr_reader :options
attr_reader :consumer_classes
def initialize(options = {}, consumer_classes)
@options = options
@consumer_classes = consumer_classes
end
def run
consumer_classes.each do |consumer_class|
launch_consumer(consumer_class)
end
block
end
def connection
@connection ||= begin
conn = Bunny.new(connection_opts)
conn.start
conn
end
end
def publish_channel
@publish_channel ||= connection.create_channel
end
def logger
@logger ||= begin
logger = Logger.new(log_target)
logger.level = log_level
logger
end
end
private
def block
loop { sleep 1 }
end
def launch_consumer(consumer_class)
consumer = consumer_class.new(
connection: connection,
publish_channel: publish_channel,
default_prefetch: options.prefetch,
logger: logger
)
consumer.start
end
def connection_opts
return options.url if options.url
{
host: options.host,
port: options.port,
ssl: options.ssl,
vhost: options.vhost,
user: options.user,
pass: options.pass
}
end
def log_target
if options.log_target.casecmp('stdout').zero?
STDOUT
else
options.log_target
end
end
def log_level
Kernel.const_get("::Logger::#{options.log_level.upcase}")
end
end
end

3
lib/bunnyrun/version.rb Normal file
View File

@@ -0,0 +1,3 @@
module BunnyRun
VERSION = '0.1.0'.freeze
end

11
spec/bunnyrun_spec.rb Normal file
View File

@@ -0,0 +1,11 @@
require 'spec_helper'
RSpec.describe Bunnyrun do
it 'has a version number' do
expect(Bunnyrun::VERSION).not_to be nil
end
it 'does something useful' do
expect(false).to eq(true)
end
end

11
spec/spec_helper.rb Normal file
View File

@@ -0,0 +1,11 @@
require 'bundler/setup'
require 'bunnyrun'
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = '.rspec_status'
config.expect_with :rspec do |c|
c.syntax = :expect
end
end