lapine 1.0.1 → 1.1.2

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: 17ff6eda9eaea1807ee77240e11fa2dc336f7350
4
- data.tar.gz: b94a4cbada011e8f4fe12e6873b2def2ab923e27
3
+ metadata.gz: 564e7001cbbd4468d842e013cc437610a25b0c24
4
+ data.tar.gz: 90a86db4889bed75801d7278b0710b36e0c3f38c
5
5
  SHA512:
6
- metadata.gz: f68160452a12c78121c0175e2cbf8448924a6d4dd075de8e5051628240cb097b2e3bf8ecd6bf3b5779a83b3a8b24fb020fdd7b977caa71d236628bb44ea68d13
7
- data.tar.gz: 391d24cbd65e613f44f46df60eca15c1958c20466b01eb3425faea7448225127f45f6e78758ddc7f3b2b9ef56ce4259bfa1fe283c68c768b5bbde7bd214b9dbf
6
+ metadata.gz: 9096e3d40022bfb83e8aff47aac3f299e162cd644f56679c6294ba8bbd3d3438fa8df47b1fbfffe5f466743b208df7334ea0b2cb8d032ce0ee9a0b990224e4db
7
+ data.tar.gz: 41f326c4642272bf3ea3f9a32a964dae81e07dfea940edfcc14017b5828ebe0baf77206c17430f50754f7e8b39400581f1c98a2f1c34aeaee4c5b0fec69aed74
@@ -1,6 +1,21 @@
1
1
  Change Log
2
2
  ==========
3
3
 
4
+ ## 1.1.2
5
+
6
+ * Exchanges are saved using thread variables instead of fiber variables
7
+ * Move memoization of connections and exchanges to Configuration
8
+
9
+ ## 1.1.1
10
+
11
+ * Fix potential thread safety issue with publisher connections to
12
+ RabbitMQ
13
+
14
+ ## 1.1.0
15
+
16
+ * Lapine consumer can be configured with middleware
17
+ * Error handling, json decoding, and message acknowledgement now happen in middleware
18
+
4
19
  ## 1.0.1
5
20
 
6
21
  * Increased verbosity of errors
@@ -22,17 +22,17 @@ module Lapine
22
22
  end
23
23
 
24
24
  def self.find_exchange(name)
25
- exchange = config.exchange_properties[name]
26
- raise UndefinedExchange.new("No exchange for #{name}") unless exchange
27
- return config.exchanges[name].exchange if config.exchanges[name]
28
- config.exchanges[name] = Lapine::Exchange.new(name, exchange)
25
+ exchange = config.exchanges[name]
26
+ return exchange.exchange if (exchange && exchange.connected?)
27
+
28
+ exchange_configuration = config.exchange_properties[name]
29
+ raise UndefinedExchange.new("No exchange configuration for #{name}") unless exchange_configuration
30
+
31
+ config.exchanges[name] = Lapine::Exchange.new(name, exchange_configuration)
29
32
  config.exchanges[name].exchange
30
33
  end
31
34
 
32
35
  def self.close_connections!
33
- config.exchanges.values.each do |exchange|
34
- exchange.close!
35
- config.exchanges.delete exchange.name
36
- end
36
+ config.close_connections!
37
37
  end
38
38
  end
@@ -1,5 +1,9 @@
1
1
  module Lapine
2
2
  class Configuration
3
+ def initialize
4
+ @active_connections = {}
5
+ end
6
+
3
7
  def connections
4
8
  @connections ||= {}
5
9
  end
@@ -8,12 +12,58 @@ module Lapine
8
12
  @connection_properties ||= {}
9
13
  end
10
14
 
15
+ def channels_by_exchange_id
16
+ @channels_by_exchange_id ||= {}
17
+ end
18
+
19
+ def register_channel(object_id, channel)
20
+ channels_by_exchange_id[object_id] = channel
21
+ end
22
+
23
+ def cleanup_exchange(id)
24
+ return unless channels_by_exchange_id[id]
25
+ channel = channels_by_exchange_id[id]
26
+ channel.connection.logger.info "Closing channel for exchange #{id}, thread: #{Thread.current.object_id}"
27
+ channel.close
28
+ channels_by_exchange_id[id] = nil
29
+ end
30
+
31
+ # Exchanges need to be saved in a thread-local variable, rather than a fiber-local variable,
32
+ # because in the context of some applications (such as Sidekiq, which uses Celluloid) individual
33
+ # bits of work are done in fibers that are immediately reaped.
11
34
  def exchanges
