mongo 2.11.1 → 2.11.2

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 (50) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -1
  3. data.tar.gz.sig +0 -0
  4. data/Rakefile +24 -0
  5. data/lib/mongo/auth.rb +30 -10
  6. data/lib/mongo/auth/cr.rb +1 -0
  7. data/lib/mongo/auth/cr/conversation.rb +13 -13
  8. data/lib/mongo/auth/ldap.rb +2 -1
  9. data/lib/mongo/auth/ldap/conversation.rb +9 -12
  10. data/lib/mongo/auth/scram.rb +1 -0
  11. data/lib/mongo/auth/scram/conversation.rb +36 -27
  12. data/lib/mongo/auth/x509.rb +2 -1
  13. data/lib/mongo/auth/x509/conversation.rb +9 -9
  14. data/lib/mongo/client.rb +17 -6
  15. data/lib/mongo/cluster.rb +65 -49
  16. data/lib/mongo/cluster/sdam_flow.rb +87 -3
  17. data/lib/mongo/database.rb +1 -1
  18. data/lib/mongo/server.rb +13 -6
  19. data/lib/mongo/server/connection.rb +12 -4
  20. data/lib/mongo/server/connection_base.rb +7 -4
  21. data/lib/mongo/server/description.rb +34 -21
  22. data/lib/mongo/session.rb +10 -10
  23. data/lib/mongo/version.rb +1 -1
  24. data/spec/README.md +13 -0
  25. data/spec/integration/auth_spec.rb +27 -8
  26. data/spec/integration/client_construction_spec.rb +14 -0
  27. data/spec/mongo/auth/ldap/conversation_spec.rb +1 -1
  28. data/spec/mongo/auth/scram/conversation_spec.rb +23 -14
  29. data/spec/mongo/auth/x509/conversation_spec.rb +1 -1
  30. data/spec/mongo/client_construction_spec.rb +1 -21
  31. data/spec/mongo/cluster_spec.rb +38 -0
  32. data/spec/mongo/collection/view/map_reduce_spec.rb +1 -1
  33. data/spec/mongo/server/connection_spec.rb +67 -0
  34. data/spec/runners/sdam/verifier.rb +6 -3
  35. data/spec/spec_tests/data/sdam/rs/primary_address_change.yml +29 -0
  36. data/spec/spec_tests/data/sdam/rs/primary_mismatched_me.yml +27 -23
  37. data/spec/spec_tests/data/sdam/rs/primary_to_no_primary_mismatched_me.yml +56 -79
  38. data/spec/spec_tests/data/sdam/sharded/primary_address_change.yml +21 -0
  39. data/spec/spec_tests/data/sdam/sharded/primary_mismatched_me.yml +22 -0
  40. data/spec/spec_tests/data/sdam/single/primary_address_change.yml +24 -0
  41. data/spec/spec_tests/data/sdam/single/primary_mismatched_me.yml +25 -0
  42. data/spec/spec_tests/data/sdam_monitoring/replica_set_with_me_mismatch.yml +159 -0
  43. data/spec/spec_tests/data/sdam_monitoring/{replica_set_other_seed.yml → replica_set_with_primary_change.yml} +97 -101
  44. data/spec/spec_tests/data/sdam_monitoring/replica_set_with_primary_removal.yml +22 -18
  45. data/spec/spec_tests/data/sdam_monitoring/standalone_to_rs_with_me_mismatch.yml +90 -0
  46. data/spec/support/cluster_config.rb +36 -0
  47. data/spec/support/constraints.rb +18 -18
  48. data/spec/support/server_discovery_and_monitoring.rb +2 -0
  49. metadata +18 -4
  50. metadata.gz.sig +0 -0
@@ -756,8 +756,8 @@ module Mongo
756
756
  #
757
757
  # @since 2.5.0
758
758
  def start_session(options = {})
759
- cluster.send(:get_session, self, options.merge(implicit: false)) ||
760
- (raise Error::InvalidSession.new(Session::SESSIONS_NOT_SUPPORTED))
759
+ get_session(options.merge(implicit: false)) or
760
+ raise Error::InvalidSession.new(Session::SESSIONS_NOT_SUPPORTED)
761
761
  end
762
762
 
763
763
  # As of version 3.6 of the MongoDB server, a ``$changeStream`` pipeline stage is supported
@@ -827,14 +827,25 @@ module Mongo
827
827
  # The session is implicit unless options[:implicit] is given.
