lapine 0.1.1 → 0.2.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.
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: