mongo 2.19.2 → 2.20.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 (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/Rakefile +27 -154
  4. data/lib/mongo/cluster/topology/base.rb +16 -0
  5. data/lib/mongo/cluster.rb +27 -1
  6. data/lib/mongo/collection/view/iterable.rb +1 -0
  7. data/lib/mongo/collection.rb +27 -3
  8. data/lib/mongo/error/transactions_not_supported.rb +34 -0
  9. data/lib/mongo/error.rb +1 -0
  10. data/lib/mongo/grid/fs_bucket.rb +6 -0
  11. data/lib/mongo/monitoring/event/secure.rb +1 -1
  12. data/lib/mongo/operation/create_search_indexes/op_msg.rb +31 -0
  13. data/lib/mongo/operation/create_search_indexes.rb +15 -0
  14. data/lib/mongo/operation/drop_search_index/op_msg.rb +33 -0
  15. data/lib/mongo/operation/drop_search_index.rb +15 -0
  16. data/lib/mongo/operation/shared/executable.rb +43 -27
  17. data/lib/mongo/operation/shared/response_handling.rb +23 -25
  18. data/lib/mongo/operation/shared/specifiable.rb +7 -0
  19. data/lib/mongo/operation/update_search_index/op_msg.rb +34 -0
  20. data/lib/mongo/operation/update_search_index.rb +15 -0
  21. data/lib/mongo/operation.rb +3 -0
  22. data/lib/mongo/retryable/read_worker.rb +7 -6
  23. data/lib/mongo/retryable/write_worker.rb +7 -4
  24. data/lib/mongo/retryable.rb +2 -2
  25. data/lib/mongo/search_index/view.rb +232 -0
  26. data/lib/mongo/server/app_metadata/environment.rb +64 -9
  27. data/lib/mongo/server/app_metadata.rb +5 -4
  28. data/lib/mongo/server/description/features.rb +1 -0
  29. data/lib/mongo/server_selector/base.rb +32 -6
  30. data/lib/mongo/session/server_session/dirtyable.rb +52 -0
  31. data/lib/mongo/session/server_session.rb +3 -0
  32. data/lib/mongo/session/session_pool.rb +12 -18
  33. data/lib/mongo/session.rb +32 -0
  34. data/lib/mongo/uri.rb +0 -4
  35. data/lib/mongo/version.rb +1 -1
  36. data/lib/mongo.rb +1 -0
  37. data/mongo.gemspec +1 -7
  38. data/spec/atlas/atlas_connectivity_spec.rb +5 -9
  39. data/spec/atlas/operations_spec.rb +1 -5
  40. data/spec/faas/ruby-sam-app/Gemfile +9 -0
  41. data/spec/faas/ruby-sam-app/mongodb/Gemfile +4 -0
  42. data/spec/faas/ruby-sam-app/mongodb/app.rb +149 -0
  43. data/spec/faas/ruby-sam-app/template.yaml +48 -0
  44. data/spec/integration/client_side_encryption/corpus_spec.rb +10 -2
  45. data/spec/integration/retryable_reads_errors_spec.rb +161 -8
  46. data/spec/integration/retryable_writes_errors_spec.rb +156 -0
  47. data/spec/integration/search_indexes_prose_spec.rb +168 -0
  48. data/spec/lite_spec_helper.rb +32 -10
  49. data/spec/mongo/cluster_spec.rb +36 -0
  50. data/spec/mongo/collection/view/aggregation_spec.rb +6 -1
  51. data/spec/mongo/collection/view/explainable_spec.rb +2 -0
  52. data/spec/mongo/collection_crud_spec.rb +1 -1
  53. data/spec/mongo/operation/insert_spec.rb +1 -1
  54. data/spec/mongo/retryable/write_worker_spec.rb +39 -0
  55. data/spec/mongo/server/app_metadata/environment_spec.rb +135 -0
  56. data/spec/mongo/server/app_metadata_spec.rb +12 -2
  57. data/spec/mongo/server/connection_spec.rb +4 -0
  58. data/spec/mongo/session/session_pool_spec.rb +1 -16
  59. data/spec/mongo/session_transaction_spec.rb +15 -0
  60. data/spec/mongo/uri_spec.rb +0 -9
  61. data/spec/runners/crud/test.rb +0 -8
  62. data/spec/runners/crud.rb +1 -1
  63. data/spec/runners/transactions/test.rb +12 -3
  64. data/spec/runners/unified/assertions.rb +16 -3
  65. data/spec/runners/unified/crud_operations.rb +12 -0
  66. data/spec/runners/unified/search_index_operations.rb +63 -0
  67. data/spec/runners/unified/support_operations.rb +3 -5
  68. data/spec/runners/unified/test.rb +11 -2
  69. data/spec/shared/lib/mrss/docker_runner.rb +3 -0
  70. data/spec/shared/share/Dockerfile.erb +20 -69
  71. data/spec/shared/shlib/server.sh +1 -0
  72. data/spec/shared/shlib/set_env.sh +5 -28
  73. data/spec/spec_helper.rb +1 -1
  74. data/spec/spec_tests/data/client_side_encryption/explain.yml +2 -2
  75. data/spec/spec_tests/data/connection_string/invalid-uris.yml +0 -10
  76. data/spec/spec_tests/data/connection_string/valid-options.yml +13 -0
  77. data/spec/spec_tests/data/crud_unified/find-test-all-options.yml +348 -0
  78. data/spec/spec_tests/data/index_management/createSearchIndex.yml +64 -0
  79. data/spec/spec_tests/data/index_management/createSearchIndexes.yml +86 -0
  80. data/spec/spec_tests/data/index_management/dropSearchIndex.yml +43 -0
  81. data/spec/spec_tests/data/index_management/listSearchIndexes.yml +91 -0
  82. data/spec/spec_tests/data/index_management/updateSearchIndex.yml +46 -0
  83. data/spec/spec_tests/data/retryable_writes/unified/bulkWrite-serverErrors.yml +3 -6
  84. data/spec/spec_tests/data/retryable_writes/unified/insertOne-serverErrors.yml +3 -6
  85. data/spec/spec_tests/data/run_command_unified/runCommand.yml +319 -0
  86. data/spec/spec_tests/data/sessions_unified/driver-sessions-dirty-session-errors.yml +351 -0
  87. data/spec/spec_tests/data/unified/valid-pass/poc-crud.yml +1 -1
  88. data/spec/spec_tests/data/unified/valid-pass/poc-retryable-writes.yml +7 -7
  89. data/spec/spec_tests/data/unified/valid-pass/poc-sessions.yml +3 -4
  90. data/spec/spec_tests/data/unified/valid-pass/poc-transactions-convenient-api.yml +1 -1
  91. data/spec/spec_tests/data/unified/valid-pass/poc-transactions-mongos-pin-auto.yml +1 -1
  92. data/spec/spec_tests/data/unified/valid-pass/poc-transactions.yml +3 -3
  93. data/spec/spec_tests/index_management_unified_spec.rb +13 -0
  94. data/spec/spec_tests/run_command_unified_spec.rb +13 -0
  95. data/spec/spec_tests/sdam_unified_spec.rb +2 -0
  96. data/spec/support/constraints.rb +6 -0
  97. data/spec/support/faas/app/aws_lambda/mongodb/Gemfile.lock +19 -0
  98. data/spec/support/ocsp +1 -1
  99. data/spec/support/recording_logger.rb +27 -0
  100. data/spec/support/spec_config.rb +5 -0
  101. data.tar.gz.sig +0 -0
  102. metadata +1329 -1285
  103. metadata.gz.sig +3 -2
  104. data/spec/spec_tests/data/cmap/pool-clear-interrupt-immediately.yml +0 -49
@@ -260,6 +260,13 @@ module Mongo
260
260
  spec[INDEX]
261
261
  end
262
262
 
263
+ # Get the index id from the spec.
264
+ #
265
+ # @return [ String ] The index id.
266
+ def index_id
267
+ spec[:index_id]
268
+ end
269
+
263
270
  # Get the index name from the spec.
264
271
  #
265
272
  # @example Get the index name.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongo
4
+ module Operation
5
+ class UpdateSearchIndex
6
+ # A MongoDB updateSearchIndex operation sent as an op message.
7
+ #
8
+ # @api private
9
+ class OpMsg < OpMsgBase
10
+ include ExecutableTransactionLabel
11
+
12
+ private
13
+
14
+ # Returns the command to send to the database, describing the
15
+ # desired updateSearchIndex operation.
16
+ #
17
+ # @param [ Mongo::Server ] _server the server that will receive the
18
+ # command
19
+ #
20
+ # @return [ Hash ] the selector
21
+ def selector(_server)
22
+ {
23
+ updateSearchIndex: coll_name,
24
+ :$db => db_name,
25
+ definition: index,
26
+ }.tap do |sel|
27
+ sel[:id] = index_id if index_id
28
+ sel[:name] = index_name if index_name
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mongo/operation/update_search_index/op_msg'
4
+
5
+ module Mongo
6
+ module Operation
7
+ # A MongoDB updateSearchIndex command operation.
8
+ #
9
+ # @api private
10
+ class UpdateSearchIndex
11
+ include Specifiable
12
+ include OpMsgExecutable
13
+ end
14
+ end
15
+ end
@@ -51,6 +51,9 @@ require 'mongo/operation/update_user'
51
51
  require 'mongo/operation/remove_user'
52
52
  require 'mongo/operation/create_index'
53
53
  require 'mongo/operation/drop_index'
54
+ require 'mongo/operation/create_search_indexes'
55
+ require 'mongo/operation/drop_search_index'
56
+ require 'mongo/operation/update_search_index'
54
57
 
55
58
  module Mongo
56
59
 
@@ -190,12 +190,13 @@ module Mongo
190
190
  #
191
191
  # @return [ Result ] The result of the operation.
192
192
  def modern_read_with_retry(session, server_selector, &block)
193
- yield select_server(cluster, server_selector, session)
193
+ server = select_server(cluster, server_selector, session)
194
+ yield server
194
195
  rescue *retryable_exceptions, Error::OperationFailure, Auth::Unauthorized, Error::PoolError => e
195
196
  e.add_notes('modern retry', 'attempt 1')
196
197
  raise e if session.in_transaction?
197
198
  raise e if !is_retryable_exception?(e) && !e.write_retryable?
198
- retry_read(e, session, server_selector, &block)
199
+ retry_read(e, session, server_selector, failed_server: server, &block)
199
200
  end
200
201
 
201
202
  # Attempts to do a "legacy" read with retry. The operation will be
@@ -257,12 +258,14 @@ module Mongo
257
258
  # being run on.
258
259
  # @param [ Mongo::ServerSelector::Selectable ] server_selector Server
259
260
  # selector for the operation.
261
+ # @param [ Mongo::Server ] failed_server The server on which the original
262
+ # operation failed.
260
263
  # @param [ Proc ] block The block to execute.
261
264
  #
262
265
  # @return [ Result ] The result of the operation.
263
- def retry_read(original_error, session, server_selector, &block)
266
+ def retry_read(original_error, session, server_selector, failed_server: nil, &block)
264
267
  begin
265
- server = select_server(cluster, server_selector, session)
268
+ server = select_server(cluster, server_selector, session, failed_server)
266
269
  rescue Error, Error::AuthError => e
267
270
  original_error.add_note("later retry failed: #{e.class}: #{e}")
268
271
  raise original_error
@@ -289,8 +292,6 @@ module Mongo
289
292
  raise original_error
290
293
  end
291
294
  end
292
-
293
295
  end
294
-
295
296
  end
296
297
  end
@@ -103,8 +103,9 @@ module Mongo
103
103
  def nro_write_with_retry(write_concern, context:, &block)
104
104
  session = context.session
105
105
  server = select_server(cluster, ServerSelector.primary, session)
106
+ options = session&.client&.options || {}
106
107
 
107
- if session&.client.options[:retry_writes]
108
+ if options[:retry_writes]
108
109
  begin
109
110
  server.with_connection(connection_global_id: context.connection_global_id) do |connection|
110
111
  yield connection, nil, context
@@ -240,7 +241,7 @@ module Mongo
240
241
 
241
242
  # Context#with creates a new context, which is not necessary here
242
243
  # but the API is less prone to misuse this way.
243
- retry_write(e, txn_num, context: context.with(is_retry: true), &block)
244
+ retry_write(e, txn_num, context: context.with(is_retry: true), failed_server: server, &block)
244
245
  end
245
246
 
246
247
  # Called after a failed write, this will retry the write no more than
@@ -250,9 +251,11 @@ module Mongo
250
251
  # retry.
251
252
  # @param [ Number ] txn_num The transaction number.
252
253
  # @param [ Operation::Context ] context The context for the operation.
254
+ # @param [ Mongo::Server ] failed_server The server on which the original
255
+ # operation failed.
253
256
  #
254
257
  # @return [ Result ] The result of the operation.
255
- def retry_write(original_error, txn_num, context:, &block)
258
+ def retry_write(original_error, txn_num, context:, failed_server: nil, &block)
256
259
  session = context.session
257
260
 
258
261
  # We do not request a scan of the cluster here, because error handling
@@ -260,7 +263,7 @@ module Mongo
260
263
  # server description and/or topology as necessary (specifically,
261
264
  # a socket error or a not master error should have marked the respective
262
265
  # server unknown). Here we just need to wait for server selection.
263
- server = select_server(cluster, ServerSelector.primary, session)
266
+ server = select_server(cluster, ServerSelector.primary, session, failed_server)
264
267
 
265
268
  unless server.retry_writes?
266
269
  # Do not need to add "modern retry" here, it should already be on
@@ -46,8 +46,8 @@ module Mongo
46
46
  # @api private
47
47
  #
48
48
  # @return [ Mongo::Server ] A server matching the server preference.
49
- def select_server(cluster, server_selector, session)
50
- server_selector.select_server(cluster, nil, session)
49
+ def select_server(cluster, server_selector, session, failed_server = nil)
50
+ server_selector.select_server(cluster, nil, session, deprioritized: [failed_server].compact)
51
51
  end
52
52
 
53
53
  # Returns the read worker for handling retryable reads.
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongo
4
+ module SearchIndex
5
+ # A class representing a view of search indexes.
6
+ class View
7
+ include Enumerable
8
+ include Retryable
9
+ include Collection::Helpers
10
+
11
+ # @return [ Mongo::Collection ] the collection this view belongs to
12
+ attr_reader :collection
13
+
14
+ # @return [ nil | String ] the index id to query
15
+ attr_reader :requested_index_id
16
+
17
+ # @return [ nil | String ] the index name to query
18
+ attr_reader :requested_index_name
19
+
20
+ # @return [ Hash ] the options hash to use for the aggregate command
21
+ # when querying the available indexes.
22
+ attr_reader :aggregate_options
23
+
24
+ # Create the new search index view.
25
+ #
26
+ # @param [ Collection ] collection The collection.
27
+ # @param [ Hash ] options The options that configure the behavior of the view.
28
+ #
29
+ # @option options [ String ] :id The specific index id to query (optional)
30
+ # @option options [ String ] :name The name of the specific index to query (optional)
31
+ # @option options [ Hash ] :aggregate The options hash to send to the
32
+ # aggregate command when querying the available indexes.
33
+ def initialize(collection, options = {})
34
+ @collection = collection
35
+ @requested_index_id = options[:id]
36
+ @requested_index_name = options[:name]
37
+ @aggregate_options = options[:aggregate] || {}
38
+
39
+ return if @aggregate_options.is_a?(Hash)
40
+
41
+ raise ArgumentError, "The :aggregate option must be a Hash (got a #{@aggregate_options.class})"
42
+ end
43
+
44
+ # Create a single search index with the given definition. If the name is
45
+ # provided, the new index will be given that name.
46
+ #
47
+ # @param [ Hash ] definition The definition of the search index.
48
+ # @param [ nil | String ] name The name to give the new search index.
49
+ #
50
+ # @return [ String ] the name of the new search index.
51
+ def create_one(definition, name: nil)
52
+ create_many([ { name: name, definition: definition } ]).first
53
+ end
54
+
55
+ # Create multiple search indexes with a single command.
56
+ #
57
+ # @param [ Array<Hash> ] indexes The description of the indexes to
58
+ # create. Each element of the list must be a hash with a definition
59
+ # key, and an optional name key.
60
+ #
61
+ # @return [ Array<String> ] the names of the new search indexes.
62
+ def create_many(indexes)
63
+ spec = spec_with(indexes: indexes.map { |v| validate_search_index!(v) })
64
+ result = Operation::CreateSearchIndexes.new(spec).execute(next_primary, context: execution_context)
65
+ result.first['indexesCreated'].map { |idx| idx['name'] }
66
+ end
67
+
68
+ # Drop the search index with the given id, or name. One or the other must
69
+ # be specified, but not both.
70
+ #
71
+ # @param [ String ] id the id of the index to drop
72
+ # @param [ String ] name the name of the index to drop
73
+ #
74
+ # @return [ Mongo::Operation::Result | false ] the result of the
75
+ # operation, or false if the given index does not exist.
76
+ def drop_one(id: nil, name: nil)
77
+ validate_id_or_name!(id, name)
78
+
79
+ spec = spec_with(index_id: id, index_name: name)
80
+ op = Operation::DropSearchIndex.new(spec)
81
+
82
+ # per the spec:
83
+ # Drivers MUST suppress NamespaceNotFound errors for the
84
+ # ``dropSearchIndex`` helper. Drop operations should be idempotent.
85
+ do_drop(op, nil, execution_context)
86
+ end
87
+
88
+ # Iterate over the search indexes.
89
+ #
90
+ # @param [ Proc ] block if given, each search index will be yieleded to
91
+ # the block.
92
+ #
93
+ # @return [ self | Enumerator ] if a block is given, self is returned.
94
+ # Otherwise, an enumerator will be returned.
95
+ def each(&block)
96
+ @result ||= begin
97
+ spec = {}.tap do |s|
98
+ s[:id] = requested_index_id if requested_index_id
99
+ s[:name] = requested_index_name if requested_index_name
100
+ end
101
+
102
+ collection.aggregate(
103
+ [ { '$listSearchIndexes' => spec } ],
104
+ aggregate_options
105
+ )
106
+ end
107
+
108
+ return @result.to_enum unless block
109
+
110
+ @result.each(&block)
111
+ self
112
+ end
113
+
114
+ # Update the search index with the given id or name. One or the other
115
+ # must be provided, but not both.
116
+ #
117
+ # @param [ Hash ] definition the definition to replace the given search
118
+ # index with.
119
+ # @param [ nil | String ] id the id of the search index to update
120
+ # @param [ nil | String ] name the name of the search index to update
121
+ #
122
+ # @return [ Mongo::Operation::Result ] the result of the operation
123
+ def update_one(definition, id: nil, name: nil)
124
+ validate_id_or_name!(id, name)
125
+
126
+ spec = spec_with(index_id: id, index_name: name, index: definition)
127
+ Operation::UpdateSearchIndex.new(spec).execute(next_primary, context: execution_context)
128
+ end
129
+
130
+ # The following methods are to make the view act more like an array,
131
+ # without having to explicitly make it an array...
132
+
133
+ # Queries whether the search index enumerable is empty.
134
+ #
135
+ # @return [ true | false ] whether the enumerable is empty or not.
136
+ def empty?
137
+ count.zero?
138
+ end
139
+
140
+ private
141
+
142
+ # A helper method for building the specification document with certain
143
+ # values pre-populated.
144
+ #
145
+ # @param [ Hash ] extras the values to put into the specification
146
+ #
147
+ # @return [ Hash ] the specification document
148
+ def spec_with(extras)
149
+ {
150
+ coll_name: collection.name,
151
+ db_name: collection.database.name,
152
+ }.merge(extras)
153
+ end
154
+
155
+ # A helper method for retrieving the primary server from the cluster.
156
+ #
157
+ # @return [ Mongo::Server ] the server to use
158
+ def next_primary(ping = nil, session = nil)
159
+ collection.cluster.next_primary(ping, session)
160
+ end
161
+
162
+ # A helper method for constructing a new operation context for executing
163
+ # an operation.
164
+ #
165
+ # @return [ Mongo::Operation::Context ] the operation context
166
+ def execution_context
167
+ Operation::Context.new(client: collection.client)
168
+ end
169
+
170
+ # Validates the given id and name, ensuring that exactly one of them
171
+ # is non-nil.
172
+ #
173
+ # @param [ nil | String ] id the id to validate
174
+ # @param [ nil | String ] name the name to validate
175
+ #
176
+ # @raise [ ArgumentError ] if neither or both arguments are nil
177
+ def validate_id_or_name!(id, name)
178
+ return unless (id.nil? && name.nil?) || (!id.nil? && !name.nil?)
179
+
180
+ raise ArgumentError, 'exactly one of id or name must be specified'
181
+ end
182
+
183
+ # Validates the given search index document, ensuring that it has no
184
+ # extra keys, and that the name and definition are valid.
185
+ #
186
+ # @param [ Hash ] doc the document to validate
187
+ #
188
+ # @raise [ ArgumentError ] if the document is invalid.
189
+ def validate_search_index!(doc)
190
+ validate_search_index_keys!(doc.keys)
191
+ validate_search_index_name!(doc[:name] || doc['name'])
192
+ validate_search_index_definition!(doc[:definition] || doc['definition'])
193
+ doc
194
+ end
195
+
196
+ # Validates the keys of a search index document, ensuring that
197
+ # they are all valid.
198
+ #
199
+ # @param [ Array<String | Hash> ] keys the keys of a search index document
200
+ #
201
+ # @raise [ ArgumentError ] if the list contains any invalid keys
202
+ def validate_search_index_keys!(keys)
203
+ extras = keys - [ 'name', 'definition', :name, :definition ]
204
+
205
+ raise ArgumentError, "invalid keys in search index creation: #{extras.inspect}" if extras.any?
206
+ end
207
+
208
+ # Validates the name of a search index, ensuring that it is either a
209
+ # String or nil.
210
+ #
211
+ # @param [ nil | String ] name the name of a search index
212
+ #
213
+ # @raise [ ArgumentError ] if the name is not valid
214
+ def validate_search_index_name!(name)
215
+ return if name.nil? || name.is_a?(String)
216
+
217
+ raise ArgumentError, "search index name must be nil or a string (got #{name.inspect})"
218
+ end
219
+
220
+ # Validates the definition of a search index.
221
+ #
222
+ # @param [ Hash ] definition the definition of a search index
223
+ #
224
+ # @raise [ ArgumentError ] if the definition is not valid
225
+ def validate_search_index_definition!(definition)
226
+ return if definition.is_a?(Hash)
227
+
228
+ raise ArgumentError, "search index definition must be a Hash (got #{definition.inspect})"
229
+ end
230
+ end
231
+ end
232
+ end
@@ -18,9 +18,12 @@ module Mongo
18
18
  class Server
19
19
  class AppMetadata
20
20
  # Implements the logic from the handshake spec, for deducing and
21
- # reporting the current FaaS environment in which the program is
21
+ # reporting the current environment in which the program is
22
22
  # executing.
23
23
  #
24
+ # This includes FaaS environment checks, as well as checks for the
25
+ # presence of a container (Docker) and/or orchestrator (Kubernetes).
26
+ #
24
27
  # @api private
25
28
  class Environment
26
29
  # Error class for reporting that too many discriminators were found
@@ -39,6 +42,10 @@ module Mongo
39
42
  # Error class for reporting that the value for a field is too long.
40
43
  class ValueTooLong < Mongo::Error; end
41
44
 
45
+ # The name and location of the .dockerenv file that will signal the
46
+ # presence of Docker.
47
+ DOCKERENV_PATH = '/.dockerenv'
48
+
42
49
  # This value is not explicitly specified in the spec, only implied to be
43
50
  # less than 512.
44
51
  MAXIMUM_VALUE_LENGTH = 500
@@ -102,9 +109,11 @@ module Mongo
102
109
  # if the environment contains invalid or contradictory state, it will
103
110
  # be initialized with {{name}} set to {{nil}}.
104
111
  def initialize
112
+ @fields = {}
105
113
  @error = nil
106
114
  @name = detect_environment
107
- populate_fields
115
+ populate_faas_fields
116
+ detect_container
108
117
  rescue TooManyEnvironments => e
109
118
  self.error = "too many environments detected: #{e.message}"
110
119
  rescue MissingVariable => e
@@ -115,6 +124,23 @@ module Mongo
115
124
  self.error = "value for #{e.message} is too long"
116
125
  end
117
126
 
127
+ # Queries the detected container information.
128
+ #
129
+ # @return [ Hash | nil ] the detected container information, or
130
+ # nil if no container was detected.
131
+ def container
132
+ fields[:container]
133
+ end
134
+
135
+ # Queries whether any environment information was able to be
136
+ # detected.
137
+ #
138
+ # @return [ true | false ] if any environment information was
139
+ # detected.
140
+ def present?
141
+ @name || fields.any?
142
+ end
143
+
118
144
  # Queries whether the current environment is a valid FaaS environment.
119
145
  #
120
146
  # @return [ true | false ] whether the environment is a FaaS
@@ -159,14 +185,11 @@ module Mongo
159
185
  @name == 'vercel'
160
186
  end
161
187
 
162
- # Compiles the detected environment information into a Hash. It will
163
- # always include a {{name}} key, but may include other keys as well,
164
- # depending on the detected FaaS environment. (See the handshake
165
- # spec for details.)
188
+ # Compiles the detected environment information into a Hash.
166
189
  #
167
190
  # @return [ Hash ] the detected environment information.
168
191
  def to_h
169
- fields.merge(name: name)
192
+ name ? fields.merge(name: name) : fields
170
193
  end
171
194
 
172
195
  private
@@ -192,6 +215,38 @@ module Mongo
192
215
  names.first
193
216
  end
194
217
 
218
+ # Looks for the presence of a container. Currently can detect
219
+ # Docker (by the existence of a .dockerenv file in the root
220
+ # directory) and Kubernetes (by the existence of the KUBERNETES_SERVICE_HOST
221
+ # environment variable).
222
+ def detect_container
223
+ runtime = docker_present? && 'docker'
224
+ orchestrator = kubernetes_present? && 'kubernetes'
225
+
226
+ return unless runtime || orchestrator
227
+
228
+ fields[:container] = {}
229
+ fields[:container][:runtime] = runtime if runtime
230
+ fields[:container][:orchestrator] = orchestrator if orchestrator
231
+ end
232
+
233
+ # Checks for the existence of a .dockerenv in the root directory.
234
+ def docker_present?
235
+ File.exist?(dockerenv_path)
236
+ end
237
+
238
+ # Implementing this as a method so that it can be mocked in tests, to
239
+ # test the presence or absence of Docker.
240
+ def dockerenv_path
241
+ DOCKERENV_PATH
242
+ end
243
+
244
+ # Checks for the presence of a non-empty KUBERNETES_SERVICE_HOST
245
+ # environment variable.
246
+ def kubernetes_present?
247
+ !ENV['KUBERNETES_SERVICE_HOST'].to_s.empty?
248
+ end
249
+
195
250
  # Determines whether the named environment variable exists, and (if
196
251
  # a pattern has been declared for that descriminator) whether the
197
252
  # pattern matches the value of the variable.
@@ -212,10 +267,10 @@ module Mongo
212
267
  # Extracts environment information from the current environment
213
268
  # variables, based on the detected FaaS environment. Populates the
214
269
  # {{@fields}} instance variable.
215
- def populate_fields
270
+ def populate_faas_fields
216
271
  return unless name
217
272
 
218
- @fields = FIELDS[name].each_with_object({}) do |(var, defn), fields|
273
+ FIELDS[name].each_with_object(@fields) do |(var, defn), fields|
219
274
  fields[defn[:field]] = extract_field(var, defn)
220
275
  end
221
276
  end
@@ -187,13 +187,14 @@ module Mongo
187
187
  }
