mongo 2.24.0 → 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 (139) 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.rb +10 -2
  13. data/lib/mongo/bulk_write/result_combiner.rb +11 -4
  14. data/lib/mongo/bulk_write.rb +1 -1
  15. data/lib/mongo/client.rb +82 -12
  16. data/lib/mongo/cluster/reapers/cursor_reaper.rb +12 -5
  17. data/lib/mongo/cluster/sdam_flow.rb +4 -4
  18. data/lib/mongo/cluster/topology/base.rb +1 -1
  19. data/lib/mongo/cluster.rb +12 -28
  20. data/lib/mongo/collection/view/aggregation/behavior.rb +1 -1
  21. data/lib/mongo/collection/view/aggregation.rb +2 -4
  22. data/lib/mongo/collection/view/builder/aggregation.rb +3 -5
  23. data/lib/mongo/collection/view/builder/map_reduce.rb +1 -1
  24. data/lib/mongo/collection/view/change_stream.rb +14 -9
  25. data/lib/mongo/collection/view/map_reduce.rb +2 -11
  26. data/lib/mongo/collection/view/readable.rb +11 -7
  27. data/lib/mongo/collection/view/writable.rb +4 -4
  28. data/lib/mongo/collection/view.rb +8 -4
  29. data/lib/mongo/collection.rb +1 -1
  30. data/lib/mongo/config.rb +5 -0
  31. data/lib/mongo/crypt/auto_encrypter.rb +1 -1
  32. data/lib/mongo/crypt/binary.rb +7 -4
  33. data/lib/mongo/crypt/binding.rb +88 -10
  34. data/lib/mongo/crypt/context.rb +50 -19
  35. data/lib/mongo/crypt/encryption_io.rb +11 -5
  36. data/lib/mongo/crypt/handle.rb +17 -5
  37. data/lib/mongo/crypt/kms/credentials.rb +70 -31
  38. data/lib/mongo/crypt/kms/kmip/master_document.rb +30 -1
  39. data/lib/mongo/crypt/kms/master_key_document.rb +11 -6
  40. data/lib/mongo/crypt/kms.rb +12 -1
  41. data/lib/mongo/cursor.rb +8 -2
  42. data/lib/mongo/database/cursor_command_view.rb +95 -0
  43. data/lib/mongo/database.rb +144 -18
  44. data/lib/mongo/error/bulk_write_error.rb +29 -1
  45. data/lib/mongo/error/invalid_server_preference.rb +3 -0
  46. data/lib/mongo/error/invalid_uri.rb +5 -1
  47. data/lib/mongo/error/operation_failure.rb +42 -3
  48. data/lib/mongo/error/parser.rb +3 -3
  49. data/lib/mongo/error/sdam_error_detection.rb +2 -3
  50. data/lib/mongo/grid/fs_bucket.rb +27 -21
  51. data/lib/mongo/grid/stream/write.rb +2 -2
  52. data/lib/mongo/index/view.rb +13 -7
  53. data/lib/mongo/monitoring/event/cmap/connection_check_out_failed.rb +1 -0
  54. data/lib/mongo/monitoring/event/cmap/connection_check_out_started.rb +1 -0
  55. data/lib/mongo/monitoring/event/cmap/connection_checked_in.rb +1 -0
  56. data/lib/mongo/monitoring/event/cmap/connection_checked_out.rb +1 -0
  57. data/lib/mongo/monitoring/event/cmap/connection_closed.rb +1 -0
  58. data/lib/mongo/monitoring/event/cmap/connection_created.rb +1 -0
  59. data/lib/mongo/monitoring/event/cmap/connection_ready.rb +1 -0
  60. data/lib/mongo/monitoring/event/cmap/pool_cleared.rb +1 -0
  61. data/lib/mongo/monitoring/event/cmap/pool_closed.rb +1 -0
  62. data/lib/mongo/monitoring/event/cmap/pool_created.rb +1 -0
  63. data/lib/mongo/monitoring/event/cmap/pool_ready.rb +1 -0
  64. data/lib/mongo/monitoring/event/command_failed.rb +1 -0
  65. data/lib/mongo/monitoring/event/command_started.rb +1 -0
  66. data/lib/mongo/monitoring/event/command_succeeded.rb +1 -0
  67. data/lib/mongo/monitoring/event/secure.rb +12 -17
  68. data/lib/mongo/monitoring/event/server_closed.rb +1 -0
  69. data/lib/mongo/monitoring/event/server_description_changed.rb +1 -0
  70. data/lib/mongo/monitoring/event/server_heartbeat_failed.rb +1 -0
  71. data/lib/mongo/monitoring/event/server_heartbeat_started.rb +1 -0
  72. data/lib/mongo/monitoring/event/server_heartbeat_succeeded.rb +1 -0
  73. data/lib/mongo/monitoring/event/server_opening.rb +1 -0
  74. data/lib/mongo/monitoring/event/topology_changed.rb +1 -0
  75. data/lib/mongo/monitoring/event/topology_closed.rb +1 -0
  76. data/lib/mongo/monitoring/event/topology_opening.rb +1 -0
  77. data/lib/mongo/monitoring/server_description_changed_log_subscriber.rb +2 -0
  78. data/lib/mongo/monitoring/topology_changed_log_subscriber.rb +3 -0
  79. data/lib/mongo/operation/create/op_msg.rb +1 -0
  80. data/lib/mongo/operation/create_index/op_msg.rb +2 -1
  81. data/lib/mongo/operation/cursor_command/op_msg.rb +37 -0
  82. data/lib/mongo/operation/cursor_command/result.rb +60 -0
  83. data/lib/mongo/operation/cursor_command.rb +33 -0
  84. data/lib/mongo/operation/delete/op_msg.rb +1 -0
  85. data/lib/mongo/operation/drop/op_msg.rb +1 -0
  86. data/lib/mongo/operation/drop_database/op_msg.rb +1 -0
  87. data/lib/mongo/operation/drop_index/op_msg.rb +1 -0
  88. data/lib/mongo/operation/find/builder/command.rb +1 -1
  89. data/lib/mongo/operation/insert/bulk_result.rb +2 -4
  90. data/lib/mongo/operation/insert/op_msg.rb +1 -0
  91. data/lib/mongo/operation/result.rb +14 -26
  92. data/lib/mongo/operation/shared/causal_consistency_supported.rb +4 -2
  93. data/lib/mongo/operation/shared/executable.rb +2 -10
  94. data/lib/mongo/operation/shared/result/aggregatable.rb +2 -2
  95. data/lib/mongo/operation/shared/sessions_supported.rb +3 -5
  96. data/lib/mongo/operation/update/bulk_result.rb +1 -1
  97. data/lib/mongo/operation/update/op_msg.rb +1 -0
  98. data/lib/mongo/operation/write_command/op_msg.rb +2 -0
  99. data/lib/mongo/operation.rb +1 -0
  100. data/lib/mongo/options/redacted.rb +2 -2
  101. data/lib/mongo/protocol/compressed.rb +15 -11
  102. data/lib/mongo/protocol/message.rb +16 -16
  103. data/lib/mongo/protocol/registry.rb +1 -1
  104. data/lib/mongo/protocol/serializers.rb +1 -3
  105. data/lib/mongo/query_cache.rb +3 -5
  106. data/lib/mongo/server/connection.rb +3 -1
  107. data/lib/mongo/server/connection_common.rb +9 -1
  108. data/lib/mongo/server/connection_pool/generation_manager.rb +1 -1
  109. data/lib/mongo/server/connection_pool.rb +79 -18
  110. data/lib/mongo/server/description/features.rb +2 -2
  111. data/lib/mongo/server/description.rb +24 -2
  112. data/lib/mongo/server/monitor/connection.rb +3 -2
  113. data/lib/mongo/server/monitor.rb +133 -30
  114. data/lib/mongo/server/pending_connection.rb +1 -0
  115. data/lib/mongo/server/push_monitor.rb +16 -2
  116. data/lib/mongo/server.rb +27 -7
  117. data/lib/mongo/server_selector/base.rb +16 -6
  118. data/lib/mongo/server_selector/nearest.rb +3 -0
  119. data/lib/mongo/server_selector/primary.rb +3 -0
  120. data/lib/mongo/server_selector/primary_preferred.rb +3 -0
  121. data/lib/mongo/server_selector/secondary.rb +3 -0
  122. data/lib/mongo/server_selector/secondary_preferred.rb +7 -1
  123. data/lib/mongo/session.rb +56 -24
  124. data/lib/mongo/socket/ssl.rb +3 -3
  125. data/lib/mongo/socket.rb +1 -7
  126. data/lib/mongo/srv/monitor.rb +11 -2
  127. data/lib/mongo/srv/resolver.rb +1 -1
  128. data/lib/mongo/srv/result.rb +5 -3
  129. data/lib/mongo/timeout.rb +10 -15
  130. data/lib/mongo/tracing/open_telemetry/command_tracer.rb +43 -2
  131. data/lib/mongo/uri/options_mapper.rb +21 -9
  132. data/lib/mongo/uri/srv_protocol.rb +4 -4
  133. data/lib/mongo/uri.rb +56 -8
  134. data/lib/mongo/utils.rb +1 -1
  135. data/lib/mongo/version.rb +1 -1
  136. data/lib/mongo.rb +1 -0
  137. metadata +6 -4
  138. data/lib/mongo/auth/stringprep/unicode_normalize/normalize.rb +0 -173
  139. data/lib/mongo/auth/stringprep/unicode_normalize/tables.rb +0 -1164
