logstash-input-redis 1.0.0 → 1.0.1

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
  SHA1:
3
- metadata.gz: f3c4e21e1c031b454b041be5fd084352b6439dea
4
- data.tar.gz: 9116bd121eea1697bacfdab2a90e5b08fbe13488
3
+ metadata.gz: ddb2a94b6e3c3cff06790e2434bc9f6aaad46760
4
+ data.tar.gz: e251dbde7ef57f43b921c6ebf2d6cd3d897d1889
5
5
  SHA512:
6
- metadata.gz: 3bb19842af185d1a2fb32f41b6f90b620827c509741dcd5fdb4c1fe092a10d04d01431c8718f6b98b215c24fe506a4658252bc161d14bb3e19971f2f9e36513a
7
- data.tar.gz: ea05bcbb2ce3b47c21663970817ecd3c7e88c4fd9e21a341d96f22e2556f0b3336e3d00e4af349082b3dda0b9579b3cc5a5a070c55f6eb8913608b486a6c7dc6
6
+ metadata.gz: ea5587b7e19d151161b5065a1efaff804d62621e58c919feac72b01c6f3d7e5e2915422f18e3afcbe721ecf8a6dec69252a92b0fa215aab2c7f9b614502345d6
7
+ data.tar.gz: bdf90d25cef6ac38a917251428a057c7007f6226058985a18f97d58509412689c4eb630ea328ce469c6b07b1c7d9a84849133448dd807de22415529893ac7af2
data/CHANGELOG.md CHANGED
@@ -0,0 +1,3 @@
1
+ ## 1.0.1
2
+ - Make teardown more reliable
3
+ - Re-organise code and tests
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
+ require "logstash/namespace"
2
3
  require "logstash/inputs/base"
3
4
  require "logstash/inputs/threadable"
4
- require "logstash/namespace"
5
5
 
6
6
  # This input will read events from a Redis instance; it supports both Redis channels and lists.
7
7
  # The list command (BLPOP) used by Logstash is supported in Redis v1.3.1+, and
@@ -15,7 +15,9 @@ require "logstash/namespace"
15
15
  # `batch_count` note: If you use the `batch_count` setting, you *must* use a Redis version 2.6.0 or
16
16
  # newer. Anything older does not support the operations used by batching.
17
17
  #
18
- class LogStash::Inputs::Redis < LogStash::Inputs::Threadable
18
+ module Logstash module Inputs class Redis < LogStash::Inputs::Threadable
19
+ # class LogStash::Inputs::Redis < LogStash::Inputs::Threadable
20
+
19
21
  config_name "redis"
20
22
 
21
23
  default :codec, "json"
@@ -57,14 +59,30 @@ class LogStash::Inputs::Redis < LogStash::Inputs::Threadable
57
59
  config :batch_count, :validate => :number, :default => 1
58
60
 
59
61
  public
62
+ # public API
63
+ # use to store a proc that can provide a redis instance or mock
64
+ def add_external_redis_builder(builder) #callable
65
+ @redis_builder = builder
66
+ self
67
+ end
68
+
69
+ # use to apply an instance directly and bypass the builder
70
+ def use_redis(instance)
71
+ @redis = instance
72
+ self
73
+ end
74
+
75
+ def new_redis_instance
76
+ @redis_builder.call
77
+ end
78
+
60
79
  def register
61
80
  require 'redis'
62
- @redis = nil
63
81
  @redis_url = "redis://#{@password}@#{@host}:#{@port}/#{@db}"
64
82
 
65
83
  # TODO remove after setting key and data_type to true
66
84
  if @queue
67
- if @key or @data_type
85
+ if @key || @data_type
68
86
  raise RuntimeError.new(
69
87
  "Cannot specify queue parameter and key or data_type"
70
88
  )
@@ -73,38 +91,79 @@ class LogStash::Inputs::Redis < LogStash::Inputs::Threadable
73
91
  @data_type = 'list'
74
92
  end
75
93
 
76
- if not @key or not @data_type
94
+ if !@key || !@data_type
77
95
  raise RuntimeError.new(
78
96
  "Must define queue, or key and data_type parameters"
79
97
  )
80
98
  end
81
99
  # end TODO
82
100
 
