Skip to content

Commit 87cc6c2

Browse files
p-mongop
andauthored
RUBY-2132 add address, connection generation & monitoring flag fields to sockets for diagnostics (#1946)
* rename address to human_address * rework socket closes in encryption i/o * track connection address, generation and monitoring flag on sockets * move constructors to the top * extract a base constructor * add a socket summary method * fix unit test * fix ssl socket tests Co-authored-by: Oleg Pudeyev <oleg@bsdpower.com>
1 parent 3d445c9 commit 87cc6c2

File tree

12 files changed

+198
-104
lines changed

12 files changed

+198
-104
lines changed

lib/mongo/address.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def socket(socket_timeout, ssl_options = {}, options = {})
192192

193193
options = {
194194
connect_timeout: Server::CONNECT_TIMEOUT,
195-
}.update(options)
195+
}.update(Hash[options.map { |k, v| [k.to_sym, v] }])
196196

197197
# When the driver connects to "localhost", it only attempts IPv4
198198
# connections. When the driver connects to other hosts, it will

lib/mongo/crypt/encryption_io.rb

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -251,38 +251,55 @@ def with_ssl_socket(endpoint)
251251
host, port = endpoint.split(':')
252252
port ||= 443 # Default port for AWS KMS API
253253

254+
# Create TCPSocket and set nodelay option
255+
tcp_socket = TCPSocket.open(host, port)
254256
begin
255-
# Create TCPSocket and set nodelay option
256-
tcp_socket = TCPSocket.open(host, port)
257257
tcp_socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
258258

259259
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket)
260-
ssl_socket.sync_close = true # tcp_socket will be closed when ssl_socket is closed
261-
ssl_socket.hostname = "#{host}:#{port}" # perform SNI
260+
begin
261+
# tcp_socket will be closed when ssl_socket is closed
262+
ssl_socket.sync_close = true
263+
# perform SNI
264+
ssl_socket.hostname = "#{host}:#{port}"
265+
266+
Timeout.timeout(
267+
SOCKET_TIMEOUT,
268+
Error::SocketTimeoutError,
269+
"KMS socket connection timed out after #{SOCKET_TIMEOUT} seconds",
270+
) do
271+
ssl_socket.connect
272+
end
262273

263-
Timeout.timeout(
264-
SOCKET_TIMEOUT,
265-
Error::SocketTimeoutError,
266-
'Socket connection timed out'
267-
) do
268-
ssl_socket.connect
274+
yield(ssl_socket)
275+
ensure
276+
begin
277+
Timeout.timeout(
278+
SOCKET_TIMEOUT,
279+
Error::SocketTimeoutError,
280+
'KMS SSL socket close timed out'
281+
) do
282+
ssl_socket.sysclose
283+
end
284+
rescue
285+
end
269286
end
270-
271-
yield(ssl_socket)
272-
rescue => e
273-
raise Error::KmsError, "Error decrypting data key. #{e.class}: #{e.message}"
274287
ensure
275-
# If there is an error during socket creation, the
276-
# ssl_socket object won't exist in this scope and this line will
277-
# raise an exception
278-
Timeout.timeout(
279-
SOCKET_TIMEOUT,
280-
Error::SocketTimeoutError,
281-
'Socket close timed out'
282-
) do
283-
ssl_socket.sysclose rescue nil
288+
# Still close tcp socket manually in case ssl socket creation
289+
# fails.
290+
begin
291+
Timeout.timeout(
292+
SOCKET_TIMEOUT,
293+
Error::SocketTimeoutError,
294+
'KMS TCP socket close timed out'
295+
) do
296+
tcp_socket.close
297+
end
298+
rescue
284299
end
285300
end
301+
rescue => e
302+
raise Error::KmsError, "Error decrypting data key: #{e.class}: #{e.message}"
286303
end
287304
end
288305
end

lib/mongo/server/connection.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ def connect!
181181
# returned socket.
182182
private def do_connect
183183
socket = add_server_diagnostics do
184-
address.socket(socket_timeout, ssl_options, address.options)
184+
address.socket(socket_timeout, ssl_options, address.options.merge(
185+
connection_address: address, connection_generation: generation).update(ssl_options))
185186
end
186187

187188
begin

lib/mongo/server/monitor/connection.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ def connect!
162162
end
163163

164164
@socket = add_server_diagnostics do
165-
address.socket(socket_timeout, ssl_options, address.options)
165+
address.socket(socket_timeout, ssl_options, address.options.merge(
166+
connection_address: address, monitor: true).update(ssl_options))
166167
end
167168
true
168169
end

lib/mongo/socket.rb

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@ class Socket
4545
# @api private
4646
WRITE_CHUNK_SIZE = 65536
4747

48+
# Initializes common socket attributes.
49+
#
50+
# @param [ Float ] timeout The socket timeout value.
51+
# @param [ Hash ] options The options.
52+
#
53+
# @option options [ Float ] :connect_timeout Connect timeout.
54+
# @option options [ Address ] :connection_address Address of the
55+
# connection that created this socket.
56+
# @option options [ Integer ] :connection_generation Generation of the
57+
# connection (for non-monitoring connections) that created this socket.
58+
# @option options [ true | false ] :monitor Whether this socket was
59+
# created by a monitoring connection.
60+
#
61+
# @api private
62+
def initialize(timeout, options)
63+
@timeout = timeout
64+
@options = options
65+
end
66+
4867
# @return [ Integer ] family The type of host family.
4968
attr_reader :family
5069

@@ -57,6 +76,41 @@ class Socket
5776
# @return [ Float ] timeout The socket timeout.
5877
attr_reader :timeout
5978

79+
# @return [ Address ] Address of the connection that created this socket.
80+
#
81+
# @api private
82+
def connection_address
83+
options[:connection_address]
84+
end
85+
86+
# @return [ Integer ] Generation of the connection (for non-monitoring
87+
# connections) that created this socket.
88+
#
89+
# @api private
90+
def connection_generation
91+
options[:connection_generation]
92+
end
93+
94+
# @return [ true | false ] Whether this socket was created by a monitoring
95+
# connection.
96+
#
97+
# @api private
98+
def monitor?
99+
!!options[:monitor]
100+
end
101+
102+
# @return [ String ] Human-readable summary of the socket for debugging.
103+
#
104+
# @api private
105+
def summary
106+
fileno = @socket&.fileno rescue '<no socket>' || '<no socket>'
107+
if monitor?
108+
"#{connection_address}:m #{fileno}"
109+
else
110+
"#{connection_address}:c:#{connection_generation} #{fileno}"
111+
end
112+
end
113+
60114
# Is the socket connection alive?
61115
#
62116
# @example Is the socket alive?
@@ -350,15 +404,15 @@ def map_exceptions
350404
begin
351405
yield
352406
rescue Errno::ETIMEDOUT => e
353-
raise Error::SocketTimeoutError, "#{e.class}: #{e} (for #{address})"
407+
raise Error::SocketTimeoutError, "#{e.class}: #{e} (for #{human_address})"
354408
rescue IOError, SystemCallError => e
355-
raise Error::SocketError, "#{e.class}: #{e} (for #{address})"
409+
raise Error::SocketError, "#{e.class}: #{e} (for #{human_address})"
356410
rescue OpenSSL::SSL::SSLError => e
357-
raise Error::SocketError, "#{e.class}: #{e} (for #{address}) (#{SSL_ERROR})"
411+
raise Error::SocketError, "#{e.class}: #{e} (for #{human_address}) (#{SSL_ERROR})"
358412
end
359413
end
360414

361-
def address
415+
def human_address
362416
raise NotImplementedError
363417
end
364418
end

lib/mongo/socket/ssl.rb

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,42 @@ class Socket
2121
class SSL < Socket
2222
include OpenSSL
2323

24+
# Initializes a new SSL socket.
25+
#
26+
# @example Create the SSL socket.
27+
# SSL.new('::1', 27017, 30)
28+
#
29+
# @param [ String ] host The hostname or IP address.
30+
# @param [ Integer ] port The port number.
31+
# @param [ Float ] timeout The socket timeout value.
32+
# @param [ Integer ] family The socket family.
33+
# @param [ Hash ] options The options.
34+
#
35+
# @option options [ Float ] :connect_timeout Connect timeout.
36+
# @option options [ Address ] :connection_address Address of the
37+
# connection that created this socket.
38+
# @option options [ Integer ] :connection_generation Generation of the
39+
# connection (for non-monitoring connections) that created this socket.
40+
# @option options [ true | false ] :monitor Whether this socket was
41+
# created by a monitoring connection.
42+
#
43+
# @since 2.0.0
44+
def initialize(host, port, host_name, timeout, family, options = {})
45+
super(timeout, options)
46+
@host, @port, @host_name = host, port, host_name
47+
@context = create_context(options)
48+
@family = family
49+
@tcp_socket = ::Socket.new(family, SOCK_STREAM, 0)
50+
begin
51+
@tcp_socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
52+
set_socket_options(@tcp_socket)
53+
connect!
54+
rescue
55+
@tcp_socket.close
56+
raise
57+
end
58+
end
59+
2460
# @return [ SSLContext ] context The ssl context.
2561
attr_reader :context
2662