@@ -141,6 +141,13 @@ module Mongo
141
141
  @pending_connections = Set.new
142
142
  @interrupt_connections = []
143
143
 
144
+ # RUBY-3364: count threads currently blocked on size_cv /
145
+ # max_connecting_cv. When non-zero, a newly-arriving thread must
146
+ # enter the wait queue even if the gate predicate is currently
147
+ # satisfied, to prevent barging past existing waiters.
148
+ @size_waiters = 0
149
+ @max_connecting_waiters = 0
150
+
144
151
  # Mutex used for synchronizing access to @available_connections and
145
152
  # @checked_out_connections. The pool object is thread-safe, thus
146
153
  # all methods that retrieve or modify instance variables generally
@@ -862,6 +869,22 @@ module Mongo
862
869
  end
863
870
  end
864
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
+
865
888
  private
866
889
 
867
890
  # Returns the next available connection, optionally with given
@@ -939,7 +962,7 @@ module Mongo
939
962
 
940
963
  # Removes and disconnects all stale available connections.
941
964
  def remove_stale_connection
942
- return unless conn = @available_connections.detect { |c| connection_stale_unlocked?(c) }
965
+ return unless (conn = @available_connections.detect { |c| connection_stale_unlocked?(c) })
943
966
 
944
967
  conn.disconnect!(reason: :stale)