12
- @exchanges ||= {}
35
+ Thread.current.thread_variable_get(:lapine_exchanges) ||
36
+ Thread.current.thread_variable_set(:lapine_exchanges, {})
13
37
  end
14
38
 
15
39
  def exchange_properties
16
40
  @exchange_properties ||= {}
17
41
  end
42
+
43
+ def active_connection(name)
44
+ conn = @active_connections[name]
45
+ return conn if (conn && conn.connected?)
46
+
47
+ @active_connections[name] = begin
48
+ @conn = Bunny.new(connection_props_for(name)).tap do |conn|
49
+ conn.start
50
+ end
51
+ end
52
+ end
53
+
54
+ def close_connections!
55
+ @active_connections.values.map(&:close)
56
+ @active_connections = {}
57
+ Thread.current.thread_variable_set(:lapine_exchanges, nil)
58
+ end
59
+
60
+ def connection_props_for(name)
61
+ return unless connection_properties[name]
62
+ connection_properties[name].dup.tap do |props|
63
+ if defined?(Rails)
64
+ props.merge!(logger: Rails.logger)
65
+ end
66
+ end
67
+ end
18
68
  end
19
69
  end
@@ -5,13 +5,12 @@ module Lapine
5
5
  module Consumer
6
6
  class Dispatcher
7
7
  class DefaultErrorHandler
8
-
9
8
  def call(e, data, _metadata)
10
9
  $stderr.puts "Lapine::Dispatcher unable to dispatch, #{e.message}, data: #{data}"
11
10
  end
12
11
  end
13
12
 
14
- attr_reader :delegate_class, :raw_payload, :metadata, :logger
13
+ attr_reader :delegate_class, :message, :payload
15
14
 
16
15
  def self.error_handler=(handler)
17
16
  @error_handler = handler
@@ -21,24 +20,16 @@ module Lapine
21
20
  @error_handler || DefaultErrorHandler.new
22
21
  end
23
22
 
24
- def initialize(delegate_class, raw_payload, metadata, logger)
23
+ def initialize(delegate_class, message)
25
24
  @delegate_class = delegate_class
26
- @raw_payload = raw_payload
27
- @metadata = metadata
28
- @logger = logger
25
+ @message = message
26
+ @payload = message.decoded_payload
29
27
  end
30
28
 
31
29
  def dispatch
32
- Lapine::DTrace.fire!(:dispatch_enter, delegate_class.name, raw_payload)
33
- begin
34
- json = Oj.load(raw_payload)
35
- with_timed_logging(json) { do_dispatch(json) }
36
- rescue Oj::Error => e
37
- self.class.error_handler.call(e, raw_payload, metadata)
38
- rescue StandardError => e
39
- self.class.error_handler.call(e, json, metadata)
40
- end
41
- Lapine::DTrace.fire!(:dispatch_return, delegate_class.name, raw_payload)
30
+ Lapine::DTrace.fire!(:dispatch_enter, delegate_class.name, message.payload)
31
+ with_timed_logging(payload) { do_dispatch(payload) }
32
+ Lapine::DTrace.fire!(:dispatch_return, delegate_class.name, message.payload)
42
33
  end
43
34
 
44
35
  private
@@ -48,7 +39,7 @@ module Lapine
48
39
  ret = yield
49
40
  time_end = Time.now
50
41
  duration = (time_end - time) * 1000
51
- logger.info "Processing rabbit message handler:#{delegate_class.name} duration(ms):#{duration} payload:#{json.inspect}"
42
+ message.logger.info "Processing rabbit message handler:#{delegate_class.name} duration(ms):#{duration} payload:#{json.inspect}"
52
43
  ret
