mongo 2.24.1 → 2.26.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 (143) 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/combineable.rb +0 -7
  13. data/lib/mongo/bulk_write/result_combiner.rb +3 -3
  14. data/lib/mongo/bulk_write/validatable.rb +0 -4
  15. data/lib/mongo/bulk_write.rb +1 -35
  16. data/lib/mongo/client.rb +117 -17
  17. data/lib/mongo/client_encryption.rb +36 -11
  18. data/lib/mongo/cluster/reapers/cursor_reaper.rb +12 -5
  19. data/lib/mongo/cluster/sdam_flow.rb +4 -4
  20. data/lib/mongo/cluster/topology/base.rb +1 -1
  21. data/lib/mongo/cluster.rb +13 -30
  22. data/lib/mongo/collection/queryable_encryption.rb +7 -6
  23. data/lib/mongo/collection/view/aggregation/behavior.rb +1 -1
  24. data/lib/mongo/collection/view/aggregation.rb +2 -4
  25. data/lib/mongo/collection/view/builder/aggregation.rb +3 -5
  26. data/lib/mongo/collection/view/builder/map_reduce.rb +1 -1
  27. data/lib/mongo/collection/view/change_stream.rb +14 -9
  28. data/lib/mongo/collection/view/map_reduce.rb +2 -11
  29. data/lib/mongo/collection/view/readable.rb +11 -7
  30. data/lib/mongo/collection/view/writable.rb +4 -39
  31. data/lib/mongo/collection/view.rb +8 -4
  32. data/lib/mongo/collection.rb +1 -1
  33. data/lib/mongo/crypt/auto_encrypter.rb +6 -2
  34. data/lib/mongo/crypt/binary.rb +7 -4
  35. data/lib/mongo/crypt/binding.rb +154 -11
  36. data/lib/mongo/crypt/context.rb +50 -19
  37. data/lib/mongo/crypt/encryption_io.rb +11 -5
  38. data/lib/mongo/crypt/explicit_encrypter.rb +27 -12
  39. data/lib/mongo/crypt/explicit_encryption_context.rb +51 -12
  40. data/lib/mongo/crypt/handle.rb +35 -5
  41. data/lib/mongo/crypt/kms/credentials.rb +70 -31
  42. data/lib/mongo/crypt/kms/kmip/master_document.rb +30 -1
  43. data/lib/mongo/crypt/kms/master_key_document.rb +11 -6
  44. data/lib/mongo/crypt/kms.rb +12 -1
  45. data/lib/mongo/cursor.rb +8 -2
  46. data/lib/mongo/database/cursor_command_view.rb +95 -0
  47. data/lib/mongo/database.rb +144 -18
  48. data/lib/mongo/error/invalid_uri.rb +5 -1
  49. data/lib/mongo/error/operation_failure.rb +2 -2
  50. data/lib/mongo/error/parser.rb +3 -3
  51. data/lib/mongo/error/sdam_error_detection.rb +2 -3
  52. data/lib/mongo/error/unsupported_option.rb +0 -40
  53. data/lib/mongo/grid/fs_bucket.rb +29 -23
  54. data/lib/mongo/grid/stream/read.rb +2 -2
  55. data/lib/mongo/grid/stream/write.rb +3 -3
  56. data/lib/mongo/index/view.rb +13 -7
  57. data/lib/mongo/monitoring/event/cmap/connection_check_out_failed.rb +1 -0
  58. data/lib/mongo/monitoring/event/cmap/connection_check_out_started.rb +1 -0
  59. data/lib/mongo/monitoring/event/cmap/connection_checked_in.rb +1 -0
  60. data/lib/mongo/monitoring/event/cmap/connection_checked_out.rb +1 -0
  61. data/lib/mongo/monitoring/event/cmap/connection_closed.rb +1 -0
  62. data/lib/mongo/monitoring/event/cmap/connection_created.rb +1 -0
  63. data/lib/mongo/monitoring/event/cmap/connection_ready.rb +1 -0
  64. data/lib/mongo/monitoring/event/cmap/pool_cleared.rb +1 -0
  65. data/lib/mongo/monitoring/event/cmap/pool_closed.rb +1 -0
  66. data/lib/mongo/monitoring/event/cmap/pool_created.rb +1 -0
  67. data/lib/mongo/monitoring/event/cmap/pool_ready.rb +1 -0
  68. data/lib/mongo/monitoring/event/command_failed.rb +1 -0
  69. data/lib/mongo/monitoring/event/command_started.rb +1 -0
  70. data/lib/mongo/monitoring/event/command_succeeded.rb +1 -0
  71. data/lib/mongo/monitoring/event/secure.rb +12 -17
  72. data/lib/mongo/monitoring/event/server_closed.rb +1 -0
  73. data/lib/mongo/monitoring/event/server_description_changed.rb +1 -0
  74. data/lib/mongo/monitoring/event/server_heartbeat_failed.rb +1 -0
  75. data/lib/mongo/monitoring/event/server_heartbeat_started.rb +1 -0
  76. data/lib/mongo/monitoring/event/server_heartbeat_succeeded.rb +1 -0
  77. data/lib/mongo/monitoring/event/server_opening.rb +1 -0
  78. data/lib/mongo/monitoring/event/topology_changed.rb +1 -0
  79. data/lib/mongo/monitoring/event/topology_closed.rb +1 -0
  80. data/lib/mongo/monitoring/event/topology_opening.rb +1 -0
  81. data/lib/mongo/operation/create/op_msg.rb +1 -0
  82. data/lib/mongo/operation/create_index/op_msg.rb +2 -1
  83. data/lib/mongo/operation/cursor_command/op_msg.rb +37 -0
  84. data/lib/mongo/operation/cursor_command/result.rb +60 -0
  85. data/lib/mongo/operation/cursor_command.rb +33 -0
  86. data/lib/mongo/operation/delete/op_msg.rb +1 -0
  87. data/lib/mongo/operation/drop/op_msg.rb +1 -0
  88. data/lib/mongo/operation/drop_database/op_msg.rb +1 -0
  89. data/lib/mongo/operation/drop_index/op_msg.rb +1 -0
  90. data/lib/mongo/operation/find/builder/command.rb +1 -1
  91. data/lib/mongo/operation/insert/bulk_result.rb +2 -4
  92. data/lib/mongo/operation/insert/op_msg.rb +1 -0
  93. data/lib/mongo/operation/result.rb +21 -25
  94. data/lib/mongo/operation/shared/causal_consistency_supported.rb +4 -2
  95. data/lib/mongo/operation/shared/executable.rb +2 -15
  96. data/lib/mongo/operation/shared/result/aggregatable.rb +2 -2
  97. data/lib/mongo/operation/shared/sessions_supported.rb +3 -5
  98. data/lib/mongo/operation/update/bulk_result.rb +1 -1
  99. data/lib/mongo/operation/update/op_msg.rb +1 -0
  100. data/lib/mongo/operation/write_command/op_msg.rb +2 -0
  101. data/lib/mongo/operation.rb +1 -0
  102. data/lib/mongo/options/redacted.rb +2 -2
  103. data/lib/mongo/protocol/compressed.rb +15 -11
  104. data/lib/mongo/protocol/message.rb +16 -20
  105. data/lib/mongo/protocol/msg.rb +1 -12
  106. data/lib/mongo/protocol/registry.rb +1 -1
  107. data/lib/mongo/protocol/serializers.rb +1 -3
  108. data/lib/mongo/query_cache.rb +3 -5
  109. data/lib/mongo/retryable/backpressure.rb +17 -2
  110. data/lib/mongo/retryable/read_worker.rb +1 -1
  111. data/lib/mongo/retryable/retry_policy.rb +2 -2
  112. data/lib/mongo/retryable/write_worker.rb +2 -2
  113. data/lib/mongo/retryable.rb +1 -1
  114. data/lib/mongo/server/app_metadata.rb +1 -1
  115. data/lib/mongo/server/connection.rb +3 -1
  116. data/lib/mongo/server/connection_common.rb +9 -1
  117. data/lib/mongo/server/connection_pool/generation_manager.rb +1 -1
  118. data/lib/mongo/server/connection_pool.rb +24 -10
  119. data/lib/mongo/server/description/features.rb +1 -2
  120. data/lib/mongo/server/description.rb +1 -2
  121. data/lib/mongo/server/monitor/connection.rb +3 -2
  122. data/lib/mongo/server/monitor.rb +133 -30
  123. data/lib/mongo/server/pending_connection.rb +1 -5
  124. data/lib/mongo/server/push_monitor.rb +16 -2
  125. data/lib/mongo/server.rb +28 -9
  126. data/lib/mongo/server_selector/base.rb +3 -6
  127. data/lib/mongo/server_selector/secondary_preferred.rb +4 -1
  128. data/lib/mongo/session.rb +25 -26
  129. data/lib/mongo/socket/ssl.rb +3 -3
  130. data/lib/mongo/socket.rb +1 -7
  131. data/lib/mongo/srv/monitor.rb +11 -2
  132. data/lib/mongo/srv/resolver.rb +1 -1
  133. data/lib/mongo/srv/result.rb +5 -3
  134. data/lib/mongo/timeout.rb +10 -15
  135. data/lib/mongo/tracing/open_telemetry/command_tracer.rb +16 -2
  136. data/lib/mongo/uri/options_mapper.rb +36 -13
  137. data/lib/mongo/uri/srv_protocol.rb +4 -4
  138. data/lib/mongo/uri.rb +56 -8
  139. data/lib/mongo/utils.rb +1 -1
  140. data/lib/mongo/version.rb +1 -1
  141. metadata +6 -4
  142. data/lib/mongo/auth/stringprep/unicode_normalize/normalize.rb +0 -173
  143. data/lib/mongo/auth/stringprep/unicode_normalize/tables.rb +0 -1164