83
- @logger.info("Registering Redis", :identity => identity)
101
+ @redis_builder ||= method(:internal_redis_builder)
102
+
103
+ # just switch on data_type once
104
+ if @data_type == 'list' || @data_type == 'dummy'
105
+ @run_method = method(:list_runner)
106
+ @teardown_method = method(:list_teardown)
107
+ elsif @data_type == 'channel'
108
+ @run_method = method(:channel_runner)
109
+ @teardown_method = method(:subscribe_teardown)
110
+ elsif @data_type == 'pattern_channel'
111
+ @run_method = method(:pattern_channel_runner)
112
+ @teardown_method = method(:subscribe_teardown)
113
+ end
114
+
115
+ # TODO(sissel, boertje): set @identity directly when @name config option is removed.
116
+ @identity = @name != 'default' ? @name : "#{@redis_url} #{@data_type}:#{@key}"
117
+ @logger.info("Registering Redis", :identity => @identity)
84
118
  end # def register
85
119
 
86
- # A string used to identify a Redis instance in log messages
87
- # TODO(sissel): Use instance variables for this once the @name config
88
- # option is removed.
89
- private
90
- def identity
91
- @name || "#{@redis_url} #{@data_type}:#{@key}"
120
+ def run(output_queue)
121
+ @run_method.call(output_queue)
122
+ rescue LogStash::ShutdownSignal
123
+ # ignore and quit
124
+ end # def run
125
+
126
+ def teardown
127
+ @shutdown_requested = true
128
+ @teardown_method.call
92
129
  end
93
130
 
131
+ # private methods -----------------------------
94
132
  private
95
- def connect
96
- redis = Redis.new(
133
+
134
+ def batched?
135
+ @batch_count > 1
136
+ end
137
+
138
+ # private
139
+ def is_list_type?
140
+ @data_type == 'list'
141
+ end
142
+
143
+ # private
144
+ def redis_params
145
+ {
97
146
  :host => @host,
98
147
  :port => @port,
99
148
  :timeout => @timeout,
100
149
  :db => @db,
101
150
  :password => @password.nil? ? nil : @password.value
102
- )
103
- load_batch_script(redis) if @data_type == 'list' && (@batch_count > 1)
104
- return redis
151
+ }
152
+ end
153
+
154
+ # private
155
+ def internal_redis_builder
156
+ ::Redis.new(redis_params)
157
+ end
158
+
159
+ # private
160
+ def connect
161
+ redis = new_redis_instance
162
+ load_batch_script(redis) if batched? && is_list_type?
163
+ redis
105
164
  end # def connect
106
165
 
107
- private
166
+ # private
108
167
  def load_batch_script(redis)
109
168
  #A Redis Lua EVAL script to fetch a count of keys
110
169
  #in case count is bigger than current items in queue whole queue will be returned without extra nil values
@@ -126,7 +185,7 @@ EOF
126
185
  @redis_script_sha = redis.script(:load, redis_script)
127
186
  end
128
187
 
129
- private
188
+ # private
130
189
  def queue_event(msg, output_queue)
131
190
  begin
132
191
  @codec.decode(msg) do |event|
@@ -141,7 +200,40 @@ EOF
141
200
  end
142
201
  end
143
202
 
144
- private
203
+ # private
204
+ def shutting_down?
205
+ @shutdown_requested
206
+ end
207
+
208
+ # private
209
+ def running?
210
+ !@shutdown_requested
211
+ end
212
+
213
+ # private
214
+ def list_teardown
215
+ return if @redis.nil? || !@redis.connected?
216
+
217
+ @redis.quit rescue nil
218
+ @redis = nil
219
+ end
220
+
221
+ # private
222
+ def list_runner(output_queue)
223
+ while running?
224
+ begin
225
+ @redis ||= connect
226
+ list_listener(@redis, output_queue)
227
+ rescue ::Redis::BaseError => e
228
+ @logger.warn("Redis connection problem", :exception => e)
229
+ # Reset the redis variable to trigger reconnect
230
+ @redis = nil
231
+ sleep 1
232
+ end
233
+ end
234
+ end
235
+
236
+ # private
145
237
  def list_listener(redis, output_queue)
146
238
 
147
239
  item = redis.blpop(@key, 0, :timeout => 1)
@@ -149,10 +241,10 @@ EOF
149
241
 
150
242
  # blpop returns the 'key' read from as well as the item result
151
243
  # we only care about the result (2nd item in the list).
152
- queue_event(item[1], output_queue)
244
+ queue_event(item.last, output_queue)
153
245
 
154
246
  # If @batch_count is 1, there's no need to continue.
155
- return if @batch_count == 1
247
+ return if !batched?
156
248
 
157
249
  begin
158
250
  redis.evalsha(@redis_script_sha, [@key], [@batch_count-1]).each do |item|
@@ -173,7 +265,7 @@ EOF
173
265
  #queue_event(item, output_queue) if item
174
266
  #end
175
267
  # --- End commented out implementation of 'batch fetch'
176
- rescue Redis::CommandError => e
268
+ rescue ::Redis::CommandError => e
177
269
  if e.to_s =~ /NOSCRIPT/ then
178
270
  @logger.warn("Redis may have been restarted, reloading Redis batch EVAL script", :exception => e);
179
271
  load_batch_script(redis)
@@ -184,15 +276,49 @@ EOF
184
276
  end
185
277
  end
186
278
 
187
- private
188
- def channel_listener(redis, output_queue)
189
- redis.subscribe @key do |on|
279
+ # private
280
+ def subscribe_teardown
281
+ return if @redis.nil? || !@redis.connected?
282
+ # if its a SubscribedClient then:
283
+ # it does not have a disconnect method (yet)
284
+ if @redis.client.is_a?(::Redis::SubscribedClient)
285
+ @redis.client.unsubscribe
286
+ else
287
+ @redis.client.disconnect
288
+ end
289
+ @redis = nil
290
+ end
291
+
292
+ # private
293
+ def redis_runner
294
+ begin
295
+ @redis ||= connect
296
+ yield
297
+ rescue ::Redis::BaseError => e
298
+ @logger.warn("Redis connection problem", :exception => e)
299
+ # Reset the redis variable to trigger reconnect
300
+ @redis = nil
301
+ sleep 1
302
+ retry
303
+ end
304
+ end
305
+
306
+ # private
307
+ def channel_runner(output_queue)
308
+ redis_runner do
309
+ channel_listener(output_queue)
310
+ end
311
+ end
312
+
313
+ # private
314
+ def channel_listener(output_queue)
315
+ @redis.subscribe(@key) do |on|
190
316
  on.subscribe do |channel, count|
191
317
  @logger.info("Subscribed", :channel => channel, :count => count)
192
318
  end
193
319
 
194
320
  on.message do |channel, message|
195
- queue_event message, output_queue
321
+ queue_event(message, output_queue)
196
322
  end
197
323
 
198
324
  on.unsubscribe do |channel, count|
@@ -201,15 +327,21 @@ EOF
201
327
  end
202
328
  end
203
329
 
204
- private
205
- def pattern_channel_listener(redis, output_queue)
206
- redis.psubscribe @key do |on|
330
+ def pattern_channel_runner(output_queue)
331
+ redis_runner do
332
+ pattern_channel_listener(output_queue)
333
+ end
334
+ end
335
+
336
+ # private
337
+ def pattern_channel_listener(output_queue)
338
+ @redis.psubscribe @key do |on|
207
339
  on.psubscribe do |channel, count|
208
340
  @logger.info("Subscribed", :channel => channel, :count => count)
209
341
  end
210
342
 
211
- on.pmessage do |ch, event, message|
212
- queue_event message, output_queue
343
+ on.pmessage do |pattern, channel, message|
344
+ queue_event(message, output_queue)
213
345
  end
214
346
 
215
347
  on.punsubscribe do |channel, count|
@@ -218,51 +350,6 @@ EOF
218
350
  end
219
351
  end
220
352
 