53
44
  end
54
45
 
@@ -58,7 +49,7 @@ module Lapine
58
49
 
59
50
  def do_dispatch(payload)
60
51
  delegate_method_names.each do |meth|
61
- return delegate_class.send(meth, payload, metadata) if delegate_class.respond_to?(meth)
52
+ return delegate_class.send(meth, payload, message.metadata) if delegate_class.respond_to?(meth)
62
53
  end
63
54
  end
64
55
  end
@@ -0,0 +1,28 @@
1
+ module Lapine
2
+ module Consumer
3
+ class Message < Hash
4
+ def initialize(payload, metadata, logger)
5
+ super(nil)
6
+ self['payload'] = payload
7
+ self['metadata'] = metadata
8
+ self['logger'] = logger
9
+ end
10
+
11
+ def payload
12
+ self['payload']
13
+ end
14
+
15
+ def decoded_payload
16
+ self['decoded_payload']
17
+ end
18
+
19
+ def metadata
20
+ self['metadata']
21
+ end
22
+
23
+ def logger
24
+ self['logger']
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,129 @@
1
+ require 'lapine/consumer/middleware/error_handler'
2
+ require 'lapine/consumer/middleware/message_ack_handler'
3
+ require 'lapine/consumer/middleware/json_decoder'
4
+
5
+ module Lapine
6
+ module Consumer
7
+ #
8
+ # Consumer middleware can be registered as follows:
9
+ #
10
+ # Lapine::Consumer::Middleware.add MyClass
11
+ # Lapine::Consumer::Middleware.add MyClass, argument
12
+ # Lapine::Consumer::Middleware.add_before MyClass, MyOtherClass, argument
13
+ # Lapine::Consumer::Middleware.add_after MyClass, MyOtherClass, argument
14
+ #
15
+ # Middleware should follow the pattern:
16
+ #
17
+ # class MyMiddleware
18
+ # attr_reader :app
19
+ #
20
+ # def initialize(app, *arguments)
21
+ # @app = app
22
+ # end
23
+ #
24
+ # def call(message)
25
+ # # do stuff
26
+ # app.call(message)
27
+ # end
28
+ # end
29
+ #
30
+ module Middleware
31
+ # A Register of a middleware class that messages will be passed through
32
+ # on the way to being dispatched.
33
+ class Register < Struct.new(:klass, :args)
34
+ def create_new(app)
35
+ klass.new(app, *args)
36
+ end
37
+ end
38
+
39
+ DEFAULT_MIDDLEWARE = [
40
+ Register.new(MessageAckHandler),
41
+ Register.new(ErrorHandler),
42
+ Register.new(JsonDecoder)
43
+ ].freeze
44
+
45
+ class << self
46
+ def add(klass, *args)
47
+ registry << [klass, args]
48
+ end
49
+
50
+ def add_before(before_klass, klass, *args)
51
+ idx = registry.index_of(before_klass)
52
+ raise MiddlewareNotFound.new("#{before_klass} not registered in Lapine middleware") unless idx
53
+ registry.insert(idx, klass, args)
54
+ end
55
+
56
+ def add_after(after_klass, klass, *args)
57
+ idx = registry.index_of(after_klass)
58
+ raise MiddlewareNotFound.new("#{after_klass} not registered in Lapine middleware") unless idx
59
+ registry.insert(idx + 1, klass, args)
60
+ end
61
+
62
+ def delete(klass)
63
+ registry.delete(klass)
64
+ end
65
+
66
+ def registry
67
+ @registry ||= Registry.new(DEFAULT_MIDDLEWARE.dup)
68
+ end
69
+
70
+ def create_chain(app)
71
+ registry.map { |r| r.create_new(app) }
72
+ end
73
+
74
+ def app
75
+ App.new.tap do |app|
76
+ app.chain = create_chain(app)
77
+ end
78
+ end
79
+ end
80
+
81
+ class App
82
+ attr_accessor :chain
83
+
84
+ def call(message, &block)
85
+ chain << block if block_given?
86
+ current_register = chain.shift
87
+ current_register.call(message) if current_register
88
+ end
89
+ end
90
+
91
+ # Registry holds records of each middleware class that is added to the
92
+ # consumer middleware chain.
93
+ class Registry
94
+ include Enumerable
95
+
96
+ attr_reader :registry
97
+
98
+ def initialize(registry = [])
99
+ @registry = registry
100
+ end
101
+
102
+ def all
103
+ registry
104
+ end
105
+
106
+ def each(&blk)
107
+ all.each(&blk)
108
+ end
109
+
110
+ def delete(klass)
111
+ registry.reject! { |register| register.klass == klass }
112
+ end
113
+
114
+ def <<(klass_args)
115
+ insert(-1, klass_args[0], klass_args[1])
116
+ end
117
+
118
+ def index_of(klass)
119
+ registry.find_index { |register| register.klass == klass }
120
+ end
121
+
122
+ def insert(index, klass, args)
123
+ raise Lapine::DuplicateMiddleware if index_of(klass)
124
+ registry.insert(index, Register.new(klass, args))
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,19 @@
1
+ module Lapine
2
+ module Consumer
3
+ module Middleware
4
+ class ErrorHandler
5
+ attr_reader :app
6
+
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(message)
12
+ app.call(message)
13
+ rescue StandardError => e
14
+ Lapine::Consumer::Dispatcher.error_handler.call(e, message.payload, message.metadata)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ require 'oj'
2
+
3
+ module Lapine
4
+ module Consumer
5
+ module Middleware
6
+ class JsonDecoder
7
+ attr_reader :app
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(message)
14
+ message['decoded_payload'] = Oj.load(message.payload)
15
+ app.call(message)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ module Lapine
2
+ module Consumer
3
+ module Middleware
4
+ class MessageAckHandler
5
+ attr_reader :app
6
+
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(message)
12
+ app.call(message)
13
+ ensure
14
+ message.metadata.ack
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -5,6 +5,8 @@ require 'lapine/annotated_logger'
5
5
  require 'lapine/consumer/config'
