33require "logstash/inputs/base"
44require "logstash/inputs/threadable"
55require 'redis'
6+ require 'concurrent'
7+ require 'concurrent/executors'
68
79# This input will read events from a Redis instance; it supports both Redis channels and lists.
810# 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
4951 config :key , :validate => :string , :required => true
5052
5153 # 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
5559
5660 # The number of events to return from Redis using EVAL.
5761 config :batch_count , :validate => :number , :default => 125
5862
5963 # Redefined Redis commands to be passed to the Redis client.
6064 config :command_map , :validate => :hash , :default => { }
6165
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+
6278 public
6379 # public API
6480 # use to store a proc that can provide a Redis instance or mock
@@ -77,6 +93,15 @@ def new_redis_instance
7793 @redis_builder . call
7894 end
7995
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+
80105 def register
81106 @redis_url = @path . nil? ? "redis://#{ @password } @#{ @host } :#{ @port } /#{ @db } " : "#{ @password } @#{ @path } /#{ @db } "
82107
@@ -86,6 +111,9 @@ def register
86111 if @data_type == 'list' || @data_type == 'dummy'
87112 @run_method = method ( :list_runner )
88113 @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 )
89117 elsif @data_type == 'channel'
90118 @run_method = method ( :channel_runner )
91119 @stop_method = method ( :subscribe_stop )
@@ -94,8 +122,6 @@ def register
94122 @stop_method = method ( :subscribe_stop )
95123 end
96124
97- @list_method = batched? ? method ( :list_batch_listener ) : method ( :list_single_listener )
98-
99125 @identity = "#{ @redis_url } #{ @data_type } :#{ @key } "
100126 @logger . info ( "Registering Redis" , :identity => @identity )
101127 end # def register
@@ -119,7 +145,7 @@ def batched?
119145
120146 # private
121147 def is_list_type?
122- @data_type == 'list'
148+ @data_type == 'list' || @data_type == 'pattern_list'
123149 end
124150
125151 # private
@@ -193,15 +219,21 @@ def queue_event(msg, output_queue, channel=nil)
193219 end
194220
195221 # private
196- def list_stop
222+ def reset_redis
197223 return if @redis . nil? || !@redis . connected?
198224
199225 @redis . quit rescue nil
200226 @redis = nil
201227 end
202228
229+ # private
230+ def list_stop
231+ reset_redis
232+ end
233+
203234 # private
204235 def list_runner ( output_queue )
236+ @list_method = batched? ? method ( :list_batch_listener ) : method ( :list_single_listener )
205237 while !stop?
206238 begin
207239 @redis ||= connect
@@ -217,16 +249,113 @@ def list_runner(output_queue)
217249 end
218250 end
219251
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 )
221300 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 )
225325 end
326+ end
327+ end
226328
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?
229337 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
230359
231360 # Below is a commented-out implementation of 'batch fetch'
232361 # using pipelined LPOP calls. This in practice has been observed to
@@ -255,6 +384,10 @@ def list_batch_listener(redis, output_queue)
255384 end
256385 end
257386
387+ def list_batch_listener ( redis , output_queue )
388+ process_batch ( redis , output_queue , @key , @batch_count , BATCH_EMPTY_SLEEP )
389+ end
390+
258391 def list_single_listener ( redis , output_queue )
259392 item = redis . blpop ( @key , 0 , :timeout => 1 )
260393 return unless item # from timeout or other conditions
0 commit comments