Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

out_http: Add option to reuse connections #4330

Merged
merged 7 commits into from Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 48 additions & 7 deletions lib/fluent/plugin/out_http.rb
Expand Up @@ -37,6 +37,8 @@ class HTTPOutput < Output

class RetryableResponse < StandardError; end

ConnectionCache = Struct.new(:uri, :conn)

helpers :formatter

desc 'The endpoint for HTTP request, e.g. http://example.com/api'
Expand All @@ -60,6 +62,8 @@ class RetryableResponse < StandardError; end
config_param :read_timeout, :integer, default: nil
desc 'The TLS timeout in seconds'
config_param :ssl_timeout, :integer, default: nil
desc 'Try to reuse connections'
config_param :reuse_connections, :bool, default: false

desc 'The CA certificate path for TLS'
config_param :tls_ca_cert_path, :string, default: nil
Expand Down Expand Up @@ -106,11 +110,23 @@ def initialize
@uri = nil
@proxy_uri = nil
@formatter = nil

@connection_cache = []
@connection_cache_id_mutex = Mutex.new
end

def close
super

@connection_cache.each {|entry| entry.conn.finish if entry.conn&.started? }
end

def configure(conf)
super

@connection_cache = Array.new(actual_flush_thread_count, ConnectionCache.new("", nil)) if @reuse_connections
@connection_cache_id = 0

if @retryable_response_codes.nil?
log.warn('Status code 503 is going to be removed from default `retryable_response_codes` from fluentd v2. Please add it by yourself if you wish')
@retryable_response_codes = [503]
Expand Down Expand Up @@ -302,16 +318,41 @@ def create_request(chunk, uri)
req
end

def make_request_cached(uri, req)
id = Thread.current.thread_variable_get(plugin_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not appropriate to use plugin_id as a key, it should be used with a suffix like "#{plugin_id}_connection_cache_id".

In addition, we prefer to use Thread#[] instead of Thread#thread_variable_get if there is no particular reason.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I agree.
Done in 9ce635c.

if id.nil?
@connection_cache_id_mutex.synchronize {
id = @connection_cache_id
@connection_cache_id += 1
}
Thread.current.thread_variable_set(plugin_id, id)
end
uri_str = uri.to_s
if @connection_cache[id].uri != uri_str
@connection_cache[id].conn.finish if @connection_cache[id].conn&.started?
http = if @proxy_uri
Net::HTTP.start(uri.host, uri.port, @proxy_uri.host, @proxy_uri.port, @proxy_uri.user, @proxy_uri.password, @http_opt)
else
Net::HTTP.start(uri.host, uri.port, @http_opt)
end
@connection_cache[id] = ConnectionCache.new(uri_str, http)
end
@connection_cache[id].conn.request(req)
end

def make_request(uri, req, &block)
if @proxy_uri
Net::HTTP.start(uri.host, uri.port, @proxy_uri.host, @proxy_uri.port, @proxy_uri.user, @proxy_uri.password, @http_opt, &block)
else
Net::HTTP.start(uri.host, uri.port, @http_opt, &block)
end
end

def send_request(uri, req)
res = if @proxy_uri
Net::HTTP.start(uri.host, uri.port, @proxy_uri.host, @proxy_uri.port, @proxy_uri.user, @proxy_uri.password, @http_opt) { |http|
http.request(req)
}
res = if @reuse_connections
make_request_cached(uri, req)
else
Net::HTTP.start(uri.host, uri.port, @http_opt) { |http|
http.request(req)
}
make_request(uri, req) { |http| http.request(req) }
end

if res.is_a?(Net::HTTPSuccess)
Expand Down
36 changes: 36 additions & 0 deletions test/plugin/test_out_http.rb
Expand Up @@ -518,4 +518,40 @@ def test_write_with_https
assert_not_empty result.headers
end
end

sub_test_case 'connection_reuse' do
def server_port
19883
end

def test_connection_recreation
d = create_driver(%[
endpoint http://127.0.0.1:#{server_port}/test
reuse_connections true
])

d.run(default_tag: 'test.http', shutdown: false) do
d.feed(test_events[0])
end

data = @@result.data

# Restart server to simulate connection loss
@@http_server_thread.kill
@@http_server_thread.join
@@http_server_thread = Thread.new do
run_http_server
end

d.run(default_tag: 'test.http') do
d.feed(test_events[1])
end

result = @@result
assert_equal 'POST', result.method
assert_equal 'application/x-ndjson', result.content_type
assert_equal test_events, data.concat(result.data)
assert_not_empty result.headers
end
end
end