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
@@ -21,28 +21,17 @@ module Mongo
21
21
  #
22
22
  # @api private
23
23
  class Credentials
24
- # @return [ Credentials::AWS | nil ] AWS KMS credentials.
25
- attr_reader :aws
24
+ # KMS provider types that support on-demand credential retrieval.
25
+ ON_DEMAND_PROVIDERS = %w[aws gcp azure].freeze
26
26
 
27
- # @return [ Credentials::Azure | nil ] Azure KMS credentials.
28
- attr_reader :azure
27
+ attr_reader :credentials_map
29
28
 
30
- # @return [ Credentials::GCP | nil ] GCP KMS credentials.
31
- attr_reader :gcp
32
-
33
- # @return [ Credentials::KMIP | nil ] KMIP KMS credentials.
34
- attr_reader :kmip
35
-
36
- # @return [ Credentials::Local | nil ] Local KMS credentials.
37
- attr_reader :local
38
-
39
- # Creates a KMS credentials object form a parameters hash.
29
+ # Creates a KMS credentials object from a parameters hash.
40
30
  #
41
- # @param [ Hash ] kms_providers A hash that contains credential for
42
- # KMS providers. The hash should have KMS provider names as keys,
43
- # and required parameters for every provider as values.
44
- # Required parameters for KMS providers are described in corresponding
45
- # classes inside Mongo::Crypt::KMS module.
31
+ # @param [ Hash ] kms_providers A hash that contains credentials for
32
+ # KMS providers. Keys may be provider types (:aws, :local, etc.) or
33
+ # named provider identifiers ("aws:name1", "local:name2", etc.).
34
+ # Values are hashes of credentials for the corresponding provider type.
46
35
  #
47
36
  # @note There may be more than one KMS provider specified.
48
37
  #
@@ -51,29 +40,79 @@ module Mongo
51
40
  def initialize(kms_providers)
52
41
  raise ArgumentError.new('KMS providers options must not be nil') if kms_providers.nil?
53
42
 
54
- @aws = AWS::Credentials.new(kms_providers[:aws]) if kms_providers.key?(:aws)
55
- @azure = Azure::Credentials.new(kms_providers[:azure]) if kms_providers.key?(:azure)
56
- @gcp = GCP::Credentials.new(kms_providers[:gcp]) if kms_providers.key?(:gcp)
57
- @kmip = KMIP::Credentials.new(kms_providers[:kmip]) if kms_providers.key?(:kmip)
58
- @local = Local::Credentials.new(kms_providers[:local]) if kms_providers.key?(:local)
59
- return unless @aws.nil? && @azure.nil? && @gcp.nil? && @kmip.nil? && @local.nil?
43
+ @credentials_map = {}
44
+
45
+ kms_providers.each do |identifier, opts|
46
+ identifier_str = identifier.to_s
47
+ provider_type = KMS.provider_base_type(identifier_str)
48
+
49
+ creds = case provider_type
50
+ when 'aws' then AWS::Credentials.new(opts)
51
+ when 'azure' then Azure::Credentials.new(opts)
52
+ when 'gcp' then GCP::Credentials.new(opts)
53
+ when 'kmip' then KMIP::Credentials.new(opts)
54
+ when 'local' then Local::Credentials.new(opts)
55
+ else
56
+ raise ArgumentError.new(
57
+ 'KMS providers options must have one of the following keys: ' \
58
+ ':aws, :azure, :gcp, :kmip, :local'
59
+ )
60
+ end
61
+
62
+ @credentials_map[identifier_str] = creds
63
+ end
64
+
65
+ return unless @credentials_map.empty?
60
66
 
61
67
  raise ArgumentError.new(
62
- 'KMS providers options must have one of the following keys: ' +
68
+ 'KMS providers options must have one of the following keys: ' \
63
69
  ':aws, :azure, :gcp, :kmip, :local'
64
70
  )
65
71
  end
66
72
 
