lapine 1.0.1 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Changelog.md +15 -0
- data/lib/lapine.rb +8 -8
- data/lib/lapine/configuration.rb +51 -1
- data/lib/lapine/consumer/dispatcher.rb +9 -18
- data/lib/lapine/consumer/message.rb +28 -0
- data/lib/lapine/consumer/middleware.rb +129 -0
- data/lib/lapine/consumer/middleware/error_handler.rb +19 -0
- data/lib/lapine/consumer/middleware/json_decoder.rb +20 -0
- data/lib/lapine/consumer/middleware/message_ack_handler.rb +19 -0
- data/lib/lapine/consumer/runner.rb +12 -7
- data/lib/lapine/errors.rb +3 -0
- data/lib/lapine/exchange.rb +14 -12
- data/lib/lapine/test/exchange.rb +4 -0
- data/lib/lapine/version.rb +1 -1
- data/spec/lib/lapine/consumer/dispatcher_spec.rb +23 -46
- data/spec/lib/lapine/consumer/middleware_spec.rb +166 -0
- data/spec/lib/lapine_spec.rb +13 -9
- data/spec/spec_helper.rb +6 -0
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 564e7001cbbd4468d842e013cc437610a25b0c24
|
4
|
+
data.tar.gz: 90a86db4889bed75801d7278b0710b36e0c3f38c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9096e3d40022bfb83e8aff47aac3f299e162cd644f56679c6294ba8bbd3d3438fa8df47b1fbfffe5f466743b208df7334ea0b2cb8d032ce0ee9a0b990224e4db
|
7
|
+
data.tar.gz: 41f326c4642272bf3ea3f9a32a964dae81e07dfea940edfcc14017b5828ebe0baf77206c17430f50754f7e8b39400581f1c98a2f1c34aeaee4c5b0fec69aed74
|
data/Changelog.md
CHANGED
@@ -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
|
data/lib/lapine.rb
CHANGED
@@ -22,17 +22,17 @@ module Lapine
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def self.find_exchange(name)
|
25
|
-
exchange = config.
|
26
|
-
|
27
|
-
|
28
|
-
config.
|
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.
|
34
|
-
exchange.close!
|
35
|
-
config.exchanges.delete exchange.name
|
36
|
-
end
|
36
|
+
config.close_connections!
|
37
37
|
end
|
38
38
|
end
|
data/lib/lapine/configuration.rb
CHANGED
@@ -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
|
-
|
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, :
|
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,
|
23
|
+
def initialize(delegate_class, message)
|
25
24
|
@delegate_class = delegate_class
|
26
|
-
@
|
27
|
-
@
|
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,
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
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
|
data/lib/lapine/errors.rb
CHANGED
data/lib/lapine/exchange.rb
CHANGED
@@ -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
|
15
|
-
|
16
|
-
|
17
|
-
@exchange
|
15
|
+
def connected?
|
16
|
+
@exchange.channel.connection.connected? &&
|
17
|
+
@exchange.channel.open?
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
30
|
+
def close
|
31
|
+
@exchange.channel.close
|
30
32
|
end
|
31
33
|
end
|
32
34
|
end
|
data/lib/lapine/test/exchange.rb
CHANGED
data/lib/lapine/version.rb
CHANGED
@@ -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,
|
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(
|
11
|
-
let(:delegate) { double(
|
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
|
20
|
-
context
|
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
|
26
|
-
it
|
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
|
34
|
-
it
|
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
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
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
|
data/spec/lib/lapine_spec.rb
CHANGED
@@ -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
|
51
|
-
let(:connection2) { double('blargh connection', close
|
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.
|
55
|
-
|
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)
|
data/spec/spec_helper.rb
CHANGED
@@ -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.
|
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-
|
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
|