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 +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
|