@@ -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
@@ -24,7 +24,6 @@ module Mongo
24
24
  # List of features and the wire protocol version they appear in.
25
25
  #
26
26
  # Wire protocol versions map to server releases as follows:
27
- # - 8 => 4.2
28
27
  # - 9 => 4.4
29
28
  # - 13 => 5.0
30
29
  # - 14 => 5.1
@@ -61,7 +60,7 @@ module Mongo
61
60
  # The wire protocol versions that this version of the driver supports.
62
61
  #
63
62
  # @since 2.0.0
64
- DRIVER_WIRE_VERSIONS = 8..29
63
+ DRIVER_WIRE_VERSIONS = 9..29
65
64
 
66
65
  # The wire protocol versions that are deprecated in this version of the
67
66
  # driver. Support for these versions will be removed in the future.
@@ -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}"
@@ -905,7 +905,6 @@ module Mongo
905
905
  '5.1' => 14,
906
906
  '5.0' => 12,
907
907
  '4.4' => 9,
908
- '4.2' => 8,
909
908
  }.freeze
910
909
 
911
910
  # @api private
@@ -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
@@ -51,11 +52,6 @@ module Mongo
51
52
  # SCRAM-SHA-1. The use of SCRAM-SHA-256 by default is thus
52
53
  # sensible, and it is also mandated by the speculative auth spec.
53
54
  #
54
- # If no mechanism was specified and we are talking to a 4.2
55
- # server, we'll send speculative auth document, the server will
56
- # ignore it and we'll perform authentication using explicit
57
- # command.
58
- #
59
55
  # If no mechanism was specified and we are talking to a 4.4+
60
56
  # server and the user account doesn't allow SCRAM-SHA-256, we will
61
57
  # authenticate in a separate command with SCRAM-SHA-1 after
@@ -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
@@ -564,12 +567,18 @@ module Mongo
564
567
  # @option options [ true | false ] :keep_connection_pool Usually when the
565
568
  # new server description is unknown, the connection pool on the
566
569
  # respective server is cleared. Set this option to true to keep the
567
- # existing connection pool (required when handling not master errors
568
- # on 4.2+ servers).
570
+ # existing connection pool (required when handling not master errors).
569
571
  # @option options [ TopologyVersion ] :topology_version Topology version
570
572
  # 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.
573
+ # @option options [ true | false ] :stop_push_monitor Set when the server
574
+ # is marked Unknown from a connection-establishment error (e.g. an
575
+ # authentication failure). Stops the streaming PushMonitor associated with
576
+ # the server, if any.
577
+ # @option options [ true | false ] :network_error Set when the server is
578
+ # marked Unknown from a network error. In addition to stopping the
579
+ # PushMonitor, cancels the monitor's in-progress check and closes its
580
+ # connection so the next check must establish a fresh one, per the Server
581
+ # Monitoring spec.
573
582
  # @option options [ Object ] :service_id Discard state for the specified