73
+ # @return [ Credentials::AWS | nil ] AWS KMS credentials (unnamed provider only).
74
+ def aws
75
+ @credentials_map['aws']
76
+ end
77
+
78
+ # @return [ Credentials::Azure | nil ] Azure KMS credentials (unnamed provider only).
79
+ def azure
80
+ @credentials_map['azure']
81
+ end
82
+
83
+ # @return [ Credentials::GCP | nil ] GCP KMS credentials (unnamed provider only).
84
+ def gcp
85
+ @credentials_map['gcp']
86
+ end
87
+
88
+ # @return [ Credentials::KMIP | nil ] KMIP KMS credentials (unnamed provider only).
89
+ def kmip
90
+ @credentials_map['kmip']
91
+ end
92
+
93
+ # @return [ Credentials::Local | nil ] Local KMS credentials (unnamed provider only).
94
+ def local
95
+ @credentials_map['local']
96
+ end
97
+
98
+ # Returns true if any configured provider supports on-demand credential
99
+ # retrieval and has been configured with empty credentials.
100
+ #
101
+ # @return [ Boolean ]
102
+ def any_on_demand?
103
+ @credentials_map.any? do |identifier, creds|
104
+ ON_DEMAND_PROVIDERS.include?(KMS.provider_base_type(identifier)) && creds.empty?
105
+ end
106
+ end
107
+
67
108
  # Convert credentials object to a BSON document in libmongocrypt format.
68
109
  #
69
110
  # @return [ BSON::Document ] Credentials as BSON document.
70
111
  def to_document
71
112
  BSON::Document.new.tap do |bson|
72
- bson[:aws] = @aws.to_document if @aws
73
- bson[:azure] = @azure.to_document if @azure
74
- bson[:gcp] = @gcp.to_document if @gcp
75
- bson[:kmip] = @kmip.to_document if @kmip
76
- bson[:local] = @local.to_document if @local
113
+ @credentials_map.each do |identifier, creds|
114
+ bson[identifier] = creds.to_document
115
+ end
77
116
  end
78
117
  end
79
118
  end
@@ -31,8 +31,12 @@ module Mongo
31
31
  # @return [ String | nil ] KMIP KMS endpoint with optional port.
32
32
  attr_reader :endpoint
33
33
 
34
+ # @return [ true | false | nil ] Whether the KMIP server performs
35
+ # encryption and decryption of the data key.
36
+ attr_reader :delegated
37
+
34
38
  FORMAT_HINT = 'KMIP KMS key document must be in the format: ' +
35
- "{ key_id: 'KEY-ID', endpoint: 'ENDPOINT' }"
39
+ "{ key_id: 'KEY-ID', endpoint: 'ENDPOINT', delegated: true|false }"
36
40
 
37
41
  # Creates a master key document object form a parameters hash.
38
42
  #
@@ -42,6 +46,9 @@ module Mongo
42
46
  # a 96 byte KMIP Secret Data managed object, optional. If key_id
43
47
  # is omitted, the driver creates a random 96 byte identifier.
44
48
  # @option opts [ String | nil ] :endpoint KMIP endpoint, optional.
49
+ # @option opts [ true | false | nil ] :delegated If true, the KMIP
50
+ # server performs encryption and decryption of the data key,
51
+ # optional. Defaults to false.
45
52
  #
46
53
  # @raise [ ArgumentError ] If required options are missing or incorrectly
47
54
  # formatted.
@@ -52,6 +59,7 @@ module Mongo
52
59
  @endpoint = validate_param(
53
60
  :endpoint, opts, FORMAT_HINT, required: false
54
61
  )
62
+ @delegated = validate_delegated(opts)
55
63
  end
56
64
 
57
65
  # Convert master key document object to a BSON document in libmongocrypt format.
@@ -63,8 +71,29 @@ module Mongo
63
71
  }).tap do |bson|
64
72
  bson.update({ endpoint: endpoint }) unless endpoint.nil?
65
73
  bson.update({ keyId: key_id }) unless key_id.nil?
