message-driver 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +2 -0
- data/.rubocop.yml +26 -2
- data/.rubocop_todo.yml +15 -123
- data/.travis.yml +2 -1
- data/CHANGELOG.md +10 -1
- data/Gemfile +15 -6
- data/Rakefile +23 -12
- data/ci/reset_vhost +8 -3
- data/ci/travis_setup +0 -3
- data/features/.nav +6 -1
- data/features/CHANGELOG.md +10 -1
- data/features/amqp_specific_features/binding_amqp_destinations.feature +1 -0
- data/features/amqp_specific_features/declaring_amqp_exchanges.feature +1 -0
- data/features/amqp_specific_features/server_named_destinations.feature +1 -0
- data/features/destination_metadata.feature +26 -0
- data/features/logging.feature +1 -1
- data/features/middleware/middleware_basics.feature +91 -0
- data/features/middleware/middleware_ordering.feature +60 -0
- data/features/middleware/middleware_parameters.feature +43 -0
- data/features/middleware/middleware_with_blocks.feature +85 -0
- data/features/step_definitions/dynamic_destinations_steps.rb +1 -1
- data/features/step_definitions/message_consumers_steps.rb +5 -0
- data/features/step_definitions/middleware_steps.rb +10 -0
- data/features/step_definitions/steps.rb +10 -2
- data/features/support/env.rb +4 -3
- data/features/support/firewall_helper.rb +1 -1
- data/features/support/message_table_matcher.rb +3 -2
- data/features/support/no_error_matcher.rb +2 -2
- data/features/support/test_runner.rb +11 -57
- data/features/support/transforms.rb +12 -10
- data/lib/message_driver.rb +3 -1
- data/lib/message_driver/adapters/base.rb +11 -11
- data/lib/message_driver/adapters/bunny_adapter.rb +189 -132
- data/lib/message_driver/adapters/in_memory_adapter.rb +51 -34
- data/lib/message_driver/adapters/stomp_adapter.rb +22 -23
- data/lib/message_driver/broker.rb +21 -16
- data/lib/message_driver/client.rb +15 -16
- data/lib/message_driver/destination.rb +26 -8
- data/lib/message_driver/message.rb +5 -4
- data/lib/message_driver/middleware.rb +8 -0
- data/lib/message_driver/middleware/base.rb +19 -0
- data/lib/message_driver/middleware/block_middleware.rb +33 -0
- data/lib/message_driver/middleware/middleware_stack.rb +61 -0
- data/lib/message_driver/subscription.rb +2 -2
- data/lib/message_driver/version.rb +1 -1
- data/message-driver.gemspec +3 -4
- data/spec/integration/bunny/amqp_integration_spec.rb +21 -82
- data/spec/integration/bunny/bunny_adapter_spec.rb +288 -268
- data/spec/integration/in_memory/in_memory_adapter_spec.rb +93 -90
- data/spec/integration/stomp/stomp_adapter_spec.rb +126 -93
- data/spec/spec_helper.rb +11 -9
- data/spec/support/shared/adapter_examples.rb +1 -1
- data/spec/support/shared/client_ack_examples.rb +4 -4
- data/spec/support/shared/context_examples.rb +6 -4
- data/spec/support/shared/destination_examples.rb +54 -14
- data/spec/support/shared/subscription_examples.rb +33 -26
- data/spec/support/shared/transaction_examples.rb +12 -12
- data/spec/support/utils.rb +1 -1
- data/spec/units/message_driver/adapters/base_spec.rb +42 -40
- data/spec/units/message_driver/broker_spec.rb +38 -38
- data/spec/units/message_driver/client_spec.rb +87 -87
- data/spec/units/message_driver/destination_spec.rb +16 -11
- data/spec/units/message_driver/message_spec.rb +96 -70
- data/spec/units/message_driver/middleware/base_spec.rb +30 -0
- data/spec/units/message_driver/middleware/block_middleware_spec.rb +82 -0
- data/spec/units/message_driver/middleware/middleware_stack_spec.rb +165 -0
- data/spec/units/message_driver/subscription_spec.rb +18 -16
- data/test_lib/broker_config.rb +21 -5
- data/test_lib/coverage.rb +11 -0
- data/test_lib/provider/base.rb +59 -0
- data/test_lib/provider/in_memory.rb +6 -0
- data/test_lib/provider/rabbitmq.rb +67 -0
- metadata +46 -35
@@ -3,16 +3,17 @@ module MessageDriver
|
|
3
3
|
class Base
|
4
4
|
include Logging
|
5
5
|
|
6
|
-
attr_reader :ctx, :body, :headers, :properties
|
6
|
+
attr_reader :ctx, :body, :raw_body, :headers, :properties
|
7
7
|
|
8
|
-
def initialize(ctx, body, headers, properties)
|
8
|
+
def initialize(ctx, body, headers, properties, raw_body = nil)
|
9
9
|
@ctx = ctx
|
10
10
|
@body = body
|
11
11
|
@headers = headers
|
12
12
|
@properties = properties
|
13
|
+
@raw_body = raw_body.nil? ? body : raw_body
|
13
14
|
end
|
14
15
|
|
15
|
-
def ack(options={})
|
16
|
+
def ack(options = {})
|
16
17
|
if ctx.supports_client_acks?
|
17
18
|
ctx.ack_message(self, options)
|
18
19
|
else
|
@@ -20,7 +21,7 @@ module MessageDriver
|
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
23
|
-
def nack(options={})
|
24
|
+
def nack(options = {})
|
24
25
|
if ctx.supports_client_acks?
|
25
26
|
ctx.nack_message(self, options)
|
26
27
|
else
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module MessageDriver
|
2
|
+
module Middleware
|
3
|
+
class Base
|
4
|
+
attr_reader :destination
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
@destination = args.shift
|
8
|
+
end
|
9
|
+
|
10
|
+
def on_publish(body, headers, properties)
|
11
|
+
[body, headers, properties]
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_consume(body, headers, properties)
|
15
|
+
[body, headers, properties]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module MessageDriver
|
2
|
+
module Middleware
|
3
|
+
class BlockMiddleware < Base
|
4
|
+
attr_reader :on_publish_block, :on_consume_block
|
5
|
+
|
6
|
+
def initialize(destination, opts)
|
7
|
+
super(destination)
|
8
|
+
fail ArgumentError, 'you must provide at least one of :on_publish and :on_consume' \
|
9
|
+
unless opts.keys.any? { |k| [:on_publish, :on_consume].include? k }
|
10
|
+
@on_publish_block = opts[:on_publish]
|
11
|
+
@on_consume_block = opts[:on_consume]
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_publish(body, headers, properties)
|
15
|
+
delegate_to_block(on_publish_block, body, headers, properties)
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_consume(body, headers, properties)
|
19
|
+
delegate_to_block(on_consume_block, body, headers, properties)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def delegate_to_block(block, body, headers, properties)
|
25
|
+
if block.nil?
|
26
|
+
[body, headers, properties]
|
27
|
+
else
|
28
|
+
block.call(body, headers, properties)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module MessageDriver
|
2
|
+
module Middleware
|
3
|
+
class MiddlewareStack
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_reader :destination
|
7
|
+
|
8
|
+
def initialize(destination)
|
9
|
+
@destination = destination
|
10
|
+
@middlewares = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def middlewares
|
14
|
+
@middlewares.dup.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def append(middleware_class, *args)
|
18
|
+
middleware = build_middleware(middleware_class, *args)
|
19
|
+
@middlewares << middleware
|
20
|
+
middleware
|
21
|
+
end
|
22
|
+
|
23
|
+
def prepend(middleware_class, *args)
|
24
|
+
middleware = build_middleware(middleware_class, *args)
|
25
|
+
@middlewares.unshift middleware
|
26
|
+
middleware
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_publish(body, headers, properties)
|
30
|
+
@middlewares.reduce([body, headers, properties]) do |args, middleware|
|
31
|
+
middleware.on_publish(*args)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_consume(body, headers, properties)
|
36
|
+
@middlewares.reverse.reduce([body, headers, properties]) do |args, middleware|
|
37
|
+
middleware.on_consume(*args)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def empty?
|
42
|
+
@middlewares.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
def each
|
46
|
+
@middlewares.each { |x| yield x }
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def build_middleware(middleware_type, *args)
|
52
|
+
case middleware_type
|
53
|
+
when Hash
|
54
|
+
BlockMiddleware.new(destination, middleware_type)
|
55
|
+
else
|
56
|
+
middleware_type.new(destination, *args)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -4,7 +4,7 @@ module MessageDriver
|
|
4
4
|
include Logging
|
5
5
|
attr_reader :adapter, :destination, :consumer, :options
|
6
6
|
|
7
|
-
def initialize(adapter, destination, consumer, options={})
|
7
|
+
def initialize(adapter, destination, consumer, options = {})
|
8
8
|
@adapter = adapter
|
9
9
|
@destination = destination
|
10
10
|
@consumer = consumer
|
@@ -12,7 +12,7 @@ module MessageDriver
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def unsubscribe
|
15
|
-
|
15
|
+
fail 'must be implemented in subclass'
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
data/message-driver.gemspec
CHANGED
@@ -14,15 +14,14 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.license = 'MIT'
|
15
15
|
|
16
16
|
gem.files = `git ls-files`.split($RS)
|
17
|
-
gem.executables = gem.files.grep(
|
18
|
-
gem.test_files = gem.files.grep(
|
17
|
+
gem.executables = gem.files.grep(/^bin\//).map { |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(/^(test|spec|features)\//)
|
19
19
|
gem.require_paths = %w(lib)
|
20
20
|
|
21
21
|
gem.required_ruby_version = '>= 1.9.2'
|
22
22
|
|
23
23
|
gem.add_development_dependency 'rake'
|
24
|
-
gem.add_development_dependency 'rspec', '~>
|
24
|
+
gem.add_development_dependency 'rspec', '~> 3.1.0'
|
25
25
|
gem.add_development_dependency 'cucumber'
|
26
26
|
gem.add_development_dependency 'aruba'
|
27
|
-
gem.add_development_dependency 'rubocop'
|
28
27
|
end
|
@@ -1,15 +1,14 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe 'AMQP Integration', :bunny, type: :integration do
|
3
|
+
RSpec.describe 'AMQP Integration', :bunny, type: :integration do
|
4
4
|
let!(:broker) { MessageDriver::Broker.configure BrokerConfig.config }
|
5
5
|
|
6
6
|
context "when a queue can't be found" do
|
7
7
|
let(:queue_name) { 'my.lost.queue' }
|
8
8
|
it 'raises a MessageDriver::QueueNotFound error' do
|
9
|
-
expect
|
9
|
+
expect do
|
10
10
|
broker.dynamic_destination(queue_name, passive: true)
|
11
|
-
|
12
|
-
expect(err.queue_name).to eq(queue_name)
|
11
|
+
end.to raise_error(MessageDriver::QueueNotFound) do |err|
|
13
12
|
expect(err.nested).to be_a Bunny::NotFound
|
14
13
|
end
|
15
14
|
end
|
@@ -17,93 +16,33 @@ describe 'AMQP Integration', :bunny, type: :integration do
|
|
17
16
|
|
18
17
|
context 'when a channel level exception occurs' do
|
19
18
|
it 'raises a MessageDriver::WrappedError error' do
|
20
|
-
expect
|
19
|
+
expect do
|
21
20
|
broker.dynamic_destination('not.a.queue', passive: true)
|
22
|
-
|
21
|
+
end.to raise_error(MessageDriver::WrappedError) { |err| expect(err.nested).to be_a Bunny::ChannelLevelException }
|
23
22
|
end
|
24
23
|
|
25
24
|
it 'reestablishes the channel transparently' do
|
26
|
-
expect
|
25
|
+
expect do
|
27
26
|
broker.dynamic_destination('not.a.queue', passive: true)
|
28
|
-
|
29
|
-
expect
|
27
|
+
end.to raise_error(MessageDriver::WrappedError)
|
28
|
+
expect do
|
30
29
|
broker.dynamic_destination('', exclusive: true)
|
31
|
-
|
30
|
+
end.to_not raise_error
|
32
31
|
end
|
33
32
|
|
34
33
|
context 'when in a transaction' do
|
35
34
|
it 'sets the channel_context as rollback-only until the transaction is finished' do
|
36
35
|
MessageDriver::Client.with_message_transaction do
|
37
|
-
expect
|
36
|
+
expect do
|
38
37
|
broker.dynamic_destination('not.a.queue', passive: true)
|
39
|
-
|
40
|
-
expect
|
38
|
+
end.to raise_error(MessageDriver::WrappedError)
|
39
|
+
expect do
|
41
40
|
broker.dynamic_destination('', exclusive: true)
|
42
|
-
|
41
|
+
end.to raise_error(MessageDriver::TransactionRollbackOnly)
|
43
42
|
end
|
44
|
-
expect
|
43
|
+
expect do
|
45
44
|
broker.dynamic_destination('', exclusive: true)
|
46
|
-
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
context 'when the broker connection fails', pending: 'these spec are busted' do
|
52
|
-
def disrupt_connection
|
53
|
-
#yes, this is very implementation specific
|
54
|
-
broker.adapter.connection.instance_variable_get(:@transport).close
|
55
|
-
end
|
56
|
-
|
57
|
-
def create_destination(queue_name)
|
58
|
-
broker.dynamic_destination(queue_name, exclusive: true)
|
59
|
-
end
|
60
|
-
|
61
|
-
it 'raises a MessageDriver::ConnectionError' do
|
62
|
-
dest = create_destination('test_queue')
|
63
|
-
disrupt_connection
|
64
|
-
expect {
|
65
|
-
dest.publish('Reconnection Test')
|
66
|
-
}.to raise_error(MessageDriver::ConnectionError) do |err|
|
67
|
-
expect(err.nested).to be_a Bunny::NetworkErrorWrapper
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
it 'seemlessly reconnects' do
|
72
|
-
dest = create_destination('seemless.reconnect.queue')
|
73
|
-
disrupt_connection
|
74
|
-
expect {
|
75
|
-
dest.publish('Reconnection Test 1')
|
76
|
-
}.to raise_error(MessageDriver::ConnectionError)
|
77
|
-
dest = create_destination('seemless.reconnect.queue')
|
78
|
-
dest.publish('Reconnection Test 2')
|
79
|
-
msg = dest.pop_message
|
80
|
-
expect(msg).to_not be_nil
|
81
|
-
expect(msg.body).to eq('Reconnection Test 2')
|
82
|
-
end
|
83
|
-
|
84
|
-
context 'when in a transaction' do
|
85
|
-
it 'raises a MessageDriver::ConnectionError' do
|
86
|
-
expect {
|
87
|
-
MessageDriver::Client.with_message_transaction do
|
88
|
-
disrupt_connection
|
89
|
-
broker.dynamic_destination('', exclusive: true)
|
90
|
-
end
|
91
|
-
}.to raise_error(MessageDriver::ConnectionError)
|
92
|
-
end
|
93
|
-
|
94
|
-
it 'sets the channel_context as rollback-only until the transaction is finished' do
|
95
|
-
MessageDriver::Client.with_message_transaction do
|
96
|
-
disrupt_connection
|
97
|
-
expect {
|
98
|
-
broker.dynamic_destination('', exclusive: true)
|
99
|
-
}.to raise_error(MessageDriver::ConnectionError)
|
100
|
-
expect {
|
101
|
-
broker.dynamic_destination('', exclusive: true)
|
102
|
-
}.to raise_error(MessageDriver::TransactionRollbackOnly)
|
103
|
-
end
|
104
|
-
expect {
|
105
|
-
broker.dynamic_destination('', exclusive: true)
|
106
|
-
}.to_not raise_error
|
45
|
+
end.to_not raise_error
|
107
46
|
end
|
108
47
|
end
|
109
48
|
end
|
@@ -112,22 +51,22 @@ describe 'AMQP Integration', :bunny, type: :integration do
|
|
112
51
|
let(:destination) { broker.dynamic_destination('', exclusive: true) }
|
113
52
|
|
114
53
|
it 'rolls back the transaction' do
|
115
|
-
expect
|
54
|
+
expect do
|
116
55
|
MessageDriver::Client.with_message_transaction do
|
117
56
|
destination.publish('Test Message')
|
118
|
-
|
57
|
+
fail 'unhandled error'
|
119
58
|
end
|
120
|
-
|
59
|
+
end.to raise_error 'unhandled error'
|
121
60
|
expect(destination.pop_message).to be_nil
|
122
61
|
end
|
123
62
|
|
124
63
|
it 'allows the next transaction to continue' do
|
125
|
-
expect
|
64
|
+
expect do
|
126
65
|
MessageDriver::Client.with_message_transaction do
|
127
66
|
destination.publish('Test Message 1')
|
128
|
-
|
67
|
+
fail 'unhandled error'
|
129
68
|
end
|
130
|
-
|
69
|
+
end.to raise_error 'unhandled error'
|
131
70
|
expect(destination.pop_message).to be_nil
|
132
71
|
|
133
72
|
MessageDriver::Client.with_message_transaction do
|
@@ -2,351 +2,371 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
require 'message_driver/adapters/bunny_adapter'
|
4
4
|
|
5
|
-
module MessageDriver
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
5
|
+
module MessageDriver
|
6
|
+
module Adapters
|
7
|
+
RSpec.describe BunnyAdapter, :bunny, type: :integration do
|
8
|
+
let(:valid_connection_attrs) { BrokerConfig.config }
|
9
|
+
|
10
|
+
describe '#initialize' do
|
11
|
+
context 'differing bunny versions' do
|
12
|
+
let(:broker) { double('broker') }
|
13
|
+
shared_examples 'raises an error' do
|
14
|
+
it 'raises an error' do
|
15
|
+
stub_const('Bunny::VERSION', version)
|
16
|
+
expect do
|
17
|
+
described_class.new(broker, valid_connection_attrs)
|
18
|
+
end.to raise_error MessageDriver::Error, 'bunny 1.2.2 or later is required for the bunny adapter'
|
19
|
+
end
|
19
20
|
end
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
21
|
+
shared_examples "doesn't raise an error" do
|
22
|
+
it "doesn't raise an an error" do
|
23
|
+
stub_const('Bunny::VERSION', version)
|
24
|
+
adapter = nil
|
25
|
+
expect do
|
26
|
+
adapter = described_class.new(broker, valid_connection_attrs)
|
27
|
+
end.to_not raise_error
|
28
|
+
end
|
28
29
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
%w(0.8.0 0.9.0 0.9.8 0.10.7 1.0.3 1.1.2 1.2.1).each do |v|
|
31
|
+
context "bunny version #{v}" do
|
32
|
+
let(:version) { v }
|
33
|
+
include_examples 'raises an error'
|
34
|
+
end
|
34
35
|
end
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
%w(1.2.2 1.3.2 1.4.0).each do |v|
|
37
|
+
context "bunny version #{v}" do
|
38
|
+
let(:version) { v }
|
39
|
+
include_examples "doesn't raise an error"
|
40
|
+
end
|
40
41
|
end
|
41
42
|
end
|
42
|
-
end
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
44
|
+
it 'connects to the rabbit broker' do
|
45
|
+
broker = double(:broker)
|
46
|
+
adapter = described_class.new(broker, valid_connection_attrs)
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
|
48
|
+
expect(adapter.connection).to be_a Bunny::Session
|
49
|
+
expect(adapter.connection).to be_open
|
50
|
+
end
|
51
51
|
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
it 'connects to the rabbit broker lazily' do
|
53
|
+
broker = double(:broker)
|
54
|
+
adapter = described_class.new(broker, valid_connection_attrs)
|
55
55
|
|
56
|
-
|
56
|
+
expect(adapter.connection(false)).to be_nil
|
57
|
+
end
|
57
58
|
end
|
58
|
-
end
|
59
59
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
60
|
+
shared_context 'a connected bunny adapter' do
|
61
|
+
let(:broker) { MessageDriver::Broker.configure(valid_connection_attrs) }
|
62
|
+
subject(:adapter) { broker.adapter }
|
63
|
+
let(:connection) { adapter.connection }
|
64
64
|
|
65
|
-
|
66
|
-
|
65
|
+
after do
|
66
|
+
adapter.stop
|
67
|
+
end
|
67
68
|
end
|
68
|
-
end
|
69
|
-
|
70
|
-
shared_context 'with a queue' do
|
71
|
-
include_context 'a connected bunny adapter'
|
72
|
-
|
73
|
-
let(:channel) { connection.create_channel }
|
74
|
-
let(:tmp_queue_name) { 'my_temp_queue' }
|
75
|
-
let(:tmp_queue) { channel.queue(tmp_queue_name, exclusive: true) }
|
76
|
-
end
|
77
|
-
|
78
|
-
it_behaves_like 'an adapter' do
|
79
|
-
include_context 'a connected bunny adapter'
|
80
|
-
end
|
81
69
|
|
82
|
-
|
83
|
-
|
70
|
+
shared_context 'with a queue' do
|
71
|
+
include_context 'a connected bunny adapter'
|
84
72
|
|
85
|
-
|
86
|
-
|
73
|
+
let(:channel) { connection.create_channel }
|
74
|
+
let(:tmp_queue_name) { 'my_temp_queue' }
|
75
|
+
let(:tmp_queue) { channel.queue(tmp_queue_name, exclusive: true) }
|
87
76
|
end
|
88
|
-
end
|
89
77
|
|
90
|
-
|
91
|
-
|
92
|
-
subject(:adapter_context) { adapter.new_context }
|
93
|
-
around(:each) do |ex|
|
94
|
-
MessageDriver::Client.with_adapter_context(adapter_context) do
|
95
|
-
ex.run
|
96
|
-
end
|
78
|
+
it_behaves_like 'an adapter' do
|
79
|
+
include_context 'a connected bunny adapter'
|
97
80
|
end
|
98
81
|
|
99
|
-
|
100
|
-
|
101
|
-
it_behaves_like 'client acks are supported'
|
102
|
-
it_behaves_like 'subscriptions are supported', BunnyAdapter::Subscription
|
82
|
+
describe '#new_context' do
|
83
|
+
include_context 'a connected bunny adapter'
|
103
84
|
|
104
|
-
|
105
|
-
|
106
|
-
|
85
|
+
it 'returns a BunnyAdapter::BunnyContext' do
|
86
|
+
expect(subject.new_context).to be_a BunnyAdapter::BunnyContext
|
87
|
+
end
|
107
88
|
end
|
108
89
|
|
109
|
-
describe
|
110
|
-
|
111
|
-
|
112
|
-
|
90
|
+
describe BunnyAdapter::BunnyContext do
|
91
|
+
include_context 'a connected bunny adapter'
|
92
|
+
subject(:adapter_context) { adapter.new_context }
|
93
|
+
around(:example) do |ex|
|
94
|
+
MessageDriver::Client.with_adapter_context(adapter_context) do
|
95
|
+
ex.run
|
113
96
|
end
|
114
|
-
subject.invalidate
|
115
|
-
expect(subject.instance_variable_get(:@channel)).to be_closed
|
116
97
|
end
|
117
|
-
end
|
118
|
-
|
119
|
-
describe '#create_destination' do
|
120
98
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
99
|
+
it_behaves_like 'an adapter context'
|
100
|
+
it_behaves_like 'transactions are supported'
|
101
|
+
it_behaves_like 'client acks are supported'
|
102
|
+
it_behaves_like 'subscriptions are supported', BunnyAdapter::Subscription
|
125
103
|
|
126
|
-
|
127
|
-
|
104
|
+
describe '#pop_message' do
|
105
|
+
include_context 'with a queue'
|
106
|
+
it 'needs some real tests'
|
128
107
|
end
|
129
108
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
expect(
|
109
|
+
describe '#invalidate' do
|
110
|
+
it 'closes the channel' do
|
111
|
+
subject.with_channel(false) do |ch|
|
112
|
+
expect(ch).to be_open
|
113
|
+
end
|
114
|
+
subject.invalidate
|
115
|
+
expect(subject.instance_variable_get(:@channel)).to be_closed
|
137
116
|
end
|
138
117
|
end
|
139
118
|
|
140
|
-
|
141
|
-
context 'and there is no destination name given' do
|
142
|
-
subject(:destination) { adapter_context.create_destination('', type: :queue, exclusive: true) }
|
143
|
-
it { should be_a BunnyAdapter::QueueDestination }
|
144
|
-
its(:name) { should be_a String }
|
145
|
-
its(:name) { should_not be_empty }
|
146
|
-
end
|
147
|
-
context 'the resulting destination' do
|
148
|
-
let(:dest_name) { 'my_dest' }
|
149
|
-
subject(:destination) { adapter_context.create_destination(dest_name, type: :queue, exclusive: true) }
|
150
|
-
before do
|
151
|
-
destination
|
152
|
-
end
|
119
|
+
describe '#create_destination' do
|
153
120
|
|
154
|
-
|
155
|
-
|
156
|
-
|
121
|
+
context 'with defaults' do
|
122
|
+
context 'the resulting destination' do
|
123
|
+
let(:dest_name) { 'my_dest' }
|
124
|
+
subject(:result) { adapter_context.create_destination(dest_name, exclusive: true) }
|
157
125
|
|
158
|
-
|
126
|
+
it { is_expected.to be_a BunnyAdapter::QueueDestination }
|
127
|
+
end
|
128
|
+
end
|
159
129
|
|
160
|
-
|
161
|
-
|
130
|
+
shared_examples 'supports publisher confirmations' do
|
131
|
+
let(:properties) { { persistent: false, confirm: true } }
|
132
|
+
it 'switches the channel to confirms mode' do
|
133
|
+
expect(adapter_context.channel.using_publisher_confirms?).to eq(true)
|
134
|
+
end
|
135
|
+
it 'waits until the confirm comes in' do
|
136
|
+
expect(adapter_context.channel.unconfirmed_set).to be_empty
|
162
137
|
end
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'the type is queue' do
|
141
|
+
context 'and there is no destination name given' do
|
142
|
+
subject(:destination) { adapter_context.create_destination('', type: :queue, exclusive: true) }
|
143
|
+
it { is_expected.to be_a BunnyAdapter::QueueDestination }
|
144
|
+
|
145
|
+
describe '#name' do
|
146
|
+
it 'is a non-empty String' do
|
147
|
+
expect(subject.name).to be_a String
|
148
|
+
expect(subject.name).not_to be_empty
|
167
149
|
end
|
168
|
-
|
150
|
+
end
|
169
151
|
end
|
170
|
-
|
171
|
-
|
172
|
-
let(:
|
173
|
-
|
152
|
+
|
153
|
+
context 'the resulting destination' do
|
154
|
+
let(:dest_name) { 'my_dest' }
|
155
|
+
subject(:destination) { adapter_context.create_destination(dest_name, type: :queue, exclusive: true) }
|
174
156
|
before do
|
175
|
-
|
157
|
+
destination
|
158
|
+
end
|
159
|
+
|
160
|
+
it { is_expected.to be_a BunnyAdapter::QueueDestination }
|
161
|
+
|
162
|
+
describe '#name' do
|
163
|
+
it 'is the destination name' do
|
164
|
+
expect(subject.name).to be_a String
|
165
|
+
expect(subject.name).to eq(dest_name)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
include_examples 'supports #message_count'
|
170
|
+
include_examples 'supports #consumer_count'
|
171
|
+
|
172
|
+
it "strips off the type so it isn't set on the destination" do
|
173
|
+
expect(subject.dest_options).to_not have_key :type
|
176
174
|
end
|
177
|
-
it '
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
expect(msg.delivery_info.routing_key).to eq(subject.name)
|
175
|
+
it 'ensures the queue is declared' do
|
176
|
+
expect do
|
177
|
+
connection.with_channel do |ch|
|
178
|
+
ch.queue(dest_name, passive: true)
|
179
|
+
end
|
180
|
+
end.to_not raise_error
|
184
181
|
end
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
182
|
+
context 'publishing a message' do
|
183
|
+
let(:body) { 'Testing the QueueDestination' }
|
184
|
+
let(:headers) { { 'foo' => 'bar' } }
|
185
|
+
let(:properties) { { persistent: false } }
|
186
|
+
before do
|
187
|
+
subject.publish(body, headers, properties)
|
188
|
+
end
|
189
|
+
it 'publishes via the default exchange' do
|
190
|
+
msg = subject.pop_message
|
191
|
+
expect(msg.body).to eq(body)
|
192
|
+
expect(msg.headers).to eq(headers)
|
193
|
+
expect(msg.properties[:delivery_mode]).to eq(1)
|
194
|
+
expect(msg.delivery_info.exchange).to eq('')
|
195
|
+
expect(msg.delivery_info.routing_key).to eq(subject.name)
|
196
|
+
end
|
197
|
+
include_examples 'supports publisher confirmations'
|
198
|
+
end
|
199
|
+
it_behaves_like 'a destination'
|
197
200
|
end
|
201
|
+
context 'and bindings are provided' do
|
202
|
+
let(:dest_name) { 'binding_test_queue' }
|
203
|
+
let(:exchange) { adapter_context.create_destination('amq.direct', type: :exchange) }
|
204
|
+
|
205
|
+
it "raises an exception if you don't provide a source" do
|
206
|
+
expect do
|
207
|
+
adapter_context.create_destination('bad_bind_queue', type: :queue, exclusive: true, bindings: [{ args: { routing_key: 'test_exchange_bind' } }])
|
208
|
+
end.to raise_error MessageDriver::Error, /must provide a source/
|
209
|
+
end
|
198
210
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
211
|
+
it 'routes message to the queue through the exchange' do
|
212
|
+
destination = adapter_context.create_destination(dest_name, type: :queue,
|
213
|
+
exclusive: true,
|
214
|
+
bindings: [{
|
215
|
+
source: 'amq.direct',
|
216
|
+
args: { routing_key: 'test_queue_bind' }
|
217
|
+
}]
|
218
|
+
)
|
219
|
+
exchange.publish('test queue bindings', {}, routing_key: 'test_queue_bind')
|
220
|
+
message = destination.pop_message
|
221
|
+
expect(message).to_not be_nil
|
222
|
+
expect(message.body).to eq('test queue bindings')
|
223
|
+
end
|
205
224
|
end
|
206
|
-
end
|
207
225
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
226
|
+
context 'we are not yet connected to the broker and :no_declare is provided' do
|
227
|
+
it "doesn't cause a connection to the broker" do
|
228
|
+
connection.stop
|
229
|
+
adapter_context.create_destination('test_queue', no_declare: true, type: :queue, exclusive: true)
|
230
|
+
expect(adapter.connection(false)).to_not be_open
|
231
|
+
end
|
214
232
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
233
|
+
context 'with a server-named queue' do
|
234
|
+
it 'raises an error' do
|
235
|
+
expect do
|
236
|
+
adapter_context.create_destination('', no_declare: true, type: :queue, exclusive: true)
|
237
|
+
end.to raise_error MessageDriver::Error, 'server-named queues must be declared, but you provided :no_declare => true'
|
238
|
+
end
|
220
239
|
end
|
221
|
-
end
|
222
240
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
241
|
+
context 'with bindings' do
|
242
|
+
it 'raises an error' do
|
243
|
+
expect do
|
244
|
+
adapter_context.create_destination('tmp_queue', no_declare: true, bindings: [{ source: 'amq.fanout' }], type: :queue, exclusive: true)
|
245
|
+
end.to raise_error MessageDriver::Error, 'queues with bindings must be declared, but you provided :no_declare => true'
|
246
|
+
end
|
228
247
|
end
|
229
248
|
end
|
230
249
|
end
|
231
|
-
end
|
232
250
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
251
|
+
context 'the type is exchange' do
|
252
|
+
context 'the resulting destination' do
|
253
|
+
let(:dest_name) { 'my_dest' }
|
254
|
+
subject(:destination) { adapter_context.create_destination(dest_name, type: :exchange) }
|
237
255
|
|
238
|
-
|
239
|
-
|
256
|
+
it { is_expected.to be_a BunnyAdapter::ExchangeDestination }
|
257
|
+
include_examples "doesn't support #message_count"
|
258
|
+
include_examples "doesn't support #consumer_count"
|
240
259
|
|
241
|
-
|
242
|
-
|
243
|
-
|
260
|
+
it "strips off the type so it isn't set on the destination" do
|
261
|
+
expect(subject.dest_options).to_not have_key :type
|
262
|
+
end
|
244
263
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
264
|
+
it 'raises an error when pop_message is called' do
|
265
|
+
expect do
|
266
|
+
subject.pop_message(dest_name)
|
267
|
+
end.to raise_error MessageDriver::Error, "You can't pop a message off an exchange"
|
268
|
+
end
|
250
269
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
270
|
+
context 'publishing a message' do
|
271
|
+
let(:body) { 'Testing the ExchangeDestination' }
|
272
|
+
let(:headers) { { 'foo' => 'bar' } }
|
273
|
+
let(:properties) { { persistent: false } }
|
274
|
+
before { connection.with_channel { |ch| ch.fanout(dest_name, auto_delete: true) } }
|
275
|
+
let!(:queue) do
|
276
|
+
q = nil
|
277
|
+
connection.with_channel do |ch|
|
278
|
+
q = ch.queue('', exclusive: true)
|
279
|
+
q.bind(dest_name)
|
280
|
+
end
|
281
|
+
q
|
282
|
+
end
|
283
|
+
before do
|
284
|
+
subject.publish(body, headers, properties)
|
261
285
|
end
|
262
|
-
q
|
263
|
-
end
|
264
|
-
before do
|
265
|
-
subject.publish(body, headers, properties)
|
266
|
-
end
|
267
286
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
287
|
+
it 'publishes to the specified exchange' do
|
288
|
+
connection.with_channel do |ch|
|
289
|
+
q = ch.queue(queue.name, passive: true)
|
290
|
+
msg = q.pop
|
291
|
+
expect(msg[2]).to eq(body)
|
292
|
+
expect(msg[0].exchange).to eq(dest_name)
|
293
|
+
expect(msg[1][:headers]).to eq(headers)
|
294
|
+
expect(msg[1][:delivery_mode]).to eq(1)
|
295
|
+
end
|
276
296
|
end
|
297
|
+
include_examples 'supports publisher confirmations'
|
277
298
|
end
|
278
|
-
include_examples 'supports publisher confirmations'
|
279
299
|
end
|
280
|
-
end
|
281
300
|
|
282
|
-
|
283
|
-
|
301
|
+
context 'declaring an exchange on the broker' do
|
302
|
+
let(:dest_name) { 'my.cool.exchange' }
|
284
303
|
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
304
|
+
it "creates the exchange if you include 'declare' in the options" do
|
305
|
+
exchange = adapter_context.create_destination(dest_name, type: :exchange, declare: { type: :fanout, auto_delete: true })
|
306
|
+
queue = adapter_context.create_destination('', type: :queue, exclusive: true, bindings: [{ source: dest_name }])
|
307
|
+
exchange.publish('test declaring exchange')
|
308
|
+
message = queue.pop_message
|
309
|
+
expect(message).to_not be_nil
|
310
|
+
expect(message.body).to eq('test declaring exchange')
|
311
|
+
end
|
312
|
+
|
313
|
+
it "raises an error if you don't provide a type" do
|
314
|
+
expect do
|
315
|
+
adapter_context.create_destination(dest_name, type: :exchange, declare: { auto_delete: true })
|
316
|
+
end.to raise_error MessageDriver::Error, /you must provide a valid exchange type/
|
317
|
+
end
|
293
318
|
|
294
|
-
it "raises an error if you don't provide a type" do
|
295
|
-
expect {
|
296
|
-
adapter_context.create_destination(dest_name, type: :exchange, declare: {auto_delete: true})
|
297
|
-
}.to raise_error MessageDriver::Error, /you must provide a valid exchange type/
|
298
319
|
end
|
299
320
|
|
300
|
-
|
321
|
+
context 'and bindings are provided' do
|
322
|
+
let(:dest_name) { 'binding_exchange_queue' }
|
323
|
+
let(:exchange) { adapter_context.create_destination('amq.direct', type: :exchange) }
|
301
324
|
|
302
|
-
|
303
|
-
|
304
|
-
|
325
|
+
it "raises an exception if you don't provide a source" do
|
326
|
+
expect do
|
327
|
+
adapter_context.create_destination('amq.fanout', type: :exchange, bindings: [{ args: { routing_key: 'test_exchange_bind' } }])
|
328
|
+
end.to raise_error MessageDriver::Error, /must provide a source/
|
329
|
+
end
|
305
330
|
|
306
|
-
|
307
|
-
|
308
|
-
adapter_context.create_destination(
|
309
|
-
|
331
|
+
it 'routes message to the queue through the exchange' do
|
332
|
+
adapter_context.create_destination('amq.fanout', type: :exchange, bindings: [{ source: 'amq.direct', args: { routing_key: 'test_exchange_bind' } }])
|
333
|
+
destination = adapter_context.create_destination(dest_name, type: :queue, exclusive: true, bindings: [{ source: 'amq.fanout' }])
|
334
|
+
exchange.publish('test exchange bindings', {}, routing_key: 'test_exchange_bind')
|
335
|
+
message = destination.pop_message
|
336
|
+
expect(message).to_not be_nil
|
337
|
+
expect(message.body).to eq('test exchange bindings')
|
338
|
+
end
|
310
339
|
end
|
311
340
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
expect(message.body).to eq('test exchange bindings')
|
341
|
+
context 'we are not yet connected to the broker' do
|
342
|
+
it "doesn't cause a connection to the broker" do
|
343
|
+
connection.stop
|
344
|
+
adapter_context.create_destination('amq.fanout', type: :exchange)
|
345
|
+
expect(adapter.connection(false)).to_not be_open
|
346
|
+
end
|
319
347
|
end
|
320
348
|
end
|
321
349
|
|
322
|
-
context '
|
323
|
-
it
|
324
|
-
|
325
|
-
|
326
|
-
|
350
|
+
context 'the type is invalid' do
|
351
|
+
it 'raises in an error' do
|
352
|
+
expect do
|
353
|
+
adapter_context.create_destination('my_dest', type: :foo_bar)
|
354
|
+
end.to raise_error MessageDriver::Error, "invalid destination type #{:foo_bar}"
|
327
355
|
end
|
328
356
|
end
|
329
357
|
end
|
330
358
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
end
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
describe '#subscribe' do
|
341
|
-
context 'the destination is an ExchangeDestination' do
|
342
|
-
let(:dest_name) { 'my_dest' }
|
343
|
-
let(:destination) { adapter_context.create_destination(dest_name, type: :exchange) }
|
344
|
-
let(:consumer) { lambda do |_|; end }
|
359
|
+
describe '#subscribe' do
|
360
|
+
context 'the destination is an ExchangeDestination' do
|
361
|
+
let(:dest_name) { 'my_dest' }
|
362
|
+
let(:destination) { adapter_context.create_destination(dest_name, type: :exchange) }
|
363
|
+
let(:consumer) { ->(_) {} }
|
345
364
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
365
|
+
it 'raises an error' do
|
366
|
+
expect do
|
367
|
+
adapter_context.subscribe(destination, &consumer)
|
368
|
+
end.to raise_error MessageDriver::Error, /QueueDestination/
|
369
|
+
end
|
350
370
|
end
|
351
371
|
end
|
352
372
|
end
|