574
583
  # service id only.
575
584
  #
@@ -590,7 +599,7 @@ module Mongo
590
599
  # for example if there haven't been any successful connections yet to
591
600
  # this server, but the server can still be marked unknown if one
592
601
  # of such connections failed midway through its establishment.
593
- if service_id = options[:service_id]
602
+ if (service_id = options[:service_id])
594
603
  pool&.disconnect!(service_id: service_id)
595
604
  end
596
605
  return
@@ -605,7 +614,17 @@ module Mongo
605
614
  return
606
615
  end
607
616
 
608
- monitor&.stop_push_monitor! if options[:stop_push_monitor]
617
+ if options[:network_error]
618
+ # A network error implies every connection to the server is suspect
619
+ # (Server Monitoring spec, "hello or legacy hello Cancellation"): cancel
620
+ # the monitor's in-progress check and close its connection so the next
621
+ # check must establish a fresh one.
622
+ monitor&.cancel_check!
623
+ elsif options[:stop_push_monitor]
624
+ # Non-network errors (e.g. an authentication failure) only tear down the
625
+ # streaming PushMonitor; the monitor's connection is not suspect.
626
+ monitor&.stop_push_monitor!
627
+ end
609
628
 
610
629
  # SDAM flow will update description on the server without in-place
611
630
  # mutations and invoke SDAM transitions as needed.
@@ -239,7 +239,7 @@ module Mongo
239
239
 
240
240
  session.unpin unless session.in_transaction?
241
241
 
242
- if server = session.pinned_server
242
+ if (server = session.pinned_server)
243
243
  # Here we assume that a mongos stays in the topology indefinitely.
244
244
  # This will no longer be the case once SRV polling is implemented.
245
245
 
@@ -398,9 +398,7 @@ module Mongo
398
398
  # @api private
399
399
  def candidates(cluster, deprioritized = [])
400
400
  servers = cluster.servers.reject { |s| deprioritized.include?(s) }
401
- if cluster.single?
402
- servers
403
- elsif cluster.sharded?
401
+ if cluster.single? || cluster.sharded?
404
402
  servers
405
403
  elsif cluster.replica_set?
406
404
  select_in_replica_set(servers)
@@ -689,8 +687,7 @@ module Mongo
689
687
  msg = ''
690
688
  dead_monitors = []
691
689
  cluster.servers_list.each do |server|
692
- thread = server.monitor.instance_variable_get(:@thread)
693
- dead_monitors << server if thread.nil? || !thread.alive?
690
+ dead_monitors << server unless server.monitor&.running?
694
691
  end
695
692
  if dead_monitors.any?
696
693
  msg += ". The following servers have dead monitor threads: #{dead_monitors.map(&:summary).join(', ')}"
@@ -101,7 +101,10 @@ module Mongo
101
101
  #
102
102
  # @since 2.0.0
103
103
  def select_in_replica_set(candidates)
104
- near_servers(secondaries(candidates)) + primary(candidates)
104
+ eligible = secondaries(candidates)
105
+ return near_servers(eligible) unless eligible.empty?
106
+
107
+ primary(candidates)
105
108
  end
106
109
 
107
110
  def max_staleness_allowed?