mongo 2.24.1 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mongo_console +2 -2
  3. data/lib/mongo/address/validator.rb +1 -1
  4. data/lib/mongo/address.rb +1 -3
  5. data/lib/mongo/auth/aws/conversation.rb +0 -4
  6. data/lib/mongo/auth/aws/credentials_retriever.rb +3 -8
  7. data/lib/mongo/auth/base.rb +1 -1
  8. data/lib/mongo/auth/sasl_conversation_base.rb +1 -1
  9. data/lib/mongo/auth/scram_conversation_base.rb +1 -8
  10. data/lib/mongo/auth/stringprep.rb +1 -6
  11. data/lib/mongo/auth/user/view.rb +1 -1
  12. data/lib/mongo/bulk_write/result_combiner.rb +3 -3
  13. data/lib/mongo/bulk_write.rb +1 -1
  14. data/lib/mongo/client.rb +82 -12
  15. data/lib/mongo/cluster/reapers/cursor_reaper.rb +12 -5
  16. data/lib/mongo/cluster/sdam_flow.rb +4 -4
  17. data/lib/mongo/cluster/topology/base.rb +1 -1
  18. data/lib/mongo/cluster.rb +12 -28
  19. data/lib/mongo/collection/view/aggregation/behavior.rb +1 -1
  20. data/lib/mongo/collection/view/aggregation.rb +2 -4
  21. data/lib/mongo/collection/view/builder/aggregation.rb +3 -5
  22. data/lib/mongo/collection/view/builder/map_reduce.rb +1 -1
  23. data/lib/mongo/collection/view/change_stream.rb +14 -9
  24. data/lib/mongo/collection/view/map_reduce.rb +2 -11
  25. data/lib/mongo/collection/view/readable.rb +11 -7
  26. data/lib/mongo/collection/view/writable.rb +4 -4
  27. data/lib/mongo/collection/view.rb +8 -4
  28. data/lib/mongo/collection.rb +1 -1
  29. data/lib/mongo/crypt/auto_encrypter.rb +1 -1
  30. data/lib/mongo/crypt/binary.rb +7 -4
  31. data/lib/mongo/crypt/binding.rb +88 -10
  32. data/lib/mongo/crypt/context.rb +50 -19
  33. data/lib/mongo/crypt/encryption_io.rb +11 -5
  34. data/lib/mongo/crypt/handle.rb +17 -5
  35. data/lib/mongo/crypt/kms/credentials.rb +70 -31
  36. data/lib/mongo/crypt/kms/kmip/master_document.rb +30 -1
  37. data/lib/mongo/crypt/kms/master_key_document.rb +11 -6
  38. data/lib/mongo/crypt/kms.rb +12 -1
  39. data/lib/mongo/cursor.rb +8 -2
  40. data/lib/mongo/database/cursor_command_view.rb +95 -0
  41. data/lib/mongo/database.rb +144 -18
  42. data/lib/mongo/error/invalid_uri.rb +5 -1
  43. data/lib/mongo/error/operation_failure.rb +2 -2
  44. data/lib/mongo/error/parser.rb +3 -3
  45. data/lib/mongo/error/sdam_error_detection.rb +2 -3
  46. data/lib/mongo/grid/fs_bucket.rb +27 -21
  47. data/lib/mongo/grid/stream/write.rb +2 -2
  48. data/lib/mongo/index/view.rb +13 -7
  49. data/lib/mongo/monitoring/event/cmap/connection_check_out_failed.rb +1 -0
  50. data/lib/mongo/monitoring/event/cmap/connection_check_out_started.rb +1 -0
  51. data/lib/mongo/monitoring/event/cmap/connection_checked_in.rb +1 -0
  52. data/lib/mongo/monitoring/event/cmap/connection_checked_out.rb +1 -0
  53. data/lib/mongo/monitoring/event/cmap/connection_closed.rb +1 -0
  54. data/lib/mongo/monitoring/event/cmap/connection_created.rb +1 -0
  55. data/lib/mongo/monitoring/event/cmap/connection_ready.rb +1 -0
  56. data/lib/mongo/monitoring/event/cmap/pool_cleared.rb +1 -0
  57. data/lib/mongo/monitoring/event/cmap/pool_closed.rb +1 -0
  58. data/lib/mongo/monitoring/event/cmap/pool_created.rb +1 -0
  59. data/lib/mongo/monitoring/event/cmap/pool_ready.rb +1 -0
  60. data/lib/mongo/monitoring/event/command_failed.rb +1 -0
  61. data/lib/mongo/monitoring/event/command_started.rb +1 -0
  62. data/lib/mongo/monitoring/event/command_succeeded.rb +1 -0
  63. data/lib/mongo/monitoring/event/secure.rb +12 -17
  64. data/lib/mongo/monitoring/event/server_closed.rb +1 -0
  65. data/lib/mongo/monitoring/event/server_description_changed.rb +1 -0
  66. data/lib/mongo/monitoring/event/server_heartbeat_failed.rb +1 -0
  67. data/lib/mongo/monitoring/event/server_heartbeat_started.rb +1 -0
  68. data/lib/mongo/monitoring/event/server_heartbeat_succeeded.rb +1 -0
  69. data/lib/mongo/monitoring/event/server_opening.rb +1 -0
  70. data/lib/mongo/monitoring/event/topology_changed.rb +1 -0
  71. data/lib/mongo/monitoring/event/topology_closed.rb +1 -0
  72. data/lib/mongo/monitoring/event/topology_opening.rb +1 -0
  73. data/lib/mongo/operation/create/op_msg.rb +1 -0
  74. data/lib/mongo/operation/create_index/op_msg.rb +2 -1
  75. data/lib/mongo/operation/cursor_command/op_msg.rb +37 -0
  76. data/lib/mongo/operation/cursor_command/result.rb +60 -0
  77. data/lib/mongo/operation/cursor_command.rb +33 -0
  78. data/lib/mongo/operation/delete/op_msg.rb +1 -0
  79. data/lib/mongo/operation/drop/op_msg.rb +1 -0
  80. data/lib/mongo/operation/drop_database/op_msg.rb +1 -0
  81. data/lib/mongo/operation/drop_index/op_msg.rb +1 -0
  82. data/lib/mongo/operation/find/builder/command.rb +1 -1
  83. data/lib/mongo/operation/insert/bulk_result.rb +2 -4
  84. data/lib/mongo/operation/insert/op_msg.rb +1 -0
  85. data/lib/mongo/operation/result.rb +12 -25
  86. data/lib/mongo/operation/shared/causal_consistency_supported.rb +4 -2
  87. data/lib/mongo/operation/shared/executable.rb +1 -9
  88. data/lib/mongo/operation/shared/result/aggregatable.rb +2 -2
  89. data/lib/mongo/operation/shared/sessions_supported.rb +3 -5
  90. data/lib/mongo/operation/update/bulk_result.rb +1 -1
  91. data/lib/mongo/operation/update/op_msg.rb +1 -0
  92. data/lib/mongo/operation/write_command/op_msg.rb +2 -0
  93. data/lib/mongo/operation.rb +1 -0
  94. data/lib/mongo/options/redacted.rb +2 -2
  95. data/lib/mongo/protocol/compressed.rb +15 -11
  96. data/lib/mongo/protocol/message.rb +16 -16
  97. data/lib/mongo/protocol/registry.rb +1 -1
  98. data/lib/mongo/protocol/serializers.rb +1 -3
  99. data/lib/mongo/query_cache.rb +3 -5
  100. data/lib/mongo/server/connection.rb +3 -1
  101. data/lib/mongo/server/connection_common.rb +9 -1
  102. data/lib/mongo/server/connection_pool/generation_manager.rb +1 -1
  103. data/lib/mongo/server/connection_pool.rb +24 -10
  104. data/lib/mongo/server/description/features.rb +1 -1
  105. data/lib/mongo/server/description.rb +1 -1
  106. data/lib/mongo/server/monitor/connection.rb +3 -2
  107. data/lib/mongo/server/monitor.rb +133 -30
  108. data/lib/mongo/server/pending_connection.rb +1 -0
  109. data/lib/mongo/server/push_monitor.rb +16 -2
  110. data/lib/mongo/server.rb +27 -7
  111. data/lib/mongo/server_selector/base.rb +3 -6
  112. data/lib/mongo/server_selector/secondary_preferred.rb +4 -1
  113. data/lib/mongo/session.rb +22 -23
  114. data/lib/mongo/socket/ssl.rb +3 -3
  115. data/lib/mongo/socket.rb +1 -7
  116. data/lib/mongo/srv/monitor.rb +11 -2
  117. data/lib/mongo/srv/resolver.rb +1 -1
  118. data/lib/mongo/srv/result.rb +5 -3
  119. data/lib/mongo/timeout.rb +10 -15
  120. data/lib/mongo/tracing/open_telemetry/command_tracer.rb +16 -2
  121. data/lib/mongo/uri/options_mapper.rb +21 -9
  122. data/lib/mongo/uri/srv_protocol.rb +4 -4
  123. data/lib/mongo/uri.rb +56 -8
  124. data/lib/mongo/utils.rb +1 -1
  125. data/lib/mongo/version.rb +1 -1
  126. metadata +6 -4
  127. data/lib/mongo/auth/stringprep/unicode_normalize/normalize.rb +0 -173
  128. data/lib/mongo/auth/stringprep/unicode_normalize/tables.rb +0 -1164