74
+ bson.update({ delegated: delegated }) unless delegated.nil?
66
75
  end
67
76
  end
77
+
78
+ private
79
+
80
+ # Validate the optional :delegated KMIP master key option.
81
+ #
82
+ # @param [ Hash ] opts Master key options.
83
+ #
84
+ # @return [ true | false | nil ] The delegated value, or nil if absent.
85
+ #
86
+ # @raise [ ArgumentError ] If delegated is present but not a boolean.
87
+ def validate_delegated(opts)
88
+ return nil unless opts.key?(:delegated)
89
+
90
+ value = opts[:delegated]
91
+ return value if value == true || value == false || value.nil?
92
+
93
+ raise ArgumentError.new(
94
+ "The delegated option must be a boolean; currently have #{value}"
95
+ )
96
+ end
68
97
  end
69
98
  end
70
99
  end
@@ -22,23 +22,28 @@ module Mongo
22
22
  #
23
23
  # @api private
24
24
  class MasterKeyDocument
25
- # Known KMS provider names.
25
+ # Known KMS provider types.
26
26
  KMS_PROVIDERS = %w[aws azure gcp kmip local].freeze
27
27
 
28
- # Creates a master key document object form a parameters hash.
28
+ # Creates a master key document object from a parameters hash.
29
29
  #
30
- # @param [ String ] kms_provider. KMS provider name.
30
+ # @param [ String ] kms_provider KMS provider identifier. May be a
31
+ # provider type (e.g. "aws") or a named provider (e.g. "aws:name1").
31
32
  # @param [ Hash ] options A hash that contains master key options for
32
33
  # the KMS provider.
33
34
  # Required parameters for KMS providers are described in corresponding
34
35
  # classes inside Mongo::Crypt::KMS module.
35
36
  #
36
- # @raise [ ArgumentError ] If required options are missing or incorrectly.
37
+ # @raise [ ArgumentError ] If required options are missing or incorrectly
38
+ # formatted.
37
39
  def initialize(kms_provider, options)
38
40
  raise ArgumentError.new('Key document options must not be nil') if options.nil?
39
41
 
42
+ @provider = kms_provider.to_s
43
+ provider_type = KMS.provider_base_type(@provider)
44
+
40
45
  master_key = options.fetch(:master_key, {})
41
- @key_document = case kms_provider.to_s
46
+ @key_document = case provider_type
42
47
  when 'aws' then KMS::AWS::MasterKeyDocument.new(master_key)
43
48
  when 'azure' then KMS::Azure::MasterKeyDocument.new(master_key)
44
49
  when 'gcp' then KMS::GCP::MasterKeyDocument.new(master_key)
@@ -53,7 +58,7 @@ module Mongo
53
58
  #
54
59
  # @return [ BSON::Document ] Master key document as BSON document.
55
60
  def to_document
56
- @key_document.to_document
61
+ @key_document.to_document.merge(provider: @provider)
57
62
  end
58
63
  end
59
64
  end
@@ -16,13 +16,24 @@
16
16
 
17
17
  module Mongo
18
18
  module Crypt
19
- module KMS
19
+ module KMS # rubocop:disable Style/Documentation
20
20
  # This error indicates that we could not obtain credential for
21
21
  # a KMS service.
22
22
  #
23
23
  # @api private
24
24
  class CredentialsNotFound < RuntimeError; end
25
25
 
26
+ # Returns the base provider type from a KMS provider identifier.
27
+ # For example, "aws:name1" returns "aws" and :aws returns "aws".
28
+ #
29
+ # @param [ String | Symbol ] identifier The KMS provider identifier.
30
+ # @return [ String ] The base provider type.
31
+ #
32
+ # @api private
33
+ def self.provider_base_type(identifier)
34
+ identifier.to_s.split(':').first
35
+ end
36
+
26
37
  # This module contains helper methods for validating KMS parameters.
27
38
  #
28
39
  # @api private