945
968
  @available_connections.delete(conn)
@@ -951,7 +974,7 @@ module Mongo
951
974
  return false if @interrupt_connections.empty?
952
975
 
953
976
  gens = Set.new
954
- while conn = @interrupt_connections.pop
977
+ while (conn = @interrupt_connections.pop)
955
978
  if @checked_out_connections.include?(conn)
956
979
  # If the connection has been checked out, mark it as interrupted and it will
957
980
  # be disconnected on check in.
@@ -1035,7 +1058,11 @@ module Mongo
1035
1058
  @server.unknown!(
1036
1059
  generation: e.generation,
1037
1060
  service_id: e.service_id,
1038
- 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)
1039
1066
  )
1040
1067
  end
1041
1068
  connection.disconnect!(reason: :error)
@@ -1139,12 +1166,6 @@ module Mongo
1139
1166
  raise Error::ConnectionCheckOutTimeout.new(msg, address: @server.address)
1140
1167
  end
1141
1168
 
1142
- def raise_check_out_timeout_locked!(connection_global_id)
1143
- @lock.synchronize do
1144
- raise_check_out_timeout!(connection_global_id)
1145
- end
1146
- end
1147
-
1148
1169
  def raise_if_pool_closed!
1149
1170
  return unless closed?
1150
1171
 
@@ -1239,7 +1260,7 @@ module Mongo
1239
1260
  # @raise [ Timeout::Error ] If the connection pool is at maximum size
1240
1261
  # and remains so for longer than the wait timeout.
1241
1262
  def get_connection(pid, connection_global_id)