@@ -268,11 +268,11 @@ module Mongo
268
268
  buf = BSON::ByteBuffer.new(chunk)
269
269
 
270
270
  message = Registry.get(_op_code).allocate
271
- message.send(:fields).each do |field|
271
+ message.fields.each do |field|
272
272
  if field[:multi]
273
- deserialize_array(message, buf, field, options)
273
+ message.deserialize_array(buf, field, options)
274
274
  else
275
- deserialize_field(message, buf, field, options)
275
+ message.deserialize_field(buf, field, options)
276
276
  end
277
277
  end
278
278
  message.fix_after_deserialization if message.is_a?(Msg)
@@ -369,7 +369,6 @@ module Mongo
369
369
  # deserialized field specified in the class by the field dsl under
370
370
  # the key +:multi+
371
371
  #
372
- # @param message [Message] Message to contain the deserialized array.
373
372
  # @param io [IO] Stream containing the array to deserialize.
374
373
  # @param field [Hash] Hash representing a field.
375
374
  # @param options [ Hash ]
@@ -377,18 +376,17 @@ module Mongo
377
376
  # @option options [ Boolean ] :deserialize_as_bson Whether to deserialize
378
377
  # each of the elements in this array using BSON types wherever possible.
379
378
  #
380
- # @return [Message] Message with deserialized array.
379
+ # @return [Array] The deserialized array.
381
380
  # @api private
