logstash-input-redis 3.6.1 → 3.7.0

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: 63317e087d685718d569e6dde70a5e007d0d988abcac587f55f8f439ca354c87
4
- data.tar.gz: 39410717e0a0a8fdcb3fd2a61553846bee7e82b3417a111ac36e1b222ccc782e
3
+ metadata.gz: 0be6dfe6d1465214b44d17ae4a45482ba364735abb7032fa26e37660845aaba8
4
+ data.tar.gz: d68f634cac89442c536f8f57713354af640f61dc0cc72561ba0972503c9b2d10
5
5
  SHA512:
6
- metadata.gz: e3612cb6febfe05864710e56a02b4eb2c899e75fbd1e9f25a4b9bbca4e62114c13846f05fe9c73f92b45ad2e8301baf6d9d5ab7719d576bd9ec13a2026894f9a
7
- data.tar.gz: c10673819296c367d1ca7de3c795fd2c212099fc505c6bbe0b6a169f2ca87801afc70dc521d8a5db4a3f53e77c3a476bfef9e4d5f7cdc3fcfae414f9f832a5d2
6
+ metadata.gz: 7c662434d97dc7d4811f062ef9dfd3c0d6fe1eec8083f27b7f0a0638e35e5ad7ef10a5b322439ed5df5bf45acaa9fff645bf9592cd5a481b9c8a9d753cd022c6
7
+ data.tar.gz: f96f01c43dfaaa0843645fb737af52408709c27b962231b10781353951c849aa8b83bad85dc4a92fc2b76c5731bca1d01bb5e1998aa2e79799744078b64b8c42
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 3.7.0
2
+ - Fix: better (Redis) exception handling [#89](https://github.com/logstash-plugins/logstash-input-redis/pull/89)
3
+ - Test: start running integration specs on CI
4
+
1
5
  ## 3.6.1
2
6
  - Fix: resolve crash when commands_map is set [#86](https://github.com/logstash-plugins/logstash-input-redis/pull/86)
3
7
 
@@ -107,26 +107,22 @@ module LogStash module Inputs class Redis < LogStash::Inputs::Threadable
107
107
 
108
108
  # private
109
109
  def redis_params
110
+ params = {
111
+ :timeout => @timeout,
112
+ :db => @db,
113
+ :password => @password.nil? ? nil : @password.value,
114
+ :ssl => @ssl
115
+ }
116
+
110
117
  if @path.nil?
111
- connectionParams = {
112
- :host => @host,
113
- :port => @port
114
- }
118
+ params[:host] = @host
119
+ params[:port] = @port
115
120
  else
116
121
  @logger.warn("Parameter 'path' is set, ignoring parameters: 'host' and 'port'")
117
- connectionParams = {
118
- :path => @path
119
- }
122
+ params[:path] = @path
120
123
  end
121
124
 
122
- baseParams = {
123
- :timeout => @timeout,
124
- :db => @db,
125
- :password => @password.nil? ? nil : @password.value,
126
- :ssl => @ssl
127
- }
128
-
129
- return connectionParams.merge(baseParams)
125
+ params
130
126
  end
131
127
 
132
128
  def new_redis_instance
@@ -174,9 +170,12 @@ EOF
174
170
 
175
171
  # private
176
172
  def list_stop
177
- return if @redis.nil? || !@redis.connected?
173
+ redis = @redis # might change during method invocation
174
+ return if redis.nil? || !redis.connected?
178
175
 
179
- @redis.quit rescue nil
176
+ redis.quit rescue nil # does client.disconnect internally
177
+ # check if input retried while executing
178
+ list_stop unless redis.equal? @redis
180
179
  @redis = nil
181
180
  end
182
181
 
@@ -186,15 +185,9 @@ EOF
186
185
  begin
187
186
  @redis ||= connect
188
187
  @list_method.call(@redis, output_queue)
189
- rescue ::Redis::BaseError => e
190
- info = { message: e.message, exception: e.class }
191
- info[:backtrace] = e.backtrace if @logger.debug?
192
- @logger.warn("Redis connection problem", info)
193
- # Reset the redis variable to trigger reconnect
194
- @redis = nil
195
- # this sleep does not need to be stoppable as its
196
- # in a while !stop? loop
197
- sleep 1
188
+ rescue => e
189
+ log_error(e)
190
+ retry if reset_for_error_retry(e)
198
191
  end
199
192
  end
200
193
  end
@@ -248,18 +241,19 @@ EOF
248
241
 
249
242
  # private
250
243
  def subscribe_stop
251
- return if @redis.nil? || !@redis.connected?
252
- # if its a SubscribedClient then:
253
- # it does not have a disconnect method (yet)
254
- if @redis.subscribed?
244
+ redis = @redis # might change during method invocation
245
+ return if redis.nil? || !redis.connected?
246
+
247
+ if redis.subscribed?
255
248
  if @data_type == 'pattern_channel'
256
- @redis.punsubscribe
249
+ redis.punsubscribe
257
250
  else
258
- @redis.unsubscribe
251
+ redis.unsubscribe
259
252
  end
260
- else
261
- @redis.disconnect!
262
253
  end
254
+ redis.close rescue nil # does client.disconnect
255
+ # check if input retried while executing
256
+ subscribe_stop unless redis.equal? @redis
263
257
  @redis = nil
264
258
  end
265
259
 
@@ -268,15 +262,43 @@ EOF
268
262
  begin
269
263
  @redis ||= connect
270
264
  yield
271
- rescue ::Redis::BaseError => e
272
- @logger.warn("Redis connection problem", :exception => e)
273
- # Reset the redis variable to trigger reconnect
274
- @redis = nil
275
- Stud.stoppable_sleep(1) { stop? }
276
- retry if !stop?
265
+ rescue => e
266
+ log_error(e)
267
+ retry if reset_for_error_retry(e)
268
+ end
269
+ end
270
+
271
+ def log_error(e)
272
+ info = { message: e.message, exception: e.class }
273
+ info[:backtrace] = e.backtrace if @logger.debug?
274
+
275
+ case e
276
+ when ::Redis::TimeoutError
277
+ # expected for channels in case no data is available
278
+ @logger.debug("Redis timeout, retrying", info)
279
+ when ::Redis::BaseConnectionError, ::Redis::ProtocolError
280
+ @logger.warn("Redis connection error", info)
281
+ when ::Redis::BaseError
282
+ @logger.error("Redis error", info)
283
+ when ::LogStash::ShutdownSignal
284
+ @logger.debug("Received shutdown signal")
285
+ else
286
+ info[:backtrace] ||= e.backtrace
287
+ @logger.error("Unexpected error", info)
277
288
  end
278
289
  end
279
290
 
291
+ # @return [true] if operation is fine to retry
292
+ def reset_for_error_retry(e)
293
+ return if e.is_a?(::LogStash::ShutdownSignal)
294
+
295
+ # Reset the redis variable to trigger reconnect
296
+ @redis = nil
297
+
298
+ Stud.stoppable_sleep(1) { stop? }
299
+ !stop? # retry if not stop-ing
300
+ end
301
+
280
302
  # private
281
303
  def channel_runner(output_queue)
282
304
  redis_runner do
@@ -324,6 +346,4 @@ EOF
324
346
  end
325
347
  end
326
348
 
327
- # end
328
-
329
349
  end end end # Redis Inputs LogStash
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-input-redis'
4
- s.version = '3.6.1'
4
+ s.version = '3.7.0'
5
5
  s.licenses = ['Apache License (2.0)']
6
6
  s.summary = "Reads events from a Redis instance"
7
7
  s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
@@ -17,11 +17,15 @@ def populate(key, event_count)
17
17
  end
18
18
 
19
19
  def process(conf, event_count)
20
- events = input(conf) do |pipeline, queue|
21
- event_count.times.map{queue.pop}
20
+ events = input(conf) do |_, queue|
21
+ sleep 0.1 until queue.size >= event_count
22
+ queue.size.times.map { queue.pop }
22
23
  end
23
-
24
- expect(events.map{|evt| evt.get("sequence")}).to eq((0..event_count.pred).to_a)
24
+ # due multiple workers we get events out-of-order in the output
25
+ events.sort! { |a, b| a.get('sequence') <=> b.get('sequence') }
26
+ expect(events[0].get('sequence')).to eq(0)
27
+ expect(events[100].get('sequence')).to eq(100)
28
+ expect(events[1000].get('sequence')).to eq(1000)
25
29
  end
26
30
 
27
31
  # integration tests ---------------------
@@ -31,7 +35,6 @@ describe "inputs/redis", :redis => true do
31
35
  it "should read events from a list" do
32
36
  key = SecureRandom.hex
33
37
  event_count = 1000 + rand(50)
34
- # event_count = 100
35
38
  conf = <<-CONFIG
36
39
  input {
37
40
  redis {
@@ -163,7 +166,6 @@ describe LogStash::Inputs::Redis do
163
166
  allow_any_instance_of( Redis::Client ).to receive(:call_with_timeout) do |_, command, timeout, &block|
164
167
  expect(command[0]).to eql :blpop
165
168
  expect(command[1]).to eql ['foo', 0]
166
- expect(command[2]).to eql 1
167
169
  end.and_return ['foo', "{\"foo1\":\"bar\""], nil
168
170
 
169
171
  tt = Thread.new do
@@ -178,6 +180,69 @@ describe LogStash::Inputs::Redis do
178
180
  expect( queue.size ).to be > 0
179
181
  end
180
182
 
183
+ it 'keeps running when a connection error occurs' do
184
+ raised = false
185
+ allow_any_instance_of( Redis::Client ).to receive(:call_with_timeout) do |_, command, timeout, &block|
186
+ expect(command[0]).to eql :blpop
187
+ unless raised
188
+ raised = true
189
+ raise Redis::CannotConnectError.new('test')
190
+ end
191
+ ['foo', "{\"after\":\"raise\"}"]
192
+ end
193
+
194
+ expect(subject.logger).to receive(:warn).with('Redis connection error',
195
+ hash_including(:message=>"test", :exception=>Redis::CannotConnectError)
196
+ ).and_call_original
197
+
198
+ tt = Thread.new do
199
+ sleep 2.0 # allow for retry (sleep) after handle_error
200
+ subject.do_stop
201
+ end
202
+
203
+ subject.run(queue)
204
+
205
+ tt.join
206
+
207
+ try(3) { expect( queue.size ).to be > 0 }
208
+ end
209
+
210
+ context 'error handling' do
211
+
212
+ let(:config) do
213
+ super().merge 'batch_count' => 2
214
+ end
215
+
216
+ it 'keeps running when a (non-Redis) io error occurs' do
217
+ raised = false
218
+ allow(subject).to receive(:connect).and_return redis = double('redis')
219
+ allow(redis).to receive(:blpop).and_return nil
220
+ expect(redis).to receive(:evalsha) do
221
+ unless raised
222
+ raised = true
223
+ raise IOError.new('closed stream')
224
+ end
225
+ []
226
+ end.at_least(1)
227
+ redis
228
+ allow(subject).to receive(:stop)
229
+
230
+ expect(subject.logger).to receive(:error).with('Unexpected error',
231
+ hash_including(:message=>'closed stream', :exception=>IOError)
232
+ ).and_call_original
233
+
234
+ tt = Thread.new do
235
+ sleep 2.0 # allow for retry (sleep) after handle_error
236
+ subject.do_stop
237
+ end
238
+
239
+ subject.run(queue)
240
+
241
+ tt.join
242
+ end
243
+
244
+ end
245
+
181
246
  context "when the batch size is greater than 1" do
182
247
  let(:batch_count) { 10 }
183
248
 
@@ -233,9 +298,6 @@ describe LogStash::Inputs::Redis do
233
298
  end
234
299
 
235
300
  it 'multiple close calls, calls to redis once' do
236
- # subject.use_redis(redis)
237
- # allow(redis).to receive(:blpop).and_return(['foo', 'l1'])
238
- # expect(redis).to receive(:connected?).and_return(connected.last)
239
301
  allow_any_instance_of( Redis::Client ).to receive(:connected?).and_return true, false
240
302
  # allow_any_instance_of( Redis::Client ).to receive(:disconnect)
241
303
  quit_calls.each do |call|
@@ -249,6 +311,9 @@ describe LogStash::Inputs::Redis do
249
311
  end
250
312
 
251
313
  context 'for the subscribe data_types' do
314
+
315
+ before { subject.register }
316
+
252
317
  def run_it_thread(inst)
253
318
  Thread.new(inst) do |subj|
254
319
  subj.run(queue)
@@ -289,6 +354,8 @@ describe LogStash::Inputs::Redis do
289
354
  let(:data_type) { 'channel' }
290
355
  let(:quit_calls) { [:unsubscribe, :connection] }
291
356
 
357
+ before { subject.register }
358
+
292
359
  context 'mocked redis' do
293
360
  it 'multiple stop calls, calls to redis once', type: :mocked do
294
361
  subject.do_stop
@@ -367,6 +434,10 @@ describe LogStash::Inputs::Redis do
367
434
 
368
435
  ["list", "channel", "pattern_channel"].each do |data_type|
369
436
  context data_type do
437
+ # TODO pending
438
+ # redis-rb ends up in a read wait loop since we do not use subscribe_with_timeout
439
+ next unless data_type == 'list'
440
+
370
441
  it_behaves_like "an interruptible input plugin", :redis => true do
371
442
  let(:config) { { 'key' => 'foo', 'data_type' => data_type } }
372
443
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-input-redis
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.1
4
+ version: 3.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-07 00:00:00.000000000 Z
11
+ date: 2021-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement