logstash-input-lumberjack 0.1.9 → 0.1.10

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: da715b62794c6355a47c844a678f452afd568478
4
- data.tar.gz: 866e0a9fcfcdecd61a018fdc33c991393aa65216
3
+ metadata.gz: a24179f3e3ca8a3092c1d692875ea2a88edfa557
4
+ data.tar.gz: 46e2bde92697647cb0b26e66484124e7e4ae6c11
5
5
  SHA512:
6
- metadata.gz: c9a7534c7192428a7169eada97f06a6ec40f10bdee769764ab7d059af2eff2f8b12a68c1fe6da3d5148bda4b679715d13422573cf291c19f4b8907145d4289ea
7
- data.tar.gz: c6357995a2a4bc630e49ecab1b9d23703264579915b7ffbdc70d9dc858f7e645c7cd0630a5a2e5a196e95f2faf3c1e2845748877fc7667081b5f239c8f739b8e
6
+ metadata.gz: c21484a7108a55aabe51d75d8877ddc749c1abfe30a033096ec662fa020f46335ffb8a66cba3de79a20b85d24f31f4b7dc51435544263d0f7bdd815ee96387c2
7
+ data.tar.gz: cc0df5cd9059650ebf13c4d18956ff2953535e8683ea2ecb167d2592a7bdd43eab06353ac4d77e03133ff3106b2ba2f5996f53d7825e0c78756c5c371e90d88f
@@ -0,0 +1,4 @@
1
+ # 0.1.10
2
+ - Deprecating the `max_clients` option
3
+ - Use a circuit breaker to start refusing new connection when the queue is blocked for too long.
4
+ - Add an internal `SizeQueue` with a timeout to drop blocked connections. (https://github.com/logstash-plugins/logstash-input-lumberjack/pull/12)
@@ -0,0 +1,5 @@
1
+ Elasticsearch
2
+ Copyright 2012-2015 Elasticsearch
3
+
4
+ This product includes software developed by The Apache Software
5
+ Foundation (http://www.apache.org/).
@@ -0,0 +1,96 @@
1
+ require "thread"
2
+ require "cabin"
3
+
4
+ module LogStash
5
+ # Largely inspired by Martin's fowler circuit breaker
6
+ class CircuitBreaker
7
+ class OpenBreaker < StandardError; end
8
+
9
+ # Error threshold before opening the breaker,
10
+ # if the breaker is open it wont execute the code.
11
+ DEFAULT_ERROR_THRESHOLD = 5
12
+
13
+ # Recover time after the breaker is open to start
14
+ # executing the method again.
15
+ DEFAULT_TIME_BEFORE_RETRY = 30
16
+
17
+ # Exceptions catched by the circuit breaker,
18
+ # too much errors and the breaker will trip.
19
+ DEFAULT_EXCEPTION_RESCUED = [StandardError]
20
+
21
+ def initialize(name, options = {}, &block)
22
+ @exceptions = Array(options.fetch(:exceptions, [StandardError]))
23
+ @error_threshold = options.fetch(:error_threshold, DEFAULT_ERROR_THRESHOLD)
24
+ @time_before_retry = options.fetch(:time_before_retry, DEFAULT_TIME_BEFORE_RETRY)
25
+ @block = block
26
+ @name = name
27
+ @mutex = Mutex.new
28
+ reset
29
+ end
30
+
31
+ def execute(args = nil)
32
+ case state
33
+ when :open
34
+ logger.warn("CircuitBreaker::Open", :name => @name)
35
+ raise OpenBreaker, "for #{@name}"
36
+ when :close, :half_open
37
+ if block_given?
38
+ yield args
39
+ else
40
+ @block.call(args)
41
+ end
42
+
43
+ if state == :half_open
44
+ logger.warn("CircuitBreaker::Close", :name => @name)
45
+ reset
46
+ end
47
+ end
48
+ rescue *@exceptions => e
49
+ logger.warn("CircuitBreaker::rescuing exceptions", :name => @name, :exception => e.class)
50
+ increment_errors(e)
51
+ end
52
+
53
+ def closed?
54
+ state == :close || state == :half_open
55
+ end
56
+
57
+ private
58
+ def logger
59
+ @logger ||= Cabin::Channel.get(LogStash)
60
+ end
61
+
62
+ def reset
63
+ @mutex.synchronize do
64
+ @errors_count = 0
65
+ @last_failure_time = nil
66
+ end
67
+ end
68
+
69
+ def increment_errors(exception)
70
+ @mutex.synchronize do
71
+ @errors_count += 1
72
+ @last_failure_time = Time.now
73
+
74
+ logger.debug("CircuitBreaker increment errors",
75
+ :errors_count => @errors_count,
76
+ :error_threshold => @error_threshold,
77
+ :exception => exception.class,
78
+ :message => exception.message) if logger.debug?
79
+ end
80
+ end
81
+
82
+ def state
83
+ @mutex.synchronize do
84
+ if @errors_count >= @error_threshold
85
+ if Time.now - @last_failure_time > @time_before_retry
86
+ :half_open
87
+ else
88
+ :open
89
+ end
90
+ else
91
+ :close
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -29,18 +29,23 @@ class LogStash::Inputs::Lumberjack < LogStash::Inputs::Base
29
29
  # SSL key passphrase to use.
30
30
  config :ssl_key_passphrase, :validate => :password
31
31
 
32
- # Number of maximum clients that the lumberjack input will accept, this allow you
33
- # to control the back pressure up to the client and stop logstash to go OOM with
34
- # connection. This settings is a temporary solution and will be deprecated really soon.
35
- config :max_clients, :validate => :number, :default => 1000, :deprecated => true
32
+ # The lumberjack input using a fixed thread pool to do the actual work and
33
+ # will accept a number of client in a queue, before starting to refuse new
34
+ # connection. This solve an issue when logstash-forwarder clients are
35
+ # trying to connect to logstash which have a blocked pipeline and will
36
+ # make logstash crash with an out of memory exception.
37
+ config :max_clients, :validate => :number, :default => 1000
36
38
 
37
39
  # TODO(sissel): Add CA to authenticate clients with.
38
40
 
39
- public
41
+ BUFFERED_QUEUE_SIZE = 20
42
+ RECONNECT_BACKOFF_SLEEP = 0.5
43
+
40
44
  def register
41
45
  require "lumberjack/server"
42
- require "concurrent"
43
46
  require "concurrent/executors"
47
+ require "logstash/circuit_breaker"
48
+ require "logstash/sized_queue_timeout"
44
49
 
45
50
  @logger.info("Starting lumberjack input listener", :address => "#{@host}:#{@port}")
46
51
  @lumberjack = Lumberjack::Server.new(:address => @host, :port => @port,
@@ -48,6 +53,7 @@ class LogStash::Inputs::Lumberjack < LogStash::Inputs::Base
48
53
  :ssl_key_passphrase => @ssl_key_passphrase)
49
54
 
50
55
  # Limit the number of thread that can be created by the
56
+ # Limit the number of thread that can be created by the
51
57
  # lumberjack output, if the queue is full the input will
52
58
  # start rejecting new connection and raise an exception
53
59
  @threadpool = Concurrent::ThreadPoolExecutor.new(
@@ -56,37 +62,59 @@ class LogStash::Inputs::Lumberjack < LogStash::Inputs::Base
56
62
  :max_queue => 1, # in concurrent-ruby, bounded queue need to be at least 1.
57
63
  fallback_policy: :abort
58
64
  )
65
+ @threadpool = Concurrent::CachedThreadPool.new(:idletime => 15)
66
+
67
+ # in 1.5 the main SizeQueue doesnt have the concept of timeout
68
+ # We are using a small plugin buffer to move events to the internal queue
69
+ @buffered_queue = LogStash::SizedQueueTimeout.new(BUFFERED_QUEUE_SIZE)
70
+
71
+ @circuit_breaker = LogStash::CircuitBreaker.new("Lumberjack input",
72
+ :exceptions => [LogStash::SizedQueueTimeout::TimeoutError])
73
+
59
74
  end # def register
60
75
 
61
76
  def run(output_queue)
77
+ start_buffer_broker(output_queue)
78
+
62
79
  while true do
63
- accept do |connection, codec|
64
- invoke(connection, codec) do |_codec, line, fields|
65
- _codec.decode(line) do |event|
66
- decorate(event)
67
- fields.each { |k,v| event[k] = v; v.force_encoding(Encoding::UTF_8) }
68
- output_queue << event
80
+ begin
81
+ # Wrapping the accept call into a CircuitBreaker
82
+ if @circuit_breaker.closed?
83
+ connection = @lumberjack.accept # Blocking call that creates a new connection
84
+
85
+ invoke(connection, codec.clone) do |_codec, line, fields|
86
+ _codec.decode(line) do |event|
87
+ decorate(event)
88
+ fields.each { |k,v| event[k] = v; v.force_encoding(Encoding::UTF_8) }
89
+
90
+ @circuit_breaker.execute { @buffered_queue << event }
91
+ end
69
92
  end
93
+ else
94
+ @logger.warn("Lumberjack input: the pipeline is blocked, temporary refusing new connection.")
95
+ sleep(RECONNECT_BACKOFF_SLEEP)
70
96
  end
97
+ # When too many errors happen inside the circuit breaker it will throw
98
+ # this exception and start refusing connection, we need to catch it but
99
+ # it's safe to ignore.
100
+ rescue LogStash::CircuitBreaker::OpenBreaker => e
71
101
  end
72
102
  end
103
+ rescue LogStash::ShutdownSignal
104
+ @logger.info("Lumberjack input: received ShutdownSignal")
73
105
  rescue => e
74
- @logger.error("Exception in lumberjack input", :exception => e)
106
+ @logger.error("Lumberjack input: unhandled exception", :exception => e, :backtrace => e.backtrace)
107
+ ensure
75
108
  shutdown(output_queue)
76
109
  end # def run
77
110
 
78
111
  private
79
112
  def accept(&block)
80
113
  connection = @lumberjack.accept # Blocking call that creates a new connection
81
-
82
- if @threadpool.length < @threadpool.max_length
83
- block.call(connection, @codec.clone)
84
- else
85
- @logger.warn("Lumberjack input, maximum connection exceeded, new connection are rejected.", :max_clients => @max_clients)
86
- connection.close
87
- end
114
+ block.call(connection, @codec.clone)
88
115
  end
89
116
 
117
+ private
90
118
  def invoke(connection, codec, &block)
91
119
  @threadpool.post do
92
120
  begin
@@ -98,4 +126,12 @@ class LogStash::Inputs::Lumberjack < LogStash::Inputs::Base
98
126
  end
99
127
  end
100
128
  end
129
+
130
+ def start_buffer_broker(output_queue)
131
+ @threadpool.post do
132
+ while true
133
+ output_queue << @buffered_queue.pop_no_timeout
134
+ end
135
+ end
136
+ end
101
137
  end # class LogStash::Inputs::Lumberjack
@@ -0,0 +1,58 @@
1
+ require "concurrent/atomic/condition"
2
+ require "thread"
3
+
4
+ module LogStash
5
+ # Minimal subset implement of a SizedQueue supporting
6
+ # a timeout option on the lock.
7
+ #
8
+ # This will be part of the main Logstash's sized queue
9
+ class SizedQueueTimeout
10
+ class TimeoutError < StandardError; end
11
+
12
+ DEFAULT_TIMEOUT = 2 # in seconds
13
+
14
+ def initialize(max_size, options = {})
15
+ @condition_in = Concurrent::Condition.new
16
+ @condition_out = Concurrent::Condition.new
17
+
18
+ @max_size = max_size
19
+ @queue = []
20
+ @mutex = Mutex.new
21
+ end
22
+
23
+ def push(obj, timeout = DEFAULT_TIMEOUT)
24
+ @mutex.synchronize do
25
+ while full? # wake up check
26
+ result = @condition_out.wait(@mutex, timeout)
27
+ raise TimeoutError if result.timed_out?
28
+ end
29
+
30
+ @queue << obj
31
+ @condition_in.signal
32
+
33
+ return obj
34
+ end
35
+ end
36
+ alias_method :<<, :push
37
+
38
+ def size
39
+ @mutex.synchronize { @queue.size }
40
+ end
41
+
42
+ def pop_no_timeout
43
+ @mutex.synchronize do
44
+ @condition_in.wait(@mutex) while @queue.empty? # Wake up check
45
+
46
+ obj = @queue.shift
47
+ @condition_out.signal
48
+
49
+ return obj
50
+ end
51
+ end
52
+
53
+ private
54
+ def full?
55
+ @queue.size == @max_size
56
+ end
57
+ end
58
+ end
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-input-lumberjack'
4
- s.version = '0.1.9'
4
+ s.version = '0.1.10'
5
5
  s.licenses = ['Apache License (2.0)']
6
6
  s.summary = "Receive events using the lumberjack protocol."
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"
@@ -29,5 +29,7 @@ Gem::Specification.new do |s|
29
29
  s.add_development_dependency 'logstash-devutils'
30
30
  s.add_development_dependency 'stud'
31
31
  s.add_development_dependency 'logstash-codec-multiline'
32
+ s.add_development_dependency "flores"
33
+ s.add_development_dependency "stud"
32
34
  end
33
35
 
@@ -6,7 +6,6 @@ require "logstash/codecs/plain"
6
6
  require "logstash/codecs/multiline"
7
7
  require "logstash/event"
8
8
  require "lumberjack/client"
9
- require_relative "../support/logstash_test"
10
9
 
11
10
  describe LogStash::Inputs::Lumberjack do
12
11
  let(:connection) { double("connection") }
@@ -47,58 +46,4 @@ describe LogStash::Inputs::Lumberjack do
47
46
  end
48
47
  end
49
48
  end
50
-
51
- context "when we have the maximum clients connected" do
52
- let(:max_clients) { 1 }
53
- let(:window_size) { 1 }
54
- let(:config) do
55
- {
56
- "port" => port,
57
- "ssl_certificate" => certificate.ssl_cert,
58
- "ssl_key" => certificate.ssl_key,
59
- "type" => "testing",
60
- "max_clients" => max_clients
61
- }
62
- end
63
-
64
- let(:client_options) do
65
- {
66
- :port => port,
67
- :address => "127.0.0.1",
68
- :ssl_certificate => certificate.ssl_cert,
69
- :window_size => window_size
70
- }
71
- end
72
-
73
- before do
74
- lumberjack.register
75
-
76
- @server = Thread.new do
77
- lumberjack.run(queue)
78
- end
79
-
80
- sleep(0.1) # wait for the server to correctly accept messages
81
- end
82
-
83
- after do
84
- @server.raise(LogStash::ShutdownSignal)
85
- @server.join
86
- end
87
-
88
- it "stops accepting new connection" do
89
- client1 = Lumberjack::Socket.new(client_options)
90
-
91
- # Since the connection is stopped on the other side and OS X and
92
- # linux doesn't behave the same. The client could raise a IOError
93
- # or an SSLError. On OSX I had to try to send some data to trip
94
- # the error.
95
- expect {
96
- client2 = Lumberjack::Socket.new(client_options)
97
-
98
- (window_size + 1).times do
99
- client2.write_hash({"line" => "message"})
100
- end
101
- }.to raise_error
102
- end
103
- end
104
49
  end
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+ require "logstash/circuit_breaker"
3
+
4
+ class DummyErrorTest < StandardError; end
5
+
6
+ describe LogStash::CircuitBreaker do
7
+ let(:error_threshold) { 1 }
8
+ let(:options) do
9
+ {
10
+ :exceptions => [DummyErrorTest],
11
+ :error_threshold => error_threshold
12
+ }
13
+ end
14
+
15
+ subject { LogStash::CircuitBreaker.new("testing", options) }
16
+
17
+
18
+ it "closed by default" do
19
+ expect(subject.closed?).to eq(true)
20
+ end
21
+
22
+ context "when having too many errors" do
23
+ let(:future_time) { Time.now + 3600 }
24
+ before do
25
+ subject.execute do
26
+ raise DummyErrorTest
27
+ end
28
+ end
29
+
30
+ it "raised an exception if we have too many errors" do
31
+ expect {
32
+ subject.execute do
33
+ raise DummyErrorTest
34
+ end
35
+ }.to raise_error(LogStash::CircuitBreaker::OpenBreaker)
36
+ end
37
+
38
+ it "sets the breaker to open" do
39
+ expect(subject.closed?).to eq(false)
40
+ end
41
+
42
+ it "resets the breaker after the time before retry" do
43
+ expect(Time).to receive(:now).at_least(1).and_return(future_time)
44
+ expect(subject.closed?).to eq(true)
45
+ end
46
+
47
+ it "doesnt run the command" do
48
+ runned = false
49
+
50
+ begin
51
+ subject.execute do
52
+ runned = true
53
+ end
54
+ rescue LogStash::CircuitBreaker::OpenBreaker
55
+ end
56
+
57
+ expect(runned).to eq(false)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,100 @@
1
+ require "spec_helper"
2
+ require "logstash/sized_queue_timeout"
3
+ require "flores/random"
4
+ require "stud/try"
5
+
6
+ describe "LogStash::SizedQueueTimeout" do
7
+ let(:max_size) { Flores::Random.integer(2..100) }
8
+ let(:element) { Flores::Random.text(0..100) }
9
+
10
+ subject { LogStash::SizedQueueTimeout.new(max_size) }
11
+
12
+ it "adds element to the queue" do
13
+ subject << element
14
+ expect(subject.size).to eq(1)
15
+ end
16
+
17
+ it "allow to pop element from the queue" do
18
+ subject << element
19
+ subject << "awesome"
20
+
21
+ expect(subject.pop_no_timeout).to eq(element)
22
+ end
23
+
24
+ context "when the queue is full" do
25
+ before do
26
+ max_size.times { subject << element }
27
+ end
28
+
29
+ it "block with a timeout" do
30
+ expect {
31
+ subject << element
32
+ }.to raise_error(LogStash::SizedQueueTimeout::TimeoutError)
33
+ end
34
+
35
+ it "unblock when we pop" do
36
+ blocked = Thread.new do
37
+ subject << element
38
+ end
39
+ sleep(0.1) until blocked.stop?
40
+
41
+ expect(blocked.status).to eq("sleep")
42
+
43
+ th = Thread.new do
44
+ subject.pop_no_timeout
45
+ end
46
+ sleep(0.1) until th.stop?
47
+
48
+ expect(blocked.status).to eq(false)
49
+
50
+ blocked.join
51
+ th.join
52
+ end
53
+ end
54
+
55
+ context "when the queue is empty" do
56
+ it "block on pop" do
57
+ blocked = Thread.new do
58
+ subject.pop_no_timeout
59
+ end
60
+ sleep(0.1) until blocked.stop?
61
+
62
+ expect(blocked.status).to eq("sleep")
63
+
64
+ th = Thread.new do
65
+ subject << element
66
+ end
67
+ sleep(0.1) until th.stop?
68
+
69
+ expect(blocked.status).to eq(false)
70
+ th.join
71
+ blocked.join
72
+ end
73
+ end
74
+
75
+ context "when the queue is occupied but not full" do
76
+ before :each do
77
+ Flores::Random.iterations(0..max_size) { subject << "hurray" }
78
+ end
79
+
80
+ it "doesnt block on pop" do
81
+ th = Thread.new do
82
+ subject.pop_no_timeout
83
+ end
84
+ sleep(0.1) until th.stop?
85
+
86
+ expect(th.status).to eq(false)
87
+ th.join
88
+ end
89
+
90
+ it "doesnt block on push" do
91
+ th = Thread.new do
92
+ subject << element
93
+ end
94
+ sleep(0.1) until th.stop?
95
+
96
+ expect(th.status).to eq(false)
97
+ th.join
98
+ end
99
+ end
100
+ end
@@ -1,2 +1,3 @@
1
1
  require "logstash/devutils/rspec/spec_helper"
2
2
  require "logstash/codecs/plain"
3
+ require_relative "support/logstash_test"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-input-lumberjack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.10
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-12 00:00:00.000000000 Z
11
+ date: 2015-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logstash-core
@@ -114,6 +114,34 @@ dependencies:
114
114
  version: '0'
115
115
  prerelease: false
116
116
  type: :development
117
+ - !ruby/object:Gem::Dependency
118
+ name: flores
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirement: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ prerelease: false
130
+ type: :development
131
+ - !ruby/object:Gem::Dependency
132
+ name: stud
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirement: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ prerelease: false
144
+ type: :development
117
145
  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
118
146
  email: info@elastic.co
119
147
  executables: []
@@ -125,11 +153,16 @@ files:
125
153
  - CONTRIBUTORS
126
154
  - Gemfile
127
155
  - LICENSE
156
+ - NOTICE.TXT
128
157
  - README.md
129
158
  - Rakefile
159
+ - lib/logstash/circuit_breaker.rb
130
160
  - lib/logstash/inputs/lumberjack.rb
161
+ - lib/logstash/sized_queue_timeout.rb
131
162
  - logstash-input-lumberjack.gemspec
132
163
  - spec/inputs/lumberjack_spec.rb
164
+ - spec/logstash/circuit_breaker_spec.rb
165
+ - spec/logstash/size_queue_timeout_spec.rb
133
166
  - spec/spec_helper.rb
134
167
  - spec/support/logstash_test.rb
135
168
  homepage: http://www.elastic.co/guide/en/logstash/current/index.html
@@ -160,5 +193,7 @@ specification_version: 4
160
193
  summary: Receive events using the lumberjack protocol.
161
194
  test_files:
162
195
  - spec/inputs/lumberjack_spec.rb
196
+ - spec/logstash/circuit_breaker_spec.rb
197
+ - spec/logstash/size_queue_timeout_spec.rb
163
198
  - spec/spec_helper.rb
164
199
  - spec/support/logstash_test.rb