382
- def self.deserialize_array(message, io, field, options = {})
381
+ def deserialize_array(io, field, options = {})
383
382
  elements = []
384
- count = message.instance_variable_get(field[:multi])
383
+ count = instance_variable_get(field[:multi])
385
384
  count.times { elements << field[:type].deserialize(io, options) }
386
- message.instance_variable_set(field[:name], elements)
385
+ instance_variable_set(field[:name], elements)
387
386
  end
388
387
 
389
- # Deserializes a single field in a message
388
+ # Deserializes a single field into this message.
390
389
  #
391
- # @param message [Message] Message to contain the deserialized field.
392
390
  # @param io [IO] Stream containing the field to deserialize.
393
391
  # @param field [Hash] Hash representing a field.
394
392
  # @param options [ Hash ]
@@ -396,20 +394,19 @@ module Mongo
396
394
  # @option options [ Boolean ] :deserialize_as_bson Whether to deserialize
397
395
  # this field using BSON types wherever possible.
398
396
  #
399
- # @return [Message] Message with deserialized field.
397
+ # @return [Object] The deserialized field value.
400
398
  # @api private
401
- def self.deserialize_field(message, io, field, options = {})
402
- message.instance_variable_set(
399
+ def deserialize_field(io, field, options = {})
400
+ instance_variable_set(
403
401
  field[:name],
404
402
  field[:type].deserialize(io, options)
405
403
  )
406
404
  end
407
405
 
408
- private
409
-
410
406
  # A method for getting the fields for a message class
411
407
  #
412
- # @return [Integer] the fields for the message class
408
+ # @return [Array<Hash>] The fields for the message class
409
+ # @api private
413
410
  def fields
414
411
  self.class.fields
415
412
  end
@@ -418,6 +415,7 @@ module Mongo
418
415
  #
419
416
  # @param buffer [String] buffer to receive the field
420
417
  # @return [String] buffer with serialized field
418
+ # @api private
421
419
  def serialize_fields(buffer, max_bson_size = nil)
422
420
  fields.each do |field|
423
421
  value = instance_variable_get(field[:name])
@@ -437,6 +435,8 @@ module Mongo
437
435
  end
438
436
  end
439
437
 
438
+ private
439
+
440
440
  # Serializes the header of the message consisting of 4 32bit integers
441
441
  #
442
442
  # The integers represent a message length placeholder (calculation of
@@ -36,7 +36,7 @@ module Mongo
36
36
  #
37
37
  # @since 2.5.0
38
38
  def get(op_code, _message = nil)
39
- if type = MAPPINGS[op_code]
39
+ if (type = MAPPINGS[op_code])
40
40
  type
41
41
  else
42
42
  handle_unsupported_op_code!(op_code)
@@ -181,9 +181,7 @@ module Mongo
181
181
  def self.serialize(buffer, value, max_bson_size = nil, _validating_keys = nil)
182
182
  value.each do |section|
183
183
  case section[:type]
184
- when PayloadZero::TYPE
185
- PayloadZero.serialize(buffer, section[:payload], max_bson_size)
186
- when nil
184
+ when PayloadZero::TYPE, nil
187
185
  PayloadZero.serialize(buffer, section[:payload], max_bson_size)
188
186
  when PayloadOne::TYPE
189
187
  PayloadOne.serialize(buffer, section[:payload], max_bson_size)
@@ -199,11 +199,9 @@ module Mongo
199
199
  # Otherwise, return nil because the stored cursor will not satisfy
200
200
  # the query.
201
201
 
202
- if limit && (caching_cursor_limit.nil? || caching_cursor_limit >= limit)
203
- caching_cursor
204
- elsif limit.nil? && caching_cursor_limit.nil?
205
- caching_cursor
206
- end
202
+ return unless caching_cursor_limit.nil? || (limit && caching_cursor_limit >= limit)
203
+
204
+ caching_cursor
207
205
  end
208
206
 
209
207
  def normalized_limit(limit)
@@ -88,6 +88,7 @@ module Mongo
88
88
  #
89
89
  # @since 2.0.0
90
90
  def initialize(server, options = {})
91
+ super()
91
92
  if server.load_balancer? && options[:generation]
92
93
  raise ArgumentError, 'Generation cannot be set when server is a load balancer'
93
94
  end
@@ -422,7 +423,8 @@ module Mongo
422
423
  generation: e.generation,
423
424
  # or description.service_id?
424
425
  service_id: e.service_id,
425
- stop_push_monitor: true
426
+ stop_push_monitor: true,
427
+ network_error: true
426
428
  )
