Skip to content

Commit 159d3ec

Browse files
authored
feat: I/O timeouts (#1025)
1 parent 5261fb9 commit 159d3ec

File tree

2 files changed

+44
-5
lines changed

2 files changed

+44
-5
lines changed

lib/dalli/socket.rb

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,19 +117,33 @@ def self.create_socket_with_timeout(host, port, options)
117117
end
118118

119119
def self.init_socket_options(sock, options)
120+
configure_tcp_options(sock, options)
121+
configure_socket_buffers(sock, options)
122+
configure_timeout(sock, options)
123+
end
124+
125+
def self.configure_tcp_options(sock, options)
120126
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
121127
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options[:keepalive]
128+
end
129+
130+
def self.configure_socket_buffers(sock, options)
122131
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
123132
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
133+
end
124134

135+
def self.configure_timeout(sock, options)
125136
return unless options[:socket_timeout]
126137

127-
seconds, fractional = options[:socket_timeout].divmod(1)
128-
microseconds = fractional * 1_000_000
129-
timeval = [seconds, microseconds].pack('l_2')
138+
if sock.respond_to?(:timeout=)
139+
sock.timeout = options[:socket_timeout]
140+
else
141+
seconds, fractional = options[:socket_timeout].divmod(1)
142+
timeval = [seconds, fractional * 1_000_000].pack('l_2')
130143

131-
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
132-
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
144+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
145+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
146+
end
133147
end
134148

135149
def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
@@ -168,9 +182,16 @@ def self.open(path, options = {})
168182
Timeout.timeout(options[:socket_timeout]) do
169183
sock = new(path)
170184
sock.options = { path: path }.merge(options)
185+
init_socket_options(sock, options)
171186
sock
172187
end
173188
end
189+
190+
def self.init_socket_options(sock, options)
191+
# https://man7.org/linux/man-pages/man7/unix.7.html
192+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
193+
sock.timeout = options[:socket_timeout] if options[:socket_timeout] && sock.respond_to?(:timeout=)
194+
end
174195
end
175196
end
176197
end

test/integration/test_network.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,24 @@
7474
end
7575
end
7676

77+
it 'handles operation timeouts' do
78+
next if p == :binary
79+
80+
memcached_mock(lambda { |sock|
81+
# handle initial version call
82+
sock.gets
83+
sock.write("VERSION 1.6.0\r\n")
84+
85+
sleep(0.3)
86+
}) do
87+
dc = Dalli::Client.new('localhost:19123', socket_timeout: 0.1, protocol: p, socket_max_failures: 0,
88+
socket_failure_delay: 0.0, down_retry_delay: 0.0)
89+
assert_raises Dalli::RingError, message: 'No server available' do
90+
dc.get('abc')
91+
end
92+
end
93+
end
94+
7795
it 'opens a standard TCP connection when ssl_context is not configured' do
7896
memcached_persistent(p) do |dc|
7997
server = dc.send(:ring).servers.first

0 commit comments

Comments
 (0)