828
828
  # If deployment does not support session, returns nil.
829
829
  #
830
- # @note This method will return nil if deployment has no data-bearing
831
- # servers at the time of the call.
830
+ # @return [ Session | nil ] Session object or nil if sessions are not
831
+ # supported by the deployment.
832
832
  def get_session(options = {})
833
- cluster.send(:get_session, self, options)
833
+ if options[:session]
834
+ return options[:session].validate!(self)
835
+ end
836
+
837
+ if cluster.sessions_supported?
838
+ Session.new(cluster.session_pool.checkout, self, { implicit: true }.merge(options))
839
+ end
834
840
  end
835
841
 
836
842
  def with_session(options = {}, &block)
837
- cluster.send(:with_session, self, options, &block)
843
+ session = get_session(options)
844
+ yield(session)
845
+ ensure
846
+ if session && session.implicit?
847
+ session.end_session
848
+ end
838
849
  end
839
850
 
840
851
  def initialize_copy(original)
@@ -162,12 +162,9 @@ module Mongo
162
162
  return
163
163
  end
164
164
 
165
- # Need to record start time prior to starting monitoring
166
- start_time = Time.now
167
-
168
- servers.each do |server|
169
- server.start_monitoring
170
- end
165
+ # Update instance variables prior to starting monitoring threads.
166
+ @connecting = false
167
+ @connected = true
171
168
 
172
169
  if options[:cleanup] != false
173
170
  @cursor_reaper = CursorReaper.new
@@ -182,8 +179,12 @@ module Mongo
182
179
  @periodic_executor.run!
183
180
  end
184
181
 
185
- @connecting = false
186
- @connected = true
182
+ # Need to record start time prior to starting monitoring
183
+ start_time = Time.now
184
+
185
+ servers.each do |server|
186
+ server.start_monitoring
187
+ end
187
188
 
188
189
  if options[:scan] != false
189
190
  server_selection_timeout = options[:server_selection_timeout] || ServerSelector::SERVER_SELECTION_TIMEOUT
@@ -743,30 +744,49 @@ module Mongo
743
744
  # server.remove('127.0.0.1:27017')
744
745
  #
745
746
  # @param [ String ] host The host/port or socket address.
747
+ # @param [ true | false ] disconnect Whether to disconnect the servers
748
+ # being removed. For internal driver use only.
749
+ #
750
+ # @return [ Array<Server> | true | false ] If disconnect is any value other
751
+ # than false, including nil, returns whether any servers were removed.
752
+ # If disconnect is false, returns an array of servers that were removed
753
+ # (and should be disconnected by the caller).
746
754
  #
747
- # @return [ true|false ] Whether any servers were removed.
755
+ # @note The return value of this method is not part of the driver's
756
+ # public API.
748
757
  #
749
- # @since 2.0.0, return value added in 2.7.0
750
- def remove(host)
758
+ # @since 2.0.0
759
+ def remove(host, disconnect: true)
751
760
  address = Address.new(host)
752
761
  removed_servers = @servers.select { |s| s.address == address }
753
762
  @update_lock.synchronize { @servers = @servers - removed_servers }
754
- removed_servers.each do |server|
755
- if server.connected?
756
- server.disconnect!
757
- publish_sdam_event(
758
- Monitoring::SERVER_CLOSED,
759
- Monitoring::Event::ServerClosed.new(address, topology)
760
- )
763
+ if disconnect != false
764
+ removed_servers.each do |server|
765
+ disconnect_server_if_connected(server)
761
766
  end
762
767
  end
763
- removed_servers.any?
768
+ if disconnect != false
769
+ removed_servers.any?
770
+ else
771
+ removed_servers
772
+ end
764
773
  end
765
774
 
766
775
  # @api private
767
776
  def update_topology(new_topology)
768
777
  old_topology = topology
769
778
  @topology = new_topology