6
6
  require 'lapine/consumer/connection'
7
7
  require 'lapine/consumer/environment'
8
+ require 'lapine/consumer/message'
9
+ require 'lapine/consumer/middleware'
8
10
  require 'lapine/consumer/topology'
9
11
  require 'lapine/consumer/dispatcher'
10
12
 
@@ -29,15 +31,18 @@ module Lapine
29
31
  topology.each_binding do |q, conn, routing_key, classes|
30
32
  queue = conn.channel.queue(q, @queue_properties).bind(conn.exchange, routing_key: routing_key)
31
33
  queue.subscribe(ack: true) do |metadata, payload|
32
- classes.each do |clazz|
33
- Lapine::Consumer::Dispatcher.new(clazz, payload, metadata, logger).dispatch
34
- end
35
34
 
36
- if config.debug?
37
- @message_count += 1
38
- @running_message_count += 1
35
+ message = Consumer::Message.new(payload, metadata, logger)
36
+ Middleware.app.call(message) do |message|
37
+ classes.each do |clazz|
38
+ Lapine::Consumer::Dispatcher.new(clazz, message).dispatch
39
+ end
40
+
41
+ if config.debug?
42
+ @message_count += 1
43
+ @running_message_count += 1
44
+ end
39
45
  end
40
- metadata.ack
41
46
 
42
47
  EventMachine.stop_event_loop if should_exit?
43
48
  end
@@ -3,4 +3,7 @@ module Lapine
3
3
  class UndefinedConnection < LapineError; end
4
4
  class UndefinedExchange < LapineError; end
5
5
  class NilExchange < LapineError; end
6
+
7
+ class MiddlewareNotFound < LapineError; end
8
+ class DuplicateMiddleware < LapineError; end
6
9
  end
@@ -9,24 +9,26 @@ module Lapine
9
9
  @props = properties.dup
10
10
  @connection_name = props.delete(:connection)
11
11
  @exchange_type = props.delete(:type)
12
+ ObjectSpace.define_finalizer(self, proc { |id| Lapine.config.cleanup_exchange(id) })
12
13
  end
