3
3
require "logstash/inputs/base"
4
4
require "logstash/inputs/threadable"
5
5
require 'redis'
6
+ require 'concurrent'
7
+ require 'concurrent/executors'
6
8
7
9
# This input will read events from a Redis instance; it supports both Redis channels and lists.
8
10
# The list command (BLPOP) used by Logstash is supported in Redis v1.3.1+, and
@@ -49,16 +51,30 @@ module LogStash module Inputs class Redis < LogStash::Inputs::Threadable
49
51
config :key , :validate => :string , :required => true
50
52
51
53
# Specify either list or channel. If `data_type` is `list`, then we will BLPOP the
52
- # key. If `data_type` is `channel`, then we will SUBSCRIBE to the key.
53
- # If `data_type` is `pattern_channel`, then we will PSUBSCRIBE to the key.
54
- config :data_type , :validate => [ "list" , "channel" , "pattern_channel" ] , :required => true
54
+ # key. If `data_type` is `pattern_list`, then we will spawn a number of worker
55
+ # threads that will LPOP from keys matching that pattern. If `data_type` is
56
+ # `channel`, then we will SUBSCRIBE to the key. If `data_type` is `pattern_channel`,
57
+ # then we will PSUBSCRIBE to the key.
58
+ config :data_type , :validate => [ "list" , "pattern_list" , "channel" , "pattern_channel" ] , :required => true
55
59
56
60
# The number of events to return from Redis using EVAL.
57
61
config :batch_count , :validate => :number , :default => 125
58
62
59
63
# Redefined Redis commands to be passed to the Redis client.
60
64
config :command_map , :validate => :hash , :default => { }
61
65
66
+ # Maximum number of worker threads to spawn when using `data_type` `pattern_list`.
67
+ config :pattern_list_threads , :validate => :number , :default => 20
68
+
69
+ # Maximum number of items for a single worker thread to process when `data_type` is `pattern_list`.
70
+ # After the list is empty or this number of items have been processed, the thread will exit and a
71
+ # new one will be started if there are non-empty lists matching the pattern without a consumer.
72
+ config :pattern_list_max_items , :validate => :number , :default => 1000
73
+
74
+ # Time to sleep in main loop after checking if more threads can/need to be spawned.
75
+ # Applies to `data_type` is `pattern_list`
76
+ config :pattern_list_threadpool_sleep , :validate => :number , :default => 0.2
77
+
62
78
public
63
79
# public API
64
80
# use to store a proc that can provide a Redis instance or mock
@@ -77,6 +93,15 @@ def new_redis_instance
77
93
@redis_builder . call
78
94
end
79
95
96
+ def init_threadpool
97
+ @threadpool ||= Concurrent ::ThreadPoolExecutor . new (
98
+ min_threads : @pattern_list_threads ,
99
+ max_threads : @pattern_list_threads ,
100
+ max_queue : 2 * @pattern_list_threads
101
+ )
102
+ @current_workers ||= Concurrent ::Set . new
103
+ end
104
+
80
105
def register
81
106
@redis_url = @path . nil? ? "redis://#{ @password } @#{ @host } :#{ @port } /#{ @db } " : "#{ @password } @#{ @path } /#{ @db } "
82
107
@@ -86,6 +111,9 @@ def register
86
111
if @data_type == 'list' || @data_type == 'dummy'
87
112
@run_method = method ( :list_runner )
88
113
@stop_method = method ( :list_stop )
114
+ elsif @data_type == 'pattern_list'
115
+ @run_method = method ( :pattern_list_runner )
116
+ @stop_method = method ( :pattern_list_stop )
89
117
elsif @data_type == 'channel'
90
118
@run_method = method ( :channel_runner )
91
119
@stop_method = method ( :subscribe_stop )
@@ -94,8 +122,6 @@ def register
94
122
@stop_method = method ( :subscribe_stop )
95
123
end
96
124
97
- @list_method = batched? ? method ( :list_batch_listener ) : method ( :list_single_listener )
98
-
99
125
@identity = "#{ @redis_url } #{ @data_type } :#{ @key } "
100
126
@logger . info ( "Registering Redis" , :identity => @identity )
101
127
end # def register
@@ -119,7 +145,7 @@ def batched?
119
145
120
146
# private
121
147
def is_list_type?
122
- @data_type == 'list'
148
+ @data_type == 'list' || @data_type == 'pattern_list'
123
149
end
124
150
125
151
# private
@@ -193,15 +219,21 @@ def queue_event(msg, output_queue, channel=nil)
193
219
end
194
220
195
221
# private
196
- def list_stop
222
+ def reset_redis
197
223
return if @redis . nil? || !@redis . connected?
198
224
199
225
@redis . quit rescue nil
200
226
@redis = nil
201
227
end
202
228
229
+ # private
230
+ def list_stop
231
+ reset_redis
232
+ end
233
+
203
234
# private
204
235
def list_runner ( output_queue )
236
+ @list_method = batched? ? method ( :list_batch_listener ) : method ( :list_single_listener )
205
237
while !stop?
206
238
begin
207
239
@redis ||= connect
@@ -217,16 +249,113 @@ def list_runner(output_queue)
217
249
end
218
250
end
219
251
220
- def list_batch_listener ( redis , output_queue )
252
+ #private
253
+ def reset_threadpool
254
+ return if @threadpool . nil?
255
+ @threadpool . shutdown
256
+ @threadpool . wait_for_termination
257
+ @threadpool = nil
258
+ end
259
+
260
+ # private
261
+ def pattern_list_stop
262
+ reset_redis
263
+ reset_threadpool
264
+ end
265
+
266
+ # private
267
+ def pattern_list_process_item ( redis , output_queue , key )
268
+ if stop?
269
+ @logger . debug ( "Breaking from thread #{ key } as it was requested to stop" )
270
+ return false
271
+ end
272
+ value = redis . lpop ( key )
273
+ return false if value . nil?
274
+ queue_event ( value , output_queue )
275
+ true
276
+ end
277
+
278
+ # private
279
+ def pattern_list_single_processor ( redis , output_queue , key )
280
+ ( 0 ...@pattern_list_max_items ) . each do
281
+ break unless pattern_list_process_item ( redis , output_queue , key )
282
+ end
283
+ end
284
+
285
+ # private
286
+ def pattern_list_batch_processor ( redis , output_queue , key )
287
+ items_left = @pattern_list_max_items
288
+ while items_left > 0
289
+ limit = [ items_left , @batch_count ] . min
290
+ processed = process_batch ( redis , output_queue , key , limit , 0 )
291
+ if processed . zero? || processed < limit
292
+ return
293
+ end
294
+ items_left -= processed
295
+ end
296
+ end
297
+
298
+ # private
299
+ def pattern_list_worker_consume ( output_queue , key )
221
300
begin
222
- results = redis . evalsha ( @redis_script_sha , [ @key ] , [ @batch_count -1 ] )
223
- results . each do |item |
224
- queue_event ( item , output_queue )
301
+ redis ||= connect
302
+ @pattern_list_processor . call ( redis , output_queue , key )
303
+ rescue ::Redis ::BaseError => e
304
+ @logger . warn ( "Redis connection problem in thread for key #{ key } . Sleeping a while before exiting thread." , :exception => e )
305
+ sleep 1
306
+ return
307
+ ensure
308
+ redis . quit rescue nil
309
+ end
310
+ end
311
+
312
+ # private
313
+ def threadpool_capacity?
314
+ @threadpool . remaining_capacity > 0
315
+ end
316
+
317
+ # private
318
+ def pattern_list_launch_worker ( output_queue , key )
319
+ @current_workers . add ( key )
320
+ @threadpool . post do
321
+ begin
322
+ pattern_list_worker_consume ( output_queue , key )
323
+ ensure
324
+ @current_workers . delete ( key )
225
325
end
326
+ end
327
+ end
226
328
227
- if results . size . zero?
228
- sleep BATCH_EMPTY_SLEEP
329
+ # private
330
+ def pattern_list_ensure_workers ( output_queue )
331
+ return unless threadpool_capacity?
332
+ redis_runner do
333
+ @redis . keys ( @key ) . shuffle . each do |key |
334
+ next if @current_workers . include? ( key )
335
+ pattern_list_launch_worker ( output_queue , key )
336
+ break unless threadpool_capacity?
229
337
end
338
+ end
339
+ end
340
+
341
+ # private
342
+ def pattern_list_runner ( output_queue )
343
+ @pattern_list_processor = batched? ? method ( :pattern_list_batch_processor ) : method ( :pattern_list_single_processor )
344
+ while !stop?
345
+ init_threadpool if @threadpool . nil?
346
+ pattern_list_ensure_workers ( output_queue )
347
+ sleep ( @pattern_list_threadpool_sleep )
348
+ end
349
+ end
350
+
351
+ def process_batch ( redis , output_queue , key , batch_size , sleep_time )
352
+ begin
353
+ results = redis . evalsha ( @redis_script_sha , [ key ] , [ batch_size -1 ] )
354
+ results . each do |item |
355
+ queue_event ( item , output_queue )
356
+ end
357
+ sleep sleep_time if results . size . zero? && sleep_time > 0
358
+ results . size
230
359
231
360
# Below is a commented-out implementation of 'batch fetch'
232
361
# using pipelined LPOP calls. This in practice has been observed to
@@ -255,6 +384,10 @@ def list_batch_listener(redis, output_queue)
255
384
end
256
385
end
257
386
387
+ def list_batch_listener ( redis , output_queue )
388
+ process_batch ( redis , output_queue , @key , @batch_count , BATCH_EMPTY_SLEEP )
389
+ end
390
+
258
391
def list_single_listener ( redis , output_queue )
259
392
item = redis . blpop ( @key , 0 , :timeout => 1 )
260
393
return unless item # from timeout or other conditions
0 commit comments