427
429
  raise
428
430
  rescue Error::SocketTimeoutError => e
@@ -99,6 +99,14 @@ module Mongo
99
99
  end
100
100
  end
101
101
 
102
+ # Closes the underlying socket, if one is open, to interrupt an
103
+ # in-progress blocking read on the connection.
104
+ #
105
+ # @api private
106
+ def interrupt_socket
107
+ socket&.close
108
+ end
109
+
102
110
  private
103
111
 
104
112
  HELLO_DOC = BSON::Document.new({ hello: 1 }).freeze
@@ -112,7 +120,7 @@ module Mongo
112
120
 
113
121
  return unless options[:compressors]
114
122
 
115
- if intersection = (server_compressors & options[:compressors])
123
+ if (intersection = (server_compressors & options[:compressors]))
116
124
  @compressor = intersection.first
117
125
  else
118
126
  msg = if server_compressors
@@ -125,7 +125,7 @@ module Mongo
125
125
 
126
126
  # Close all fds scheduled for closing.
127
127
  def close_all_scheduled
128
- while pipe = @scheduled_for_close.pop
128
+ while (pipe = @scheduled_for_close.pop)
129
129
  pipe.close
130
130
  end
131
131
  end
@@ -869,6 +869,22 @@ module Mongo
869
869
  end
870
870
  end
871
871
 
872
+ # Returns statistics about the internal state of the connection pool,
873
+ # primarily for testing and debugging purposes.
874
+ #
875
+ # @return [ Hash ] The state of the connection pool
876
+ #
877
+ # @api private
878
+ def state
879
+ @lock.synchronize do
880
+ { available_connections: @available_connections.length,
881
+ checked_out_connections: @checked_out_connections.length,
882
+ pending_connections: @pending_connections.length,
883
+ interrupt_connections: @interrupt_connections.length,
884
+ connection_requests: @connection_requests }
885
+ end
886
+ end
887
+
872
888
  private
873
889
 
874
890
  # Returns the next available connection, optionally with given
@@ -946,7 +962,7 @@ module Mongo
946
962
 
947
963
  # Removes and disconnects all stale available connections.
948
964
  def remove_stale_connection
949
- return unless conn = @available_connections.detect { |c| connection_stale_unlocked?(c) }
965
+ return unless (conn = @available_connections.detect { |c| connection_stale_unlocked?(c) })
950
966
 
951
967
  conn.disconnect!(reason: :stale)
952
968
  @available_connections.delete(conn)
@@ -958,7 +974,7 @@ module Mongo
958
974
  return false if @interrupt_connections.empty?
959
975
 
960
976
  gens = Set.new
961
- while conn = @interrupt_connections.pop
977
+ while (conn = @interrupt_connections.pop)
962
978
  if @checked_out_connections.include?(conn)
963
979
  # If the connection has been checked out, mark it as interrupted and it will
964
980
  # be disconnected on check in.
@@ -1042,7 +1058,11 @@ module Mongo
1042
1058
  @server.unknown!(
1043
1059
  generation: e.generation,
1044
1060
  service_id: e.service_id,
1045
- stop_push_monitor: true
1061
+ stop_push_monitor: true,
1062
+ # Only a network error makes the monitor connection suspect and
1063
+ # requires cancelling its check; an auth failure does not.
1064
+ network_error: e.is_a?(Mongo::Error::SocketError) ||
1065
+ e.is_a?(Mongo::Error::SocketTimeoutError)
1046
1066
  )
1047
1067
  end
1048
1068
  connection.disconnect!(reason: :error)
@@ -1146,12 +1166,6 @@ module Mongo
1146
1166
  raise Error::ConnectionCheckOutTimeout.new(msg, address: @server.address)
1147
1167
  end
1148
1168
 
1149
- def raise_check_out_timeout_locked!(connection_global_id)
1150
- @lock.synchronize do
1151
- raise_check_out_timeout!(connection_global_id)
1152
- end
1153
- end
1154
-
1155
1169
  def raise_if_pool_closed!
1156
1170
  return unless closed?
1157
1171
 
@@ -1246,7 +1260,7 @@ module Mongo
1246
1260
  # @raise [ Timeout::Error ] If the connection pool is at maximum size
1247
1261
  # and remains so for longer than the wait timeout.
1248
1262
  def get_connection(pid, connection_global_id)
1249
- if connection = next_available_connection(connection_global_id)
1263
+ if (connection = next_available_connection(connection_global_id))
1250
1264
  return nil unless valid_available_connection?(connection, pid, connection_global_id)
1251
1265
 
1252
1266
  # We've got a connection, so we decrement the number of connection
@@ -74,7 +74,7 @@ module Mongo
74
74
  #
75
75
  # If there are no currently-deprecated wire versions, this should be
76
76
  # set to an empty range (e.g. the EMPTY_RANGE constant).
77
- DEPRECATED_WIRE_VERSIONS = EMPTY_RANGE
77
+ DEPRECATED_WIRE_VERSIONS = 8..8
78
78
 
79
79
  # make sure the deprecated versions are valid
80
80
  if DEPRECATED_WIRE_VERSIONS.min && (DRIVER_WIRE_VERSIONS.min > DEPRECATED_WIRE_VERSIONS.max)
@@ -279,7 +279,7 @@ module Mongo
279
279
  "The server at #{address.seed} did not provide a service id in handshake response"
280
280
  end
281
281
 
282
- fake_service_id = if process_id = topology_version && topology_version['processId']
282
+ fake_service_id = if (process_id = topology_version && topology_version['processId'])
283
283
  "process:#{process_id}"
284
284
  else
285
285
  "fake:#{rand((2**32) - 1) + 1}"
@@ -54,9 +54,10 @@ module Mongo
54
54
  #
55
55
  # @since 2.0.0
56
56
  def initialize(address, options = {})
57
+ super()
57
58
  @address = address
58
59
  @options = options.dup.freeze
59
- raise ArgumentError, 'App metadata is required' unless @app_metadata = options[:app_metadata]
60
+ raise ArgumentError, 'App metadata is required' unless (@app_metadata = options[:app_metadata])
60
61
 
61
62
  @socket = nil
62
63
  @pid = Process.pid
@@ -232,7 +233,7 @@ module Mongo
232
233
  end
233
234
  # compressors must be set to maintain correct compression status
234
235
  # in the server description. See RUBY-2427
235
- if compressors = options[:compressors]
236
+ if (compressors = options[:compressors])
236
237
  doc = doc.merge(compression: compressors)
237
238
  end
238
239
  doc
@@ -86,14 +86,23 @@ module Mongo
86
86
  @sdam_mutex = Mutex.new
87
87
  @next_earliest_scan = @next_wanted_scan = Time.now
88
88
  @update_mutex = Mutex.new
89
+ # Guards reads and writes of @connection so the polling connection can
90
+ # be cancelled from another thread. Per the Server Monitoring spec's
91
+ # cancelCheck pseudocode, the lock is only held long enough to copy or
92
+ # assign the reference - never across the blocking check.
93
+ @connection_lock = Mutex.new
89
94
  end
90
95
 
91
96
  # @return [ Server ] server The server that this monitor is monitoring.
92
97
  # @api private
93
98
  attr_reader :server
94
99
 
95
- # @return [ Mongo::Server::Monitor::Connection ] connection The connection to use.
96
- attr_reader :connection
100
+ # @return [ Mongo::Server::Monitor::Connection | nil ] The connection to
101
+ # use, read under @connection_lock so callers never observe a stale
102
+ # reference after a concurrent cancel_check! clears it.
103
+ def connection
104
+ @connection_lock.synchronize { @connection }
105
+ end
97
106
 
98
107
  # @return [ Hash ] options The server options.
99
108
  attr_reader :options
@@ -159,7 +168,11 @@ module Mongo
159
168
  end
160
169
  end
161
170
 
162
- def create_push_monitor!(topology_version)
171
+ # @param [ Mongo::Server::Monitor::Connection ] connection The freshly
172
+ # established monitoring connection. Passed explicitly (rather than read
173
+ # from @connection) so a concurrent cancel_check! cannot nil it out from
174
+ # under us between establishing it and building the PushMonitor.
175
+ def create_push_monitor!(topology_version, connection)
163
176
  @update_mutex.synchronize do
164
177
  @push_monitor = nil if @push_monitor && !@push_monitor.running?
165
178
 
@@ -170,7 +183,7 @@ module Mongo
170
183
  **Utils.shallow_symbolize_keys(options.merge(
171
184
  socket_timeout: heartbeat_interval + connection.socket_timeout,
172
185
  app_metadata: options[:push_monitor_app_metadata],
173
- check_document: @connection.check_document
186
+ check_document: connection.check_document
174
187
  ))
175
188
  )
176
189
  end
@@ -185,6 +198,31 @@ module Mongo
185
198
  end
186
199
  end
187
200
 
201
+ # Cancel the in-progress check and close the monitoring connection.
202
+ #
203
+ # Called when the server is marked Unknown from a network error, per the
204
+ # Server Monitoring spec ("hello or legacy hello Cancellation"). Stops the
205
+ # streaming PushMonitor (interrupting its awaited hello read) and closes
206
+ # the polling connection, so the next check must establish a fresh one
207
+ # rather than re-validating the server over a possibly-dead socket.
208
+ #
209
+ # @api private
210
+ def cancel_check!
211
+ stop_push_monitor!
212
+
213
+ # Copy the connection reference under the lock, then interrupt and close
214
+ # it outside the lock. Closing the socket interrupts any in-progress
215
+ # read on the monitor thread; nil-ing the reference forces the next
216
+ # check to reconnect. The monitor thread is the only writer of a new
217
+ # connection, so it is safe for this thread to clear it.
218
+ connection = @connection_lock.synchronize do
219
+ conn = @connection
220
+ @connection = nil
221
+ conn
222
+ end
223
+ connection&.disconnect!
224
+ end
225
+
188
226
  # Perform a check of the server with throttling, and update
189
227
  # the server's description and average round trip time.
190
228
  #
@@ -210,28 +248,41 @@ module Mongo
210
248
  @mutex.synchronize do
211
249
  throttle_scan_frequency!
212
250
 
251
+ # When the streaming protocol is active the PushMonitor is the
252
+ # authoritative SDAM source and this scan only measures RTT on the
253
+ # dedicated connection. Per the Server Monitoring spec, an RTT
254
+ # command MUST NOT publish events or update the topology. Compute
255
+ # this before do_scan, which may (re)connect and change the state.
256
+ rtt_only = rtt_measurement_only?
257
+
213
258
  begin
214
- result = do_scan
259
+ result = do_scan(publish_heartbeat: !rtt_only)
215
260
  rescue StandardError => e
216
- run_sdam_flow({}, scan_error: e)
261
+ run_sdam_flow({}, scan_error: e, rtt_only: rtt_only)
217
262
  else
218
- run_sdam_flow(result)
263
+ run_sdam_flow(result, rtt_only: rtt_only)
219
264
  end
220
265
  end
221
266
  end
222
267
 
223
- def run_sdam_flow(result, awaited: false, scan_error: nil)
268
+ def run_sdam_flow(result, awaited: false, scan_error: nil, rtt_only: false)
224
269
  @sdam_mutex.synchronize do
225
270
  old_description = server.description
226
271
 
227
- new_description = Description.new(
228
- server.address,
229
- result,
230
- average_round_trip_time: server.round_trip_time_calculator.average_round_trip_time,
231
- minimum_round_trip_time: server.round_trip_time_calculator.minimum_round_trip_time
232
- )
233
-
234
- server.cluster.run_sdam_flow(server.description, new_description, awaited: awaited, scan_error: scan_error)
272
+ # An RTT-only measurement (streaming protocol active) must not update
273
+ # the topology or publish SDAM events. The RTT it gathered is
274
+ # incorporated into the next streaming-hello description via the
275
+ # shared RTT calculator. The scheduling below still runs so the
276
+ # monitor keeps pacing its checks.
277
+ unless rtt_only
278
+ new_description = Description.new(
279
+ server.address,
280
+ result,
281
+ average_round_trip_time: server.round_trip_time_calculator.average_round_trip_time,
282
+ minimum_round_trip_time: server.round_trip_time_calculator.minimum_round_trip_time
283
+ )
284
+ server.cluster.run_sdam_flow(server.description, new_description, awaited: awaited, scan_error: scan_error)
285
+ end
235
286
 
236
287
  server.description.tap do |new_description|
237
288
  unless awaited
@@ -272,8 +323,12 @@ module Mongo
272
323
  server.scan_semaphore.signal
273
324
  end
274
325
 
275
- def do_scan
276
- monitoring.publish_heartbeat(server) do
326
+ def do_scan(publish_heartbeat: true)
327
+ if publish_heartbeat
328
+ monitoring.publish_heartbeat(server) do
329
+ check
330
+ end
331
+ else
277
332
  check
278
333
  end
279
334
  rescue StandardError => e
@@ -285,24 +340,53 @@ module Mongo
285
340
  raise e
286
341
  end
287
342
 
343
+ # Returns whether this scan is only an RTT measurement, which is the case
344
+ # when the streaming protocol is active: a dedicated connection is already
345
+ # established and the PushMonitor is running as the authoritative SDAM
346
+ # source. In the polling protocol there is no running PushMonitor, so the
347
+ # connection-reuse check is a real server check and not RTT-only.
348
+ #
349
+ # @return [ true | false ]
350
+ def rtt_measurement_only?
351
+ return false if connection.nil?
352
+
353
+ # Only suppress the check while the server is in a known state and the
354
+ # PushMonitor is the authoritative streaming source. If the server is
355
+ # Unknown (e.g. an operation error or a streaming failure just marked
356
+ # it so), the polling Monitor must run a full check to recover it
357
+ # rather than waiting for the next streaming response - otherwise the
358
+ # server can stay Unknown long enough to fail server selection.
359
+ return false if server.unknown?
360
+
361
+ pm = push_monitor
362
+ !pm.nil? && pm.running?
363
+ end
364
+
288
365
  def check
289
- if @connection && @connection.pid != Process.pid
290
- log_warn("Detected PID change - Mongo client should have been reconnected (old pid #{@connection.pid}, new pid #{Process.pid}")
291
- @connection.disconnect!
292
- @connection = nil
366
+ # Snapshot the connection under the lock. A concurrent cancel_check!
367
+ # may nil @connection from another thread; working on a local copy keeps
368
+ # this check consistent, and the guarded writeback below never clobbers
369
+ # a connection the monitor thread did not itself establish.
370
+ connection = self.connection
371
+
372
+ if connection && connection.pid != Process.pid
373
+ log_warn("Detected PID change - Mongo client should have been reconnected (old pid #{connection.pid}, new pid #{Process.pid}")
374
+ connection.disconnect!
375
+ clear_connection(connection)
376
+ connection = nil
293
377
  end
294
378
 
295
- if @connection
379
+ if connection
296
380
  result = server.round_trip_time_calculator.measure do
297
- doc = @connection.check_document
381
+ doc = connection.check_document
298
382
  cmd = Protocol::Query.new(
299
383
  Database::ADMIN, Database::COMMAND, doc, limit: -1
300
384
  )
301
- message = @connection.dispatch_bytes(cmd.serialize.to_s)
385
+ message = connection.dispatch_bytes(cmd.serialize.to_s)
302
386
  message.documents.first
303
387
  rescue Mongo::Error
304
- @connection.disconnect!
305
- @connection = nil
388
+ connection.disconnect!
389
+ clear_connection(connection)
306
390
  raise
307
391
  end
308
392
  else
@@ -311,10 +395,13 @@ module Mongo
311
395
  result = server.round_trip_time_calculator.measure do
312
396
  connection.handshake!
313
397
  end
314
- @connection = connection
315
- if tv_doc = result['topologyVersion']
398
+ store_connection(connection)
399
+ if (tv_doc = result['topologyVersion'])
316
400
  if streaming_enabled?
317
- create_push_monitor!(TopologyVersion.new(tv_doc))
401
+ # Run the instance we just created rather than re-reading the
402
+ # push_monitor getter: a concurrent cancel_check! may have nil'd
403
+ # @push_monitor between the two calls.
404
+ push_monitor = create_push_monitor!(TopologyVersion.new(tv_doc), connection)
318
405
  push_monitor.run!
319
406
  else
320
407
  stop_push_monitor!
@@ -328,6 +415,22 @@ module Mongo
328
415
  result
329
416
  end
330
417
 
418
+ # Store a freshly established monitoring connection.
419
+ def store_connection(connection)
420
+ @connection_lock.synchronize do
421
+ @connection = connection
422
+ end
423
+ end
424
+
425
+ # Clear the monitoring connection, but only if it is still the one passed
426
+ # in. A concurrent cancel_check! may have already cleared or replaced it,
427
+ # in which case we must leave the current connection alone.
428
+ def clear_connection(connection)
429
+ @connection_lock.synchronize do
430
+ @connection = nil if @connection.equal?(connection)
431
+ end
432
+ end
433
+
331
434
  # @note If the system clock is set to a time in the past, this method
332
435
  # can sleep for a very long time.
333
436
  def throttle_scan_frequency!
@@ -23,6 +23,7 @@ module Mongo
23
23
  extend Forwardable
24
24
 
25
25
  def initialize(socket, server, monitoring, options = {})
26
+ super()
26
27
  @socket = socket
27
28
  @options = options
28
29
  @server = server
@@ -72,7 +72,7 @@ module Mongo
72
72
  # Interrupt any in-progress exhausted hello reads by
73
73
  # disconnecting the connection.
74
74
  begin
75
- @connection.send(:socket).close
75
+ @connection.interrupt_socket
76
76
  rescue StandardError
77
77
  nil
78
78
  end
@@ -118,6 +118,13 @@ module Mongo
118
118
  log_prefix: options[:log_prefix],
119
119
  bg_error_backtrace: options[:bg_error_backtrace])
120
120
 