1242
- if connection = next_available_connection(connection_global_id)
1263
+ if (connection = next_available_connection(connection_global_id))
1243
1264
  return nil unless valid_available_connection?(connection, pid, connection_global_id)
1244
1265
 
1245
1266
  # We've got a connection, so we decrement the number of connection
@@ -1301,13 +1322,27 @@ module Mongo
1301
1322
  connection = nil
1302
1323
 
1303
1324
  @lock.synchronize do
1304
- # The first gate to checking out a connection. Make sure the number of
1305
- # unavailable connections is less than the max pool size.
1306
- until max_size == 0 || unavailable_connections < max_size
1325
+ # RUBY-3364: if any thread is already waiting for a size slot,
1326
+ # join the queue even when the gate predicate is currently
1327
+ # satisfied. Without this, re-entering threads (those that just
1328
+ # checked a connection back in) barge past existing waiters and
1329
+ # the 195 blocked threads in a 200:5 scenario never wake.
1330
+ # Skip the gate for unlimited pools (max_size == 0) where there
1331
+ # is no size constraint to wait on.
1332
+ must_wait = max_size != 0 && @size_waiters > 0
1333
+ until (max_size == 0 || unavailable_connections < max_size) && !must_wait
1307
1334
  wait = deadline - Utils.monotonic_time
1308
1335
  raise_check_out_timeout!(connection_global_id) if wait <= 0
1309
- @size_cv.wait(wait)
1336
+ @size_waiters += 1
1337
+ begin
1338
+ @size_cv.wait(wait)
1339
+ ensure
1340
+ @size_waiters -= 1
1341
+ end
1310
1342
  raise_if_not_ready!
1343
+ # After one wait cycle we have served our "queue tax" and
1344
+ # compete for the slot on the next predicate check.
1345
+ must_wait = false
1311
1346
  end
1312
1347
  @connection_requests += 1
1313
1348
  connection = wait_for_connection(connection_global_id, deadline)
@@ -1319,8 +1354,17 @@ module Mongo
1319
1354
  @checked_out_connections << connection
1320
1355
  @pending_connections.delete(connection) if @pending_connections.include?(connection)
1321
1356
  @max_connecting_cv.signal
1322
- # no need to signal size_cv here since the number of unavailable
1323
- # connections is unchanged.
1357
+ # RUBY-3364: hand off the baton. A waiter that arrived during
1358
+ # our wake-up window (seeing our stale @size_waiters > 0) may be
1359
+ # parked on @size_cv with capacity already available. The
1360
+ # regular check-in path is the only other place that signals
1361
+ # @size_cv, so we wake the next waiter only when the predicate
1362
+ # is actually satisfied for them. Signaling unconditionally
1363
+ # would re-queue a waiter at the back of the FIFO and break
1364
+ # ordering.
1365
+ if @size_waiters > 0 && (max_size == 0 || unavailable_connections < max_size)
1366
+ @size_cv.signal
1367
+ end
1324
1368
  end
1325
1369
 
1326
1370
  connection
@@ -1342,9 +1386,12 @@ module Mongo
1342
1386
  def wait_for_connection(connection_global_id, deadline)
1343
1387
  connection = nil
1344
1388
  while connection.nil?
1389
+ # RUBY-3364: as above, yield to any thread already queued for
1390
+ # a max_connecting slot before competing ourselves.
1391
+ must_wait = @max_connecting_waiters > 0
1345
1392
  # The second gate to checking out a connection. Make sure 1) there
1346
1393
  # exists an available connection and 2) we are under max_connecting.
1347
- until @available_connections.any? || @pending_connections.length < @max_connecting
1394
+ until (@available_connections.any? || @pending_connections.length < @max_connecting) && !must_wait
1348
1395
  wait = deadline - Utils.monotonic_time
1349
1396
  if wait <= 0
1350
1397
  # We are going to raise a timeout error, so the connection
@@ -1353,10 +1400,16 @@ module Mongo
1353
1400
  decrement_connection_requests_and_signal
1354
1401
  raise_check_out_timeout!(connection_global_id)
1355
1402
  end
1356
- @max_connecting_cv.wait(wait)
1403
+ @max_connecting_waiters += 1
1404
+ begin
1405
+ @max_connecting_cv.wait(wait)
1406
+ ensure
1407
+ @max_connecting_waiters -= 1
1408
+ end
1357
1409
  # We do not need to decrement the connection_requests counter
1358
1410
  # or signal here because the pool is not ready yet.
1359
1411
  raise_if_not_ready!
1412
+ must_wait = false
1360
1413
  end
1361
1414
 
1362
1415
  connection = get_connection(Process.pid, connection_global_id)
@@ -1370,6 +1423,14 @@ module Mongo
1370
1423
  raise_check_out_timeout!(connection_global_id)
1371
1424
  end
1372
1425
 
1426
+ # RUBY-3364: hand off the baton for max_connecting_cv. Signal
1427
+ # only if the gate predicate is satisfied for the next waiter, to
1428
+ # avoid re-queuing a waiter at the back of the FIFO.
1429
+ if @max_connecting_waiters > 0 &&
1430
+ (@available_connections.any? || @pending_connections.length < @max_connecting)
1431
+ @max_connecting_cv.signal
1432
+ end
1433
+
1373
1434
  connection
1374
1435
  end
1375
1436
 
@@ -61,7 +61,7 @@ module Mongo
61
61
  # The wire protocol versions that this version of the driver supports.
62
62
  #
63
63
  # @since 2.0.0
64
- DRIVER_WIRE_VERSIONS = 8..25
64
+ DRIVER_WIRE_VERSIONS = 8..29
65
65
 
66
66
  # The wire protocol versions that are deprecated in this version of the
67
67
  # driver. Support for these versions will be removed in the future.
@@ -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)
@@ -185,14 +185,35 @@ module Mongo
185
185
  # @api private
186
186
  CONNECTION_ID = 'connectionId'
187
187
 
188
+ # Constant for reading the modern primary flag from config.
189
+ #
190
+ # @api private
191
+ IS_WRITABLE_PRIMARY = 'isWritablePrimary'
192
+
193
+ # Constant for reading the helloOk capability flag from config.
194
+ #
195
+ # @api private
196
+ HELLO_OK = 'helloOk'
197
+
188
198
  # Fields to exclude when comparing two descriptions.
189
199
  #
200
+ # The PRIMARY (legacy `ismaster`), IS_WRITABLE_PRIMARY (modern `hello`),
201
+ # and HELLO_OK keys are excluded because the driver does a one-time
202
+ # legacy-`isMaster` to modern-`hello` protocol switch on the initial
203
+ # handshake (per the SDAM Server Monitoring spec). Two responses for the
204
+ # same logical role differ only in which of these keys is populated.
205
+ # The role itself is still differentiated by the SECONDARY flag and the
206
+ # remaining replica-set metadata.
207
+ #
190
208
  # @since 2.0.6
191
209
  EXCLUDE_FOR_COMPARISON = [ LOCAL_TIME,
192
210
  LAST_WRITE,
193
211
  OPERATION_TIME,
194
212
  Operation::CLUSTER_TIME,
195
- CONNECTION_ID, ].freeze
213
+ CONNECTION_ID,
214
+ PRIMARY,
215
+ IS_WRITABLE_PRIMARY,
216
+ HELLO_OK, ].freeze
196
217
 
197
218
  # Instantiate the new server description from the result of the hello
198
219
  # command or fabricate a placeholder description for Unknown and
@@ -258,7 +279,7 @@ module Mongo
258
279
  "The server at #{address.seed} did not provide a service id in handshake response"
259
280
  end
260
281
 
261
- fake_service_id = if process_id = topology_version && topology_version['processId']
282
+ fake_service_id = if (process_id = topology_version && topology_version['processId'])
262
283
  "process:#{process_id}"
263
284
  else
264
285
  "fake:#{rand((2**32) - 1) + 1}"
@@ -866,6 +887,7 @@ module Mongo
866
887
  def ==(other)
867
888
  return false if self.class != other.class
868
889
  return false if unknown? || other.unknown?
890
+ return false if server_type != other.server_type
869
891
 
870
892
  (config.keys + other.config.keys).uniq.all? do |k|
871
893
  config[k] == other.config[k] || EXCLUDE_FOR_COMPARISON.include?(k)
@@ -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.