188
188
  end
189
189
 
190
- # Returns the environment doc describing the current FaaS environment.
190
+ # Returns the environment doc describing the current execution
191
+ # environment.
191
192
  #
192
- # @return [ Hash | nil ] the environment doc (or nil if not in a FaaS
193
- # environment).
193
+ # @return [ Hash | nil ] the environment doc (or nil if no relevant
194
+ # environment info was detected)
194
195
  def env_doc
195
196
  env = Environment.new
196
- env.faas? ? env.to_h : nil
197
+ env.present? ? env.to_h : nil
197
198
  end
198
199
 
199
200
  def type
@@ -48,6 +48,7 @@ module Mongo
48
48
  # provided by the client during findAndModify operations, requiring the
49
49
  # driver to raise client-side errors when those options are provided.
50
50
  find_and_modify_option_validation: 8,
51
+ sharded_transactions: 8,
51
52
  transactions: 7,
52
53
  scram_sha_256: 7,
53
54
  array_filters: 6,
@@ -164,6 +164,10 @@ module Mongo
164
164
  # for mongos pinning. Added in version 2.10.0.
165
165
  # @param [ true | false ] write_aggregation Whether we need a server that
166
166
  # supports writing aggregations (e.g. with $merge/$out) on secondaries.
167
+ # @param [ Array<Server> ] deprioritized A list of servers that should
168
+ # be selected from only if no other servers are available. This is
169
+ # used to avoid selecting the same server twice in a row when
170
+ # retrying a command.
167
171
  #
