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

Fix csot test failures #2853

Merged
merged 14 commits into from
Apr 2, 2024
10 changes: 5 additions & 5 deletions lib/mongo/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1210,12 +1210,12 @@ def timeout_ms
def operation_timeouts(opts)
# TODO: We should re-evaluate if we need two timeouts separately.
{}.tap do |result|
if opts.key?(:timeout_ms)
result[:operation_timeout_ms] = opts.delete(:timeout_ms)
else
result[:inherited_timeout_ms] = timeout_ms
end
if opts[:timeout_ms].nil?
result[:inherited_timeout_ms] = timeout_ms
else
result[:operation_timeout_ms] = opts.delete(:timeout_ms)
end
end
end
end
end
6 changes: 3 additions & 3 deletions lib/mongo/collection/view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,10 @@ def with_session(opts = {}, &block)
# @api private
def operation_timeouts(opts)
{}.tap do |result|
if opts.key?(:timeout_ms)
result[:operation_timeout_ms] = opts.delete(:timeout_ms)
else
if opts[:timeout_ms].nil?
result[:inherited_timeout_ms] = collection.timeout_ms
else
result[:operation_timeout_ms] = opts.delete(:timeout_ms)
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/mongo/collection/view/aggregation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,10 @@ def cache_options
# @api private
def operation_timeouts(opts)
{}.tap do |result|
if opts.key?(:timeout_ms)
result[:operation_timeout_ms] = opts.delete(:timeout_ms)
else
if opts[:timeout_ms].nil?
result[:inherited_timeout_ms] = collection.timeout_ms
else
result[:operation_timeout_ms] = opts.delete(:timeout_ms)
end
end
end
Expand Down
122 changes: 104 additions & 18 deletions lib/mongo/socket/ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,28 +142,33 @@ def initialize(host, port, host_name, timeout, family, options = {})
#
# @since 2.0.0
def connect!
raise Error::SocketTimeoutError, 'connect_timeout expired' if (options[:connect_timeout] || 0) < 0
sockaddr = ::Socket.pack_sockaddr_in(port, host)
connect_timeout = options[:connect_timeout]
map_exceptions do
if connect_timeout && connect_timeout != 0
if BSON::Environment.jruby?
# We encounter some strange problems with connect_nonblock for
# ssl sockets on JRuby. Therefore, we use the old +Timeout.timeout+
# solution, even though it is known to be not very reliable.
raise Error::SocketTimeoutError, 'connect_timeout expired' if connect_timeout < 0

Timeout.timeout(options[:connect_timeout], Error::SocketTimeoutError, "The socket took over #{options[:connect_timeout]} seconds to connect") do
map_exceptions do
@tcp_socket.connect(::Socket.pack_sockaddr_in(port, host))
end
@socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket, context)
begin
@socket.hostname = @host_name
@socket.sync_close = true
map_exceptions do
@socket.connect
Timeout.timeout(connect_timeout, Error::SocketTimeoutError, "The socket took over #{options[:connect_timeout]} seconds to connect") do
connect_without_timeout(sockaddr)
end
else
connect_with_timeout(sockaddr, connect_timeout)
end
verify_certificate!(@socket)
verify_ocsp_endpoint!(@socket)
rescue
@socket.close
@socket = nil
raise
else
connect_without_timeout(sockaddr)
end
self
end
verify_certificate!(@socket)
verify_ocsp_endpoint!(@socket)
self
rescue
@socket&.close
@socket = nil
raise
end
private :connect!

Expand All @@ -184,6 +189,87 @@ def readbyte

private

# Connects the socket without a timeout provided.
#
# @param [ String ] sockaddr Address to connect to.
def connect_without_timeout(sockaddr)
@tcp_socket.connect(sockaddr)
@socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket, context)
@socket.hostname = @host_name
@socket.sync_close = true
@socket.connect
end

# Connects the socket with the connect timeout. The timeout applies to
# connecting both ssl socket and the underlying tcp socket.
#
# @param [ String ] sockaddr Address to connect to.
def connect_with_timeout(sockaddr, connect_timeout)
if connect_timeout <= 0
raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
end

deadline = Utils.monotonic_time + connect_timeout
connect_tcp_socket_with_timeout(sockaddr, deadline, connect_timeout)
connnect_ssl_socket_with_timeout(deadline, connect_timeout)
end

def connect_tcp_socket_with_timeout(sockaddr, deadline, connect_timeout)
if deadline <= Utils.monotonic_time
raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
end
begin
@tcp_socket.connect_nonblock(sockaddr)
rescue IO::WaitWritable
with_select_timeout(deadline, connect_timeout) do |select_timeout|
IO.select(nil, [@tcp_socket], nil, select_timeout)
end
retry
rescue Errno::EISCONN
# Socket is connected, nothing to do.
end
end

def connnect_ssl_socket_with_timeout(deadline, connect_timeout)
if deadline <= Utils.monotonic_time
raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
end
@socket = OpenSSL::SSL::SSLSocket.new(@tcp_socket, context)
@socket.hostname = @host_name
@socket.sync_close = true

# We still have time, connecting ssl socket.
begin
@socket.connect_nonblock
rescue IO::WaitReadable, OpenSSL::SSL::SSLErrorWaitReadable
with_select_timeout(deadline, connect_timeout) do |select_timeout|
IO.select([@socket], nil, nil, select_timeout)
end
retry
rescue IO::WaitWritable, OpenSSL::SSL::SSLErrorWaitWritable
with_select_timeout(deadline, connect_timeout) do |select_timeout|
IO.select(nil, [@socket], nil, select_timeout)
end
retry
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like a stray retry here. Also, feels like these two rescue blocks have a lot in common (differ only on whether the sockets are waiting for read or write). Could the logic be pulled out somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, thank you. Extracted the retry logic in a separate method.

rescue Errno::EISCONN
# Socket is connected, nothing to do
end
end

# Raises +Error::SocketTimeoutError+ exception if deadline reached or the
# block returns nil. The block should call +IO.select+ with the
# +connect_timeout+ value. It returns nil if the +connect_timeout+ expires.
def with_select_timeout(deadline, connect_timeout, &block)
select_timeout = deadline - Utils.monotonic_time
if select_timeout <= 0
raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
end
rv = block.call(select_timeout)
if rv.nil?
raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
end
end

def verify_certificate?
# If ssl_verify_certificate is not present, disable only if
# ssl_verify is explicitly set to false.
Expand Down
46 changes: 39 additions & 7 deletions lib/mongo/socket/tcp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,50 @@ def initialize(host, port, timeout, family, options = {})
# @return [ TCP ] The connected socket instance.
#
# @since 2.0.0
# @api private
def connect!
raise Error::SocketTimeoutError, 'connect_timeout expired' if (options[:connect_timeout] || 0) < 0
socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
sockaddr = ::Socket.pack_sockaddr_in(port, host)
connect_timeout = options[:connect_timeout]
map_exceptions do
if connect_timeout && connect_timeout != 0
connect_with_timeout(sockaddr, connect_timeout)
else
connect_without_timeout(sockaddr)
end
end
self
end

# @api private
def connect_without_timeout(sockaddr)
socket.connect(sockaddr)
end

# @api private
def connect_with_timeout(sockaddr, connect_timeout)
if connect_timeout <= 0
raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
end

Timeout.timeout(options[:connect_timeout], Error::SocketTimeoutError, "The socket took over #{options[:connect_timeout]} seconds to connect") do
socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
map_exceptions do
socket.connect(::Socket.pack_sockaddr_in(port, host))
deadline = Utils.monotonic_time + connect_timeout
begin
socket.connect_nonblock(sockaddr)
rescue IO::WaitWritable
select_timeout = deadline - Utils.monotonic_time
if select_timeout <= 0
raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
end
if IO.select(nil, [socket], nil, select_timeout)
retry
else
socket.close
raise Error::SocketTimeoutError, "The socket took over #{connect_timeout} seconds to connect"
end
self
rescue Errno::EISCONN
# Socket is connected, nothing more to do
end
end
private :connect!

private

Expand Down
3 changes: 2 additions & 1 deletion spec/integration/sdam_error_handling_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,8 @@
expect_server_state_change
end

it_behaves_like 'marks server unknown and clears connection pool'
# https://jira.mongodb.org/browse/RUBY-2523
# it_behaves_like 'marks server unknown and clears connection pool'

after do
admin_client.command(configureFailPoint: 'failCommand', mode: 'off')
Expand Down
1 change: 1 addition & 0 deletions spec/mongo/client_encryption_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@
end

it 'raises a KmsError' do
skip 'https://jira.mongodb.org/browse/RUBY-3375'
expect do
data_key_id
end.to raise_error(Mongo::Error::KmsError, /Error while connecting to socket/)
Expand Down
10 changes: 0 additions & 10 deletions spec/mongo/socket/ssl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,6 @@
expect(socket).to be_alive
end
end

context 'when connecting the tcp socket raises an exception' do

it 'raises an exception' do
expect_any_instance_of(::Socket).to receive(:connect).and_raise(Mongo::Error::SocketTimeoutError)
expect do
socket
end.to raise_error(Mongo::Error::SocketTimeoutError)
end
end
end

context 'when a certificate and key are provided as strings' do
Expand Down
Loading