@@ -67,35 +103,6 @@ def connect!
67103
end
68104
private :connect!
69105

70-
# Initializes a new SSL socket.
71-
#
72-
# @example Create the SSL socket.
73-
# SSL.new('::1', 27017, 30)
74-
#
75-
# @param [ String ] host The hostname or IP address.
76-
# @param [ Integer ] port The port number.
77-
# @param [ Float ] timeout The socket timeout value.
78-
# @param [ Integer ] family The socket family.
79-
# @param [ Hash ] options The options.
80-
#
81-
# @option options [ Float ] :connect_timeout Connect timeout.
82-
#
83-
# @since 2.0.0
84-
def initialize(host, port, host_name, timeout, family, options = {})
85-
@host, @port, @host_name, @timeout, @options = host, port, host_name, timeout, options
86-
@context = create_context(options)
87-
@family = family
88-
@tcp_socket = ::Socket.new(family, SOCK_STREAM, 0)
89-
begin
90-
@tcp_socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
91-
set_socket_options(@tcp_socket)
92-
connect!
93-
rescue
94-
@tcp_socket.close
95-
raise
96-
end
97-
end
98-
99106
# Read a single byte from the socket.
100107
#
101108
# @example Read a single byte.
@@ -292,7 +299,7 @@ def read_buffer_size
292299
16384
293300
end
294301

295-
def address
302+
def human_address
296303
"#{host}:#{port} (#{host_name}:#{port}, TLS)"
297304
end
298305
end

lib/mongo/socket/tcp.rb

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,41 @@ class Socket
2020
# @since 2.0.0
2121
class TCP < Socket
2222

23+
# Initializes a new TCP socket.
24+
#
25+
# @example Create the TCP socket.
26+
# TCP.new('::1', 27017, 30, Socket::PF_INET)
27+
# TCP.new('127.0.0.1', 27017, 30, Socket::PF_INET)
28+
#
29+
# @param [ String ] host The hostname or IP address.
30+
# @param [ Integer ] port The port number.
31+
# @param [ Float ] timeout The socket timeout value.
32+
# @param [ Integer ] family The socket family.
33+
# @param [ Hash ] options The options.
34+
#
35+
# @option options [ Float ] :connect_timeout Connect timeout.
36+
# @option options [ Address ] :connection_address Address of the
37+
# connection that created this socket.
38+
# @option options [ Integer ] :connection_generation Generation of the
39+
# connection (for non-monitoring connections) that created this socket.
40+
# @option options [ true | false ] :monitor Whether this socket was
41+
# created by a monitoring connection.
42+
#
43+
# @since 2.0.0
44+
def initialize(host, port, timeout, family, options = {})
45+
super(timeout, options)
46+
@host, @port = host, port
47+
@family = family
48+
@socket = ::Socket.new(family, SOCK_STREAM, 0)
49+
begin
50+
set_socket_options(@socket)
51+
connect!
52+
rescue
53+
@socket.close
54+
raise
55+
end
56+
end
57+
2358
# @return [ String ] host The host to connect to.
2459
attr_reader :host
2560

@@ -48,37 +83,9 @@ def connect!
4883
end
4984
private :connect!
5085

51-
# Initializes a new TCP socket.
52-
#
53-
# @example Create the TCP socket.
54-
# TCP.new('::1', 27017, 30, Socket::PF_INET)
55-
# TCP.new('127.0.0.1', 27017, 30, Socket::PF_INET)
56-
#
57-
# @param [ String ] host The hostname or IP address.
58-
# @param [ Integer ] port The port number.
59-
# @param [ Float ] timeout The socket timeout value.
60-
# @param [ Integer ] family The socket family.
61-
# @param [ Hash ] options The options.
62-
#
63-
# @option options [ Float ] :connect_timeout Connect timeout.
64-
#
65-
# @since 2.0.0
66-
def initialize(host, port, timeout, family, options = {})
67-
@host, @port, @timeout, @options = host, port, timeout, options
68-
@family = family
69-
@socket = ::Socket.new(family, SOCK_STREAM, 0)
70-
begin
71-
set_socket_options(@socket)
72-
connect!
73-
rescue
74-
@socket.close
75-
raise
76-
end
77-
end
78-
7986
private
8087

81-
def address
88+
def human_address
8289
"#{host}:#{port} (no TLS)"
8390
end
8491
end

0 commit comments

Comments
 (0)