168
172
  # @return [ Mongo::Server ] A server matching the server preference.
169
173
  #
@@ -174,8 +178,8 @@ module Mongo
174
178
  # lint mode is enabled.
175
179
  #
176
180
  # @since 2.0.0
177
- def select_server(cluster, ping = nil, session = nil, write_aggregation: false)
178
- select_server_impl(cluster, ping, session, write_aggregation).tap do |server|
181
+ def select_server(cluster, ping = nil, session = nil, write_aggregation: false, deprioritized: [])
182
+ select_server_impl(cluster, ping, session, write_aggregation, deprioritized).tap do |server|
179
183
  if Lint.enabled? && !server.pool.ready?
180
184
  raise Error::LintError, 'Server selector returning a server with a pool which is not ready'
181
185
  end
@@ -183,7 +187,7 @@ module Mongo
183
187
  end
184
188
 
185
189
  # Parameters and return values are the same as for select_server.
186
- private def select_server_impl(cluster, ping, session, write_aggregation)
190
+ private def select_server_impl(cluster, ping, session, write_aggregation, deprioritized)
187
191
  if cluster.topology.is_a?(Cluster::Topology::LoadBalanced)
188
192
  return cluster.servers.first
189
193
  end
@@ -266,7 +270,7 @@ module Mongo
266
270
  end