13
14
 
14
- def exchange
15
- reconnect unless @exchange && conn && conn.connected?
16
- raise Lapine::NilExchange unless @exchange
17
- @exchange
15
+ def connected?
16
+ @exchange.channel.connection.connected? &&
17
+ @exchange.channel.open?
18
18
  end
19
19
 
20
- def reconnect
21
- connection_props = Lapine.config.connection_properties[connection_name]
22
- @conn = Bunny.new(connection_props)
23
- conn.start
24
- channel = conn.create_channel
25
- @exchange = Bunny::Exchange.new(channel, exchange_type, name, props)
20
+ def exchange
21
+ @exchange ||= begin
22
+ conn = Lapine.config.active_connection(connection_name)
23
+ conn.logger.info "Creating channel for #{self.object_id}, thread: #{Thread.current.object_id}"
24
+ channel = conn.create_channel
25
+ Lapine.config.register_channel(self.object_id, channel)
26
+ Bunny::Exchange.new(channel, exchange_type, name, props)
27
+ end
26
28
  end
27
29
 
28
- def close!
29
- conn.close if conn.connected?
30
+ def close
31
+ @exchange.channel.close
30
32
  end
31
33
  end
32
34
  end
@@ -84,6 +84,10 @@ module Lapine
84
84
  @exchange = nil
85
85
  true
86
86
  end
87
+
88
+ def connected?
89
+ true
90
+ end
87
91
  end
88
92
  end
89
93
  end
@@ -1,3 +1,3 @@
1
1
  module Lapine
2
- VERSION = '1.0.1'
2
+ VERSION = '1.1.2'
3
3
  end
@@ -3,12 +3,18 @@ require 'lapine/consumer/dispatcher'
3
3
 
4
4
  RSpec.describe Lapine::Consumer::Dispatcher do
5
5
 
6
- subject(:dispatcher) { Lapine::Consumer::Dispatcher.new(delegate, json, metadata, logger) }
6
+ subject(:dispatcher) { Lapine::Consumer::Dispatcher.new(delegate, message) }
7
+ let(:message) do
8
+ Lapine::Consumer::Message.new(json, metadata, logger).tap do |message|
9
+ message['decoded_payload'] = hash
10
+ end
11
+ end
12
+
7
13
  let(:logger) { double('logger') }
8
14
  let(:hash) { {'foo' => 'bar'} }
9
15
  let(:json) { Oj.dump(hash) }
10
- let(:metadata) { double("metadata") }
11
- let(:delegate) { double("delegate", name: "ClassName") }
16
+ let(:metadata) { double('metadata') }
17
+ let(:delegate) { double('delegate', name: 'ClassName') }
12
18
 
13
19
  let(:caught_errors) { [] }
14
20
 
@@ -16,22 +22,22 @@ RSpec.describe Lapine::Consumer::Dispatcher do
16
22
  Lapine::Consumer::Dispatcher.error_handler = nil
17
23
  end
18
24
 
19
- describe "#delegation" do
20
- context "success cases" do
25
+ describe '#delegation' do
26
+ context 'success cases' do
21
27
  before do
22
28
  expect(logger).to receive(:info).once.with(/Processing(.*)ClassName/)
23
29
  end
24
30
 
25
- context ".handle_lapine_payload method" do
26
- it "receives handle_lapine_payload" do
31
+ context '.handle_lapine_payload method' do
32
+ it 'receives handle_lapine_payload' do
27
33
  expect(delegate).to receive(:respond_to?).with(:handle_lapine_payload).and_return(true)
28
34
  expect(delegate).to receive(:handle_lapine_payload).once
29
35
  dispatcher.dispatch
30
36
  end
31
37
  end
32
38
 
33
- context ".perform_async method" do
34
- it "receives perform_async" do
39
+ context '.perform_async method' do
40
+ it 'receives perform_async' do
35
41
  expect(delegate).to receive(:respond_to?).with(:handle_lapine_payload).and_return(false)
36
42
  expect(delegate).to receive(:respond_to?).with(:perform_async).and_return(true)