779
+
780
+ # If new topology has data bearing servers, we know for sure whether
781
+ # sessions are supported - update our cached value.
782
+ # If new topology has no data bearing servers, leave the old value
783
+ # as it is and sessions_supported? method will perform server selection
784
+ # to try to determine session support accurately, falling back to the
785
+ # last known value.
786
+ if topology.data_bearing_servers?
787
+ @sessions_supported = !!topology.logical_session_timeout
788
+ end
789
+
770
790
  publish_sdam_event(
771
791
  Monitoring::TOPOLOGY_CHANGED,
772
792
  Monitoring::Event::TopologyChanged.new(old_topology, topology)
@@ -778,53 +798,49 @@ module Mongo
778
798
  @update_lock.synchronize { @servers.dup }
779
799
  end
780
800
 
781
- private
782
-
783
- # If options[:session] is set, validates that session and returns it.
784
- # If deployment supports sessions, creates a new session and returns it.
785
- # The session is implicit unless options[:implicit] is given.
786
- # If deployment does not support session, returns nil.
787
- #
788
- # @note This method will return nil if deployment has no data-bearing
789
- # servers at the time of the call.
790
- def get_session(client, options = {})
791
- return options[:session].validate!(self) if options[:session]
792
- if sessions_supported?
793
- Session.new(@session_pool.checkout, client, { implicit: true }.merge(options))
801
+ # @api private
802
+ def disconnect_server_if_connected(server)
803
+ if server.connected?
804
+ server.disconnect!
805
+ publish_sdam_event(
806
+ Monitoring::SERVER_CLOSED,
807
+ Monitoring::Event::ServerClosed.new(server.address, topology)
808
+ )
794
809
  end
795
810
  end
796
811
 
797
- def with_session(client, options = {})
798
- session = get_session(client, options)
799
- yield(session)
800
- ensure
801
- session.end_session if (session && session.implicit?)
802
- end
803
-
804
- # Returns whether the deployment (as this term is defined in the sessions
805
- # spec) supports sessions.
812
+ # Returns whether the deployment that the driver is connected to supports
813
+ # sessions.
806
814
  #
807
- # @note If the cluster has no data bearing servers, for example because
808
- # the deployment is in the middle of a failover, this method returns
809
- # false.
815
+ # Session support may change over time, for example due to servers in the
816
+ # deployment being upgraded or downgraded. This method returns the
817
+ # current information if the client is connected to at least one data
818
+ # bearing server. If the client is currently not connected to any data
819
+ # bearing servers, this method returns the last known value for whether
820
+ # the deployment supports sessions.
810
821
  #
811
- # @note This method returns as soon as the driver connects to any single
812
- # server in the deployment. Whether deployment overall supports sessions
813
- # can change depending on how many servers have been contacted, if
814
- # the servers are configured differently.
822
+ # @return [ true | false ] Whether deployment supports sessions.
823
+ # @api private
815
824
  def sessions_supported?
816
825
  if topology.data_bearing_servers?
817
826
  return !!topology.logical_session_timeout
818
827
  end
819
828
 
829
+ # No data bearing servers known - perform server selection to try to
830
+ # get a response from at least one of them, to return an accurate
831
+ # assessment of whether sessions are currently supported.
820
832
  begin
821
833
  ServerSelector.get(mode: :primary_preferred).select_server(self)
822
834
  !!topology.logical_session_timeout
823
835
  rescue Error::NoServerAvailable
824
- false
836
+ # We haven't been able to contact any servers - use last known
837
+ # value for esssion support.
838
+ @sessions_supported || false
825
839
  end
826
840
  end
827
841
 
842
+ private
843
+
828
844
  # @api private
829
845
  def start_stop_srv_monitor
830
846
  # SRV URI is either always given or not for a given cluster, if one
@@ -30,6 +30,7 @@ class Mongo::Cluster
30
30
  @topology = cluster.topology
31
31
  @original_desc = @previous_desc = previous_desc
32
32
  @updated_desc = updated_desc
33
+ @servers_to_disconnect = []
33
34
  end
34
35
 
35
36
  attr_reader :cluster
@@ -73,6 +74,60 @@ class Mongo::Cluster
73
74
  end
74
75
 
75
76
  def server_description_changed
77
+ if updated_desc.me_mismatch? && updated_desc.primary? &&
78
+ (topology.unknown? || topology.replica_set?)
79
+ then
80
+ # When the driver receives a description claiming to be a primary,
81
+ # we are obligated by spec tests to add and remove hosts in that
82
+ # description even if it also has a me mismatch. The me mismatch
83
+ # scenario though presents a number of problems:
84
+ #
85
+ # 1. Effectively, the server's address changes, meaning we cannot
86
+ # update the description of the server whose description change we
87
+ # are processing (instead servers are added and removed), but we
88
+ # behave to an extent as if we are updating the description, which
89
+ # causes a bunch of awkwardness.
90
+ # 2. The server for which we are processing the response will be
91
+ # removed from topology, which may cause the current thread to terminate
92
+ # prior to running the entire sdam flow. To deal with this we separate
93
+ # the removal event publication from actually removing the server
94
+ # from topology, which again complicates the flow.
95
+
96
+ # Primary-with-me-mismatch response could be the first one we receive
97
+ # when the topology is still unknown. Change to RS without primary
98
+ # in this case.
99
+ if topology.unknown?
100
+ @topology = Topology::ReplicaSetNoPrimary.new(
101
+ topology.options.merge(replica_set_name: updated_desc.replica_set_name),
102
+ topology.monitoring, self)
103
+ end
104
+
105
+ servers = add_servers_from_desc(updated_desc)
106
+ # Spec tests require us to remove servers based on data in descrptions
107
+ # with me mismatches. The driver will be more resilient if it only
108
+ # removed servers from descriptions with matching mes.
109
+ remove_servers_not_in_desc(updated_desc)
110
+
111
+ servers.each do |server|
112
+ server.start_monitoring
113
+ end
114
+
115
+ # The rest of sdam flow assumes the server being removed is not the one
116
+ # whose description we are processing, and publishes description update
117
+ # event. Since we are removing the server whose response we are
118
+ # processing, do not publish description change event but mark it
119
+ # published (by assigning to @previous_desc).
120
+ do_remove(updated_desc.address.to_s)
121
+ @previous_desc = updated_desc
122
+
123
+ # We may have removed the current primary, check if there is a primary.
124
+ check_if_has_primary
125
+ # Publish topology change event.
126
+ commit_changes
127
+ disconnect_servers
128
+ return
129
+ end
130
+
76
131
  unless update_server_descriptions
77
132
  # All of the transitions require that server whose updated_desc we are
78
133
  # processing is still in the cluster (i.e., was not removed as a result
@@ -142,6 +197,7 @@ class Mongo::Cluster
142
197
  end
143
198
 
144
199
  commit_changes
200
+ disconnect_servers
145
201
  end
146
202
 
147
203
  # Transitions from unknown to single topology type, when a standalone
@@ -337,9 +393,14 @@ class Mongo::Cluster
337
393
  end.flatten
338
394
  servers_list.each do |server|
339
395
  unless updated_desc_address_strs.include?(address_str = server.address.to_s)
396
+ updated_host = updated_desc.address.to_s
397
+ if updated_desc.me && updated_desc.me != updated_host
398
+ updated_host += " (self-identified as #{updated_desc.me})"
399
+ end
340
400
  log_warn(
341
401
  "Removing server #{address_str} because it is not in hosts reported by primary " +
342
- "#{updated_desc.address}"
402
+ "#{updated_host}. Reported hosts are: " +
403
+ updated_desc.hosts.join(', ')
343
404
  )
344
405
  do_remove(address_str)
345
406
  end
@@ -356,7 +417,21 @@ class Mongo::Cluster
356
417
  # Removes specified server from topology and warns if the topology ends
357
418
  # up with an empty server list as a result
358
419
  def do_remove(address_str)
359
- cluster.remove(address_str)
420
+ servers = cluster.remove(address_str, disconnect: false)
421
+ servers.each do |server|
422
+ # We need to publish server closed event here, but we cannot close
423
+ # the server because it could be the server owning the monitor in
424
+ # whose thread this flow is presently executing, in which case closing
425
+ # the server can terminate the thread and leave SDAM processing
426
+ # incomplete. Thus we have to remove the server from the cluster,
427
+ # publish the event, but do not call disconnect on the server until
428
+ # the very end when all processing has completed.
429
+ publish_sdam_event(
430
+ Mongo::Monitoring::SERVER_CLOSED,
431
+ Mongo::Monitoring::Event::ServerClosed.new(server.address, cluster.topology)
432
+ )
433
+ end
434
+ @servers_to_disconnect += servers
360
435
  if servers_list.empty?
361
436
  log_warn(
362
437
  "Topology now has no servers - this is likely a misconfiguration of the cluster and/or the application"
@@ -451,6 +526,15 @@ class Mongo::Cluster
451
526
  cluster.update_topology(topology)
452
527
  end
453
528
 
529
+ def disconnect_servers
530
+ while server = @servers_to_disconnect.shift
531
+ if server.connected?
532
+ # Do not publish server closed event, as this was already done
533
+ server.disconnect!
534
+ end
535
+ end
536
+ end
537
+
454
538
  # If the server being processed is identified as data bearing, creates the
455
539
  # server's connection pool so it can start populating
456
540
  def start_pool_if_data_bearing
@@ -468,7 +552,7 @@ class Mongo::Cluster
468
552
  # invoking this method.
469
553
  def check_if_has_primary
470
554
  unless topology.replica_set?
471
- raise ArgumentError, 'check_if_has_primary should only be called when topology is replica set'
555
+ raise ArgumentError, "check_if_has_primary should only be called when topology is replica set, but it is #{topology.class.name.sub(/.*::/, '')}"
472
556
  end
473
557
 
474
558
  primary = servers_list.detect do |server|
@@ -157,7 +157,7 @@ module Mongo
157
157
  # @option opts :read [ Hash ] The read preference for this command.
158
158
  # @option opts :session [ Session ] The session to use for this command.
159
159
  #
160
- # @return [ Hash ] The result of the command execution.
160
+ # @return [ Mongo::Operation::Result ] The result of the command execution.
161
161
  def command(operation, opts = {})
162
162
  txn_read_pref = if opts[:session] && opts[:session].in_transaction?
163
163
  opts[:session].txn_read_preference
@@ -306,12 +306,11 @@ module Mongo
306
306
  "#<Mongo::Server:0x#{object_id} address=#{address.host}:#{address.port}>"
307
307
  end
308
308
 
309
- # @note This method is experimental and subject to change.
309
+ # @return [ String ] String representing server status (e.g. PRIMARY).
310
310
  #
311
- # @api experimental
312
- # @since 2.7.0
313
- def summary
314
- status = case
311
+ # @api private
312
+ def status
313
+ case
315
314
  when primary?
316
315
  'PRIMARY'
317
316
  when secondary?
@@ -331,8 +330,16 @@ module Mongo
331
330
  else
332
331
  # Since the summary method is often used for debugging, do not raise
333
332
  # an exception in case none of the expected types matched
334
- ''
333
+ nil
335
334
  end
335
+ end
336
+
337
+ # @note This method is experimental and subject to change.
338
+ #
339
+ # @api experimental
340
+ # @since 2.7.0
341
+ def summary
342
+ status = self.status || ''
336
343
  if replica_set_name
337
344
  status += " replica_set=#{replica_set_name}"
338
345
  end
@@ -103,6 +103,12 @@ module Mongo
103
103
  )
104
104
  end
105
105
 
106
+ # @return [ Server::Description ] The server description obtained from
107
+ # the handshake on this connection.
108
+ #
109
+ # @api private
110
+ attr_reader :description
111
+
106
112
  # @return [ Time ] The last time the connection was checked back into a pool.
107
113
  #
108
114
  # @since 2.5.0
@@ -186,8 +192,10 @@ module Mongo
186
192
 
187
193
  begin
188
194
  handshake!(socket)
189
- pending_connection = PendingConnection.new(socket, @server, monitoring, options.merge(id: id))
190
- authenticate!(pending_connection)
195
+ unless description.arbiter?
196
+ pending_connection = PendingConnection.new(socket, @server, monitoring, options.merge(id: id))
197
+ authenticate!(pending_connection)
198
+ end
191
199
  rescue Exception
192
200
  socket.close
193
201
  raise
@@ -356,8 +364,8 @@ module Mongo
356
364
  @auth_mechanism = nil
357
365
  end
358
366
 
359
- new_description = Description.new(address, response, average_rtt)
360
- @server.cluster.run_sdam_flow(@server.description, new_description)
367
+ @description = Description.new(address, response, average_rtt)
368
+ @server.cluster.run_sdam_flow(@server.description, @description)
361
369
  end
362
370
 
363
371
  def authenticate!(pending_connection)
@@ -29,12 +29,15 @@ module Mongo
29
29
  # @return [ Hash ] options The passed in options.
30
30
  attr_reader :options
31
31
 
32
+ # @return [ Server ] The server that this connection is for.
33
+ #
34
+ # @api private
35
+ attr_reader :server
36
+
32
37
  # @return [ Mongo::Address ] address The address to connect to.
33
- def address
34
- @server.address
35
- end
38
+ def_delegators :server, :address
36
39
 
37
- def_delegators :@server,
40
+ def_delegators :server,
38
41
  :features,
39
42
  :max_bson_object_size,
40
43
  :max_message_size,