logstash-integration-rabbitmq 7.4.4-java → 7.4.5-java
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 +3 -0
- data/lib/logstash/outputs/rabbitmq.rb +56 -7
- data/spec/outputs/rabbitmq_spec.rb +185 -2
- data/version +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6bcb030b6451f96b859c628c1631a62083bfe171296d33e11555a908f59a90ef
|
|
4
|
+
data.tar.gz: 10edb4eb65e2ca490e60659025e414cb1e4fdd3dc708c3b9efeba19122643602
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73847fec6a528310e5389e0da3036c6d019b926c3338d8d6bd8760099d692ebe36948f6080f908e4354a5e67bb390465bc6e6abbef6bbe3bbc6bda63a39c7bc5
|
|
7
|
+
data.tar.gz: 4219db729cc3fed03ff1e1bf6414a485218d4abaf00af4bd405cf230e97f545de85b6eb33bf015b21cb8279b3be4c209f589e33dc3b78bcde9108fa72b93ab71
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
## 7.4.5
|
|
2
|
+
- Recover from broker-closed output channels without leaking channels or losing events [#88](https://github.com/logstash-plugins/logstash-integration-rabbitmq/pull/88)
|
|
3
|
+
|
|
1
4
|
## 7.4.4
|
|
2
5
|
- Upgrades march_hare to consume 4.10+ [#87](https://github.com/logstash-plugins/logstash-integration-rabbitmq/pull/87)
|
|
3
6
|
|
|
@@ -17,6 +17,9 @@ module LogStash
|
|
|
17
17
|
|
|
18
18
|
java_import java.util.concurrent.TimeoutException
|
|
19
19
|
java_import com.rabbitmq.client.AlreadyClosedException
|
|
20
|
+
java_import com.rabbitmq.client.ShutdownSignalException
|
|
21
|
+
|
|
22
|
+
CONFIRM_TIMEOUT_MILLIS = 10_000
|
|
20
23
|
|
|
21
24
|
include LogStash::PluginMixins::RabbitMQConnection
|
|
22
25
|
|
|
@@ -73,14 +76,26 @@ module LogStash
|
|
|
73
76
|
raise ArgumentError, "No exchange set in HareInfo!!!" unless @hare_info.exchange
|
|
74
77
|
routing_key = event.sprintf(@key)
|
|
75
78
|
message_properties = @message_properties_template.build(event)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
confirm = false
|
|
80
|
+
begin
|
|
81
|
+
@gated_executor.execute do
|
|
82
|
+
if confirm
|
|
83
|
+
publish_confirmed(message, routing_key, message_properties)
|
|
84
|
+
else
|
|
85
|
+
local_exchange.publish(message, :routing_key => routing_key, :properties => message_properties)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
rescue MarchHare::Exception, IOError, AlreadyClosedException, TimeoutException => e
|
|
89
|
+
@logger.error("Error while publishing, will retry", error_details(e, backtrace: true))
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
# the broker closed only this channel; connection-level closes are left to automatic recovery
|
|
92
|
+
if channel_level_close?(e)
|
|
93
|
+
discard_local_channel
|
|
94
|
+
confirm = true
|
|
95
|
+
end
|
|
96
|
+
sleep_for_retry
|
|
97
|
+
retry
|
|
98
|
+
end
|
|
84
99
|
end
|
|
85
100
|
|
|
86
101
|
def local_exchange
|
|
@@ -107,6 +122,40 @@ module LogStash
|
|
|
107
122
|
|
|
108
123
|
private
|
|
109
124
|
|
|
125
|
+
def channel_level_close?(e)
|
|
126
|
+
signal = e.is_a?(ShutdownSignalException) ? e : e.cause
|
|
127
|
+
signal.is_a?(ShutdownSignalException) && !signal.hard_error?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def discard_local_channel
|
|
131
|
+
channel = @thread_local_channel.get
|
|
132
|
+
@thread_local_exchange.remove
|
|
133
|
+
@thread_local_channel.remove
|
|
134
|
+
discard_channel(channel) if channel
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# unregistering keeps march_hare's automatic recovery from reopening the channel
|
|
138
|
+
def discard_channel(channel)
|
|
139
|
+
channel.close if channel.open?
|
|
140
|
+
rescue StandardError, java.lang.Exception => e
|
|
141
|
+
@logger.debug("Ignoring error while closing channel", error_details(e))
|
|
142
|
+
ensure
|
|
143
|
+
@hare_info.connection.unregister_channel(channel)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# publishes on a temporary confirm-mode channel so a rejected message is retried instead of lost
|
|
147
|
+
def publish_confirmed(message, routing_key, properties)
|
|
148
|
+
channel = @hare_info.connection.create_channel
|
|
149
|
+
begin
|
|
150
|
+
channel.confirm_select
|
|
151
|
+
exchange = declare_exchange!(channel, @exchange, @exchange_type, @durable)
|
|
152
|
+
exchange.publish(message, :routing_key => routing_key, :properties => properties)
|
|
153
|
+
raise MarchHare::Exception, "message was nacked by the broker" unless channel.wait_for_confirms(CONFIRM_TIMEOUT_MILLIS)
|
|
154
|
+
ensure
|
|
155
|
+
discard_channel(channel)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
110
159
|
# When the other end of a RabbitMQ connection is either unwilling or unable to continue reading bytes from
|
|
111
160
|
# its underlying TCP stream, the connection is flagged as "blocked", but attempts to publish onto exchanges
|
|
112
161
|
# using the connection will not block in the client.
|
|
@@ -23,6 +23,16 @@ describe LogStash::Outputs::RabbitMQ do
|
|
|
23
23
|
let(:instance) { klass.new(rabbitmq_settings) }
|
|
24
24
|
let(:hare_info) { instance.instance_variable_get(:@hare_info) }
|
|
25
25
|
|
|
26
|
+
# march_hare converts AlreadyClosedException inside a rescue, so the Java exception becomes the `cause`
|
|
27
|
+
def channel_already_closed(hard:)
|
|
28
|
+
signal = com.rabbitmq.client.ShutdownSignalException.new(hard, false, nil, nil)
|
|
29
|
+
begin
|
|
30
|
+
raise com.rabbitmq.client.AlreadyClosedException.new(signal)
|
|
31
|
+
rescue java.lang.Exception
|
|
32
|
+
raise MarchHare::ChannelAlreadyClosed, "channel is already closed"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
shared_examples 'recovers from exception gracefully' do
|
|
27
37
|
it 'should execute publish twice due to a retry' do
|
|
28
38
|
expect(exchange).to have_received(:publish).twice
|
|
@@ -156,9 +166,12 @@ describe LogStash::Outputs::RabbitMQ do
|
|
|
156
166
|
i = 0
|
|
157
167
|
allow(instance).to receive(:connect!)
|
|
158
168
|
allow(instance).to receive(:sleep_for_retry)
|
|
169
|
+
allow(connection).to receive(:unregister_channel)
|
|
159
170
|
allow(exchange).to receive(:publish).with(any_args) do
|
|
160
171
|
i += 1
|
|
161
|
-
|
|
172
|
+
if i == 1
|
|
173
|
+
exception.is_a?(Proc) ? exception.call : raise(exception)
|
|
174
|
+
end
|
|
162
175
|
end
|
|
163
176
|
|
|
164
177
|
instance.send(:publish, event, encoded_event)
|
|
@@ -167,18 +180,188 @@ describe LogStash::Outputs::RabbitMQ do
|
|
|
167
180
|
context 'when it is a MarchHare exception' do
|
|
168
181
|
let(:exception) { MarchHare::Exception }
|
|
169
182
|
it_behaves_like 'recovers from exception gracefully'
|
|
183
|
+
|
|
184
|
+
it 'should reuse the existing channel' do
|
|
185
|
+
# once on register, once for the publishing thread
|
|
186
|
+
expect(connection).to have_received(:create_channel).twice
|
|
187
|
+
end
|
|
170
188
|
end
|
|
171
189
|
|
|
172
|
-
context 'when it is a MarchHare::ChannelAlreadyClosed' do
|
|
190
|
+
context 'when it is a MarchHare::ChannelAlreadyClosed without a cause' do
|
|
173
191
|
let(:exception) { MarchHare::ChannelAlreadyClosed }
|
|
174
192
|
it_behaves_like 'recovers from exception gracefully'
|
|
175
193
|
end
|
|
176
194
|
|
|
195
|
+
context 'when it is a MarchHare::ChannelAlreadyClosed caused by a connection close' do
|
|
196
|
+
let(:exception) { -> { channel_already_closed(hard: true) } }
|
|
197
|
+
it_behaves_like 'recovers from exception gracefully'
|
|
198
|
+
|
|
199
|
+
it 'should leave the channel to automatic recovery' do
|
|
200
|
+
expect(connection).not_to have_received(:unregister_channel)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
177
204
|
context 'when it is a TimeoutException' do
|
|
178
205
|
let(:exception) { java.util.concurrent.TimeoutException.new }
|
|
179
206
|
it_behaves_like 'recovers from exception gracefully'
|
|
180
207
|
end
|
|
181
208
|
end
|
|
209
|
+
|
|
210
|
+
context 'when the broker closed the channel' do
|
|
211
|
+
let(:stale_channel) { double("Stale channel") }
|
|
212
|
+
let(:stale_exchange) { double("Stale exchange") }
|
|
213
|
+
let(:stale_open) { false }
|
|
214
|
+
let(:stale_close_error) { nil }
|
|
215
|
+
let(:confirm_channel) { double("Confirm channel", confirm_select: nil, open?: true, close: nil) }
|
|
216
|
+
let(:confirm_exchange) { double("Confirm exchange", publish: nil) }
|
|
217
|
+
let(:fresh_channel) { double("Fresh channel") }
|
|
218
|
+
let(:fresh_exchange) { double("Fresh exchange", publish: nil) }
|
|
219
|
+
# outcome of each confirmed attempt: true (ack), false (nack) or a raised error
|
|
220
|
+
let(:confirm_results) { [proc { true }] }
|
|
221
|
+
|
|
222
|
+
before do
|
|
223
|
+
allow(instance).to receive(:sleep_for_retry)
|
|
224
|
+
allow(connection).to receive(:unregister_channel)
|
|
225
|
+
allow(stale_channel).to receive(:exchange).and_return(stale_exchange)
|
|
226
|
+
allow(stale_channel).to receive(:open?).and_return(stale_open)
|
|
227
|
+
allow(stale_channel).to receive(:close) { raise stale_close_error if stale_close_error }
|
|
228
|
+
allow(stale_exchange).to receive(:publish) { channel_already_closed(hard: false) }
|
|
229
|
+
allow(confirm_channel).to receive(:exchange).and_return(confirm_exchange)
|
|
230
|
+
allow(confirm_channel).to receive(:wait_for_confirms).and_invoke(*confirm_results)
|
|
231
|
+
allow(fresh_channel).to receive(:exchange).and_return(fresh_exchange)
|
|
232
|
+
confirm_channels = [confirm_channel] * confirm_results.size
|
|
233
|
+
allow(connection).to receive(:create_channel).and_return(stale_channel, *confirm_channels, fresh_channel)
|
|
234
|
+
|
|
235
|
+
instance.send(:publish, event, encoded_event)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'should not publish to the closed exchange again' do
|
|
239
|
+
expect(stale_exchange).to have_received(:publish).once
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it 'should unregister the closed channel so recovery cannot reopen it' do
|
|
243
|
+
expect(connection).to have_received(:unregister_channel).with(stale_channel)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it 'should not close a channel that is already closed' do
|
|
247
|
+
expect(stale_channel).not_to have_received(:close)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it 'should retry the message on a channel in confirm mode' do
|
|
251
|
+
expect(confirm_channel).to have_received(:confirm_select).once
|
|
252
|
+
expect(confirm_channel).to have_received(:exchange).once
|
|
253
|
+
expect(confirm_exchange).to have_received(:publish).with(encoded_event, anything).once
|
|
254
|
+
expect(confirm_channel).to have_received(:wait_for_confirms).with(10_000)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'should close and unregister the confirm channel' do
|
|
258
|
+
expect(confirm_channel).to have_received(:close).once
|
|
259
|
+
expect(connection).to have_received(:unregister_channel).with(confirm_channel)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it 'should publish the next event on a new regular channel' do
|
|
263
|
+
instance.send(:publish, event, encoded_event)
|
|
264
|
+
|
|
265
|
+
expect(fresh_channel).to have_received(:exchange)
|
|
266
|
+
expect(fresh_exchange).to have_received(:publish).with(encoded_event, anything).once
|
|
267
|
+
expect(confirm_channel).to have_received(:confirm_select).once
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
context 'and the broker nacks the confirmed retry' do
|
|
271
|
+
let(:confirm_results) { [proc { false }, proc { true }] }
|
|
272
|
+
|
|
273
|
+
it 'should keep retrying with confirms until the message is acked' do
|
|
274
|
+
expect(confirm_exchange).to have_received(:publish).with(encoded_event, anything).twice
|
|
275
|
+
expect(instance).to have_received(:sleep_for_retry).twice
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it 'should discard every confirm channel' do
|
|
279
|
+
expect(confirm_channel).to have_received(:close).twice
|
|
280
|
+
expect(connection).to have_received(:unregister_channel).with(confirm_channel).twice
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
context 'and the broker also closes the confirm channel' do
|
|
285
|
+
let(:confirm_results) { [proc { channel_already_closed(hard: false) }, proc { true }] }
|
|
286
|
+
|
|
287
|
+
it 'should keep retrying with confirms until the message is acked' do
|
|
288
|
+
expect(stale_exchange).to have_received(:publish).once
|
|
289
|
+
expect(confirm_exchange).to have_received(:publish).with(encoded_event, anything).twice
|
|
290
|
+
expect(instance).to have_received(:sleep_for_retry).twice
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
context 'and the closed channel still reports being open' do
|
|
295
|
+
let(:stale_open) { true }
|
|
296
|
+
let(:stale_close_error) { java.io.IOException.new("channel is already closed") }
|
|
297
|
+
|
|
298
|
+
it 'should ignore the error when closing it and still unregister it' do
|
|
299
|
+
expect(stale_channel).to have_received(:close)
|
|
300
|
+
expect(connection).to have_received(:unregister_channel).with(stale_channel)
|
|
301
|
+
expect(confirm_exchange).to have_received(:publish).with(encoded_event, anything).once
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
context 'when the connection was closed' do
|
|
307
|
+
let(:stale_exchange) { double("Stale exchange") }
|
|
308
|
+
let(:fresh_channel) { double("Fresh channel") }
|
|
309
|
+
let(:fresh_exchange) { double("Fresh exchange", publish: nil) }
|
|
310
|
+
|
|
311
|
+
before do
|
|
312
|
+
allow(instance).to receive(:sleep_for_retry)
|
|
313
|
+
allow(connection).to receive(:unregister_channel)
|
|
314
|
+
allow(fresh_channel).to receive(:exchange).and_return(fresh_exchange)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
context 'while publishing' do
|
|
318
|
+
before do
|
|
319
|
+
i = 0
|
|
320
|
+
allow(exchange).to receive(:publish) do
|
|
321
|
+
i += 1
|
|
322
|
+
channel_already_closed(hard: true) if i == 1
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
instance.send(:publish, event, encoded_event)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it 'should retry on the same channel once it is recovered' do
|
|
329
|
+
expect(exchange).to have_received(:publish).with(encoded_event, anything).twice
|
|
330
|
+
# once on register, once for the publishing thread
|
|
331
|
+
expect(connection).to have_received(:create_channel).twice
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
it 'should not discard the channel' do
|
|
335
|
+
expect(connection).not_to have_received(:unregister_channel)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
context 'while creating the channel' do
|
|
340
|
+
# create_channel is a plain Java call, so it raises the unconverted Java exception
|
|
341
|
+
let(:connection_closed) do
|
|
342
|
+
com.rabbitmq.client.AlreadyClosedException.new(
|
|
343
|
+
com.rabbitmq.client.ShutdownSignalException.new(true, false, nil, nil)
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
before do
|
|
348
|
+
allow(connection).to receive(:create_channel).and_invoke(
|
|
349
|
+
-> { raise connection_closed },
|
|
350
|
+
-> { fresh_channel }
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
instance.send(:publish, event, encoded_event)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
it 'should retry until the channel can be created' do
|
|
357
|
+
expect(instance).to have_received(:sleep_for_retry).once
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it 'should send the message once the connection is back' do
|
|
361
|
+
expect(fresh_exchange).to have_received(:publish).with(encoded_event, anything).once
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
182
365
|
end
|
|
183
366
|
end
|
|
184
367
|
end
|
data/version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.4.
|
|
1
|
+
7.4.5
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: logstash-integration-rabbitmq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.4.
|
|
4
|
+
version: 7.4.5
|
|
5
5
|
platform: java
|
|
6
6
|
authors:
|
|
7
7
|
- Elastic
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-11-05 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: logstash-core-plugin-api
|
|
@@ -216,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
216
216
|
- !ruby/object:Gem::Version
|
|
217
217
|
version: '0'
|
|
218
218
|
requirements: []
|
|
219
|
-
rubygems_version: 3.
|
|
219
|
+
rubygems_version: 3.6.3
|
|
220
220
|
specification_version: 4
|
|
221
221
|
summary: Integration with RabbitMQ - input and output plugins
|
|
222
222
|
test_files:
|