From 9960d1f2e1998db9ecb7d57e777ff68c46cae512 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 8 Jul 2026 13:00:20 -0600 Subject: [PATCH 01/10] RUBY-3816 Remove instance_variable_set reach-ins in core types Replace metaprogramming reach-ins that mutated another object's private state with straightforward OO. Cluster.create and Database.create only ever mutated the client they were handed, via instance_variable_set, and were called only from Client#with. Move that behavior onto Client as reset_cluster! and reset_database!, and delete the two class factories. Convert Message.deserialize_array and .deserialize_field from class methods that set ivars on a passed-in message into instance methods, so the ivar access is ordinary self-assignment. Update both call sites. --- lib/mongo/client.rb | 29 +++++++++++++++++++++++++++-- lib/mongo/cluster.rb | 24 ------------------------ lib/mongo/database.rb | 18 ------------------ lib/mongo/protocol/compressed.rb | 4 ++-- lib/mongo/protocol/message.rb | 22 ++++++++++------------ 5 files changed, 39 insertions(+), 58 deletions(-) diff --git a/lib/mongo/client.rb b/lib/mongo/client.rb index ed6044637e..ed857402aa 100644 --- a/lib/mongo/client.rb +++ b/lib/mongo/client.rb @@ -796,10 +796,10 @@ def use(name) def with(new_options = nil) clone.tap do |client| opts = client.update_options(new_options || Options::Redacted.new) - Database.create(client) + client.reset_database! # We can't use the same cluster if some options that would affect it # have changed. - Cluster.create(client, monitoring: opts[:monitoring]) if cluster_modifying?(opts) + client.reset_cluster!(monitoring: opts[:monitoring]) if cluster_modifying?(opts) end end @@ -855,6 +855,31 @@ def update_options(new_options) end end + # Replaces this client's database with a fresh instance built from the + # client's current options. Used by #with so a reconfigured client does + # not share its database with the client it was cloned from. + # + # @api private + def reset_database! + @database = Database.new(self, options[:database], options) + end + + # Replaces this client's cluster with a fresh instance built from the + # client's current options. Used by #with so a reconfigured client does + # not share its cluster with the client it was cloned from. + # + # @param [ Monitoring | nil ] monitoring The monitoring instance to use + # with the new cluster. If nil, a new instance of Monitoring is created. + # + # @api private + def reset_cluster!(monitoring: nil) + @cluster = Cluster.new( + cluster.addresses.map(&:to_s), + monitoring || Monitoring.new, + cluster_options + ) + end + # Get the read concern for this client. # # @example Get the client read concern. diff --git a/lib/mongo/cluster.rb b/lib/mongo/cluster.rb index bb822eebfa..9752e96fe8 100644 --- a/lib/mongo/cluster.rb +++ b/lib/mongo/cluster.rb @@ -269,30 +269,6 @@ def initialize(seeds, monitoring, options = Options::Redacted.new) start_stop_srv_monitor end - # Create a cluster for the provided client, for use when we don't want the - # client's original cluster instance to be the same. - # - # @example Create a cluster for the client. - # Cluster.create(client) - # - # @param [ Client ] client The client to create on. - # @param [ Monitoring | nil ] monitoring. The monitoring instance to use - # with the new cluster. If nil, a new instance of Monitoring will be - # created. - # - # @return [ Cluster ] The cluster. - # - # @since 2.0.0 - # @api private - def self.create(client, monitoring: nil) - cluster = Cluster.new( - client.cluster.addresses.map(&:to_s), - monitoring || Monitoring.new, - client.cluster_options - ) - client.instance_variable_set(:@cluster, cluster) - end - # @return [ Hash ] The options hash. attr_reader :options diff --git a/lib/mongo/database.rb b/lib/mongo/database.rb index b22dc5daeb..deaf88d132 100644 --- a/lib/mongo/database.rb +++ b/lib/mongo/database.rb @@ -644,24 +644,6 @@ def watch(pipeline = [], options = {}) ) end - # Create a database for the provided client, for use when we don't want the - # client's original database instance to be the same. - # - # @api private - # - # @example Create a database for the client. - # Database.create(client) - # - # @param [ Client ] client The client to create on. - # - # @return [ Database ] The database. - # - # @since 2.0.0 - def self.create(client) - database = Database.new(client, client.options[:database], client.options) - client.instance_variable_set(:@database, database) - end - # @return [ Integer | nil ] Operation timeout that is for this database or # for the corresponding client. # diff --git a/lib/mongo/protocol/compressed.rb b/lib/mongo/protocol/compressed.rb index 7e586116a1..1aa221abaa 100644 --- a/lib/mongo/protocol/compressed.rb +++ b/lib/mongo/protocol/compressed.rb @@ -95,9 +95,9 @@ def maybe_inflate message.send(:fields).each do |field| if field[:multi] - Message.deserialize_array(message, buf, field) + message.deserialize_array(buf, field) else - Message.deserialize_field(message, buf, field) + message.deserialize_field(buf, field) end end message.fix_after_deserialization if message.is_a?(Msg) diff --git a/lib/mongo/protocol/message.rb b/lib/mongo/protocol/message.rb index c67388d69b..a2d0938ece 100644 --- a/lib/mongo/protocol/message.rb +++ b/lib/mongo/protocol/message.rb @@ -270,9 +270,9 @@ def self.deserialize(io, message = Registry.get(_op_code).allocate message.send(:fields).each do |field| if field[:multi] - deserialize_array(message, buf, field, options) + message.deserialize_array(buf, field, options) else - deserialize_field(message, buf, field, options) + message.deserialize_field(buf, field, options) end end message.fix_after_deserialization if message.is_a?(Msg) @@ -369,7 +369,6 @@ def self.field(name, type, multi = false) # deserialized field specified in the class by the field dsl under # the key +:multi+ # - # @param message [Message] Message to contain the deserialized array. # @param io [IO] Stream containing the array to deserialize. # @param field [Hash] Hash representing a field. # @param options [ Hash ] @@ -377,18 +376,17 @@ def self.field(name, type, multi = false) # @option options [ Boolean ] :deserialize_as_bson Whether to deserialize # each of the elements in this array using BSON types wherever possible. # - # @return [Message] Message with deserialized array. + # @return [Array] The deserialized array. # @api private - def self.deserialize_array(message, io, field, options = {}) + def deserialize_array(io, field, options = {}) elements = [] - count = message.instance_variable_get(field[:multi]) + count = instance_variable_get(field[:multi]) count.times { elements << field[:type].deserialize(io, options) } - message.instance_variable_set(field[:name], elements) + instance_variable_set(field[:name], elements) end - # Deserializes a single field in a message + # Deserializes a single field into this message. # - # @param message [Message] Message to contain the deserialized field. # @param io [IO] Stream containing the field to deserialize. # @param field [Hash] Hash representing a field. # @param options [ Hash ] @@ -396,10 +394,10 @@ def self.deserialize_array(message, io, field, options = {}) # @option options [ Boolean ] :deserialize_as_bson Whether to deserialize # this field using BSON types wherever possible. # - # @return [Message] Message with deserialized field. + # @return [Object] The deserialized field value. # @api private - def self.deserialize_field(message, io, field, options = {}) - message.instance_variable_set( + def deserialize_field(io, field, options = {}) + instance_variable_set( field[:name], field[:type].deserialize(io, options) ) From 841f86749c40b4be0d854d25241924e921b0df2f Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 8 Jul 2026 13:18:12 -0600 Subject: [PATCH 02/10] RUBY-3816 Replace send with public_send/direct calls on public methods Several sites used Object#send to invoke methods that are already public, which needlessly bypasses method visibility and hides intent. Switch them to public_send, or to a direct call where the method name is a literal. No behavior change: every affected method is public. --- lib/mongo/bulk_write/result_combiner.rb | 2 +- lib/mongo/cluster/sdam_flow.rb | 4 ++-- lib/mongo/crypt/handle.rb | 2 +- lib/mongo/cursor.rb | 2 +- lib/mongo/options/redacted.rb | 4 ++-- lib/mongo/socket/ssl.rb | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/mongo/bulk_write/result_combiner.rb b/lib/mongo/bulk_write/result_combiner.rb index 8c27eb3cf0..8337b62c03 100644 --- a/lib/mongo/bulk_write/result_combiner.rb +++ b/lib/mongo/bulk_write/result_combiner.rb @@ -92,7 +92,7 @@ def result def combine_counts!(result) Result::FIELDS.each do |field| - if result.respond_to?(field) && (value = result.send(field)) + if result.respond_to?(field) && (value = result.public_send(field)) results.merge!(field => (results[field] || 0) + value) end end diff --git a/lib/mongo/cluster/sdam_flow.rb b/lib/mongo/cluster/sdam_flow.rb index 794e2dad72..921b8cecf9 100644 --- a/lib/mongo/cluster/sdam_flow.rb +++ b/lib/mongo/cluster/sdam_flow.rb @@ -385,7 +385,7 @@ def update_rs_without_primary def add_servers_from_desc(updated_desc) added_servers = [] %w[hosts passives arbiters].each do |m| - updated_desc.send(m).each do |address_str| + updated_desc.public_send(m).each do |address_str| if (server = cluster.add(address_str, monitor: false)) added_servers << server end @@ -402,7 +402,7 @@ def add_servers_from_desc(updated_desc) # good primary). def remove_servers_not_in_desc(updated_desc) updated_desc_address_strs = %w[hosts passives arbiters].map do |m| - updated_desc.send(m) + updated_desc.public_send(m) end.flatten servers_list.each do |server| next if updated_desc_address_strs.include?(address_str = server.address.to_s) diff --git a/lib/mongo/crypt/handle.rb b/lib/mongo/crypt/handle.rb index a149182f85..0ecff0b38d 100644 --- a/lib/mongo/crypt/handle.rb +++ b/lib/mongo/crypt/handle.rb @@ -203,7 +203,7 @@ def set_bypass_query_analysis # Send the logs from libmongocrypt to the Mongo::Logger def set_logger_callback @log_callback = proc do |level, msg| - @logger.send(level, msg) + @logger.public_send(level, msg) end Binding.setopt_log_handler(@mongocrypt, @log_callback) diff --git a/lib/mongo/cursor.rb b/lib/mongo/cursor.rb index 7d86d8665e..d9181b4422 100644 --- a/lib/mongo/cursor.rb +++ b/lib/mongo/cursor.rb @@ -507,7 +507,7 @@ def use_limit? end def limit - @view.send(:limit) + @view.limit end def register diff --git a/lib/mongo/options/redacted.rb b/lib/mongo/options/redacted.rb index 767a1f037f..d18c67c45f 100644 --- a/lib/mongo/options/redacted.rb +++ b/lib/mongo/options/redacted.rb @@ -143,14 +143,14 @@ def select! def redacted_string(method) '{' + reduce([]) do |list, (k, v)| - list << "#{k.send(method)}=>#{redact(k, v, method)}" + list << "#{k.public_send(method)}=>#{redact(k, v, method)}" end.join(', ') + '}' end def redact(k, v, method) return STRING_REPLACEMENT if SENSITIVE_OPTIONS.include?(k.to_sym) - v.send(method) + v.public_send(method) end end end diff --git a/lib/mongo/socket/ssl.rb b/lib/mongo/socket/ssl.rb index ef8c8f77a8..3c02c130a8 100644 --- a/lib/mongo/socket/ssl.rb +++ b/lib/mongo/socket/ssl.rb @@ -421,13 +421,13 @@ def load_private_key(text, passphrase) # https://github.com/jruby/jruby-openssl/issues/176 if BSON::Environment.jruby? [ OpenSSL::PKey::RSA, OpenSSL::PKey::DSA ].each do |cls| - return cls.send(:new, *args) + return cls.new(*args) rescue OpenSSL::PKey::PKeyError # ignore end # Neither RSA nor DSA worked, fall through to trying PKey end - OpenSSL::PKey.send(:read, *args) + OpenSSL::PKey.read(*args) end def set_cert_verification(context, options) From 51ecb94ff37700a76c872eb99af8277ee9f62ca5 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 8 Jul 2026 14:17:36 -0600 Subject: [PATCH 03/10] RUBY-3816 Use Monitor#running? instead of reaching into @thread server_selection_diagnostic_message reached into each server monitor's private @thread ivar to decide whether the monitor was dead. BackgroundThread already exposes a public #running? predicate with exactly the needed semantics (nil thread or dead thread => not running), so use it. Guard with safe navigation: server.monitor can be nil, and the old instance_variable_get(:@thread) silently returned nil for that case (counting the monitor as dead), which &.running? preserves. --- lib/mongo/server_selector/base.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/mongo/server_selector/base.rb b/lib/mongo/server_selector/base.rb index bbab09cadf..858bbb418f 100644 --- a/lib/mongo/server_selector/base.rb +++ b/lib/mongo/server_selector/base.rb @@ -687,8 +687,7 @@ def server_selection_diagnostic_message(cluster) msg = '' dead_monitors = [] cluster.servers_list.each do |server| - thread = server.monitor.instance_variable_get(:@thread) - dead_monitors << server if thread.nil? || !thread.alive? + dead_monitors << server unless server.monitor&.running? end if dead_monitors.any? msg += ". The following servers have dead monitor threads: #{dead_monitors.map(&:summary).join(', ')}" From c20451dd651a85e90680b73f93e1eb90e1234813 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 8 Jul 2026 14:35:55 -0600 Subject: [PATCH 04/10] RUBY-3816 Call with_session and Index::View#limit without send Client#with_session is already public, so three callers that invoked it via send now call it directly. Collection::View#with_session was private and reached via send from MapReduce; promote it to a public @api private method and call it directly. Also make Index::View#limit public (@api private). Cursor#limit calls @view.limit, and @view may be an Index::View, so the method must be public there as it already is on Collection::View. This removes the send that Cursor previously needed and fixes the case exposed when Cursor#limit stopped using send. --- lib/mongo/auth/user/view.rb | 2 +- lib/mongo/collection.rb | 2 +- lib/mongo/collection/view.rb | 12 ++++++++---- lib/mongo/collection/view/map_reduce.rb | 2 +- lib/mongo/index/view.rb | 14 +++++++++----- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/mongo/auth/user/view.rb b/lib/mongo/auth/user/view.rb index 31b174c4aa..0478851033 100644 --- a/lib/mongo/auth/user/view.rb +++ b/lib/mongo/auth/user/view.rb @@ -152,7 +152,7 @@ def generate(user, options) end def execute_operation(options) - client.send(:with_session, options) do |session| + client.with_session(options) do |session| op = yield session op.execute(next_primary(nil, session), context: Operation::Context.new(client: client, session: session)) end diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index 0546abbfd8..3f4aaa2785 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -393,7 +393,7 @@ def create(opts = {}) operation = { create: name }.merge(options) operation.delete(:write) operation.delete(:write_concern) - client.send(:with_session, opts) do |session| + client.with_session(opts) do |session| write_concern = if opts[:write_concern] WriteConcern.get(opts[:write_concern]) else diff --git a/lib/mongo/collection/view.rb b/lib/mongo/collection/view.rb index fb894ab9d8..fbb217b9b0 100644 --- a/lib/mongo/collection/view.rb +++ b/lib/mongo/collection/view.rb @@ -236,6 +236,14 @@ def operation_timeouts(opts = {}) end end + # Executes the provided block within the context of a session, using + # this view's options merged with the given ones. + # + # @api private + def with_session(opts = {}, &block) + client.with_session(@options.merge(opts), &block) + end + private def initialize_copy(other) @@ -252,10 +260,6 @@ def new(options) def view self end - - def with_session(opts = {}, &block) - client.with_session(@options.merge(opts), &block) - end end end end diff --git a/lib/mongo/collection/view/map_reduce.rb b/lib/mongo/collection/view/map_reduce.rb index 843f4d6575..163bce12df 100644 --- a/lib/mongo/collection/view/map_reduce.rb +++ b/lib/mongo/collection/view/map_reduce.rb @@ -227,7 +227,7 @@ def verbose(value = nil) # # @since 2.5.0 def execute - view.send(:with_session, @options) do |session| + view.with_session(@options) do |session| write_concern = view.write_concern_with_session(session) context = Operation::Context.new(client: client, session: session) nro_write_with_retry(write_concern, context: context) do |connection, _txn_num, context| diff --git a/lib/mongo/index/view.rb b/lib/mongo/index/view.rb index 84d332fc1b..6e7cf88264 100644 --- a/lib/mongo/index/view.rb +++ b/lib/mongo/index/view.rb @@ -347,12 +347,20 @@ def operation_timeouts(opts = {}) end end + # The query limit for an index view. Always -1; present so a Cursor + # can treat an index view like a collection view. + # + # @api private + def limit + -1 + end + private def drop_by_name(name, opts = {}) session_options = @options.dup session_options[:session] = opts[:session] if opts[:session] - client.send(:with_session, session_options) do |session| + client.with_session(session_options) do |session| spec = { db_name: database.name, coll_name: collection.name, @@ -396,10 +404,6 @@ def initial_query_op(session) Operation::Indexes.new(indexes_spec(session)) end - def limit - -1 - end - def normalize_keys(spec) return false if spec.is_a?(String) From 07afd1fd55ea94f7d752d8ba37b6b424e7e637b0 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 8 Jul 2026 14:42:37 -0600 Subject: [PATCH 05/10] RUBY-3816 Expose internal Session and Client methods instead of send Session#txn_read_concern and Session#causal_consistency_doc were private and reached via send from operation and view code; make them public @api private and call them directly. Client#monitoring was public but then re-marked private, and reached via send from the auto encrypter; drop the private marker (it is already documented @api private) and call it directly. --- lib/mongo/client.rb | 1 - lib/mongo/collection/view/readable.rb | 2 +- lib/mongo/crypt/auto_encrypter.rb | 2 +- .../operation/shared/sessions_supported.rb | 2 +- lib/mongo/session.rb | 23 +++++++++++-------- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/mongo/client.rb b/lib/mongo/client.rb index ed857402aa..66fe0e49bc 100644 --- a/lib/mongo/client.rb +++ b/lib/mongo/client.rb @@ -172,7 +172,6 @@ def monitoring @monitoring end end - private :monitoring # Determine if this client is equivalent to another object. # diff --git a/lib/mongo/collection/view/readable.rb b/lib/mongo/collection/view/readable.rb index 01109dfff5..5c30e503bd 100644 --- a/lib/mongo/collection/view/readable.rb +++ b/lib/mongo/collection/view/readable.rb @@ -678,7 +678,7 @@ def timeout_ms(timeout_ms = nil) # @api private def read_concern if options[:session] && options[:session].in_transaction? - options[:session].send(:txn_read_concern) || collection.client.read_concern + options[:session].txn_read_concern || collection.client.read_concern else collection.read_concern end diff --git a/lib/mongo/crypt/auto_encrypter.rb b/lib/mongo/crypt/auto_encrypter.rb index 35d72cb6dc..5f3673f892 100644 --- a/lib/mongo/crypt/auto_encrypter.rb +++ b/lib/mongo/crypt/auto_encrypter.rb @@ -287,7 +287,7 @@ def internal_client(client) @internal_client ||= client.with( auto_encryption_options: nil, min_pool_size: 0, - monitoring: client.send(:monitoring) + monitoring: client.monitoring ) end end diff --git a/lib/mongo/operation/shared/sessions_supported.rb b/lib/mongo/operation/shared/sessions_supported.rb index eb2d50d53a..9b461ce969 100644 --- a/lib/mongo/operation/shared/sessions_supported.rb +++ b/lib/mongo/operation/shared/sessions_supported.rb @@ -61,7 +61,7 @@ def apply_causal_consistency!(selector, connection) def apply_causal_consistency_if_possible(selector, connection) return if connection.description.standalone? - cc_doc = session.send(:causal_consistency_doc) + cc_doc = session.causal_consistency_doc return unless cc_doc rc_doc = (selector[:readConcern] || read_concern || {}).merge(cc_doc) diff --git a/lib/mongo/session.rb b/lib/mongo/session.rb index e266b57a97..7fa906f074 100644 --- a/lib/mongo/session.rb +++ b/lib/mongo/session.rb @@ -1312,8 +1312,6 @@ def inside_with_transaction? @inside_with_transaction end - private - # Get the read concern the session will use when starting a transaction. # # This is a driver style hash with underscore keys. @@ -1324,11 +1322,24 @@ def inside_with_transaction? # @return [ Hash ] The read concern used for starting transactions. # # @since 2.9.0 + # @api private def txn_read_concern # Read concern is inherited from client but not db or collection. txn_options[:read_concern] || @client.read_concern end + # Returns causal consistency document if the last operation time is + # known and causal consistency is enabled, otherwise returns nil. + # + # @api private + def causal_consistency_doc + return unless operation_time && causal_consistency? + + { afterClusterTime: operation_time } + end + + private + def within_states?(*states) states.include?(@state) end @@ -1346,14 +1357,6 @@ def txn_write_concern (@client.write_concern && @client.write_concern.options) end - # Returns causal consistency document if the last operation time is - # known and causal consistency is enabled, otherwise returns nil. - def causal_consistency_doc - return unless operation_time && causal_consistency? - - { afterClusterTime: operation_time } - end - def causal_consistency? @causal_consistency ||= if @options.key?(:causal_consistency) !!@options[:causal_consistency] From dca02bfb4158d6678fce3a917827fecbd0cf1e3e Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 8 Jul 2026 14:44:48 -0600 Subject: [PATCH 06/10] RUBY-3816 Expose view server_selector and FSBucket#ensure_indexes! Collection::View#server_selector (from Readable) was private and reached via send from MapReduce and Aggregation, whose @view is a Collection::View. Make it public @api private and call it directly. FSBucket#ensure_indexes! was private and reached via send from the write stream. Make it public @api private and call it directly. --- .../collection/view/aggregation/behavior.rb | 2 +- lib/mongo/collection/view/map_reduce.rb | 2 +- lib/mongo/collection/view/readable.rb | 16 ++++--- lib/mongo/grid/fs_bucket.rb | 46 +++++++++++-------- lib/mongo/grid/stream/write.rb | 2 +- 5 files changed, 39 insertions(+), 29 deletions(-) diff --git a/lib/mongo/collection/view/aggregation/behavior.rb b/lib/mongo/collection/view/aggregation/behavior.rb index db881106a7..43fbdef416 100644 --- a/lib/mongo/collection/view/aggregation/behavior.rb +++ b/lib/mongo/collection/view/aggregation/behavior.rb @@ -85,7 +85,7 @@ def perform_setup(view, options, forbid: []) end def server_selector - @view.send(:server_selector) + @view.server_selector end def aggregate_spec(session, read_preference = nil) diff --git a/lib/mongo/collection/view/map_reduce.rb b/lib/mongo/collection/view/map_reduce.rb index 163bce12df..b55cb7d80e 100644 --- a/lib/mongo/collection/view/map_reduce.rb +++ b/lib/mongo/collection/view/map_reduce.rb @@ -241,7 +241,7 @@ def execute OUT_ACTIONS = %i[replace merge reduce].freeze def server_selector - @view.send(:server_selector) + @view.server_selector end def inline? diff --git a/lib/mongo/collection/view/readable.rb b/lib/mongo/collection/view/readable.rb index 5c30e503bd..145f533639 100644 --- a/lib/mongo/collection/view/readable.rb +++ b/lib/mongo/collection/view/readable.rb @@ -749,12 +749,10 @@ def parallel_scan(cursor_count, options = {}) end end - private - - def collation(doc = nil) - configure(:collation, doc) - end - + # The server selector for this view, derived from its read + # preference (or the collection/client default). + # + # @api private def server_selector @server_selector ||= if options[:session] && options[:session].in_transaction? ServerSelector.get(read_preference || client.server_selector) @@ -763,6 +761,12 @@ def server_selector end end + private + + def collation(doc = nil) + configure(:collation, doc) + end + def validate_doc!(doc) raise Error::InvalidDocument.new unless doc.respond_to?(:keys) end diff --git a/lib/mongo/grid/fs_bucket.rb b/lib/mongo/grid/fs_bucket.rb index 64180e9033..dae94c3dbb 100644 --- a/lib/mongo/grid/fs_bucket.rb +++ b/lib/mongo/grid/fs_bucket.rb @@ -494,6 +494,32 @@ def drop(opts = {}) chunks_collection.drop(timeout_ms: context.remaining_timeout_ms) end + # Ensures the files and chunks collections have their required indexes, + # creating any that are missing. + # + # @param [ CsotTimeoutHolder | nil ] timeout_holder The timeout holder. + # + # @api private + def ensure_indexes!(timeout_holder = nil) + fc_idx = files_collection.find( + {}, + limit: 1, + projection: { _id: 1 }, + timeout_ms: timeout_holder&.remaining_timeout_ms + ).first + create_index_if_missing!(files_collection, FSBucket::FILES_INDEX) if fc_idx.nil? + + cc_idx = chunks_collection.find( + {}, + limit: 1, + projection: { _id: 1 }, + timeout_ms: timeout_holder&.remaining_timeout_ms + ).first + return unless cc_idx.nil? + + create_index_if_missing!(chunks_collection, FSBucket::CHUNKS_INDEX, unique: true) + end + private # @param [ Hash ] opts The options. @@ -516,26 +542,6 @@ def files_name "#{prefix}.#{Grid::File::Info::COLLECTION}" end - def ensure_indexes!(timeout_holder = nil) - fc_idx = files_collection.find( - {}, - limit: 1, - projection: { _id: 1 }, - timeout_ms: timeout_holder&.remaining_timeout_ms - ).first - create_index_if_missing!(files_collection, FSBucket::FILES_INDEX) if fc_idx.nil? - - cc_idx = chunks_collection.find( - {}, - limit: 1, - projection: { _id: 1 }, - timeout_ms: timeout_holder&.remaining_timeout_ms - ).first - return unless cc_idx.nil? - - create_index_if_missing!(chunks_collection, FSBucket::CHUNKS_INDEX, unique: true) - end - def create_index_if_missing!(collection, index_spec, options = {}) indexes_view = collection.indexes begin diff --git a/lib/mongo/grid/stream/write.rb b/lib/mongo/grid/stream/write.rb index 533ab911eb..a9b228b4ed 100644 --- a/lib/mongo/grid/stream/write.rb +++ b/lib/mongo/grid/stream/write.rb @@ -212,7 +212,7 @@ def file_info end def ensure_indexes! - fs.send(:ensure_indexes!, @timeout_holder) + fs.ensure_indexes!(@timeout_holder) end def ensure_open! From 510089abd924501be8dc5e30d1d45000ec42668a Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 8 Jul 2026 14:56:30 -0600 Subject: [PATCH 07/10] RUBY-3816 Expose message field helpers and connection socket interrupt Message#fields and Message#serialize_fields were private and reached via send during (de)serialization, including from Compressed. Make them public @api private and call them directly; promote the Compressed#serialize_fields override to match. PushMonitor#stop! reached into its connection with send(:socket).close to interrupt a blocking read. Add ConnectionCommon#interrupt_socket (@api private) that encapsulates this, and call it instead. --- lib/mongo/protocol/compressed.rb | 19 ++++++++++--------- lib/mongo/protocol/message.rb | 8 +++++--- lib/mongo/server/connection_common.rb | 8 ++++++++ lib/mongo/server/push_monitor.rb | 2 +- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/mongo/protocol/compressed.rb b/lib/mongo/protocol/compressed.rb index 1aa221abaa..403553f629 100644 --- a/lib/mongo/protocol/compressed.rb +++ b/lib/mongo/protocol/compressed.rb @@ -93,7 +93,7 @@ def maybe_inflate message = Registry.get(@original_op_code).allocate buf = decompress(@compressed_message) - message.send(:fields).each do |field| + message.fields.each do |field| if field[:multi] message.deserialize_array(buf, field) else @@ -116,6 +116,15 @@ def replyable? @original_message.replyable? end + # @api private + def serialize_fields(buffer, max_bson_size) + buf = BSON::ByteBuffer.new + @original_message.serialize_fields(buf, max_bson_size) + @uncompressed_size = buf.length + @compressed_message = compress(buf) + super + end + private # The operation code for a +Compressed+ message. @@ -140,14 +149,6 @@ def replyable? # @return [ String ] The actual compressed message bytes. field :compressed_message, Bytes - def serialize_fields(buffer, max_bson_size) - buf = BSON::ByteBuffer.new - @original_message.send(:serialize_fields, buf, max_bson_size) - @uncompressed_size = buf.length - @compressed_message = compress(buf) - super - end - def compress(buffer) if @compressor_id == NOOP_BYTE buffer.to_s.force_encoding(BSON::BINARY) diff --git a/lib/mongo/protocol/message.rb b/lib/mongo/protocol/message.rb index a2d0938ece..08bff0e358 100644 --- a/lib/mongo/protocol/message.rb +++ b/lib/mongo/protocol/message.rb @@ -268,7 +268,7 @@ def self.deserialize(io, buf = BSON::ByteBuffer.new(chunk) message = Registry.get(_op_code).allocate - message.send(:fields).each do |field| + message.fields.each do |field| if field[:multi] message.deserialize_array(buf, field, options) else @@ -403,11 +403,10 @@ def deserialize_field(io, field, options = {}) ) end - private - # A method for getting the fields for a message class # # @return [Integer] the fields for the message class + # @api private def fields self.class.fields end @@ -416,6 +415,7 @@ def fields # # @param buffer [String] buffer to receive the field # @return [String] buffer with serialized field + # @api private def serialize_fields(buffer, max_bson_size = nil) fields.each do |field| value = instance_variable_get(field[:name]) @@ -435,6 +435,8 @@ def serialize_fields(buffer, max_bson_size = nil) end end + private + # Serializes the header of the message consisting of 4 32bit integers # # The integers represent a message length placeholder (calculation of diff --git a/lib/mongo/server/connection_common.rb b/lib/mongo/server/connection_common.rb index 742e4f0522..f7bf2a1b8b 100644 --- a/lib/mongo/server/connection_common.rb +++ b/lib/mongo/server/connection_common.rb @@ -99,6 +99,14 @@ def handshake_command(handshake_document) end end + # Closes the underlying socket, if one is open, to interrupt an + # in-progress blocking read on the connection. + # + # @api private + def interrupt_socket + socket&.close + end + private HELLO_DOC = BSON::Document.new({ hello: 1 }).freeze diff --git a/lib/mongo/server/push_monitor.rb b/lib/mongo/server/push_monitor.rb index 8013dac675..5f59204c4b 100644 --- a/lib/mongo/server/push_monitor.rb +++ b/lib/mongo/server/push_monitor.rb @@ -72,7 +72,7 @@ def stop! # Interrupt any in-progress exhausted hello reads by # disconnecting the connection. begin - @connection.send(:socket).close + @connection.interrupt_socket rescue StandardError nil end From 582766798d463d698eee3ee23ba5a1fbe1af031f Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 8 Jul 2026 15:08:25 -0600 Subject: [PATCH 08/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/mongo/protocol/message.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mongo/protocol/message.rb b/lib/mongo/protocol/message.rb index 08bff0e358..0b3c9f9b19 100644 --- a/lib/mongo/protocol/message.rb +++ b/lib/mongo/protocol/message.rb @@ -405,7 +405,7 @@ def deserialize_field(io, field, options = {}) # A method for getting the fields for a message class # - # @return [Integer] the fields for the message class + # @return [Array] The fields for the message class # @api private def fields self.class.fields From d2b1a589409b32e28f9bbae224f2fd413db206a3 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 8 Jul 2026 15:44:05 -0600 Subject: [PATCH 09/10] make this `@api private` instead of `private`, so it can be invoked internally --- lib/mongo/database/cursor_command_view.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mongo/database/cursor_command_view.rb b/lib/mongo/database/cursor_command_view.rb index 3cbe2474a9..6489d7cd6b 100644 --- a/lib/mongo/database/cursor_command_view.rb +++ b/lib/mongo/database/cursor_command_view.rb @@ -84,9 +84,9 @@ def operation_timeouts(opts = {}) database.operation_timeouts(opts) end - private - # Cursors do not support a limit when built from a command response. + # + # @api private def limit nil end From 85df05c91daa48d5616154427d9a53b8e1ac7984 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 8 Jul 2026 16:33:19 -0600 Subject: [PATCH 10/10] bump drivers-evergreen-tools to fix npm installation failure --- .mod/drivers-evergreen-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mod/drivers-evergreen-tools b/.mod/drivers-evergreen-tools index 9c21ebe36b..d8aa120a15 160000 --- a/.mod/drivers-evergreen-tools +++ b/.mod/drivers-evergreen-tools @@ -1 +1 @@ -Subproject commit 9c21ebe36bf099988360cb83288e4820aa3bf27c +Subproject commit d8aa120a1590a68bfb337b98da1d240914a843c2