lapine 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: db131a0aa239e502ac31258568eb2180c3699c95
4
- data.tar.gz: c2c3302d3ced52aa11d78d477241be7af4183cf7
3
+ metadata.gz: ccc8f9935974ce4c4981bc4d43da3f08cdf2c8b2
4
+ data.tar.gz: a296e0395e2c43aaf6369a47c921176c2862442e
5
5
  SHA512:
6
- metadata.gz: 9a29396753fe50307e752fd5a06cd1f02ddf64b011ca65869be06b770d47e68492c88cd8bec89019a6d2a70fe3bc658e2047f6f6895f0ea0ea8f84d8acfde102
7
- data.tar.gz: 67122efa1ccb92aa40b0e5237e5218e05c55d30aa1f26e0f9350a62d0d7c8912172a1af1d7537b4b78c593b23e7f5e257864a8d0c6975b691167b0e53149b627
6
+ metadata.gz: f2992302c858ec6589bc6b5147cd85b332bb6eb83a4ba9ee7df10be251711fbb7bc9e9d4e6d292b33b3a97bc6c7123054553ee3866adbc33de6c17d71f602c39
7
+ data.tar.gz: 61271187c14a82e828f3031c0063d8f0ecd237ac1d7dff4cb1ed5ba223dff3d9713fe3cc0c4b66ec1c8e421b6c3aba434af3afd255d5bbac3d54d3a61002f163
data/Gemfile CHANGED
@@ -2,3 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in lapine.gemspec
4
4
  gemspec
5
+
6
+ group :test do
7
+ gem 'pry-nav'
8
+ end
data/bin/lapine ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'lapine/cli'
4
+ Lapine::CLI.new(ARGV).run
5
+
@@ -0,0 +1,24 @@
1
+ connection:
2
+ host: '127.0.0.1'
3
+ port: 5672
4
+ ssl: false
5
+ vhost: '/'
6
+ username: 'guest'
7
+ password: 'guest'
8
+
9
+ require:
10
+ - example/consumer_handler
11
+
12
+ topics:
13
+ - lapine.topic
14
+
15
+ queues:
16
+ - q: handler
17
+ topic: lapine.topic
18
+ routing_key: stuff
19
+ handlers:
20
+ - ConsumerHandler
21
+
22
+
23
+
24
+
@@ -0,0 +1,5 @@
1
+ class ConsumerHandler
2
+ def self.handle_lapine_payload(hash, metadata)
3
+ puts hash, payload
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ require 'lapine'
2
+
3
+ class Producer
4
+ include Lapine::Publisher
5
+
6
+ exchange 'lapine.topic'
7
+
8
+ attr_reader :id
9
+
10
+ def initialize(id)
11
+ @id = id
12
+ end
13
+
14
+ def to_hash
15
+ {
16
+ data: id
17
+ }
18
+ end
19
+ end
20
+
data/lapine.gemspec CHANGED
@@ -6,7 +6,7 @@ require 'lapine/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'lapine'
8
8
  spec.version = Lapine::VERSION
9
- spec.authors = ['Eric Saxby','Matt Camuto']
9
+ spec.authors = ['Eric Saxby', 'Matt Camuto']
10
10
  spec.email = ['dev@wanelo.com']
11
11
  spec.summary = %q{Talk to rabbits}
12
12
  spec.description = %q{Talk to rabbits}
@@ -18,12 +18,15 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
+ spec.add_dependency 'amqp'
21
22
  spec.add_dependency 'bunny'
23
+ spec.add_dependency 'mixlib-cli'
22
24
  spec.add_dependency 'oj'
25
+ spec.add_dependency 'ruby-usdt', '>= 0.2.2'
23
26
 
24
27
  spec.add_development_dependency 'bundler', '~> 1.7'
25
28
  spec.add_development_dependency 'guard-rspec', '~> 4.3.1'
26
29
  spec.add_development_dependency 'rake', '~> 10.0'
27
30
  spec.add_development_dependency 'rspec', '~> 3.1.0'
28
-
31
+ spec.add_development_dependency 'em-spec'
29
32
  end
