mongo 2.11.1 → 2.11.2

Sign up to get free protection for your applications and to get access to all the features.
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,