37
43
  expect(delegate).to receive(:perform_async).once
@@ -39,45 +45,16 @@ RSpec.describe Lapine::Consumer::Dispatcher do
39
45
  end
40
46
  end
41
47
  end
48
+ end
49
+ end
42
50
 
43
- describe 'error cases' do
44
-
45
- context 'custom error handler' do
46
- before do
47
- Lapine::Consumer::Dispatcher.error_handler = ->(error, data, metadata) {
48
- caught_errors << [error, data, metadata]
49
- }
50
- end
51
- context 'with invalid json' do
52
- let(:json) { 'oh boy I am not actually JSON' }
53
-
54
- it 'notifies the error handler with the raw payload' do
55
- dispatcher.dispatch
56
- expect(caught_errors).to include([an_instance_of(Oj::ParseError), json, metadata])
57
- end
58
- end
59
-
60
- context 'with any other error' do
61
- before { allow(dispatcher).to receive(:do_dispatch).and_raise(ArgumentError) }
62
-
63
- it 'notifies error handler with the parsed json' do
64
- dispatcher.dispatch
65
- expect(caught_errors).to include([an_instance_of(ArgumentError), hash, metadata])
66
- end
67
- end
68
- end
69
-
70
- context 'default error handler' do
71
- before { allow(dispatcher).to receive(:do_dispatch).and_raise(ArgumentError) }
72
-
73
- it 'notifies default error handler' do
74
- expect($stderr).to receive(:puts)
75
- dispatcher.dispatch
76
- end
77
- end
78
-
51
+ RSpec.describe Lapine::Consumer::Dispatcher::DefaultErrorHandler do
52
+ let(:payload) { double('payload') }
53
+ let(:metadata) { double('metadata') }
79
54
 
80
- end
55
+ it 'puts to stderr' do
56
+ expect($stderr).to receive(:puts)
57
+ Lapine::Consumer::Dispatcher::DefaultErrorHandler.new.call(StandardError.new, payload, metadata)
81
58
  end
82
59
  end
83
60
 
@@ -0,0 +1,166 @@
1
+ require 'spec_helper'
2
+ require 'lapine/consumer/dispatcher'
3
+ require 'lapine/consumer/middleware'
4
+
5
+ RSpec.describe Lapine::Consumer::Middleware do
6
+ class MiddlewareAddLetter
7
+ def initialize(app, letter)
8
+ @app = app
9
+ @letter = letter
10
+ end
11
+
12
+ def call(message)
13
+ message['letter'] = @letter
14
+ @app.call(message)
15
+ end
16
+ end
17
+
18
+ class MiddlewareCopyLetter
19
+ def initialize(app)
20
+ @app = app
21
+ end
22
+
23
+ def call(message)
24
+ message['middleware.copy_letter.ran'] = true
25
+ message['duplicate_letter'] = message['letter']
26
+ @app.call(message)
27
+ end
28
+ end
29
+
30
+ class RaisingMiddleware
31
+ def initialize(app)
32
+ @app = app
33
+ end
34
+
35
+ def call(_message)
36
+ raise StandardError.new('Raise')
37
+ end
38
+ end
39
+
40
+ class CatchingMiddleWare
41
+ def initialize(app)
42
+ @app = app
43
+ end
44
+
45
+ def call(message)
46
+ @app.call(message)
47
+ rescue StandardError => e
48
+ message['error_message'] = e.message
49
+ end
50
+ end
51
+
52
+ let(:metadata) { double('metadata', ack: true) }
53
+ let(:payload) { '{}' }
54
+ let(:message) { Lapine::Consumer::Message.new(payload, metadata, nil) }
55
+
56
+ describe '.add' do
57
+ before do
58
+ Lapine::Consumer::Middleware.tap do |middleware|
59
+ middleware.add MiddlewareAddLetter, 'f'
60
+ end
61
+ end
62
+
63
+ it 'is adds letter to hash' do
64
+ Lapine::Consumer::Middleware.app.call(message) do |message|
65
+ expect(message['letter']).to eq('f')
66
+ end
67
+ end
68
+
69
+ context 'when duplicate middleware is added' do
70
+ it 'raises' do
71
+ expect {
72
+ Lapine::Consumer::Middleware.tap do |middleware|
73
+ middleware.add MiddlewareAddLetter, 'f'
74
+ end
75
+ }.to raise_error(Lapine::DuplicateMiddleware)
76
+ end
77
+ end
78
+ end
79
+
80
+ describe '.delete' do
81
+ let(:registry) { Lapine::Consumer::Middleware.registry }
82
+
83
+ before do
84
+ Lapine::Consumer::Middleware.tap do |middleware|
85
+ middleware.add MiddlewareAddLetter, 'f'
86
+ end
87
+ end
88
+
89
+ it 'removes register that matches class name' do
90
+ expect(registry.index_of(MiddlewareAddLetter)).to be
91
+ Lapine::Consumer::Middleware.delete(MiddlewareAddLetter)
92
+ expect(registry.index_of(MiddlewareAddLetter)).not_to be
93
+ end
94
+ end
95
+
96
+ describe 'error handling' do
97
+ describe 'with default middleware' do
98
+ let(:error) { StandardError.new('doh') }
99
+
100
+ it 'runs through the dispatcher error_handler' do
101
+ errors = []
102
+ Lapine::Consumer::Dispatcher.error_handler = ->(e, data, md) {
103
+ errors << [e, data, md]
104
+ }
105
+ Lapine::Consumer::Middleware.app.call(message) { raise error }
106
+ expect(errors).to include([error, message.payload, message.metadata])
107
+ end
108
+ end
109
+
110
+ describe 'with custom middleware' do
111
+ before do
112
+ Lapine::Consumer::Middleware.tap do |middleware|
113
+ middleware.add CatchingMiddleWare
114
+ middleware.add RaisingMiddleware
115
+ end
116
+ end
117
+
118
+ it 'catches error' do
119
+ Lapine::Consumer::Middleware.app.call(message)
120
+ expect(message['error_message']).to eq('Raise')
121
+ end
122
+
123
+ it 'halts execution' do
124
+ expectation = double(called: true)
125
+ Lapine::Consumer::Middleware.app.call(message) do
126
+ expectation.called
127
+ end
128
+ expect(expectation).not_to have_received(:called)
129
+ end
130
+ end
131
+ end
132
+
133
+ describe '.add_before' do
134
+ before do
135
+ Lapine::Consumer::Middleware.tap do |middleware|
136
+ middleware.add MiddlewareAddLetter, 'f'
137
+ middleware.add_before MiddlewareAddLetter, MiddlewareCopyLetter
138
+ end
139
+
140
+ it 'prepends middleware' do
141
+ Lapine::Consumer::Middleware.app.call(message) do |message|
142
+ expect(message['letter']).to eq('f')
143
+ expect(message['duplicate_letter']).to be nil
144
+ expect(message['middleware.copy_letter.ran']).to be true
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ context '.add_after' do
151
+ before do
152
+ Lapine::Consumer::Middleware.tap do |middleware|
153
+ middleware.add MiddlewareAddLetter, 'f'
154
+ middleware.add_after MiddlewareAddLetter, MiddlewareCopyLetter
155
+ end
156
+
157
+ it 'prepends middleware' do
158
+ Lapine::Consumer::Middleware.app.call(message) do |message|
159
+ expect(message['letter']).to eq('f')
160
+ expect(message['duplicate_letter']).to be 'f'
161
+ expect(message['middleware.copy_letter.ran']).to be true
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -1,8 +1,10 @@
1
1
  require 'spec_helper'
2
2
  require 'lapine'
3
+ require 'lapine/consumer'
3
4
 
4
5
  RSpec.describe Lapine do
5
- let(:connection) { double('connection') }
6
+ let(:connection) { double('connection', close: true, logger: logger) }
7
+ let(:logger) { double('bunny logger', info: true) }
6
8
  let(:connection_properties) do
7
9
  {
8
10
  host: 'someplace.com'
@@ -47,18 +49,20 @@ RSpec.describe Lapine do
47
49
  end
48
50
 
49
51
  describe '.close_connections!' do
50
- let(:connection1) { double('blah connection', close!: true, name: 'blah') }
51
- let(:connection2) { double('blargh connection', close!: true, name: 'blargh') }
52
+ let(:connection1) { double('blah connection', close: true, name: 'blah') }
53
+ let(:connection2) { double('blargh connection', close: true, name: 'blargh') }
52
54
 
53
55
  before do
54
- config.exchanges['blah'] = connection1
55
- config.exchanges['blargh'] = connection2
56
+ config.instance_variable_set(:@active_connections, {
57
+ 'blah' => connection1,
58
+ 'blargh' => connection2
59
+ })
56
60
  end
57
61
 
58
62
  it 'calls close on each connection' do
59
63
  Lapine.close_connections!
60
- expect(connection1).to have_received(:close!)
61
- expect(connection2).to have_received(:close!)
64
+ expect(connection1).to have_received(:close)
65
+ expect(connection2).to have_received(:close)
62
66
  end
63
67
 
64
68
  it 'clears the exchanges' do
@@ -81,8 +85,8 @@ RSpec.describe Lapine do
81
85
  end
82
86
 
83
87
  context 'when exchange has been registered' do
84
- let(:channel) { double('channel') }
85
- let(:exchange) { double('exchange') }
88
+ let(:channel) { double('channel', connection: connection, open?: true) }
89
+ let(:exchange) { double('exchange', channel: channel) }
86
90
 
87
91
  before do
88
92
  allow(connection).to receive(:start)
@@ -20,7 +20,13 @@ RSpec.configure do |config|
20
20
  Kernel.srand config.seed
21
21
 
22
22
  config.before :each do
23
+ Lapine::Consumer::Dispatcher.error_handler = nil
23
24
  Lapine.instance_variable_set(:@config, nil)
25
+ Lapine::Consumer::Middleware.instance_variable_set(:@registry,nil)
26
+ Thread.current.thread_variable_set(:lapine_exchanges, nil)
27
+ end
24
28
 
29
+ config.after :each do
30
+ Lapine.close_connections!
25
31
  end
26
32
  end
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: 1.0.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Saxby
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-02-27 00:00:00.000000000 Z
12
+ date: 2015-03-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: amqp
@@ -181,6 +181,11 @@ files:
181
181
  - lib/lapine/consumer/connection.rb
182
182
  - lib/lapine/consumer/dispatcher.rb
183
183
  - lib/lapine/consumer/environment.rb
184
+ - lib/lapine/consumer/message.rb
185
+ - lib/lapine/consumer/middleware.rb
186
+ - lib/lapine/consumer/middleware/error_handler.rb
187
+ - lib/lapine/consumer/middleware/json_decoder.rb
188
+ - lib/lapine/consumer/middleware/message_ack_handler.rb
184
189
  - lib/lapine/consumer/runner.rb
185
190
  - lib/lapine/consumer/topology.rb
186
191
  - lib/lapine/dtrace.rb
@@ -193,6 +198,7 @@ files:
193
198
  - spec/lib/lapine/consumer/config_spec.rb
194
199
  - spec/lib/lapine/consumer/connection_spec.rb
195
200
  - spec/lib/lapine/consumer/dispatcher_spec.rb
201
+ - spec/lib/lapine/consumer/middleware_spec.rb
196
202
  - spec/lib/lapine/consumer/runner_spec.rb
197
203
  - spec/lib/lapine/consumer/topology_spec.rb
198
204
  - spec/lib/lapine/publisher_spec.rb
@@ -228,6 +234,7 @@ test_files:
228
234
  - spec/lib/lapine/consumer/config_spec.rb
229
235
  - spec/lib/lapine/consumer/connection_spec.rb
230
236
  - spec/lib/lapine/consumer/dispatcher_spec.rb
237
+ - spec/lib/lapine/consumer/middleware_spec.rb
231
238
  - spec/lib/lapine/consumer/runner_spec.rb
232
239
  - spec/lib/lapine/consumer/topology_spec.rb
233
240
  - spec/lib/lapine/publisher_spec.rb