data/lib/lapine/cli.rb ADDED
@@ -0,0 +1,30 @@
1
+ module Lapine
2
+ class CLI
3
+ attr_reader :argv, :command
4
+
5
+ def initialize(argv)
6
+ @argv = argv
7
+ @command = argv.shift
8
+ end
9
+
10
+ def run
11
+ case command
12
+ when 'consume'
13
+ require 'lapine/consumer'
14
+ ::Lapine::Consumer::Runner.new(argv).run
15
+ else
16
+ usage
17
+ end
18
+ end
19
+
20
+ def usage
21
+ puts <<-EOF.gsub(/^ {8}/, '')
22
+ Usage: lapine [command] [options]
23
+
24
+ commands: consume
25
+ EOF
26
+ exit 1
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1 @@
1
+ require 'lapine/consumer/runner'
@@ -0,0 +1,127 @@
1
+ require 'mixlib/cli'
2
+ require 'yaml'
3
+
4
+ module Lapine
5
+ module Consumer
6
+ class Config
7
+ include Mixlib::CLI
8
+
9
+ banner 'Usage: lapine consume (options)'
10
+
11
+ option :config_file,
12
+ short: '-c CONFIG_FILE',
13
+ long: '--config CONFIG_FILE',
14
+ description: 'YML file with configuration of subscribers',
15
+ required: true
16
+
17
+ option :logfile,
18
+ short: '-l LOGFILE',
19
+ long: '--logfile LOGFILE',
20
+ description: 'where to log consumer info',
21
+ required: false
22
+
23
+ option :host,
24
+ short: '-H RABBIT_HOST',
25
+ long: '--host RABBIT_HOST',
26
+ description: 'IP or FQDN of RabbitMQ host (default 127.0.0.1)'
27
+
28
+ option :port,
29
+ short: '-p RABBIT_PORT',
30
+ long: '--port RABBIT_PORT',
31
+ description: 'port to use with RabbitMQ (default 5672)'
32
+
33
+ option :ssl,
34
+ short: '-S',
35
+ long: '--ssl',
36
+ description: 'use ssl to connect (default false)'
37
+
38
+ option :vhost,
39
+ short: '-V VHOST',
40
+ long: '--vhost VHOST',
41
+ description: 'RabbitMQ vhost to use (default "/")'
42
+
43
+ option :username,
44
+ short: '-U USERNAME',
45
+ long: '--username USERNAME',
46
+ description: 'RabbitMQ user (default guest)'
47
+
48
+ option :password,
49
+ short: '-P PASSWORD',
50
+ long: '--password PASSWORD',
51
+ description: 'RabbitMQ password (default guest)'
52
+
53
+ option :debug,
54
+ long: '--debug',
55
+ description: 'More verbose (and possibly non-threadsafe) log statements',
56
+ default: false
57
+
58
+ option :help,
59
+ short: '-?',
60
+ long: '--help',
61
+ description: 'Show this message',
62
+ on: :tail,
63
+ boolean: true,
64
+ show_options: true,
65
+ exit: 0
66
+
67
+ def load(argv)
68
+ parse_options argv
69
+ self
70
+ end
71
+
72
+ def debug?
73
+ config[:debug]
74
+ end
75
+
76
+ def logfile
77
+ config[:logfile]
78
+ end
79
+
80
+ def queues
81
+ yaml_config['queues']
82
+ end
83
+
84
+ def require
85
+ yaml_config['require'] || []
86
+ end
87
+
88
+ def topics
89
+ yaml_config['topics']
90
+ end
91
+
92
+ def connection_properties
93
+ {
94
+ host: '127.0.0.1',
95
+ port: 5672,
96
+ ssl: false,
97
+ vhost: '/',
98
+ username: 'guest',
99
+ password: 'guest'
100
+ }.merge(file_connection_props)
101
+ .merge(cli_connection_props)
102
+ end
103
+
104
+ private
105
+
106
+ def file_connection_props
107
+ return {} unless yaml_config['connection']
108
+ yaml_config['connection'].inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
109
+ end
110
+
111
+ def cli_connection_props
112
+ {
113
+ host: config[:host],
114
+ port: config[:port] ? config[:port].to_i : nil,
115
+ ssl: config[:ssl],
116
+ vhost: config[:vhost],
117
+ username: config[:username],
118
+ password: config[:password]
119
+ }.delete_if { |k, v| v.nil? }
120
+ end
121
+
122
+ def yaml_config
123
+ @yaml ||= YAML.load_file(config[:config_file])
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,16 @@
1
+ require 'amqp'
2
+ require 'eventmachine'
3
+
4
+ module Lapine
5
+ module Consumer
6
+ class Connection
7
+ attr_reader :connection, :channel, :exchange
8
+
9
+ def initialize(config, topic)
10
+ @connection = AMQP.connect(config.connection_properties)
11
+ @channel = AMQP::Channel.new(connection)
12
+ @exchange = AMQP::Exchange.new(channel, :topic, topic, durable: true)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,65 @@
1
+ require 'oj'
2
+ require 'lapine/dtrace'
3
+
4
+ module Lapine
5
+ module Consumer
6
+ class Dispatcher
7
+ class DefaultErrorHandler
8
+ def call(e, data)
9
+ logger.info "Lapine::Dispatcher unable to dispatch, #{e.message}, data: #{data}"
10
+ end
11
+ end
12
+
13
+ attr_reader :delegate_class, :raw_payload, :metadata, :logger
14
+
15
+ def self.error_handler=(handler)
16
+ @error_handler = handler
17
+ end
18
+
19
+ def self.error_handler
20
+ @error_handler || DefaultErrorHandler.new
21
+ end
22
+
23
+ def initialize(delegate_class, raw_payload, metadata, logger)
24
+ @delegate_class = delegate_class
25
+ @raw_payload = raw_payload
26
+ @metadata = metadata
27
+ @logger = logger
28
+ end
29
+
30
+ def dispatch
31
+ Lapine::DTrace.fire!(:dispatch_enter, delegate_class.name, raw_payload)
32
+ begin
33
+ json = Oj.load(raw_payload)
34
+ with_timed_logging(json) { do_dispatch(json) }
35
+ rescue Oj::Error => e
36
+ self.class.error_handler.call(e, raw_payload)
37
+ rescue StandardError => e
38
+ self.class.error_handler.call(e, json)
39
+ end
40
+ Lapine::DTrace.fire!(:dispatch_return, delegate_class.name, raw_payload)
41
+ end
42
+
43
+ private
44
+
45
+ def with_timed_logging(json)
46
+ time = Time.now
47
+ ret = yield
48
+ time_end = Time.now
49
+ duration = (time_end - time) * 1000
50
+ logger.info "Processing rabbit message handler:#{delegate_class.name} duration(ms):#{duration} payload:#{json.inspect}"
51
+ ret
52
+ end
53
+
54
+ def delegate_method_names
55
+ [:handle_lapine_payload, :perform_async]
56
+ end
57
+
58
+ def do_dispatch(payload)
59
+ delegate_method_names.each do |meth|
60
+ return delegate_class.send(meth, payload, metadata) if delegate_class.respond_to?(meth)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,47 @@
1
+ module Lapine
2
+ module Consumer
3
+ class Environment
4
+ attr_reader :config
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def load!
11
+ set_environment
12
+ load_rails
13
+ require_from_config
14
+ end
15
+
16
+ def set_environment
17
+ ENV['RAILS_ENV'] ||= 'development'
18
+ ENV['RACK_ENV'] = ENV['RAILS_ENV']
19
+ end
20
+
21
+ def load_rails
22
+ begin
23
+ require 'rails'
24
+ if ::Rails.application.respond_to?(:eager_load)
25
+ require File.expand_path('config/environment.rb')
26
+ ::Rails.application.eager_load!
27
+ else
28
+ require File.expand_path('config/application.rb')
29
+ ::Rails::Application.initializer "lapine.load_rails" do
30
+ ::Rails.application.config.eager_load = true
31
+ end
32
+ require File.expand_path('config/environment.rb')
33
+ end
34
+ rescue LoadError
35
+ end
36
+ end
37
+
38
+ def require_from_config
39
+ if config.require
40
+ config.require.each do |file|
41
+ require File.expand_path(file)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,69 @@
1
+ require 'amqp'
2
+ require 'digest'
3
+ require 'eventmachine'
4
+ require 'logger'
5
+ require 'lapine/consumer/config'
6
+ require 'lapine/consumer/connection'
7
+ require 'lapine/consumer/environment'
8
+ require 'lapine/consumer/topology'
9
+ require 'lapine/consumer/dispatcher'
10
+
11
+ module Lapine
12
+ module Consumer
13
+ class Runner
14
+ attr_reader :argv
15
+
16
+ def initialize(argv)
17
+ @argv = argv
18
+ @message_count = 0
19
+ end
20
+
21
+ def run
22
+ handle_signals!
23
+ Consumer::Environment.new(config).load!
24
+ logger.info 'starting Lapine::Consumer'
25
+
26
+ EventMachine.run do
27
+ topology.each_binding do |q, conn, routing_key, classes|
28
+ queue = conn.channel.queue(q).bind(conn.exchange, routing_key: routing_key)
29
+ queue.subscribe(ack: true) do |metadata, payload|
30
+ classes.each do |clazz|
31
+ Lapine::Consumer::Dispatcher.new(clazz, payload, metadata, logger).dispatch
32
+ end
33
+
34
+ @message_count += 1 if config.debug?
35
+
36
+ metadata.ack
37
+ end
38
+ end
39
+
40
+ if config.debug?
41
+ EventMachine.add_periodic_timer(10) do
42
+ logger.info "Lapine::Consumer messages processed=#{@message_count}"
43
+ @message_count = 0
44
+ end
45
+ end
46
+ end
47
+
48
+ logger.warn 'exiting Lapine::Consumer'
49
+ end
50
+
51
+ def config
52
+ @config ||= Lapine::Consumer::Config.new.load(argv)
53
+ end
54
+
55
+ def topology
56
+ @topology ||= ::Lapine::Consumer::Topology.new(config, logger)
57
+ end
58
+
59
+ def logger
60
+ @logger ||= config.logfile ? Logger.new(config.logfile) : Logger.new(STDOUT)
61
+ end
62
+
63
+ def handle_signals!
64
+ Signal.trap('INT') { EventMachine.stop }
65
+ Signal.trap('TERM') { EventMachine.stop }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,45 @@
1
+ require 'lapine/consumer/connection'
2
+
3
+ module Lapine
4
+ module Consumer
5
+ class Topology < Struct.new(:config, :logger)
6
+
7
+ def each_binding
8
+ config.queues.each do |node|
9
+ classes = node['handlers'].map do |handler|
10
+ handler.split('::').inject(Object) do |const, name|
11
+ const.const_get(name)
12
+ end
13
+ end
14
+
15
+ yield node['q'], get_conn(node['topic']), node['routing_key'], classes
16
+ end
17
+ end
18
+
19
+
20
+ def each_topic
21
+ config.topics.each do |topic|
22
+ yield topic
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def get_conn(name)
29
+ @cons ||= {}.tap do |cons|
30
+ each_topic do |topic|
31
+ debug "Connecting to RabbiMQ: topic: #{topic}, #{config.connection_properties}"
32
+ cons[topic] = Lapine::Consumer::Connection.new(config, topic)
33
+ end
34
+ end
35
+ @cons[name]
36
+ end
37
+
38
+ def debug(msg)
39
+ return unless config.debug?
40
+ return unless logger
41
+ logger.info msg
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,30 @@
1
+ require 'usdt'
2
+
3
+ module Lapine
4
+ class DTrace
5
+ attr_reader :provider, :probes
6
+
7
+ def initialize
8
+ @provider = USDT::Provider.create(:ruby, :lapine)
9
+
10
+ @probes = {
11
+ # args: Class name, payload
12
+ dispatch_enter: provider.probe(:dispatch, :enter, :string, :string),
13
+ # args: Class name, payload
14
+ dispatch_return: provider.probe(:dispatch, :return, :string, :string),
15
+ }
16
+ end
17
+
18
+ def self.provider
19
+ @provider ||= new.tap do |p|
20
+ p.provider.enable
21
+ end
22
+ end
23
+
24
+ def self.fire!(probe_name, *args)
25
+ raise "Unknown probe: #{probe_name}" unless self.provider.probes[probe_name]
26
+ probe = self.provider.probes[probe_name]
27
+ probe.fire(*args) if probe.enabled?
28
+ end
29
+ end
30
+ end
@@ -35,16 +35,21 @@ module Lapine
35
35
  end
