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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee33feab4b89f243bac29940ce92eb8650a7bc18419e764e5f0388ce4111e30d
4
- data.tar.gz: 25f6b19b9b7e15e2dfe9fce6efd096348c88669fe46fc8bedde51d9d4af7da6f
3
+ metadata.gz: 6bcb030b6451f96b859c628c1631a62083bfe171296d33e11555a908f59a90ef
4
+ data.tar.gz: 10edb4eb65e2ca490e60659025e414cb1e4fdd3dc708c3b9efeba19122643602
5
5
  SHA512:
6
- metadata.gz: d3bf562cae12a69b5cbf4a8c87bddecc79df9a9fcb7c77bc04d1159f3159a0783c15d4f44b9370337c807a3f13bd093d6e858b12ba5fa94df95b8e297bbee153
7
- data.tar.gz: 665169ebe7ad610dea3096e1aa323a95312089bea9f956bb7a7a610d607c301325e9c6322b0f81223a8e03431a261b44744a016b0a3db8304477d6572110cfe0
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
- @gated_executor.execute do
77
- local_exchange.publish(message, :routing_key => routing_key, :properties => message_properties)
78
- end
79
- rescue MarchHare::Exception, IOError, AlreadyClosedException, TimeoutException => e
80
- @logger.error("Error while publishing, will retry", error_details(e, backtrace: true))
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
- sleep_for_retry
83
- retry
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
- raise exception if i == 1
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.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
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: 1980-01-02 00:00:00.000000000 Z
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.7.2
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: