logstash-input-redis 1.0.0 → 1.0.1

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
  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