Skip to content

Commit a91e339

Browse files
committed
use nonblocking i/o to control socket timeouts
1 parent b6edb44 commit a91e339

File tree

4 files changed

+94
-8
lines changed

4 files changed

+94
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#### 1.1.0
22

33
* Renamed `cert` and `key` to `client_cert` and `client_key` respectively.
4+
* Change to short timeouts on network calls so logging doesn't go dead for extended periods.
45

56

67
#### 1.0.0

fluent-plugin-syslog-tls.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Gem::Specification.new do |s|
3030
s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
3131
s.test_files = s.files.grep(%r{^(test|spec|features)/})
3232
s.require_paths = ['lib']
33-
s.required_ruby_version = '>= 2.0.0'
33+
s.required_ruby_version = '>= 2.3.0'
3434

3535
s.add_runtime_dependency 'fluentd', '~> 0.14'
3636
s.add_runtime_dependency 'fluent-mixin-config-placeholders', '~> 0.3'

lib/syslog_tls/ssl_transport.rb

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
module SyslogTls
2020
# Supports SSL connection to remote host
2121
class SSLTransport
22+
CONNECT_TIMEOUT = 10
23+
# READ_TIMEOUT = 5
24+
WRITE_TIMEOUT = 5
25+
2226
attr_accessor :socket
2327

2428
attr_reader :host, :port, :client_cert, :client_key, :ssl_version
@@ -37,28 +41,74 @@ def initialize(host, port, client_cert: nil, client_key: nil, ssl_version: :TLSv
3741

3842
def connect
3943
@socket = get_ssl_connection
40-
@socket.connect
44+
begin
45+
begin
46+
@socket.connect_nonblock
47+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
48+
select_with_timeout(@socket, :connect_read) && retry
49+
rescue IO::WaitWritable
50+
select_with_timeout(@socket, :connect_write) && retry
51+
end
52+
rescue Errno::ETIMEDOUT
53+
raise 'Socket timeout during connect'
54+
end
55+
end
56+
57+
def get_tcp_connection
58+
tcp = nil
59+
60+
family = Socket::Constants::AF_UNSPEC
61+
sock_type = Socket::Constants::SOCK_STREAM
62+
addr_info = Socket.getaddrinfo(host, port, family, sock_type, nil, nil, false).first
63+
_, port, _, address, family, sock_type = addr_info
64+
65+
begin
66+
sock_addr = Socket.sockaddr_in(port, address)
67+
tcp = Socket.new(family, sock_type, 0)
68+
tcp.setsockopt(Socket::SOL_SOCKET, Socket::Constants::SO_REUSEADDR, true)
69+
tcp.setsockopt(Socket::SOL_SOCKET, Socket::Constants::SO_REUSEPORT, true)
70+
tcp.connect_nonblock(sock_addr)
71+
rescue Errno::EINPROGRESS
72+
select_with_timeout(tcp, :connect_write)
73+
begin
74+
tcp.connect_nonblock(sock_addr)
75+
rescue Errno::EISCONN
76+
# all good
77+
rescue SystemCallError
78+
tcp.close rescue nil
79+
raise
80+
end
81+
rescue SystemCallError
82+
tcp.close rescue nil
83+
raise
84+
end
85+
86+
tcp.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
87+
tcp
4188
end
4289

4390
def get_ssl_connection
44-
tcp = TCPSocket.new(host, port)
91+
tcp = get_tcp_connection
4592

4693
ctx = OpenSSL::SSL::SSLContext.new
47-
ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
94+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
4895
ctx.ssl_version = ssl_version
4996

5097
ctx.cert = OpenSSL::X509::Certificate.new(File.read(client_cert)) if client_cert
5198
ctx.key = OpenSSL::PKey::read(File.read(client_key)) if client_key
52-
OpenSSL::SSL::SSLSocket.new(tcp, ctx)
99+
socket = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
100+
socket.sync_close = true
101+
socket
53102
end
54103

55104
# Allow to retry on failed writes
56105
def write(s)
57106
begin
58107
retry_id ||= 0
59-
@socket.send(:write, s)
108+
do_write(s)
60109
rescue => e
61110
if (retry_id += 1) < @retries
111+
@socket.close rescue nil
62112
connect
63113
retry
64114
else
@@ -67,6 +117,41 @@ def write(s)
67117
end
68118
end
69119

120+
def do_write(data)
121+
data.force_encoding('BINARY') # so we can break in the middle of multi-byte characters
122+
loop do
123+
sent = 0
124+
begin
125+
sent = @socket.write_nonblock(data)
126+
rescue OpenSSL::SSL::SSLError, Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable => e
127+
if e.is_a?(OpenSSL::SSL::SSLError) && e.message !~ /write would block/
128+
raise e
129+
else
130+
select_with_timeout(@socket, :write) && retry
131+
end
132+
end
133+
134+
break if sent >= data.size
135+
data = data[sent, data.size]
136+
end
137+
end
138+
139+
def select_with_timeout(tcp, type)
140+
o = case type
141+
when :connect_read
142+
args = [[tcp], nil, nil, CONNECT_TIMEOUT]
143+
when :connect_write
144+
args = [nil, [tcp], nil, CONNECT_TIMEOUT]
145+
# when :read
146+
# args = [[tcp], nil, nil, READ_TIMEOUT]
147+
when :write
148+
args = [nil, [tcp], nil, WRITE_TIMEOUT]
149+
else
150+
raise "Unknown select type #{type}"
151+
end
152+
IO.select(*args) || raise("Socket timeout during #{type}")
153+
end
154+
70155
# Forward any methods directly to SSLSocket
71156
def method_missing(method_sym, *arguments, &block)
72157
@socket.send(method_sym, *arguments, &block)

test/syslog_tls/test_ssl_transport.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ def test_ok_connection
3737

3838
def test_retry
3939
client = Object.new
40-
def client.connect
40+
def client.connect_nonblock
4141
true
4242
end
43-
def client.write(s)
43+
def client.write_nonblock(s)
4444
raise "Test"
4545
end
4646

0 commit comments

Comments
 (0)