121
+ # The streaming check is the authoritative SDAM source, so its failure
122
+ # must mark the server Unknown and clear the pool here. While streaming
123
+ # is active the polling Monitor only measures RTT and ignores errors
124
+ # (per the Server Monitoring spec), so it can no longer be relied on to
125
+ # notice the failure.
126
+ monitor.run_sdam_flow({}, scan_error: e, awaited: true)
127
+
121
128
  # If a request failed on a connection, stop push monitoring.
122
129
  # In case the server is dead we don't want to have two connections
123
130
  # trying to connect unsuccessfully at the same time.
@@ -142,6 +149,13 @@ module Mongo
142
149
  @server_pushing = false
143
150
  connection = PushMonitor::Connection.new(server.address, options)
144
151
  connection.connect!
152
+ # Identify the streaming connection to the server with a metadata
153
+ # handshake (carrying the appName) before streaming, mirroring the
154
+ # polling Monitor and the Server Monitoring spec, which establishes
155
+ # the monitoring connection with a handshake before issuing the
156
+ # awaitable hello. Without this, appName-scoped server behaviour
157
+ # never applies to the streaming hello.
158
+ connection.handshake!
145
159
  @connection = connection
146
160
  end
147
161
  end
@@ -174,7 +188,7 @@ module Mongo
174
188
  end
175
189
 
176
190
  def read_response
177
- if timeout = options[:connect_timeout]
191
+ if (timeout = options[:connect_timeout])
178
192
  if timeout < 0
179
193
  raise Mongo::SocketTimeoutError, 'Requested to read with a negative timeout: '
180
194
  elsif timeout > 0
data/lib/mongo/server.rb CHANGED
@@ -489,7 +489,8 @@ module Mongo
489
489
  unknown!(
490
490
  generation: e.generation,
491
491
  service_id: e.service_id,
492
- stop_push_monitor: true
492
+ stop_push_monitor: true,
493
+ network_error: true
493
494
  )
494
495
  end
495
496
  raise
@@ -514,11 +515,13 @@ module Mongo
514
515
  raise
515
516
  rescue Mongo::Error::SocketError, Auth::Unauthorized => e
516
517
  # non-timeout network error or auth error, clear the pool and mark the
517
- # topology as unknown
518
+ # topology as unknown. Only a network error makes the monitor connection
519
+ # suspect; a pure authentication failure must not close it.
518
520
  unknown!(
519
521
  generation: e.generation,
520
522
  service_id: e.service_id,
521
- stop_push_monitor: true
523
+ stop_push_monitor: true,
524
+ network_error: e.is_a?(Mongo::Error::SocketError)
522
525
  )
523
526
  raise
524
527
  end
@@ -568,8 +571,15 @@ module Mongo
568
571
  # on 4.2+ servers).
569
572
  # @option options [ TopologyVersion ] :topology_version Topology version
570
573
  # of the error response that is causing the server to be marked unknown.
571
- # @option options [ true | false ] :stop_push_monitor Whether to stop
572
- # the PushMonitor associated with the server, if any.
574
+ # @option options [ true | false ] :stop_push_monitor Set when the server
575
+ # is marked Unknown from a connection-establishment error (e.g. an
576
+ # authentication failure). Stops the streaming PushMonitor associated with
577
+ # the server, if any.
578
+ # @option options [ true | false ] :network_error Set when the server is
579
+ # marked Unknown from a network error. In addition to stopping the
580
+ # PushMonitor, cancels the monitor's in-progress check and closes its
581
+ # connection so the next check must establish a fresh one, per the Server
582
+ # Monitoring spec.
573
583
  # @option options [ Object ] :service_id Discard state for the specified
574
584
  # service id only.
575
585
  #
@@ -590,7 +600,7 @@ module Mongo
590
600
  # for example if there haven't been any successful connections yet to
591
601
  # this server, but the server can still be marked unknown if one
592
602
  # of such connections failed midway through its establishment.
593
- if service_id = options[:service_id]
603
+ if (service_id = options[:service_id])
594
604
  pool&.disconnect!(service_id: service_id)
595
605
  end
596
606
  return
@@ -605,7 +615,17 @@ module Mongo
605
615
  return
606
616
  end
607
617
 
608
- monitor&.stop_push_monitor! if options[:stop_push_monitor]
618
+ if options[:network_error]
619
+ # A network error implies every connection to the server is suspect
620
+ # (Server Monitoring spec, "hello or legacy hello Cancellation"): cancel
621
+ # the monitor's in-progress check and close its connection so the next
622
+ # check must establish a fresh one.
623
+ monitor&.cancel_check!
624
+ elsif options[:stop_push_monitor]
625
+ # Non-network errors (e.g. an authentication failure) only tear down the
626
+ # streaming PushMonitor; the monitor's connection is not suspect.
627
+ monitor&.stop_push_monitor!
628
+ end
609
629
 
610
630
  # SDAM flow will update description on the server without in-place
611
631
  # mutations and invoke SDAM transitions as needed.