Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/dalli/protocol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,15 @@ module Protocol
else
[Timeout::Error]
end

# SSL errors that occur during read/write operations (not during initial
# handshake) should trigger reconnection. These indicate transient network
# issues, not configuration problems.
SSL_ERRORS =
if defined?(OpenSSL::SSL::SSLError)
[OpenSSL::SSL::SSLError]
else
[]
end
end
end
2 changes: 1 addition & 1 deletion lib/dalli/protocol/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def pipeline_next_responses
end

values
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
@connection_manager.error_on_request!(e)
end

Expand Down
6 changes: 3 additions & 3 deletions lib/dalli/protocol/connection_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,19 +150,19 @@ def read_line
data = @sock.gets("\r\n")
error_on_request!('EOF in read_line') if data.nil?
data
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
error_on_request!(e)
end

def read(count)
@sock.readfull(count)
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
error_on_request!(e)
end

def write(bytes)
@sock.write(bytes)
rescue SystemCallError, *TIMEOUT_ERRORS => e
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
error_on_request!(e)
end

Expand Down
47 changes: 47 additions & 0 deletions test/integration/test_network.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,53 @@
end
end

it 'handles SSL error during read operations' do
with_nil_logger do
memcached(p, 19_191) do |dc|
# First set a value so we have something to get
dc.set('ssl_test_key', 'test_value')

server = dc.send(:ring).server_for_key('ssl_test_key')

# Stub error_on_request! to verify SSLError triggers error handling
# This confirms the SSL error is caught by the rescue clause
error_handled = false
original_error_on_request = server.instance_variable_get(:@connection_manager).method(:error_on_request!)

server.instance_variable_get(:@connection_manager).define_singleton_method(:error_on_request!) do |err|
error_handled = true if err.is_a?(OpenSSL::SSL::SSLError)
original_error_on_request.call(err)
end

ssl_error = OpenSSL::SSL::SSLError.new('SSL_read: unexpected eof while reading')

# Binary protocol uses readfull for reading, meta protocol uses gets
stub_method = p == :binary ? :readfull : :gets
server.sock.stub(stub_method, proc { raise ssl_error }) do
# The operation will retry with a new connection and may succeed
# What matters is the SSLError is caught, not propagated
dc.get('ssl_test_key')
rescue Dalli::NetworkError
# Expected if retries exhausted
end

assert error_handled, 'SSLError should trigger error_on_request!'
end
end
end

it 'handles SSL error during pipelined get' do
with_nil_logger do
memcached(p, 19_191) do |dc|
ssl_error = OpenSSL::SSL::SSLError.new('SSL_read: unexpected eof while reading')

dc.send(:ring).server_for_key('abc').sock.stub(:write, proc { raise ssl_error }) do
assert_empty dc.get_multi(['abc'])
end
end
end
end

it 'handles asynchronous Thread#raise' do
with_nil_logger do
memcached(p, 19_191) do |dc|
Expand Down