267
271
  end
268
272
 
269
- server = try_select_server(cluster, write_aggregation: write_aggregation)
273
+ server = try_select_server(cluster, write_aggregation: write_aggregation, deprioritized: deprioritized)
270
274
 
271
275
  if server
272
276
  unless cluster.topology.compatible?
@@ -321,11 +325,15 @@ module Mongo
321
325
  # an eligible server.
322
326
  # @param [ true | false ] write_aggregation Whether we need a server that
323
327
  # supports writing aggregations (e.g. with $merge/$out) on secondaries.
328
+ # @param [ Array<Server> ] deprioritized A list of servers that should
329
+ # be selected from only if no other servers are available. This is
330
+ # used to avoid selecting the same server twice in a row when
331
+ # retrying a command.
324
332
  #
325
333
  # @return [ Server | nil ] A suitable server, if one exists.
326
334
  #
327
335
  # @api private
328
- def try_select_server(cluster, write_aggregation: false)
336
+ def try_select_server(cluster, write_aggregation: false, deprioritized: [])
329
337
  servers = if write_aggregation && cluster.replica_set?
330
338
  # 1. Check if ALL servers in cluster support secondary writes.
331
339
  is_write_supported = cluster.servers.reduce(true) do |res, server|
@@ -347,7 +355,7 @@ module Mongo
347
355
  # by the selector (e.g. for secondary preferred, the first
348
356
  # server may be a secondary and the second server may be primary)
349
357
  # and we should take the first server here respecting the order
350
- server = servers.first
358
+ server = suitable_server(servers, deprioritized)
351
359
 
352
360
  if server
353
361
  if Lint.enabled?
@@ -418,6 +426,24 @@ module Mongo
418
426
 
419
427
  private
420
428
 
429
+ # Returns a server from the list of servers that is suitable for
430
+ # executing the operation.
431
+ #
432
+ # @param [ Array<Server> ] servers The candidate servers.
433
+ # @param [ Array<Server> ] deprioritized A list of servers that should
434
+ # be selected from only if no other servers are available.
435
+ #
436
+ # @return [ Server | nil ] The suitable server or nil if no suitable
437
+ # server is available.
438
+ def suitable_server(servers, deprioritized)
439
+ preferred = servers - deprioritized
440
+ if preferred.empty?
441
+ servers.first
442
+ else
443
+ preferred.first
444
+ end
445
+ end
446
+
421
447
  # Convert this server preference definition into a format appropriate
422
448
  # for sending to a MongoDB server (i.e., as a command field).
423
449
  #