data/lib/mongo/cursor.rb CHANGED
@@ -382,7 +382,7 @@ module Mongo
382
382
  with_overload_retry(context: possibly_refreshed_context) do
383
383
  process(execute_operation(get_more_operation))
384
384
  end
385
- rescue Error::SocketError, Error::SocketTimeoutError
385
+ rescue Error::SocketError, Error::SocketTimeoutError, Error::ConnectionPerished
386
386
  @get_more_network_error = true
387
387
  raise
388
388
  rescue Error::OperationFailure => e
@@ -463,6 +463,12 @@ module Mongo
463
463
  if view.respond_to?(:options) && view.options.is_a?(Hash) && !view.options[:comment].nil?
464
464
  spec[:comment] = view.options[:comment]
465
465
  end
466
+ # A cursor built from a runCursorCommand response carries a getMore-specific
467
+ # maxTimeMS that is sent on getMore commands. Regular find/aggregate views
468
+ # do not expose this, so their getMores are unaffected.
469
+ if view.respond_to?(:max_time_ms_for_get_more) && view.max_time_ms_for_get_more
470
+ spec[:max_time_ms] = view.max_time_ms_for_get_more
471
+ end
466
472
  Operation::GetMore.new(spec)
467
473
  end
468
474
 
@@ -501,7 +507,7 @@ module Mongo
501
507
  end
502
508
 
503
509
  def limit
504
- @view.send(:limit)
510
+ @view.limit
505
511
  end
506
512
 
507
513
  def register
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (C) 2025 MongoDB Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module Mongo
18
+ class Database
19
+ # The minimal view a Cursor needs when it is built from an arbitrary
20
+ # command response rather than from a collection query.
21
+ #
22
+ # It carries the getMore-specific options (batchSize, maxTimeMS, comment)
23
+ # and the cursor type and timeout mode, and answers the few methods the
24
+ # Cursor reads from its view.
25
+ #
26
+ # @api private
27
+ class CursorCommandView
28
+ # @param [ Mongo::Database ] database The database the command ran on.
29
+ # @param [ Hash ] options The getMore and timeout options.
30
+ #
31
+ # @option options [ Integer ] :batch_size The batchSize for getMores.
32
+ # @option options [ Integer ] :max_time_ms The maxTimeMS for getMores.
33
+ # @option options [ Object ] :comment The comment for getMores.
34
+ # @option options [ Symbol ] :cursor_type :tailable or :tailable_await.
35
+ # @option options [ Symbol ] :timeout_mode :cursor_lifetime or :iteration.
36
+ def initialize(database, options = {})
37
+ @database = database
38
+ @options = options
39
+ end
40
+
41
+ # @return [ Mongo::Database ] The database the command ran on.
42
+ attr_reader :database
43
+
44
+ # @return [ Hash ] The view options. Used by the Cursor to read the
45
+ # getMore comment.
46
+ attr_reader :options
47
+
48
+ # @return [ Mongo::Client ] The client.
49
+ def client
50
+ database.client
51
+ end
52
+
53
+ # A placeholder collection used only so the Cursor can reach the client
54
+ # and database. The actual namespace for getMore and killCursors is taken
55
+ # from the command response, not from this collection.
56
+ #
57
+ # @return [ Mongo::Collection ] The $cmd pseudo collection.
58
+ def collection
59
+ @collection ||= Collection.new(database, '$cmd')
60
+ end
61
+
62
+ # @return [ Integer | nil ] The batchSize sent on getMore commands.
63
+ def batch_size
64
+ options[:batch_size]
65
+ end
66
+
67
+ # @return [ Integer | nil ] The maxTimeMS sent on getMore commands.
68
+ def max_time_ms_for_get_more
69
+ options[:max_time_ms]
70
+ end
71
+
72
+ # @return [ Symbol | nil ] The cursor type.
73
+ def cursor_type
74
+ options[:cursor_type]
75
+ end
76
+
77
+ # @return [ Symbol | nil ] The timeout mode.
78
+ def timeout_mode
79
+ options[:timeout_mode]
80
+ end
81
+
82
+ # @return [ Hash ] timeout values for the operation context.
83
+ def operation_timeouts(opts = {})
84
+ database.operation_timeouts(opts)
85
+ end
86
+
87
+ # Cursors do not support a limit when built from a command response.
88
+ #
89
+ # @api private
90
+ def limit
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -15,6 +15,7 @@
15
15
  # limitations under the License.
16
16
 
17
17
  require 'mongo/database/view'
18
+ require 'mongo/database/cursor_command_view'
18
19
 
19
20
  module Mongo
20
21
  # Represents a database on the db server and operations that can execute on
@@ -259,6 +260,119 @@ module Mongo
259
260
  end
260
261
  end
261
262
 
263
+ # Run a command that returns a cursor and parse the response as a cursor.
264
+ #
265
+ # The command is sent to the server unmodified; the driver MUST NOT inspect
266
+ # or alter it. If the response does not contain a cursor field an error is
267
+ # raised. The command is never retried.
268
+ #
269
+ # Note: if a +maxTimeMS+ field is already set on the command document it is
270
+ # left as-is. The +max_time_ms+ option below applies only to getMore
271
+ # commands. Setting both +timeout_ms+ and +max_time_ms+ is not supported
272
+ # and has undefined behavior.
273
+ #
274
+ # @example Run a cursor-returning command.
275
+ # database.cursor_command(checkMetadataConsistency: 1)
276
+ #
277
+ # @param [ Hash ] command The command to execute.
278
+ # @param [ Hash ] options The command options.
279
+ #
280
+ # @option options [ Hash ] :read The read preference for this command,
281
+ # used for server selection and reused for subsequent getMores.
282
+ # @option options [ Session ] :session The session to use. If none is
283
+ # given an implicit session is created and reused for the cursor's
284
+ # lifetime.
285
+ # @option options [ Integer ] :timeout_ms The operation timeout in
286
+ # milliseconds.
287
+ # @option options [ Integer ] :batch_size The batchSize to send on getMore
288
+ # commands.
289
+ # @option options [ Integer ] :max_time_ms The maxTimeMS to send on getMore
290
+ # commands.
291
+ # @option options [ Object ] :comment A comment to attach to getMore
292
+ # commands.
293
+ # @option options [ Symbol ] :cursor_type The cursor type, :tailable or
294
+ # :tailable_await. Must match the flags set on the command document.
295
+ # @option options [ Symbol ] :timeout_mode :cursor_lifetime or :iteration.
296
+ #
297
+ # @return [ Mongo::Cursor ] A cursor over the command results.
298
+ #
299
+ # @raise [ Error::InvalidCursorOperation ] If the response does not contain
300
+ # a cursor.
301
+ def cursor_command(command, options = {})
302
+ options = options.dup
303
+ execution_opts = options.delete(:execution_options) || {}
304
+ view_options = extract_cursor_command_view_options(options)
305
+
306
+ txn_read_pref = (options[:session].txn_read_preference if options[:session] && options[:session].in_transaction?)
307
+ txn_read_pref ||= options[:read] || ServerSelector::PRIMARY
308
+ Lint.validate_underscore_read_preference(txn_read_pref)
309
+ selector = ServerSelector.get(txn_read_pref)
310
+
311
+ # The session is intentionally not wrapped in #with_session: an implicit
312
+ # session must outlive this method and is ended by the cursor when it is
313
+ # exhausted or closed. Until the cursor takes ownership, the session and
314
+ # any load-balanced connection are cleaned up here on every exit path.
315
+ session = client.get_session(options)
316
+ context = Operation::Context.new(
317
+ client: client,
318
+ session: session,
319
+ operation_timeouts: operation_timeouts(options)
320
+ )
321
+ op = Operation::CursorCommand.new(
322
+ selector: command,
323
+ db_name: name,
324
+ read: selector,
325
+ session: session
326
+ )
327
+
328
+ # Per the client-backpressure spec, retrying a generic command on
329
+ # overload errors requires both retryable reads and writes to be
330
+ # enabled, same as Database#command.
331
+ retry_enabled = client.options[:retry_reads] != false &&
332
+ client.options[:retry_writes] != false
333
+
334
+ server = nil
335
+ connection = nil
336
+ cursor = nil
337
+ begin
338
+ result = with_overload_retry(context: context, retry_enabled: retry_enabled) do
339
+ server = selector.select_server(cluster, nil, session)
340
+ if server.load_balancer?
341
+ # The connection is checked in by the cursor when it is drained.
342
+ connection = check_out_cursor_command_connection(server, context)
343
+ begin
344
+ op.execute_with_connection(connection, context: context, options: execution_opts)
345
+ rescue StandardError
346
+ # Release the connection before the error propagates so that
347
+ # a retried attempt checks out a fresh one.
348
+ connection.connection_pool.check_in(connection) unless connection.pinned?
349
+ connection = nil
350
+ raise
351
+ end
352
+ else
353
+ op.execute(server, context: context, options: execution_opts)
354
+ end
355
+ end
356
+
357
+ unless result.cursor?
358
+ raise Error::InvalidCursorOperation,
359
+ 'The command response did not include a cursor. ' \
360
+ 'Use Database#command for commands that do not return a cursor.'
361
+ end
362
+
363
+ view = CursorCommandView.new(self, view_options)
364
+ cursor = Cursor.new(view, result, server, session: session, context: context)
365
+ ensure
366
+ # If the cursor was created it owns the session and connection;
367
+ # otherwise (error or no cursor in the response) release them here.
368
+ unless cursor
369
+ connection.connection_pool.check_in(connection) if connection && !connection.pinned?
370
+ session.end_session if session && session.implicit?
371
+ end
372
+ end
373
+ cursor
374
+ end
375
+
262
376
  # Execute a read command on the database, retrying the read if necessary.
263
377
  #
264
378
  # @param [ Hash ] operation The command to execute.
@@ -530,24 +644,6 @@ module Mongo
530
644
  )
531
645
  end
532
646
 
533
- # Create a database for the provided client, for use when we don't want the
534
- # client's original database instance to be the same.
535
- #
536
- # @api private
537
- #
538
- # @example Create a database for the client.
539
- # Database.create(client)
540
- #
541
- # @param [ Client ] client The client to create on.
542
- #
543
- # @return [ Database ] The database.
544
- #
545
- # @since 2.0.0
546
- def self.create(client)
547
- database = Database.new(client, client.options[:database], client.options)
548
- client.instance_variable_set(:@database, database)
549
- end
550
-
551
647
  # @return [ Integer | nil ] Operation timeout that is for this database or
552
648
  # for the corresponding client.
553
649
  #
@@ -570,5 +666,35 @@ module Mongo
570
666
  end
571
667
  end
572
668
  end
669
+
670
+ private
671
+
672
+ # Removes the getMore and cursor options from the options hash and returns
673
+ # them as a separate hash for the CursorCommandView. The remaining options
674
+ # (e.g. :session, :read, :timeout_ms) are left for command execution.
675
+ #
676
+ # @param [ Hash ] options The cursor_command options (mutated).
677
+ #
678
+ # @return [ Hash ] The view options.
679
+ def extract_cursor_command_view_options(options)
680
+ %i[ batch_size max_time_ms comment cursor_type timeout_mode ].each_with_object({}) do |key, view_options|
681
+ view_options[key] = options.delete(key) if options.key?(key)
682
+ end
683
+ end
684
+
685
+ # Checks out a load balanced connection for a cursor command. If the
686
+ # session is pinned to a connection (e.g. in a transaction), that
687
+ # connection is reused.
688
+ #
689
+ # @param [ Server ] server The load balancer server.
690
+ # @param [ Operation::Context ] context The operation context.
691
+ #
692
+ # @return [ Server::Connection ] The checked out connection.
693
+ def check_out_cursor_command_connection(server, context)
694
+ connection = if context.connection_global_id
695
+ server.pool.check_out_pinned_connection(context.connection_global_id)
696
+ end
697
+ connection || server.pool.check_out(context: context)
698
+ end
573
699
  end
574
700
  end
@@ -34,6 +34,11 @@ module Mongo
34
34
  # @return [ BSON::Document ] result The error result.
35
35
  attr_reader :result
36
36
 
37
+ # @return [ Array<String> ] Deduplicated list of "host:port" addresses of
38
+ # the servers that produced this bulk write error. Empty when no
39
+ # addresses were supplied.
40
+ attr_reader :server_addresses
41
+
37
42
  # Instantiate the new exception.
38
43
  #
39
44
  # @example Instantiate the exception.
@@ -41,10 +46,14 @@ module Mongo
41
46
  #
42
47
  # @param [ Hash ] result A processed response from the server
43
48
  # reporting results of the operation.
49
+ # @param [ Array<String | Mongo::Address | Mongo::Server::Description> ]
50
+ # server_addresses Addresses of the servers that produced this error.
51
+ # Entries are normalized to "host:port" strings.
44
52
  #
45
53
  # @since 2.0.0
46
- def initialize(result)
54
+ def initialize(result, server_addresses: nil)
47
55
  @result = result
56
+ @server_addresses = normalize_server_addresses(server_addresses)
48
57
 
49
58
  # Exception constructor behaves differently for a nil argument and
50
59
  # for no argument. Avoid passing nil explicitly.
@@ -93,8 +102,27 @@ module Mongo
93
102
 
94
103
  fragment = "Multiple errors: #{fragment}" if errors.length > 1
95
104
 
105
+ if Mongo.include_server_address_in_errors && @server_addresses.any?
106
+ fragment = "#{fragment} (on #{@server_addresses.join(', ')})"
107
+ end
108
+
96
109
  fragment
97
110
  end
111
+
112
+ def normalize_server_addresses(value)
113
+ return [] if value.nil?
114
+
115
+ Array(value).filter_map do |entry|
116
+ case entry
117
+ when String then entry
118
+ when Mongo::Address then entry.seed
119
+ when Mongo::Server::Description then entry.address&.seed
120
+ else
121
+ raise ArgumentError,
122
+ "server_addresses entries must be String, Mongo::Address, or Mongo::Server::Description; got #{entry.class}"
123
+ end
124
+ end.uniq
125
+ end
98
126
  end
99
127
  end
100
128
  end
@@ -33,6 +33,9 @@ module Mongo
33
33
  # Error message when hedge is specified for a read preference that does not support it.
34
34
  #
35
35
  # @api private
36
+ #
37
+ # @deprecated Hedged reads are deprecated in MongoDB Server 8.0 and will
38
+ # be removed in a future version.
36
39
  NO_HEDGE_SUPPORT = 'The hedge option cannot be set for this read preference'
37
40
 
38
41
  # Error message for when the max staleness is not at least twice the heartbeat frequency.
@@ -23,12 +23,16 @@ module Mongo
23
23
  class InvalidURI < Error
24
24
  # Instantiate the new exception.
25
25
  #
26
+ # The URI is redacted via {Mongo::URI.redact} before being interpolated
27
+ # so that any cleartext credentials in the original input do not end up
28
+ # in logs, error reporters, or backtraces.
29
+ #
26
30
  # @example Instantiate the exception.
27
31
  # Mongo::Error::InvalidURI.new(uri, details, format)
28
32
  #
29
33
  # @since 2.0.0
30
34
  def initialize(uri, details, format = nil)
31
- message = "Bad URI: #{uri}\n" +
35
+ message = "Bad URI: #{Mongo::URI.redact(uri)}\n" +
32
36
  "#{details}\n"
33
37
  message += "MongoDB URI must be in the following format: #{format}\n" if format
34
38
  message += "Please see the following URL for more information: #{Mongo::URI::HELP}\n"