221
- # Since both listeners have the same basic loop, we've abstracted the outer
222
- # loop.
223
- private
224
- def listener_loop(listener, output_queue)
225
- while !@shutdown_requested
226
- begin
227
- @redis ||= connect
228
- self.send listener, @redis, output_queue
229
- rescue Redis::BaseError => e
230
- @logger.warn("Redis connection problem", :exception => e)
231
- # Reset the redis variable to trigger reconnect
232
- @redis = nil
233
- sleep 1
234
- end
235
- end
236
- end # listener_loop
237
-
238
- public
239
- def run(output_queue)
240
- if @data_type == 'list'
241
- listener_loop :list_listener, output_queue
242
- elsif @data_type == 'channel'
243
- listener_loop :channel_listener, output_queue
244
- else
245
- listener_loop :pattern_channel_listener, output_queue
246
- end
247
- rescue LogStash::ShutdownSignal
248
- # ignore and quit
249
- end # def run
250
-
251
- public
252
- def teardown
253
- @shutdown_requested = true
353
+ # end
254
354
 
255
- if @redis
256
- if @data_type == 'list'
257
- @redis.quit rescue nil
258
- elsif @data_type == 'channel'
259
- @redis.unsubscribe rescue nil
260
- @redis.connection.disconnect
261
- elsif @data_type == 'pattern_channel'
262
- @redis.punsubscribe rescue nil
263
- @redis.connection.disconnect
264
- end
265
- @redis = nil
266
- end
267
- end
268
- end # class LogStash::Inputs::Redis
355
+ 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 = '1.0.0'
4
+ s.version = '1.0.1'
5
5
  s.licenses = ['Apache License (2.0)']
6
6
  s.summary = "This input will read 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/plugin install gemname. This gem is not a stand-alone program"
@@ -1,6 +1,7 @@
1
1
  require "logstash/devutils/rspec/spec_helper"
2
2
  require "redis"
3
3
  require "stud/try"
4
+ require 'logstash/inputs/redis'
4
5
 
5
6
  def populate(key, event_count)
6
7
  require "logstash/event"
@@ -23,6 +24,8 @@ def process(conf, event_count)
23
24
  end
24
25
  end # process
25
26
 
27
+ # integration tests ---------------------
28
+
26
29
  describe "inputs/redis", :redis => true do
27
30
 
28
31
  it "should read events from a list" do
@@ -61,3 +64,178 @@ describe "inputs/redis", :redis => true do
61
64
  process(conf, event_count)
62
65
  end
63
66
  end
67
+
68
+ # unit tests ---------------------
69
+
70
+ describe Logstash::Inputs::Redis do
71
+ let(:redis) { double('redis') }
72
+ let(:builder) { ->{ redis } }
73
+ let(:connection) { double('redis_connection') }
74
+ let(:connected) { [true] }
75
+ let(:data_type) { 'list' }
76
+ let(:cfg) { {'key' => 'foo', 'data_type' => data_type} }
77
+ let(:quit_calls) { [:quit] }
78
+ let(:accumulator) { [] }
79
+
80
+ subject do
81
+ described_class.new(cfg).add_external_redis_builder(builder)
82
+ end
83
+
84
+ context 'construction' do
85
+ it 'registers the input' do
86
+ expect {subject.register}.not_to raise_error
87
+ end
88
+ end
89
+
90
+ context 'runtime for list data_type' do
91
+ before do
92
+ subject.register
93
+ end
94
+
95
+ context 'teardown when redis is unset' do
96
+ let(:quit_calls) { [:quit, :unsubscribe, :punsubscribe, :connection, :disconnect!] }
97
+
98
+ it 'does not attempt to quit' do
99
+ allow(redis).to receive(:nil?).and_return(true)
100
+ quit_calls.each do |call|
101
+ expect(redis).not_to receive(call)
102
+ end
103
+ expect {subject.teardown}.not_to raise_error
104
+ end
105
+ end
106
+
107
+ it 'calling the run method, adds events to the queue' do
108
+ expect(redis).to receive(:blpop).at_least(:once).and_return(['foo', 'l1'])
109
+
110
+ allow(redis).to receive(:connected?).and_return(connected.last)
111
+ allow(redis).to receive(:quit)
112
+
113
+ tt = Thread.new do
114
+ sleep 0.01
115
+ subject.teardown
116
+ end
117
+
118
+ subject.run(accumulator)
119
+
120
+ tt.join
121
+
122
+ expect(accumulator.size).to be > 0
123
+ end
124
+
125
+ it 'multiple teardown calls, calls to redis once' do
126
+ subject.use_redis(redis)
127
+ allow(redis).to receive(:blpop).and_return(['foo', 'l1'])
128
+ expect(redis).to receive(:connected?).and_return(connected.last)
129
+ quit_calls.each do |call|
130
+ expect(redis).to receive(call).at_most(:once)
131
+ end
132
+
133
+ subject.teardown
134
+ connected.push(false) #can't use let block here so push to array
135
+ expect {subject.teardown}.not_to raise_error
136
+ subject.teardown
137
+ end
138
+ end
139
+
140
+ context 'for the subscribe data_types' do
141
+ def run_it_thread(inst)
142
+ Thread.new(inst) do |subj|
143
+ subj.run(accumulator)
144
+ end
145
+ end
146
+
147
+ def publish_thread(new_redis, prefix)
148
+ Thread.new(new_redis, prefix) do |r, p|
149
+ sleep 0.1
150
+ 2.times do |i|
151
+ r.publish('foo', "#{p}#{i.next}")
152
+ end
153
+ end
154
+ end
155
+
156
+ def teardown_thread(inst, rt)
157
+ Thread.new(inst, rt) do |subj, runner|
158
+ sleep 0.4 # allow the messages through
159
+ runner.raise(LogStash::ShutdownSignal)
160
+ subj.teardown
161
+ end
162
+ end
163
+
164
+ let(:instance) do
165
+ inst = described_class.new(cfg)
166
+ inst.register
167
+ inst
168
+ end
169
+
170
+ before do
171
+ subject.register
172
+ subject.use_redis(redis)
173
+ allow(connection).to receive(:is_a?).and_return(true)
174
+ allow(redis).to receive(:client).and_return(connection)
175
+ end
176
+
177
+ before(:example, type: :mocked) do
178
+ expect(redis).to receive(:connected?).and_return(connected.last)
179
+ expect(connection).to receive(:unsubscribe)
180
+
181
+ quit_calls.each do |call|
182
+ expect(redis).to receive(call).at_most(:once)
183
+ end
184
+ end
185
+
186
+ context 'runtime for channel data_type' do
187
+ let(:data_type) { 'channel' }
188
+ let(:quit_calls) { [:unsubscribe, :connection] }
189
+
190
+ context 'mocked redis' do
191
+ it 'multiple teardown calls, calls to redis once', type: :mocked do
192
+ subject.teardown
193
+ connected.push(false) #can't use let block here so push to array
194
+ expect {subject.teardown}.not_to raise_error
195
+ subject.teardown
196
+ end
197
+ end
198
+
199
+ context 'real redis', :redis => true do
200
+ it 'calling the run method, adds events to the queue' do
201
+ #simulate the input thread
202
+ rt = run_it_thread(instance)
203
+ #simulate the other system thread
204
+ publish_thread(instance.new_redis_instance, 'c').join
205
+ #simulate the pipeline thread
206
+ teardown_thread(instance, rt).join
207
+
208
+ expect(accumulator.size).to eq(2)
209
+ end
210
+ end
211
+ end
212
+
213
+ context 'runtime for pattern_channel data_type' do
214
+ let(:data_type) { 'pattern_channel' }
215
+ let(:quit_calls) { [:punsubscribe, :connection] }
216
+
217
+ context 'mocked redis' do
218
+ it 'multiple teardown calls, calls to redis once', type: :mocked do
219
+ subject.teardown
220
+ connected.push(false) #can't use let block here so push to array
221
+ expect {subject.teardown}.not_to raise_error
222
+ subject.teardown
223
+ end
224
+ end
225
+
226
+ context 'real redis', :redis => true do
227
+ it 'calling the run method, adds events to the queue' do
228
+ #simulate the input thread
229
+ rt = run_it_thread(instance)
230
+ #simulate the other system thread
231
+ publish_thread(instance.new_redis_instance, 'pc').join
232
+ #simulate the pipeline thread
233
+ teardown_thread(instance, rt).join
234
+
235
+ expect(accumulator.size).to eq(2)
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ 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: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-24 00:00:00.000000000 Z
11
+ date: 2015-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logstash-core
@@ -111,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
111
  version: '0'
112
112
  requirements: []
113
113
  rubyforge_project:
114
- rubygems_version: 2.2.2
114
+ rubygems_version: 2.1.9
115
115
  signing_key:
116
116
  specification_version: 4
117
117
  summary: This input will read events from a Redis instance