36
36
 
37
37
  class FakeQueue
38
- attr_reader :history
38
+ attr_reader :exchange, :message_history
39
39
 
40
40
  def bind(exchange)
41
- @history = MessageHistory.new
42
- exchange.bind history
41
+ @exchange = exchange
42
+ @message_history = MessageHistory.new
43
+ exchange.bind message_history
43
44
  self
44
45
  end
45
46
 
46
47
  def message_count
47
- history.message_count
48
+ message_history.message_count
49
+ end
50
+
51
+ def messages
52
+ message_history.messages
48
53
  end
49
54
  end
50
55
 
@@ -76,6 +81,7 @@ module Lapine
76
81
  end
77
82
 
78
83
  def close!
84
+ @exchange = nil
79
85
  true
80
86
  end
81
87
  end
@@ -3,11 +3,11 @@ require 'lapine/test/exchange'
3
3
  module Lapine
4
4
  module Test
5
5
  module RSpecHelper
6
- def self.setup(example)
7
- example.allow(Lapine::Exchange).to(
8
- example.receive(:new) { |name, properties|
6
+ def self.setup(_example = nil)
7
+ RSpec::Mocks::AllowanceTarget.new(Lapine::Exchange).to(
8
+ RSpec::Mocks::Matchers::Receive.new(:new, ->(name, properties) {
9
9
  Lapine::Test::Exchange.new(name, properties)
10
- }
10
+ })
11
11
  )
12
12
  end
13
13
 
@@ -1,3 +1,3 @@
1
1
  module Lapine
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -0,0 +1,164 @@
1
+ require 'spec_helper'
2
+ require 'lapine/consumer/config'
3
+
4
+ RSpec.describe Lapine::Consumer::Config do
5
+ let(:argv) { %w(-c /path/to/config.yml) }
6
+
7
+ subject(:config) { Lapine::Consumer::Config.new }
8
+ let(:config_from_file) { {} }
9
+
10
+ before do
11
+ config.load argv
12
+ allow(YAML).to receive(:load_file).with('/path/to/config.yml').and_return(config_from_file)
13
+ end
14
+
15
+ describe '#load' do
16
+ it 'returns self' do
17
+ expect(config.load(argv)).to eq(config)
18
+ end
19
+ end
20
+
21
+ describe '#connection_properties' do
22
+ before { config.load(argv) }
23
+
24
+ let(:connection_properties) { config.connection_properties }
25
+
26
+ describe 'host' do
27
+ it 'defaults to 127.0.0.1' do
28
+ expect(connection_properties[:host]).to eq('127.0.0.1')
29
+ end
30
+
31
+ context 'with connection info in file' do
32
+ let(:config_from_file) { { 'connection' => { 'host' => '1.1.1.1' } } }
33
+
34
+ it 'uses the config file info' do
35
+ expect(connection_properties[:host]).to eq('1.1.1.1')
36
+ end
37
+ end
38
+
39
+ context 'with command line arg' do
40
+ let(:argv) { %w(--host 2.2.2.2 -c /path/to/config.yml) }
41
+ let(:config_from_file) { { 'connection' => { 'host' => '1.1.1.1' } } }
42
+
43
+ it 'prefers the cli' do
44
+ expect(connection_properties[:host]).to eq('2.2.2.2')
45
+ end
46
+ end
47
+ end
48
+
49
+ describe 'port' do
50
+ it 'defaults to 5672' do
51
+ expect(connection_properties[:port]).to eq(5672)
52
+ end
53
+
54
+ context 'with connection info in file' do
55
+ let(:config_from_file) { { 'connection' => { 'port' => 5673 } } }
56
+
57
+ it 'uses the config file info' do
58
+ expect(connection_properties[:port]).to eq(5673)
59
+ end
60
+ end
61
+
62
+ context 'with command line arg' do
63
+ let(:argv) { %w(--port 5674 -c /path/to/config.yml) }
64
+ let(:config_from_file) { { 'connection' => { 'port' => 5673 } } }
65
+
66
+ it 'prefers the cli' do
67
+ expect(connection_properties[:port]).to eq(5674)
68
+ end
69
+ end
70
+ end
71
+
72
+ describe 'ssl' do
73
+ it 'defaults to false' do
74
+ expect(connection_properties[:ssl]).to be(false)
75
+ end
76
+
77
+ context 'with connection info in file' do
78
+ let(:config_from_file) { { 'connection' => { 'ssl' => true } } }
79
+
80
+ it 'uses the config file info' do
81
+ expect(connection_properties[:ssl]).to be(true)
82
+ end
83
+ end
84
+
85
+ context 'with command line arg' do
86
+ let(:argv) { %w(--ssl -c /path/to/config.yml) }
87
+ let(:config_from_file) { { 'connection' => { 'ssl' => false } } }
88
+
89
+ it 'prefers the cli' do
90
+ expect(connection_properties[:ssl]).to be(true)
91
+ end
92
+ end
93
+ end
94
+
95
+ describe 'vhost' do
96
+ it 'defaults to /' do
97
+ expect(connection_properties[:vhost]).to eq('/')
98
+ end
99
+
100
+ context 'with connection info in file' do
101
+ let(:config_from_file) { { 'connection' => { 'vhost' => '/blah' } } }
102
+
103
+ it 'uses the config file info' do
104
+ expect(connection_properties[:vhost]).to eq('/blah')
105
+ end
106
+ end
107
+
108
+ context 'with command line arg' do
109
+ let(:argv) { %w(--vhost /argh -c /path/to/config.yml) }
110
+ let(:config_from_file) { { 'connection' => { 'vhost' => '/blah' } } }
111
+
112
+ it 'prefers the cli' do
113
+ expect(connection_properties[:vhost]).to eq('/argh')
114
+ end
115
+ end
116
+ end
117
+
118
+ describe 'username' do
119
+ it 'defaults to guest' do
120
+ expect(connection_properties[:username]).to eq('guest')
121
+ end
122
+
123
+ context 'with connection info in file' do
124
+ let(:config_from_file) { { 'connection' => { 'username' => 'Hrairoo' } } }
125
+
126
+ it 'uses the config file info' do
127
+ expect(connection_properties[:username]).to eq('Hrairoo')
128
+ end
129
+ end
130
+
131
+ context 'with command line arg' do
132
+ let(:argv) { %w(--username Thlayli -c /path/to/config.yml) }
133
+ let(:config_from_file) { { 'connection' => { 'username' => 'Hrairoo' } } }
134
+
135
+ it 'prefers the cli' do
136
+ expect(connection_properties[:username]).to eq('Thlayli')
137
+ end
138
+ end
139
+ end
140
+
141
+ describe 'password' do
142
+ it 'defaults to guest' do
143
+ expect(connection_properties[:password]).to eq('guest')
144
+ end
145
+
146
+ context 'with connection info in file' do
147
+ let(:config_from_file) { { 'connection' => { 'password' => 'flayrah' } } }
148
+
149
+ it 'uses the config file info' do
150
+ expect(connection_properties[:password]).to eq('flayrah')
151
+ end
152
+ end
153
+
154
+ context 'with command line arg' do
155
+ let(:argv) { %w(--password pfeffa -c /path/to/config.yml) }
156
+ let(:config_from_file) { { 'connection' => { 'password' => 'flayrah' } } }
157
+
158
+ it 'prefers the cli' do
159
+ expect(connection_properties[:password]).to eq('pfeffa')
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+ require 'lapine/consumer/connection'
3
+
4
+ RSpec.describe Lapine::Consumer::Connection do
5
+
6
+ describe "initialize" do
7
+ let(:properties) { {host: '127.0.0.1', port: 5672, ssl: false, vhost: '/', username: 'guest', password: 'guest'} }
8
+ let(:connection) { double('AMQP::Session') }
9
+ let(:channel) { double('AMQP::Channel') }
10
+ let(:config) { double('config', connection_properties: properties) }
11
+
12
+ before do
13
+ expect(AMQP).to receive(:connect).with(properties) { connection }
14
+ expect(AMQP::Channel).to receive(:new).with(connection) { channel }
15
+ end
16
+
17
+ it "Builds amqp objects" do
18
+ expect(AMQP::Exchange).to receive(:new).with(channel, :topic, 'thing.topic', durable: true)
19
+ described_class.new(config, 'thing.topic')
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+ require 'lapine/consumer/dispatcher'
3
+
4
+ RSpec.describe Lapine::Consumer::Dispatcher do
5
+
6
+ subject(:dispatcher) { Lapine::Consumer::Dispatcher.new(delegate, json, metadata, logger) }
7
+ let(:logger) { double('logger') }
8
+ let(:hash) { {'foo' => 'bar'} }
9
+ let(:json) { Oj.dump(hash) }
10
+ let(:metadata) { double("metadata") }
11
+ let(:delegate) { double("delegate", name: "ClassName") }
12
+
13
+ let(:caught_errors) { [] }
14
+
15
+ before do
16
+ Lapine::Consumer::Dispatcher.error_handler = ->(error, data) {
17
+ caught_errors << [error, data]
18
+ }
19
+ end
20
+
21
+ describe "#delegation" do
22
+ context "success cases" do
23
+ before do
24
+ expect(logger).to receive(:info).once.with(/Processing(.*)ClassName/)
25
+ end
26
+
27
+ context ".handle_lapine_payload method" do
28
+ it "receives handle_lapine_payload" do
29
+ expect(delegate).to receive(:respond_to?).with(:handle_lapine_payload).and_return(true)
30
+ expect(delegate).to receive(:handle_lapine_payload).once
31
+ dispatcher.dispatch
32
+ end
33
+ end
34
+
35
+ context ".perform_async method" do
36
+ it "receives perform_async" do
37
+ expect(delegate).to receive(:respond_to?).with(:handle_lapine_payload).and_return(false)
38
+ expect(delegate).to receive(:respond_to?).with(:perform_async).and_return(true)
39
+ expect(delegate).to receive(:perform_async).once
40
+ dispatcher.dispatch
41
+ end
42
+ end
43
+ end
44
+
45
+ describe 'error cases' do
46
+ context 'with invalid json' do
47
+ let(:json) { 'oh boy I am not actually JSON' }
48
+
49
+ it 'notifies new relic with the raw payload' do
50
+ dispatcher.dispatch
51
+ expect(caught_errors).to include([an_instance_of(Oj::ParseError), json])
52
+ end
53
+ end
54
+
55
+ context 'with any other error' do
56
+ before { allow(dispatcher).to receive(:do_dispatch).and_raise(ArgumentError) }
57
+
58
+ it 'notifies new relic with the parsed json' do
59
+ dispatcher.dispatch
60
+ expect(caught_errors).to include([an_instance_of(ArgumentError), hash])
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+ require 'lapine/consumer/runner'
3
+ require 'amqp'
4
+ require 'em-spec/rspec'
5
+
6
+ RSpec.describe Lapine::Consumer::Runner do
7
+ include EM::SpecHelper
8
+
9
+ class FakerHandler
10
+ def self.handle_lapine_payload(payload, metadata)
11
+ end
12
+ end
13
+
14
+ subject(:runner) { Lapine::Consumer::Runner.new(argv) }
15
+ let(:argv) { [] }
16
+ let(:queues) do
17
+ [
18
+ {
19
+ 'q' => 'testing.test',
20
+ 'topic' => 'testing.topic',
21
+ 'routing_key' => 'testing.update',
22
+ 'handlers' =>
23
+ [
24
+ 'FakerHandler'
25
+ ]
26
+ }
27
+ ]
28
+ end
29
+
30
+ let(:config) { double('config',
31
+ logfile: '/dev/null',
32
+ yaml_config: 'fakefil',
33
+ connection_properties: connection_properties,
34
+ require: [],
35
+ queues: queues,
36
+ topics: ['testing.topic'],
37
+ debug?: true) }
38
+ let(:connection_properties) { {host: '127.0.0.1', port: 5672, ssl: false, vhost: '/', username: 'guest', password: 'guest'} }
39
+ let(:message) { Oj.dump({'pay' => 'load'}) }
40
+
41
+ describe '#run' do
42
+ before do
43
+ allow(runner).to receive(:config).and_return(config)
44
+ allow(runner).to receive(:topology).and_return(::Lapine::Consumer::Topology.new(config, runner.logger))
45
+ allow(runner).to receive(:handle_signals!)
46
+ end
47
+
48
+ it 'sends a message to handler' do
49
+ expect(FakerHandler).to receive(:handle_lapine_payload).twice
50
+ em do
51
+ subject.run
52
+ conn = Lapine::Consumer::Connection.new(config, 'testing.topic')
53
+ conn.exchange.publish(message, routing_key: 'testing.update')
54
+ conn.exchange.publish(message, routing_key: 'testing.update')
55
+ EventMachine.add_timer(2.0) { done }
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '#config' do
61
+ it 'passes argv to a new config object' do
62
+ allow(Lapine::Consumer::Config).to receive(:new).and_return(config)
63
+ expect(config).to receive(:load).with(argv).and_return(config)
64
+ expect(runner.config).to eq(config)
65
+ end
66
+ end
67
+
68
+ describe '#handle_signals!' do
69
+ it 'traps INT and TERM signals' do
70
+ expect(Signal).to receive(:trap).with('INT')
71
+ expect(Signal).to receive(:trap).with('TERM')
72
+ subject.handle_signals!
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+ require 'lapine/consumer/topology'
3
+
4
+ RSpec.describe Lapine::Consumer::Topology do
5
+ module MessageBusTest
6
+ class Clazz
7
+ end
8
+ end
9
+
10
+ let(:topics) {
11
+ [
12
+ "a.topic",
13
+ "b.topic"
14
+ ]
15
+ }
16
+ let(:queues) {
17
+ [{
18
+ "q" => "store.buyable",
19
+ "topic" => "a.topic",
20
+ "routing_key" =>
21
+ "store.buyable.update",
22
+ "handlers" => ["MessageBusTest::Clazz"]
23
+ }]
24
+ }
25
+ let(:connection_properties) {
26
+ {}
27
+ }
28
+ let(:config) do
29
+ double('config',
30
+ topics: topics,
31
+ queues: queues,
32
+ connection_properties: connection_properties,
33
+ debug?: debug)
34
+ end
35
+
36
+ subject(:topology) { Lapine::Consumer::Topology.new(config, logger) }
37
+ let(:debug) { false }
38
+ let(:logger) { nil }
39
+
40
+ describe "#each_topic" do
41
+ it "yields correct dount" do
42
+ expect { |b| topology.each_topic(&b) }.to yield_control.twice
43
+ end
44
+
45
+ it "yields all topics in order" do
46
+ expect { |b| topology.each_topic(&b) }.to yield_successive_args("a.topic", "b.topic")
47
+ end
48
+ end
49
+
50
+ describe "#each_binding" do
51
+ let(:conn) { double('connection') }
52
+
53
+ before do
54
+ allow(Lapine::Consumer::Connection).to receive(:new) { conn }
55
+ end
56
+
57
+ it "yields correct count" do
58
+ expect { |b| topology.each_binding(&b) }.to yield_control.once
59
+ end
60
+
61
+ it "yields expected arguments" do
62
+ expect { |b|
63
+ topology.each_binding(&b)
64
+ }.to yield_with_args("store.buyable",
65
+ conn,
66
+ "store.buyable.update",
67
+ [MessageBusTest::Clazz])
68
+ end
69
+
70
+ context 'with a logger and debug mode' do
71
+ let(:debug) { true }
72
+ let(:logger) { double('logger', info: true) }
73
+
74
+ it 'logs each connection' do
75
+ topology.each_binding {}
76
+ expect(logger).to have_received(:info).with("Connecting to RabbiMQ: topic: a.topic, #{config.connection_properties}")
77
+ expect(logger).to have_received(:info).with("Connecting to RabbiMQ: topic: b.topic, #{config.connection_properties}")
78
+ end
79
+ end
80
+ end
81
+ end
82
+
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+ require 'lapine/test/rspec_helper'
3
+
4
+ RSpec.describe Lapine::Test::Exchange, with_rspec_helper: true do
5
+ class Publisher
6
+ include Lapine::Publisher
7
+
8
+ exchange 'my.topic'
9
+
10
+ def to_hash
11
+ {
12
+ omg: 'lol'
13
+ }
14
+ end
15
+ end
16
+
17
+ let(:exchange) { Lapine.find_exchange('my.topic') }
18
+ let(:queue) { exchange.channel.queue.bind(exchange) }
19
+
20
+ before do
21
+ Lapine.add_connection 'conn', {}
22
+ Lapine.add_exchange 'my.topic', connection: 'conn'
23
+ queue
24
+ end
25
+
26
+ describe 'publish' do
27
+ it 'changes the queue message count' do
28
+ expect {
29
+ Publisher.new.publish
30
+ }.to change {
31
+ queue.message_count
32
+ }.to(1)
33
+ end
34
+
35
+ it 'saves message for later introspection' do
36
+ Publisher.new.publish('my.things')
37
+ message = ['{"omg":"lol"}', {routing_key: 'my.things'}]
38
+ expect(queue.messages).to include(message)
39
+ end
40
+ end
41
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,10 @@
1
1
  require 'lapine'
2
+ require 'pry'
3
+ require 'rspec/mocks'
4
+
5
+ Dir[File.expand_path('../support/**/*.rb', __FILE__)].each do |f|
6
+ require f
7
+ end
2
8
 
3
9
  RSpec.configure do |config|
4
10
  config.expect_with :rspec do |expectations|
@@ -0,0 +1,14 @@
1
+ require 'lapine/test/rspec_helper'
2
+
3
+ RSpec.configure do |config|
4
+ config.include Lapine::Test::RSpecHelper, with_rspec_helper: true
5
+
6
+ config.before :each, :with_rspec_helper do |example|
7
+ Lapine::Test::RSpecHelper.setup(example)
8
+ end
9
+
10
+ config.after :each, :with_rspec_helper do
11
+ Lapine::Test::RSpecHelper.teardown
12
+ end
13
+ end
14
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lapine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Saxby
@@ -9,8 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-11-26 00:00:00.000000000 Z
12
+ date: 2014-12-01 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: amqp
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: bunny
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -25,6 +39,20 @@ dependencies:
25
39
  - - ">="
26
40
  - !ruby/object:Gem::Version
27
41
  version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: mixlib-cli
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
28
56
  - !ruby/object:Gem::Dependency
29
57
  name: oj
30
58
  requirement: !ruby/object:Gem::Requirement
@@ -39,6 +67,20 @@ dependencies:
39
67
  - - ">="
40
68
  - !ruby/object:Gem::Version
41
69
  version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: ruby-usdt
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 0.2.2
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 0.2.2
42
84
  - !ruby/object:Gem::Dependency
43
85
  name: bundler
44
86
  requirement: !ruby/object:Gem::Requirement
@@ -95,10 +137,25 @@ dependencies:
95
137
  - - "~>"
96
138
  - !ruby/object:Gem::Version
97
139
  version: 3.1.0
140
+ - !ruby/object:Gem::Dependency
141
+ name: em-spec
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
98
154
  description: Talk to rabbits
99
155
  email:
100
156
  - dev@wanelo.com
101
- executables: []
157
+ executables:
158
+ - lapine
102
159
  extensions: []
103
160
  extra_rdoc_files: []
104
161
  files:
@@ -109,17 +166,37 @@ files:
109
166
  - LICENSE.txt
110
167
  - README.md
111
168
  - Rakefile
169
+ - bin/lapine
170
+ - example/consumer_config.yml
171
+ - example/consumer_handler.rb
172
+ - example/producer.rb
112
173
  - lapine.gemspec
113
174
  - lib/lapine.rb
175
+ - lib/lapine/cli.rb
114
176
  - lib/lapine/configuration.rb
177
+ - lib/lapine/consumer.rb
178
+ - lib/lapine/consumer/config.rb
179
+ - lib/lapine/consumer/connection.rb
180
+ - lib/lapine/consumer/dispatcher.rb
181
+ - lib/lapine/consumer/environment.rb
182
+ - lib/lapine/consumer/runner.rb
183
+ - lib/lapine/consumer/topology.rb
184
+ - lib/lapine/dtrace.rb
115
185
  - lib/lapine/exchange.rb
116
186
  - lib/lapine/publisher.rb
117
187
  - lib/lapine/test/exchange.rb
118
188
  - lib/lapine/test/rspec_helper.rb
119
189
  - lib/lapine/version.rb
190
+ - spec/lib/lapine/consumer/config_spec.rb
191
+ - spec/lib/lapine/consumer/connection_spec.rb
192
+ - spec/lib/lapine/consumer/dispatcher_spec.rb
193
+ - spec/lib/lapine/consumer/runner_spec.rb
194
+ - spec/lib/lapine/consumer/topology_spec.rb
120
195
  - spec/lib/lapine/publisher_spec.rb
196
+ - spec/lib/lapine/test/exchange_spec.rb
121
197
  - spec/lib/lapine_spec.rb
122
198
  - spec/spec_helper.rb
199
+ - spec/support/rspec_test_helper.rb
123
200
  homepage: https://github.com/wanelo/lapine
124
201
  licenses:
125
202
  - MIT
@@ -140,12 +217,19 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
217
  version: '0'
141
218
  requirements: []
142
219
  rubyforge_project:
143
- rubygems_version: 2.4.4
220
+ rubygems_version: 2.2.2
144
221
  signing_key:
145
222
  specification_version: 4
146
223
  summary: Talk to rabbits
147
224
  test_files:
225
+ - spec/lib/lapine/consumer/config_spec.rb
226
+ - spec/lib/lapine/consumer/connection_spec.rb
227
+ - spec/lib/lapine/consumer/dispatcher_spec.rb
228
+ - spec/lib/lapine/consumer/runner_spec.rb
229
+ - spec/lib/lapine/consumer/topology_spec.rb
148
230
  - spec/lib/lapine/publisher_spec.rb
231
+ - spec/lib/lapine/test/exchange_spec.rb
149
232
  - spec/lib/lapine_spec.rb
150
233
  - spec/spec_helper.rb
234
+ - spec/support/rspec_test_helper.rb
151
235
  has_rdoc: