bunny 2.4.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +1 -1
- data/ChangeLog.md +6 -1
- data/lib/bunny/channel.rb +2 -0
- data/lib/bunny/exchange.rb +11 -2
- data/lib/bunny/queue.rb +1 -1
- data/lib/bunny/session.rb +19 -20
- data/lib/bunny/version.rb +1 -1
- data/spec/higher_level_api/integration/connection_recovery_spec.rb +352 -334
- data/spec/higher_level_api/integration/publisher_confirms_spec.rb +12 -0
- data/spec/unit/exchange_recovery_spec.rb +39 -0
- metadata +5 -5
- data/spec/higher_level_api/integration/confirm_select_spec.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f628f0ace95911e5d5ce48817389f79a3a68415
|
4
|
+
data.tar.gz: a4f867d1fbe68457f311a0b3691a644c882cc1e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 792e2d8e367bca866fa48af83d640b75feb401da8a10711852df09d436ca614cdf0a7c52a72fe2a890cb627991639b771cd0ed6fa3b912bc235206c00da219ec
|
7
|
+
data.tar.gz: 8a3fa96ab3e6103513f1df439c37f8d4ce6688c9dba2c40e7fadbec265bf1cf5e2abd438b16f7e954ff6c1479130ecc0253023c8e6d50248a199ce2a13ab8098
|
data/CONTRIBUTING.md
CHANGED
@@ -55,7 +55,7 @@ it by pressing CTRL+C.
|
|
55
55
|
### Running Test Suites
|
56
56
|
|
57
57
|
Prior to running the tests, configure the RabbitMQ permissions
|
58
|
-
by running `./bin/ci/
|
58
|
+
by running `./bin/ci/before_build`. The script uses `rabbitmqctl` and `rabbitmq-plugins`
|
59
59
|
to set up RabbitMQ in a way that Bunny test suites expect. Two environment variables,
|
60
60
|
`RABBITMQCTL` and `RABBITMQ_PLUGINS`, are available to control what `rabbitmqctl` and
|
61
61
|
`rabbitmq-plugins` commands will be used. By default they are taken from `PATH`
|
data/ChangeLog.md
CHANGED
data/lib/bunny/channel.rb
CHANGED
data/lib/bunny/exchange.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'amq/protocol'
|
2
|
+
|
1
3
|
module Bunny
|
2
4
|
# Represents AMQP 0.9.1 exchanges.
|
3
5
|
#
|
@@ -80,6 +82,8 @@ module Bunny
|
|
80
82
|
@internal = @options[:internal]
|
81
83
|
@arguments = @options[:arguments]
|
82
84
|
|
85
|
+
@bindings = Set.new
|
86
|
+
|
83
87
|
declare! unless opts[:no_declare] || predeclared? || (@name == AMQ::Protocol::EMPTY_STRING)
|
84
88
|
|
85
89
|
@channel.register_exchange(self)
|
@@ -169,6 +173,7 @@ module Bunny
|
|
169
173
|
# @api public
|
170
174
|
def bind(source, opts = {})
|
171
175
|
@channel.exchange_bind(source, self, opts)
|
176
|
+
@bindings.add(source: source, opts: opts)
|
172
177
|
|
173
178
|
self
|
174
179
|
end
|
@@ -189,6 +194,7 @@ module Bunny
|
|
189
194
|
# @api public
|
190
195
|
def unbind(source, opts = {})
|
191
196
|
@channel.exchange_unbind(source, self, opts)
|
197
|
+
@bindings.delete(source: source, opts: opts)
|
192
198
|
|
193
199
|
self
|
194
200
|
end
|
@@ -214,8 +220,11 @@ module Bunny
|
|
214
220
|
|
215
221
|
# @private
|
216
222
|
def recover_from_network_failure
|
217
|
-
|
218
|
-
|
223
|
+
declare! unless @options[:no_declare] ||predefined?
|
224
|
+
|
225
|
+
@bindings.each do |b|
|
226
|
+
bind(b[:source], b[:opts])
|
227
|
+
end
|
219
228
|
end
|
220
229
|
|
221
230
|
|
data/lib/bunny/queue.rb
CHANGED
data/lib/bunny/session.rb
CHANGED
@@ -108,6 +108,7 @@ module Bunny
|
|
108
108
|
# @option connection_string_or_opts [String] :tls_key (nil) Path to client TLS/SSL private key file (.pem)
|
109
109
|
# @option connection_string_or_opts [Array<String>] :tls_ca_certificates Array of paths to TLS/SSL CA files (.pem), by default detected from OpenSSL configuration
|
110
110
|
# @option connection_string_or_opts [String] :verify_peer (true) Whether TLS peer verification should be performed
|
111
|
+
# @option connection_string_or_opts [Keyword] :tls_version (negotiated) What TLS version should be used (:TLSv1, :TLSv1_1, or :TLSv1_2)
|
111
112
|
# @option connection_string_or_opts [Integer] :continuation_timeout (15000) Timeout for client operations that expect a response (e.g. {Bunny::Queue#get}), in milliseconds.
|
112
113
|
# @option connection_string_or_opts [Integer] :connection_timeout (5) Timeout in seconds for connecting to the server.
|
113
114
|
# @option connection_string_or_opts [Proc] :hosts_shuffle_strategy A Proc that reorders a list of host strings, defaults to Array#shuffle
|
@@ -668,30 +669,28 @@ module Bunny
|
|
668
669
|
|
669
670
|
# @private
|
670
671
|
def recover_from_network_failure
|
671
|
-
|
672
|
-
|
673
|
-
@logger.debug "About to start connection recovery..."
|
672
|
+
sleep @network_recovery_interval
|
673
|
+
@logger.debug "About to start connection recovery..."
|
674
674
|
|
675
|
-
|
675
|
+
self.initialize_transport
|
676
676
|
|
677
|
-
|
678
|
-
|
677
|
+
@logger.warn "Retrying connection on next host in line: #{@transport.host}:#{@transport.port}"
|
678
|
+
self.start
|
679
679
|
|
680
|
-
|
681
|
-
|
680
|
+
if open?
|
681
|
+
@recovering_from_network_failure = false
|
682
682
|
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
end
|
683
|
+
recover_channels
|
684
|
+
end
|
685
|
+
rescue HostListDepleted
|
686
|
+
reset_address_index
|
687
|
+
retry
|
688
|
+
rescue TCPConnectionFailedForAllHosts, TCPConnectionFailed, AMQ::Protocol::EmptyResponseError => e
|
689
|
+
@logger.warn "TCP connection failed, reconnecting in #{@network_recovery_interval} seconds"
|
690
|
+
sleep @network_recovery_interval
|
691
|
+
if should_retry_recovery?
|
692
|
+
@recovery_attempts -= 1 if @recovery_attempts
|
693
|
+
retry if recoverable_network_failure?(e)
|
695
694
|
end
|
696
695
|
end
|
697
696
|
|
data/lib/bunny/version.rb
CHANGED
@@ -1,403 +1,421 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
require "rabbitmq/http/client"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
rescue Bunny::ConnectionForced
|
14
|
-
# This is not a problem, but the specs intermittently believe it is.
|
15
|
-
end
|
16
|
-
end
|
4
|
+
describe "Connection recovery" do
|
5
|
+
let(:http_client) { RabbitMQ::HTTP::Client.new("http://127.0.0.1:15672") }
|
6
|
+
let(:logger) { Logger.new($stderr).tap {|logger| logger.level = Logger::FATAL} }
|
7
|
+
let(:recovery_interval) { 0.2 }
|
8
|
+
|
9
|
+
it "reconnects after grace period" do
|
10
|
+
with_open do |c|
|
11
|
+
close_all_connections!
|
12
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
17
13
|
end
|
14
|
+
end
|
18
15
|
|
19
|
-
|
20
|
-
|
16
|
+
it "reconnects after grace period (with multiple hosts)" do
|
17
|
+
with_open_multi_host do |c|
|
18
|
+
close_all_connections!
|
19
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
21
20
|
end
|
21
|
+
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
ensure
|
28
|
-
c.close
|
29
|
-
end
|
23
|
+
it "reconnects after grace period (with multiple hosts, including a broken one)" do
|
24
|
+
with_open_multi_broken_host do |c|
|
25
|
+
close_all_connections!
|
26
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
30
27
|
end
|
28
|
+
end
|
31
29
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
30
|
+
it "recovers channels" do
|
31
|
+
with_open do |c|
|
32
|
+
ch1 = c.create_channel
|
33
|
+
ch2 = c.create_channel
|
34
|
+
close_all_connections!
|
35
|
+
poll_until { channels.count.zero? }
|
36
|
+
poll_until { channels.count == 2 }
|
37
|
+
expect(ch1).to be_open
|
38
|
+
expect(ch2).to be_open
|
41
39
|
end
|
40
|
+
end
|
42
41
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
42
|
+
it "recovers channels (with multiple hosts)" do
|
43
|
+
with_open_multi_host do |c|
|
44
|
+
ch1 = c.create_channel
|
45
|
+
ch2 = c.create_channel
|
46
|
+
close_all_connections!
|
47
|
+
poll_until { channels.count.zero? }
|
48
|
+
poll_until { channels.count == 2 }
|
49
|
+
expect(ch1).to be_open
|
50
|
+
expect(ch2).to be_open
|
53
51
|
end
|
52
|
+
end
|
54
53
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
54
|
+
it "recovers channels (with multiple hosts, including a broken one)" do
|
55
|
+
with_open_multi_broken_host do |c|
|
56
|
+
ch1 = c.create_channel
|
57
|
+
ch2 = c.create_channel
|
58
|
+
close_all_connections!
|
59
|
+
poll_until { channels.count.zero? }
|
60
|
+
poll_until { channels.count == 2 }
|
61
|
+
expect(ch1).to be_open
|
62
|
+
expect(ch2).to be_open
|
63
63
|
end
|
64
|
+
end
|
64
65
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
ch.
|
71
|
-
|
72
|
-
|
73
|
-
|
66
|
+
it "recovers basic.qos prefetch setting" do
|
67
|
+
with_open do |c|
|
68
|
+
ch = c.create_channel
|
69
|
+
ch.prefetch(11)
|
70
|
+
expect(ch.prefetch_count).to eq 11
|
71
|
+
expect(ch.prefetch_global).to be false
|
72
|
+
close_all_connections!
|
73
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
74
|
+
expect(ch).to be_open
|
75
|
+
expect(ch.prefetch_count).to eq 11
|
76
|
+
expect(ch.prefetch_global).to be false
|
74
77
|
end
|
78
|
+
end
|
75
79
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
ch.
|
81
|
-
|
82
|
-
|
83
|
-
|
80
|
+
it "recovers basic.qos prefetch global setting" do
|
81
|
+
with_open do |c|
|
82
|
+
ch = c.create_channel
|
83
|
+
ch.prefetch(42, true)
|
84
|
+
expect(ch.prefetch_count).to eq 42
|
85
|
+
expect(ch.prefetch_global).to be true
|
86
|
+
close_all_connections!
|
87
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
88
|
+
expect(ch).to be_open
|
89
|
+
expect(ch.prefetch_count).to eq 42
|
90
|
+
expect(ch.prefetch_global).to be true
|
84
91
|
end
|
92
|
+
end
|
85
93
|
|
86
|
-
|
94
|
+
it "recovers publisher confirms setting" do
|
95
|
+
with_open do |c|
|
96
|
+
ch = c.create_channel
|
87
97
|
ch.confirm_select
|
88
|
-
|
89
|
-
|
98
|
+
expect(ch).to be_using_publisher_confirms
|
99
|
+
close_all_connections!
|
100
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
101
|
+
expect(ch).to be_open
|
102
|
+
expect(ch).to be_using_publisher_confirms
|
103
|
+
end
|
104
|
+
end
|
90
105
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
106
|
+
it "recovers transactionality setting" do
|
107
|
+
with_open do |c|
|
108
|
+
ch = c.create_channel
|
109
|
+
ch.tx_select
|
110
|
+
expect(ch).to be_using_tx
|
111
|
+
close_all_connections!
|
112
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
113
|
+
expect(ch).to be_open
|
114
|
+
expect(ch).to be_using_tx
|
96
115
|
end
|
116
|
+
end
|
97
117
|
|
98
|
-
|
99
|
-
|
100
|
-
|
118
|
+
it "recovers client-named queues" do
|
119
|
+
with_open do |c|
|
120
|
+
ch = c.create_channel
|
121
|
+
q = ch.queue("bunny.tests.recovery.client-named#{rand}")
|
122
|
+
close_all_connections!
|
123
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
124
|
+
expect(ch).to be_open
|
125
|
+
ensure_queue_recovery(ch, q)
|
126
|
+
q.delete
|
127
|
+
end
|
128
|
+
end
|
101
129
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
130
|
+
# a very simplistic test for queues inspired by #412
|
131
|
+
it "recovers client-named queues declared with passive = true" do
|
132
|
+
with_open do |c|
|
133
|
+
ch = c.create_channel
|
134
|
+
ch2 = c.create_channel
|
107
135
|
|
108
|
-
|
109
|
-
|
110
|
-
end
|
111
|
-
end
|
136
|
+
n = rand
|
137
|
+
s = "bunny.tests.recovery.client-named#{n}"
|
112
138
|
|
113
|
-
|
114
|
-
|
115
|
-
close_all_connections!
|
116
|
-
sleep 0.1
|
117
|
-
expect(c).not_to be_open
|
139
|
+
q = ch.queue(s)
|
140
|
+
q2 = ch2.queue(s, no_declare: true)
|
118
141
|
|
119
|
-
|
120
|
-
|
121
|
-
|
142
|
+
close_all_connections!
|
143
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
144
|
+
expect(ch).to be_open
|
145
|
+
ensure_queue_recovery(ch, q)
|
146
|
+
q.delete
|
122
147
|
end
|
148
|
+
end
|
123
149
|
|
124
|
-
it "reconnects after grace period (with multiple hosts, including a broken one)" do
|
125
|
-
with_open_multi_broken_host do |c|
|
126
|
-
close_all_connections!
|
127
|
-
sleep 0.1
|
128
|
-
expect(c).not_to be_open
|
129
150
|
|
130
|
-
|
131
|
-
|
132
|
-
|
151
|
+
it "recovers server-named queues" do
|
152
|
+
with_open do |c|
|
153
|
+
ch = c.create_channel
|
154
|
+
q = ch.queue("", :exclusive => true)
|
155
|
+
close_all_connections!
|
156
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
157
|
+
expect(ch).to be_open
|
158
|
+
ensure_queue_recovery(ch, q)
|
133
159
|
end
|
160
|
+
end
|
134
161
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
expect(ch2).to be_open
|
146
|
-
end
|
162
|
+
it "recovers queue bindings" do
|
163
|
+
with_open do |c|
|
164
|
+
ch = c.create_channel
|
165
|
+
x = ch.fanout("amq.fanout")
|
166
|
+
q = ch.queue("", :exclusive => true)
|
167
|
+
q.bind(x)
|
168
|
+
close_all_connections!
|
169
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
170
|
+
expect(ch).to be_open
|
171
|
+
ensure_queue_binding_recovery(ch, x, q)
|
147
172
|
end
|
173
|
+
end
|
148
174
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
sleep 0.1
|
155
|
-
expect(c).not_to be_open
|
156
|
-
|
157
|
-
wait_for_recovery
|
158
|
-
expect(ch1).to be_open
|
159
|
-
expect(ch2).to be_open
|
160
|
-
end
|
161
|
-
end
|
175
|
+
it "recovers exchanges and their bindings" do
|
176
|
+
with_open do |c|
|
177
|
+
ch = c.create_channel
|
178
|
+
source = ch.fanout("source.exchange.recovery.example", auto_delete: true)
|
179
|
+
destination = ch.fanout("destination.exchange.recovery.example", auto_delete: true)
|
162
180
|
|
163
|
-
|
164
|
-
with_open_multi_broken_host do |c|
|
165
|
-
ch1 = c.create_channel
|
166
|
-
ch2 = c.create_channel
|
167
|
-
close_all_connections!
|
168
|
-
sleep 0.1
|
169
|
-
expect(c).not_to be_open
|
170
|
-
|
171
|
-
wait_for_recovery
|
172
|
-
expect(ch1).to be_open
|
173
|
-
expect(ch2).to be_open
|
174
|
-
end
|
175
|
-
end
|
181
|
+
destination.bind(source)
|
176
182
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
expect(ch.prefetch_count).to eq 11
|
182
|
-
expect(ch.prefetch_global).to be false
|
183
|
-
close_all_connections!
|
184
|
-
sleep 0.1
|
185
|
-
expect(c).not_to be_open
|
186
|
-
|
187
|
-
wait_for_recovery
|
188
|
-
expect(ch).to be_open
|
189
|
-
expect(ch.prefetch_count).to eq 11
|
190
|
-
expect(ch.prefetch_global).to be false
|
191
|
-
end
|
192
|
-
end
|
183
|
+
# Exchanges won't get auto-deleted on connection loss unless they have
|
184
|
+
# had an exclusive queue bound to them.
|
185
|
+
dst_queue = ch.queue("", exclusive: true)
|
186
|
+
dst_queue.bind(destination, routing_key: "")
|
193
187
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
expect(ch.prefetch_count).to eq 42
|
207
|
-
expect(ch.prefetch_global).to be true
|
208
|
-
end
|
188
|
+
src_queue = ch.queue("", exclusive: true)
|
189
|
+
src_queue.bind(source, routing_key: "")
|
190
|
+
|
191
|
+
close_all_connections!
|
192
|
+
|
193
|
+
wait_on_loss_and_recovery_of { exchange_names_in_vhost("/").include?(source.name) }
|
194
|
+
|
195
|
+
ch.confirm_select
|
196
|
+
|
197
|
+
source.publish("msg", routing_key: "")
|
198
|
+
ch.wait_for_confirms
|
199
|
+
expect(dst_queue.message_count).to eq 1
|
209
200
|
end
|
201
|
+
end
|
210
202
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
203
|
+
# this is a simplistic test that primarily execises the code path from #412
|
204
|
+
it "recovers exchanges that were declared with passive = true" do
|
205
|
+
with_open do |c|
|
206
|
+
ch = c.create_channel
|
207
|
+
ch2 = c.create_channel
|
208
|
+
source = ch.fanout("source.exchange.recovery.example", auto_delete: true)
|
209
|
+
destination = ch.fanout("destination.exchange.recovery.example", auto_delete: true)
|
210
|
+
|
211
|
+
source2 = ch2.fanout("source.exchange.recovery.example", no_declare: true)
|
212
|
+
destination2 = ch2.fanout("destination.exchange.recovery.example", no_declare: true)
|
213
|
+
|
214
|
+
destination.bind(source)
|
215
|
+
|
216
|
+
# Exchanges won't get auto-deleted on connection loss unless they have
|
217
|
+
# had an exclusive queue bound to them.
|
218
|
+
dst_queue = ch.queue("", exclusive: true)
|
219
|
+
dst_queue.bind(destination, routing_key: "")
|
220
|
+
|
221
|
+
src_queue = ch.queue("", exclusive: true)
|
222
|
+
src_queue.bind(source, routing_key: "")
|
223
|
+
|
224
|
+
close_all_connections!
|
225
|
+
|
226
|
+
wait_on_loss_and_recovery_of { exchange_names_in_vhost("/").include?(source.name) }
|
227
|
+
|
228
|
+
ch2.confirm_select
|
229
|
+
|
230
|
+
source2.publish("msg", routing_key: "")
|
231
|
+
ch2.wait_for_confirms
|
232
|
+
expect(dst_queue.message_count).to eq 1
|
224
233
|
end
|
234
|
+
end
|
225
235
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
236
|
+
it "recovers allocated channel ids" do
|
237
|
+
with_open do |c|
|
238
|
+
q = "queue#{Time.now.to_i}"
|
239
|
+
10.times { c.create_channel }
|
240
|
+
expect(c.queue_exists?(q)).to eq false
|
241
|
+
close_all_connections!
|
242
|
+
wait_on_loss_and_recovery_of { channels.any? }
|
243
|
+
# make sure the connection isn't closed shortly after
|
244
|
+
# due to "second 'channel.open' seen". MK.
|
245
|
+
expect(c).to be_open
|
246
|
+
sleep 0.1
|
247
|
+
expect(c).to be_open
|
248
|
+
sleep 0.1
|
249
|
+
expect(c).to be_open
|
239
250
|
end
|
251
|
+
end
|
252
|
+
|
253
|
+
it "recovers consumers" do
|
254
|
+
with_open do |c|
|
255
|
+
delivered = false
|
240
256
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
close_all_connections!
|
246
|
-
sleep 0.1
|
247
|
-
expect(c).not_to be_open
|
248
|
-
|
249
|
-
wait_for_recovery
|
250
|
-
expect(ch).to be_open
|
251
|
-
ensure_queue_recovery(ch, q)
|
252
|
-
q.delete
|
257
|
+
ch = c.create_channel
|
258
|
+
q = ch.queue("", :exclusive => true)
|
259
|
+
q.subscribe do |_, _, _|
|
260
|
+
delivered = true
|
253
261
|
end
|
262
|
+
close_all_connections!
|
263
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
264
|
+
expect(ch).to be_open
|
265
|
+
|
266
|
+
q.publish("")
|
267
|
+
|
268
|
+
poll_until { delivered }
|
254
269
|
end
|
270
|
+
end
|
255
271
|
|
272
|
+
it "recovers all consumers" do
|
273
|
+
n = 1024
|
256
274
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
275
|
+
with_open do |c|
|
276
|
+
ch = c.create_channel
|
277
|
+
q = ch.queue("", :exclusive => true)
|
278
|
+
n.times { q.subscribe { |_, _, _| } }
|
279
|
+
close_all_connections!
|
280
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
281
|
+
expect(ch).to be_open
|
282
|
+
sleep 0.5
|
264
283
|
|
265
|
-
|
266
|
-
expect(ch).to be_open
|
267
|
-
ensure_queue_recovery(ch, q)
|
268
|
-
end
|
284
|
+
expect(q.consumer_count).to eq n
|
269
285
|
end
|
286
|
+
end
|
270
287
|
|
271
|
-
|
272
|
-
|
273
|
-
ch = c.create_channel
|
274
|
-
x = ch.fanout("amq.fanout")
|
275
|
-
q = ch.queue("", :exclusive => true)
|
276
|
-
q.bind(x)
|
277
|
-
close_all_connections!
|
278
|
-
sleep 0.1
|
279
|
-
expect(c).not_to be_open
|
280
|
-
|
281
|
-
wait_for_recovery
|
282
|
-
expect(ch).to be_open
|
283
|
-
ensure_queue_binding_recovery(ch, x, q)
|
284
|
-
end
|
285
|
-
end
|
288
|
+
it "recovers all queues" do
|
289
|
+
n = 256
|
286
290
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
sleep 0.1
|
295
|
-
expect(c).not_to be_open
|
296
|
-
|
297
|
-
wait_for_recovery
|
298
|
-
expect(ch).to be_open
|
299
|
-
ensure_exchange_binding_recovery(ch, x, x2)
|
291
|
+
qs = []
|
292
|
+
|
293
|
+
with_open do |c|
|
294
|
+
ch = c.create_channel
|
295
|
+
|
296
|
+
n.times do
|
297
|
+
qs << ch.queue("", :exclusive => true)
|
300
298
|
end
|
301
|
-
|
299
|
+
close_all_connections!
|
300
|
+
wait_on_loss_and_recovery_of { queue_names.include?(qs.first.name) }
|
301
|
+
sleep 0.5
|
302
|
+
expect(ch).to be_open
|
302
303
|
|
303
|
-
|
304
|
-
|
305
|
-
q = "queue#{Time.now.to_i}"
|
306
|
-
10.times { c.create_channel }
|
307
|
-
expect(c.queue_exists?(q)).to eq false
|
308
|
-
close_all_connections!
|
309
|
-
sleep 0.1
|
310
|
-
expect(c).not_to be_open
|
311
|
-
|
312
|
-
wait_for_recovery
|
313
|
-
expect(c.queue_exists?(q)).to eq false
|
314
|
-
# make sure the connection isn't closed shortly after
|
315
|
-
# due to "second 'channel.open' seen". MK.
|
316
|
-
expect(c).to be_open
|
317
|
-
sleep 0.1
|
318
|
-
expect(c).to be_open
|
319
|
-
sleep 0.1
|
320
|
-
expect(c).to be_open
|
304
|
+
qs.each do |q|
|
305
|
+
ch.queue_declare(q.name, :passive => true)
|
321
306
|
end
|
322
307
|
end
|
308
|
+
end
|
323
309
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
wait_for_recovery
|
338
|
-
expect(ch).to be_open
|
339
|
-
|
340
|
-
q.publish("")
|
341
|
-
sleep 0.5
|
342
|
-
expect(delivered).to eq true
|
343
|
-
end
|
310
|
+
it "tries to recover for a given number of attempts" do
|
311
|
+
pending "Need a fix for https://github.com/ruby-amqp/bunny/issues/408"
|
312
|
+
with_recovery_attempts_limited_to(2) do |c|
|
313
|
+
close_all_connections!
|
314
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
315
|
+
|
316
|
+
close_all_connections!
|
317
|
+
wait_on_loss_and_recovery_of { connections.any? }
|
318
|
+
|
319
|
+
close_all_connections!
|
320
|
+
sleep(recovery_interval + 0.5)
|
321
|
+
expect(connections).to be_empty
|
344
322
|
end
|
323
|
+
end
|
345
324
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
expect(q.consumer_count).to eq n
|
366
|
-
end
|
325
|
+
def exchange_names_in_vhost(vhost)
|
326
|
+
http_client.list_exchanges(vhost).map {|e| e["name"]}
|
327
|
+
end
|
328
|
+
|
329
|
+
def connections
|
330
|
+
http_client.list_connections
|
331
|
+
end
|
332
|
+
|
333
|
+
def channels
|
334
|
+
http_client.list_channels
|
335
|
+
end
|
336
|
+
|
337
|
+
def queue_names
|
338
|
+
http_client.list_queues.map {|q| q["name"]}
|
339
|
+
end
|
340
|
+
|
341
|
+
def close_all_connections!
|
342
|
+
connections.each do |conn_info|
|
343
|
+
close_ignoring_permitted_exceptions(conn_info.name)
|
367
344
|
end
|
345
|
+
end
|
368
346
|
|
369
|
-
|
370
|
-
|
347
|
+
def close_ignoring_permitted_exceptions(connection_name)
|
348
|
+
http_client.close_connection(connection_name)
|
349
|
+
rescue Bunny::ConnectionForced
|
350
|
+
end
|
371
351
|
|
372
|
-
|
352
|
+
def wait_on_loss_and_recovery_of(&probe)
|
353
|
+
poll_while &probe
|
354
|
+
poll_until &probe
|
355
|
+
end
|
373
356
|
|
374
|
-
|
375
|
-
|
357
|
+
def poll_while(&probe)
|
358
|
+
Timeout::timeout(10) {
|
359
|
+
sleep 0.1 while probe[]
|
360
|
+
}
|
361
|
+
end
|
376
362
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
expect(c).not_to be_open
|
363
|
+
def poll_until(&probe)
|
364
|
+
Timeout::timeout(10) {
|
365
|
+
sleep 0.1 until probe[]
|
366
|
+
}
|
367
|
+
end
|
383
368
|
|
384
|
-
|
385
|
-
|
386
|
-
|
369
|
+
def with_open(c = Bunny.new(network_recovery_interval: recovery_interval,
|
370
|
+
recover_from_connection_close: true,
|
371
|
+
logger: logger), &block)
|
372
|
+
c.start
|
373
|
+
block.call(c)
|
374
|
+
ensure
|
375
|
+
c.close
|
376
|
+
end
|
387
377
|
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
378
|
+
def with_open_multi_host(&block)
|
379
|
+
c = Bunny.new(hosts: ["127.0.0.1", "localhost"],
|
380
|
+
network_recovery_interval: recovery_interval,
|
381
|
+
recover_from_connection_close: true,
|
382
|
+
logger: logger)
|
383
|
+
with_open(c, &block)
|
384
|
+
end
|
393
385
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
386
|
+
def with_open_multi_broken_host(&block)
|
387
|
+
c = Bunny.new(hosts: ["broken", "127.0.0.1", "localhost"],
|
388
|
+
hosts_shuffle_strategy: Proc.new { |hosts| hosts }, # We do not shuffle for these tests so we always hit the broken host
|
389
|
+
network_recovery_interval: recovery_interval,
|
390
|
+
recover_from_connection_close: true,
|
391
|
+
logger: logger)
|
392
|
+
with_open(c, &block)
|
393
|
+
end
|
398
394
|
|
399
|
-
|
400
|
-
|
401
|
-
|
395
|
+
def with_recovery_attempts_limited_to(attempts = 3, &block)
|
396
|
+
c = Bunny.new(recover_from_connection_close: true,
|
397
|
+
network_recovery_interval: recovery_interval,
|
398
|
+
recovery_attempts: attempts,
|
399
|
+
logger: logger)
|
400
|
+
with_open(c, &block)
|
401
|
+
end
|
402
|
+
|
403
|
+
def ensure_queue_recovery(ch, q)
|
404
|
+
ch.confirm_select
|
405
|
+
q.purge
|
406
|
+
x = ch.default_exchange
|
407
|
+
x.publish("msg", routing_key: q.name)
|
408
|
+
ch.wait_for_confirms
|
409
|
+
expect(q.message_count).to eq 1
|
410
|
+
q.purge
|
411
|
+
end
|
412
|
+
|
413
|
+
def ensure_queue_binding_recovery(ch, x, q, routing_key = "")
|
414
|
+
ch.confirm_select
|
415
|
+
q.purge
|
416
|
+
x.publish("msg", routing_key: routing_key)
|
417
|
+
ch.wait_for_confirms
|
418
|
+
expect(q.message_count).to eq 1
|
419
|
+
q.purge
|
402
420
|
end
|
403
421
|
end
|
@@ -60,6 +60,18 @@ describe Bunny::Channel do
|
|
60
60
|
}.not_to raise_error
|
61
61
|
|
62
62
|
end
|
63
|
+
|
64
|
+
it "raises an error when called on a closed channel" do
|
65
|
+
ch = connection.create_channel
|
66
|
+
|
67
|
+
ch.confirm_select
|
68
|
+
|
69
|
+
ch.close
|
70
|
+
|
71
|
+
expect {
|
72
|
+
ch.wait_for_confirms
|
73
|
+
}.to raise_error(Bunny::ChannelAlreadyClosed)
|
74
|
+
end
|
63
75
|
end
|
64
76
|
|
65
77
|
context "when some of the messages get nacked" do
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative '../../lib/bunny/channel'
|
2
|
+
require_relative '../../lib/bunny/exchange'
|
3
|
+
|
4
|
+
module Bunny
|
5
|
+
describe Exchange do
|
6
|
+
context "recovery" do
|
7
|
+
it "recovers exchange bindings, unless already unbound" do
|
8
|
+
ch = instance_double(Bunny::Channel,
|
9
|
+
exchange_declare: nil,
|
10
|
+
register_exchange: nil)
|
11
|
+
src1 = Exchange.new(ch, "direct", "src1")
|
12
|
+
src2 = Exchange.new(ch, "direct", "src2")
|
13
|
+
src3 = Exchange.new(ch, "direct", "src3")
|
14
|
+
dst = Exchange.new(ch, "direct", "dst")
|
15
|
+
|
16
|
+
original_binds_count = 5
|
17
|
+
expected_rebinds_count = 3
|
18
|
+
expected_total_binds = original_binds_count + expected_rebinds_count
|
19
|
+
allow(ch).to receive(:exchange_bind).exactly(expected_total_binds).times
|
20
|
+
|
21
|
+
dst.bind(src1, routing_key: "abc")
|
22
|
+
dst.bind(src2, routing_key: "def")
|
23
|
+
dst.bind(src2, routing_key: "ghi")
|
24
|
+
dst.bind(src3, routing_key: "jkl")
|
25
|
+
dst.bind(src3, routing_key: "jkl", arguments: {"key" => "value"})
|
26
|
+
|
27
|
+
allow(ch).to receive(:exchange_unbind).twice
|
28
|
+
dst.unbind(src2, routing_key: "def")
|
29
|
+
dst.unbind(src3, routing_key: "jkl", arguments: {"key" => "value"})
|
30
|
+
|
31
|
+
expect(ch).to receive(:exchange_bind).with(src1, dst, routing_key: "abc")
|
32
|
+
expect(ch).to receive(:exchange_bind).with(src2, dst, routing_key: "ghi")
|
33
|
+
expect(ch).to receive(:exchange_bind).with(src3, dst, routing_key: "jkl")
|
34
|
+
|
35
|
+
dst.recover_from_network_failure
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bunny
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Duncan
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date: 2016-
|
15
|
+
date: 2016-07-20 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: amq-protocol
|
@@ -150,7 +150,6 @@ files:
|
|
150
150
|
- spec/higher_level_api/integration/basic_return_spec.rb
|
151
151
|
- spec/higher_level_api/integration/channel_close_spec.rb
|
152
152
|
- spec/higher_level_api/integration/channel_open_spec.rb
|
153
|
-
- spec/higher_level_api/integration/confirm_select_spec.rb
|
154
153
|
- spec/higher_level_api/integration/connection_recovery_spec.rb
|
155
154
|
- spec/higher_level_api/integration/connection_spec.rb
|
156
155
|
- spec/higher_level_api/integration/connection_stop_spec.rb
|
@@ -209,6 +208,7 @@ files:
|
|
209
208
|
- spec/unit/concurrent/condition_spec.rb
|
210
209
|
- spec/unit/concurrent/linked_continuation_queue_spec.rb
|
211
210
|
- spec/unit/concurrent/synchronized_sorted_set_spec.rb
|
211
|
+
- spec/unit/exchange_recovery_spec.rb
|
212
212
|
- spec/unit/version_delivery_tag_spec.rb
|
213
213
|
homepage: http://rubybunny.info
|
214
214
|
licenses:
|
@@ -230,7 +230,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
230
230
|
version: '0'
|
231
231
|
requirements: []
|
232
232
|
rubyforge_project:
|
233
|
-
rubygems_version: 2.
|
233
|
+
rubygems_version: 2.5.1
|
234
234
|
signing_key:
|
235
235
|
specification_version: 4
|
236
236
|
summary: Popular easy to use Ruby client for RabbitMQ
|
@@ -250,7 +250,6 @@ test_files:
|
|
250
250
|
- spec/higher_level_api/integration/basic_return_spec.rb
|
251
251
|
- spec/higher_level_api/integration/channel_close_spec.rb
|
252
252
|
- spec/higher_level_api/integration/channel_open_spec.rb
|
253
|
-
- spec/higher_level_api/integration/confirm_select_spec.rb
|
254
253
|
- spec/higher_level_api/integration/connection_recovery_spec.rb
|
255
254
|
- spec/higher_level_api/integration/connection_spec.rb
|
256
255
|
- spec/higher_level_api/integration/connection_stop_spec.rb
|
@@ -309,5 +308,6 @@ test_files:
|
|
309
308
|
- spec/unit/concurrent/condition_spec.rb
|
310
309
|
- spec/unit/concurrent/linked_continuation_queue_spec.rb
|
311
310
|
- spec/unit/concurrent/synchronized_sorted_set_spec.rb
|
311
|
+
- spec/unit/exchange_recovery_spec.rb
|
312
312
|
- spec/unit/version_delivery_tag_spec.rb
|
313
313
|
has_rdoc: true
|
@@ -1,19 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
describe Bunny::Channel, "#confirm_select" do
|
4
|
-
let(:connection) do
|
5
|
-
c = Bunny.new(:user => "bunny_gem", :password => "bunny_password", :vhost => "bunny_testbed")
|
6
|
-
c.start
|
7
|
-
c
|
8
|
-
end
|
9
|
-
|
10
|
-
after :each do
|
11
|
-
connection.close if connection.open?
|
12
|
-
end
|
13
|
-
|
14
|
-
it "is supported" do
|
15
|
-
connection.with_channel do |ch|
|
16
|
-
ch.confirm_select
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|