logstash-input-redis 3.6.1 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
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