diff --git a/lib/mongo/cluster.rb b/lib/mongo/cluster.rb index 79b4b05da7..415bfe267a 100644 --- a/lib/mongo/cluster.rb +++ b/lib/mongo/cluster.rb @@ -22,6 +22,7 @@ module Mongo # @since 2.0.0 class Cluster extend Forwardable + include Monitoring::Publishable include Event::Subscriber include Loggable @@ -38,6 +39,9 @@ class Cluster # @return [ Hash ] The options hash. attr_reader :options + # @return [ Monitoring ] monitoring The monitoring. + attr_reader :monitoring + # @return [ Object ] The cluster topology. attr_reader :topology @@ -75,7 +79,6 @@ def add(host) address = Address.new(host) if !addresses.include?(address) if addition_allowed?(address) - log_debug("Adding #{address.to_s} to the cluster.") @update_lock.synchronize { @addresses.push(address) } server = Server.new(address, self, @monitoring, event_listeners, options) @update_lock.synchronize { @servers.push(server) } @@ -84,6 +87,34 @@ def add(host) end end + # Determine if the cluster would select a readable server for the + # provided read preference. + # + # @example Is a readable server present? + # topology.has_readable_server?(server_selector) + # + # @param [ ServerSelector ] server_selector The server + # selector. + # + # @return [ true, false ] If a readable server is present. + # + # @since 2.3.0 + def has_readable_server?(server_selector) + topology.has_readable_server?(self, server_selector) + end + + # Determine if the cluster would select a writable server. + # + # @example Is a writable server present? + # topology.has_writable_server? + # + # @return [ true, false ] If a writable server is present. + # + # @since 2.3.0 + def has_writable_server? + topology.has_writable_server?(self) + end + # Instantiate the new cluster. # # @api private @@ -104,7 +135,7 @@ def initialize(seeds, monitoring, options = Options::Redacted.new) @monitoring = monitoring @event_listeners = Event::Listeners.new @options = options.freeze - @topology = Topology.initial(seeds, options) + @topology = Topology.initial(seeds, monitoring, options) @update_lock = Mutex.new @pool_lock = Mutex.new @@ -242,11 +273,14 @@ def standalone_discovered # # @since 2.0.0 def remove(host) - log_debug("#{host} being removed from the cluster.") address = Address.new(host) removed_servers = @servers.select { |s| s.address == address } @update_lock.synchronize { @servers = @servers - removed_servers } removed_servers.each{ |server| server.disconnect! } if removed_servers + publish_sdam_event( + Monitoring::SERVER_CLOSED, + Monitoring::Event::ServerClosed.new(address, topology) + ) @update_lock.synchronize { @addresses.reject! { |addr| addr == address } } end diff --git a/lib/mongo/cluster/topology.rb b/lib/mongo/cluster/topology.rb index 09eeac80f3..efecedc547 100644 --- a/lib/mongo/cluster/topology.rb +++ b/lib/mongo/cluster/topology.rb @@ -33,7 +33,7 @@ module Topology replica_set: ReplicaSet, sharded: Sharded, direct: Single - } + }.freeze # Get the initial cluster topology for the provided options. # @@ -41,19 +41,21 @@ module Topology # Topology.initial(topology: :replica_set) # # @param [ Array ] seeds The addresses of the configured servers. + # @param [ Monitoring ] monitoring The monitoring. # @param [ Hash ] options The cluster options. # # @return [ ReplicaSet, Sharded, Single ] The topology. # # @since 2.0.0 - def initial(seeds, options) - if options.has_key?(:connect) - OPTIONS.fetch(options[:connect]).new(options, seeds) + def initial(seeds, monitoring, options) + topology = if options.has_key?(:connect) + OPTIONS.fetch(options[:connect]).new(options, monitoring, seeds) elsif options.has_key?(:replica_set) - ReplicaSet.new(options, seeds) + ReplicaSet.new(options, monitoring, options) else - Unknown.new(options, seeds) + Unknown.new(options, monitoring, seeds) end + topology end end end diff --git a/lib/mongo/cluster/topology/replica_set.rb b/lib/mongo/cluster/topology/replica_set.rb index a63fae8296..d3ceba9cfb 100644 --- a/lib/mongo/cluster/topology/replica_set.rb +++ b/lib/mongo/cluster/topology/replica_set.rb @@ -21,6 +21,7 @@ module Topology # @since 2.0.0 class ReplicaSet include Loggable + include Monitoring::Publishable # Constant for the replica set name configuration option. # @@ -30,6 +31,9 @@ class ReplicaSet # @return [ Hash ] options The options. attr_reader :options + # @return [ Monitoring ] monitoring The monitoring. + attr_reader :monitoring + # The display name for the topology. # # @since 2.0.0 @@ -79,18 +83,57 @@ def elect_primary(description, servers) self end + # Determine if the topology would select a readable server for the + # provided candidates and read preference. + # + # @example Is a readable server present? + # topology.has_readable_server?(cluster, server_selector) + # + # @param [ Cluster ] cluster The cluster. + # @param [ ServerSelector ] server_selector The server + # selector. + # + # @return [ true, false ] If a readable server is present. + # + # @since 2.3.0 + def has_readable_server?(cluster, server_selector) + server_selector.candidates(cluster).any? + end + + # Determine if the topology would select a writable server for the + # provided candidates. + # + # @example Is a writable server present? + # topology.has_writable_server?(servers) + # + # @param [ Cluster ] cluster The cluster. + # + # @return [ true, false ] If a writable server is present. + # + # @since 2.3.0 + def has_writable_server?(cluster) + cluster.servers.any?{ |server| server.primary? } + end + # Initialize the topology with the options. # # @example Initialize the topology. # ReplicaSet.new(options) # # @param [ Hash ] options The options. + # @param [ Monitoring ] monitoring The monitoring. + # @param [ Array ] seeds The seeds. # # @since 2.0.0 - def initialize(options, seeds = []) + def initialize(options, monitoring, seeds = []) @options = options + @monitoring = monitoring @max_election_id = nil @max_set_version = nil + publish_sdam_event( + Monitoring::TOPOLOGY_OPENING, + Monitoring::Event::TopologyOpening.new(self) + ) end # A replica set topology is a replica set. diff --git a/lib/mongo/cluster/topology/sharded.rb b/lib/mongo/cluster/topology/sharded.rb index 4b463429e8..a8f191395e 100644 --- a/lib/mongo/cluster/topology/sharded.rb +++ b/lib/mongo/cluster/topology/sharded.rb @@ -20,12 +20,19 @@ module Topology # # @since 2.0.0 class Sharded + include Monitoring::Publishable # The display name for the topology. # # @since 2.0.0 NAME = 'Sharded'.freeze + # @return [ Hash ] options The options. + attr_reader :options + + # @return [ Monitoring ] monitoring The monitoring. + attr_reader :monitoring + # Get the display name. # # @example Get the display name. @@ -51,16 +58,51 @@ def display_name # @return [ Sharded ] The topology. def elect_primary(description, servers); self; end + # Determine if the topology would select a readable server for the + # provided candidates and read preference. + # + # @example Is a readable server present? + # topology.has_readable_server?(cluster, server_selector) + # + # @param [ Cluster ] cluster The cluster. + # @param [ ServerSelector ] server_selector The server + # selector. + # + # @return [ true, false ] If a readable server is present. + # + # @since 2.3.0 + def has_readable_server?(cluster, server_selector); true; end + + # Determine if the topology would select a writable server for the + # provided candidates. + # + # @example Is a writable server present? + # topology.has_writable_server?(servers) + # + # @param [ Cluster ] cluster The cluster. + # + # @return [ true, false ] If a writable server is present. + # + # @since 2.3.0 + def has_writable_server?(cluster); true; end + # Initialize the topology with the options. # # @example Initialize the topology. # Sharded.new(options) # # @param [ Hash ] options The options. + # @param [ Monitoring ] monitoring The monitoring. + # @param [ Array ] seeds The seeds. # # @since 2.0.0 - def initialize(options, seeds = []) + def initialize(options, monitoring, seeds = []) @options = options + @monitoring = monitoring + publish_sdam_event( + Monitoring::TOPOLOGY_OPENING, + Monitoring::Event::TopologyOpening.new(self) + ) end # A sharded topology is not a replica set. diff --git a/lib/mongo/cluster/topology/single.rb b/lib/mongo/cluster/topology/single.rb index 4bd46aa5d8..56221349ef 100644 --- a/lib/mongo/cluster/topology/single.rb +++ b/lib/mongo/cluster/topology/single.rb @@ -20,15 +20,22 @@ module Topology # # @since 2.0.0 class Single - - # @return [ String ] seed The seed address. - attr_reader :seed + include Monitoring::Publishable # The display name for the topology. # # @since 2.0.0 NAME = 'Single'.freeze + # @return [ Hash ] options The options. + attr_reader :options + + # @return [ String ] seed The seed address. + attr_reader :seed + + # @return [ monitoring ] monitoring the monitoring. + attr_reader :monitoring + # Get the display name. # # @example Get the display name. @@ -54,17 +61,56 @@ def display_name # @return [ Single ] The topology. def elect_primary(description, servers); self; end + # Determine if the topology would select a readable server for the + # provided candidates and read preference. + # + # @example Is a readable server present? + # topology.has_readable_server?(cluster, server_selector) + # + # @param [ Cluster ] cluster The cluster. + # @param [ ServerSelector ] server_selector The server + # selector. + # + # @return [ true, false ] If a readable server is present. + # + # @since 2.3.0 + def has_readable_server?(cluster, server_selector) + server_selector.candidates(cluster).any? + end + + # Determine if the topology would select a writable server for the + # provided candidates. + # + # @example Is a writable server present? + # topology.has_writable_server?(servers) + # + # @param [ Cluster ] cluster The cluster. + # + # @return [ true, false ] If a writable server is present. + # + # @since 2.3.0 + def has_writable_server?(cluster) + cluster.servers.any?{ |server| server.primary? } + end + # Initialize the topology with the options. # # @example Initialize the topology. # Single.new(options) # # @param [ Hash ] options The options. + # @param [ Monitoring ] monitoring The monitoring. + # @param [ Array ] seeds The seeds. # # @since 2.0.0 - def initialize(options, seeds = []) + def initialize(options, monitoring, seeds = []) @options = options + @monitoring = monitoring @seed = seeds.first + publish_sdam_event( + Monitoring::TOPOLOGY_OPENING, + Monitoring::Event::TopologyOpening.new(self) + ) end # A single topology is not a replica set. diff --git a/lib/mongo/cluster/topology/unknown.rb b/lib/mongo/cluster/topology/unknown.rb index 017030393c..38d13ef2ff 100644 --- a/lib/mongo/cluster/topology/unknown.rb +++ b/lib/mongo/cluster/topology/unknown.rb @@ -21,6 +21,7 @@ module Topology # @since 2.0.0 class Unknown include Loggable + include Monitoring::Publishable # The display name for the topology. # @@ -30,6 +31,9 @@ class Unknown # @return [ Hash ] options The options. attr_reader :options + # @return [ Monitoring ] monitoring The monitoring. + attr_reader :monitoring + # Get the display name. # # @example Get the display name. @@ -55,24 +59,60 @@ def display_name # @return [ Sharded, ReplicaSet ] The new topology. def elect_primary(description, servers) if description.mongos? - log_debug("Mongos #{description.address.to_s} discovered.") - Sharded.new(options) + sharded = Sharded.new(options, monitoring) + topology_changed(sharded) + sharded else initialize_replica_set(description, servers) end end + # Determine if the topology would select a readable server for the + # provided candidates and read preference. + # + # @example Is a readable server present? + # topology.has_readable_server?(cluster, server_selector) + # + # @param [ Cluster ] cluster The cluster. + # @param [ ServerSelector ] server_selector The server + # selector. + # + # @return [ true, false ] If a readable server is present. + # + # @since 2.3.0 + def has_readable_server?(cluster, server_selector); false; end + + # Determine if the topology would select a writable server for the + # provided candidates. + # + # @example Is a writable server present? + # topology.has_writable_server?(servers) + # + # @param [ Cluster ] cluster The cluster. + # + # @return [ true, false ] If a writable server is present. + # + # @since 2.3.0 + def has_writable_server?(cluster); false; end + # Initialize the topology with the options. # # @example Initialize the topology. # Unknown.new(options) # # @param [ Hash ] options The options. + # @param [ Monitoring ] monitoring The monitoring. + # @param [ Array ] seeds The seeds. # # @since 2.0.0 - def initialize(options, seeds = []) + def initialize(options, monitoring, seeds = []) @options = options + @monitoring = monitoring @seeds = seeds + publish_sdam_event( + Monitoring::TOPOLOGY_OPENING, + Monitoring::Event::TopologyOpening.new(self) + ) end # An unknown topology is not a replica set. @@ -195,7 +235,9 @@ def remove_server?(description, server) # @since 2.0.6 def standalone_discovered if @seeds.size == 1 - Single.new(options, @seeds) + single = Single.new(options, monitoring, @seeds) + topology_changed(single) + single else self end @@ -204,16 +246,21 @@ def standalone_discovered private def initialize_replica_set(description, servers) - log_debug( - "Server #{description.address.to_s} discovered as primary in replica set: " + - "'#{description.replica_set_name}'. Changing topology to replica set." - ) servers.each do |server| if server.standalone? && server.address != description.address server.description.unknown! end end - ReplicaSet.new(options.merge(:replica_set => description.replica_set_name)) + replica_set = ReplicaSet.new(options.merge(:replica_set => description.replica_set_name), monitoring) + topology_changed(replica_set) + replica_set + end + + def topology_changed(new_topology) + publish_sdam_event( + Monitoring::TOPOLOGY_CHANGED, + Monitoring::Event::TopologyChanged.new(self, new_topology) + ) end end end diff --git a/lib/mongo/event/description_changed.rb b/lib/mongo/event/description_changed.rb index 2f5da4d708..b02be6fe52 100644 --- a/lib/mongo/event/description_changed.rb +++ b/lib/mongo/event/description_changed.rb @@ -20,10 +20,17 @@ module Event # # @since 2.0.6 class DescriptionChanged + include Monitoring::Publishable # @return [ Mongo::Cluster ] cluster The event publisher. attr_reader :cluster + # @return [ Hash ] options The options. + attr_reader :options + + # @return [ Monitoring ] monitoring The monitoring. + attr_reader :monitoring + # Initialize the new host added event handler. # # @example Create the new handler. @@ -34,6 +41,8 @@ class DescriptionChanged # @since 2.0.0 def initialize(cluster) @cluster = cluster + @options = cluster.options + @monitoring = cluster.monitoring end # This event publishes an event to add the cluster and logs the @@ -45,7 +54,16 @@ def initialize(cluster) # @param [ Server::Description ] updated The changed description. # # @since 2.0.0 - def handle(updated) + def handle(previous, updated) + publish_sdam_event( + Monitoring::SERVER_DESCRIPTION_CHANGED, + Monitoring::Event::ServerDescriptionChanged.new( + updated.address, + cluster.topology, + previous, + updated + ) + ) cluster.add_hosts(updated) cluster.remove_hosts(updated) end diff --git a/lib/mongo/monitoring.rb b/lib/mongo/monitoring.rb index c2ddf1b91d..5b81d28c6d 100644 --- a/lib/mongo/monitoring.rb +++ b/lib/mongo/monitoring.rb @@ -15,6 +15,12 @@ require 'mongo/monitoring/event' require 'mongo/monitoring/publishable' require 'mongo/monitoring/command_log_subscriber' +require 'mongo/monitoring/sdam_log_subscriber' +require 'mongo/monitoring/server_description_changed_log_subscriber' +require 'mongo/monitoring/server_closed_log_subscriber' +require 'mongo/monitoring/server_opening_log_subscriber' +require 'mongo/monitoring/topology_changed_log_subscriber' +require 'mongo/monitoring/topology_opening_log_subscriber' module Mongo @@ -28,6 +34,36 @@ class Monitoring # @since 2.1.0 COMMAND = 'Command'.freeze + # Server closed topic. + # + # @since 2.3.0 + SERVER_CLOSED = 'ServerClosed'.freeze + + # Server description changed topic. + # + # @since 2.3.0 + SERVER_DESCRIPTION_CHANGED = 'ServerDescriptionChanged'.freeze + + # Server opening topic. + # + # @since 2.3.0 + SERVER_OPENING = 'ServerOpening'.freeze + + # Topology changed topic. + # + # @since 2.3.0 + TOPOLOGY_CHANGED = 'TopologyChanged'.freeze + + # Topology closed topic. + # + # @since 2.3.0 + TOPOLOGY_CLOSED = 'TopologyClosed'.freeze + + # Topology opening topic. + # + # @since 2.3.0 + TOPOLOGY_OPENING = 'TopologyOpening'.freeze + @@operation_id = 0 @@operation_id_lock = Mutex.new @@ -101,6 +137,11 @@ def initialize(options = {}) end end subscribe(COMMAND, CommandLogSubscriber.new(options)) + subscribe(SERVER_OPENING, ServerOpeningLogSubscriber.new(options)) + subscribe(SERVER_CLOSED, ServerClosedLogSubscriber.new(options)) + subscribe(SERVER_DESCRIPTION_CHANGED, ServerDescriptionChangedLogSubscriber.new(options)) + subscribe(TOPOLOGY_OPENING, TopologyOpeningLogSubscriber.new(options)) + subscribe(TOPOLOGY_CHANGED, TopologyChangedLogSubscriber.new(options)) end end diff --git a/lib/mongo/monitoring/event.rb b/lib/mongo/monitoring/event.rb index 65c2844d18..d9bc8ad979 100644 --- a/lib/mongo/monitoring/event.rb +++ b/lib/mongo/monitoring/event.rb @@ -16,3 +16,9 @@ require 'mongo/monitoring/event/command_started' require 'mongo/monitoring/event/command_succeeded' require 'mongo/monitoring/event/command_failed' +require 'mongo/monitoring/event/server_closed' +require 'mongo/monitoring/event/server_description_changed' +require 'mongo/monitoring/event/server_opening' +require 'mongo/monitoring/event/topology_changed' +require 'mongo/monitoring/event/topology_closed' +require 'mongo/monitoring/event/topology_opening' diff --git a/lib/mongo/monitoring/event/server_closed.rb b/lib/mongo/monitoring/event/server_closed.rb new file mode 100644 index 0000000000..6313bec99a --- /dev/null +++ b/lib/mongo/monitoring/event/server_closed.rb @@ -0,0 +1,46 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + module Event + + # Event fired when the server is closed. + # + # @since 2.3.0 + class ServerClosed + + # @return [ Address ] address The server address. + attr_reader :address + + # @return [ Topology ] topology The topology. + attr_reader :topology + + # Create the event. + # + # @example Create the event. + # ServerClosed.new(address) + # + # @param [ Address ] address The server address. + # @param [ Integer ] topology The topology. + # + # @since 2.3.0 + def initialize(address, topology) + @address = address + @topology = topology + end + end + end + end +end diff --git a/lib/mongo/monitoring/event/server_description_changed.rb b/lib/mongo/monitoring/event/server_description_changed.rb new file mode 100644 index 0000000000..99f4945c2d --- /dev/null +++ b/lib/mongo/monitoring/event/server_description_changed.rb @@ -0,0 +1,58 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + module Event + + # Event fired when a server's description changes. + # + # @since 2.3.0 + class ServerDescriptionChanged + + # @return [ Address ] address The server address. + attr_reader :address + + # @return [ Topology ] topology The topology. + attr_reader :topology + + # @return [ Server::Description ] previous_description The previous server + # description. + attr_reader :previous_description + + # @return [ Server::Description ] new_description The new server + # description. + attr_reader :new_description + + # Create the event. + # + # @example Create the event. + # ServerDescriptionChanged.new(address, topology, previous, new) + # + # @param [ Address ] address The server address. + # @param [ Integer ] topology The topology. + # @param [ Server::Description ] previous_description The previous description. + # @param [ Server::Description ] new_description The new description. + # + # @since 2.3.0 + def initialize(address, topology, previous_description, new_description) + @address = address + @topology = topology + @previous_description = previous_description + @new_description = new_description + end + end + end + end +end diff --git a/lib/mongo/monitoring/event/server_opening.rb b/lib/mongo/monitoring/event/server_opening.rb new file mode 100644 index 0000000000..721f139d82 --- /dev/null +++ b/lib/mongo/monitoring/event/server_opening.rb @@ -0,0 +1,46 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + module Event + + # Event fired when the server is opening. + # + # @since 2.3.0 + class ServerOpening + + # @return [ Address ] address The server address. + attr_reader :address + + # @return [ Topology ] topology The topology. + attr_reader :topology + + # Create the event. + # + # @example Create the event. + # ServerOpening.new(address) + # + # @param [ Address ] address The server address. + # @param [ Integer ] topology The topology. + # + # @since 2.3.0 + def initialize(address, topology) + @address = address + @topology = topology + end + end + end + end +end diff --git a/lib/mongo/monitoring/event/topology_changed.rb b/lib/mongo/monitoring/event/topology_changed.rb new file mode 100644 index 0000000000..e415718db7 --- /dev/null +++ b/lib/mongo/monitoring/event/topology_changed.rb @@ -0,0 +1,46 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + module Event + + # Event fired when the topology changes. + # + # @since 2.3.0 + class TopologyChanged + + # @return [ Cluster::Topology ] previous_topology The previous topology. + attr_reader :previous_topology + + # @return [ Cluster::Topology ] new_topology The new topology. + attr_reader :new_topology + + # Create the event. + # + # @example Create the event. + # TopologyChanged.new(previous, new) + # + # @param [ Cluster::Topology ] previous_topology The previous topology. + # @param [ Cluster::Topology ] new_topology The new topology. + # + # @since 2.3.0 + def initialize(previous_topology, new_topology) + @previous_topology = previous_topology + @new_topology = new_topology + end + end + end + end +end diff --git a/lib/mongo/monitoring/event/topology_closed.rb b/lib/mongo/monitoring/event/topology_closed.rb new file mode 100644 index 0000000000..4c5970af8e --- /dev/null +++ b/lib/mongo/monitoring/event/topology_closed.rb @@ -0,0 +1,41 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + module Event + + # Event fired when the topology closes. + # + # @since 2.3.0 + class TopologyClosed + + # @return [ Topology ] topology The topology. + attr_reader :topology + + # Create the event. + # + # @example Create the event. + # TopologyClosed.new(topology) + # + # @param [ Integer ] topology The topology. + # + # @since 2.3.0 + def initialize(topology) + @topology = topology + end + end + end + end +end diff --git a/lib/mongo/monitoring/event/topology_opening.rb b/lib/mongo/monitoring/event/topology_opening.rb new file mode 100644 index 0000000000..643bd70a54 --- /dev/null +++ b/lib/mongo/monitoring/event/topology_opening.rb @@ -0,0 +1,41 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + module Event + + # Event fired when the topology is opening. + # + # @since 2.3.0 + class TopologyOpening + + # @return [ Topology ] topology The topology. + attr_reader :topology + + # Create the event. + # + # @example Create the event. + # TopologyOpening.new(topology) + # + # @param [ Integer ] topology The topology. + # + # @since 2.3.0 + def initialize(topology) + @topology = topology + end + end + end + end +end diff --git a/lib/mongo/monitoring/publishable.rb b/lib/mongo/monitoring/publishable.rb index 54b8f4ad01..62fe0ebe6b 100644 --- a/lib/mongo/monitoring/publishable.rb +++ b/lib/mongo/monitoring/publishable.rb @@ -55,6 +55,14 @@ def publish_command(messages, operation_id = Monitoring.next_operation_id) end end + def publish_event(topic, event) + monitoring.succeeded(topic, event) + end + + def publish_sdam_event(topic, event) + monitoring.succeeded(topic, event) if monitoring? + end + private def command_started(address, operation_id, payload) @@ -101,6 +109,10 @@ def duration(start) def error?(document) document && (document['ok'] == 0 || document.key?('$err')) end + + def monitoring? + options[:monitoring] != false + end end end end diff --git a/lib/mongo/monitoring/sdam_log_subscriber.rb b/lib/mongo/monitoring/sdam_log_subscriber.rb new file mode 100644 index 0000000000..494ed35adc --- /dev/null +++ b/lib/mongo/monitoring/sdam_log_subscriber.rb @@ -0,0 +1,54 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + + # Subscribes to SDAM events and logs them. + # + # @since 2.3.0 + class SDAMLogSubscriber + include Loggable + + # @return [ Hash ] options The options. + attr_reader :options + + # Create the new log subscriber. + # + # @example Create the log subscriber. + # SDAMLogSubscriber.new + # + # @param [ Hash ] options The options. + # + # @option options [ Logger ] :logger An optional custom logger. + # + # @since 2.1.0 + def initialize(options = {}) + @options = options + end + + # Handle the SDAM succeeded event. + # + # @example Handle the event. + # subscriber.succeeded(event) + # + # @param [ Event ] event The event. + # + # @since 2.3.0 + def succeeded(event) + log_event(event) if logger.debug? + end + end + end +end diff --git a/lib/mongo/monitoring/server_closed_log_subscriber.rb b/lib/mongo/monitoring/server_closed_log_subscriber.rb new file mode 100644 index 0000000000..350d5030d0 --- /dev/null +++ b/lib/mongo/monitoring/server_closed_log_subscriber.rb @@ -0,0 +1,30 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + + # Subscribes to Server Closed events and logs them. + # + # @since 2.3.0 + class ServerClosedLogSubscriber < SDAMLogSubscriber + + private + + def log_event(event) + log_debug("Server #{event.address} connection closed.") + end + end + end +end diff --git a/lib/mongo/monitoring/server_description_changed_log_subscriber.rb b/lib/mongo/monitoring/server_description_changed_log_subscriber.rb new file mode 100644 index 0000000000..e31bac0a26 --- /dev/null +++ b/lib/mongo/monitoring/server_description_changed_log_subscriber.rb @@ -0,0 +1,33 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + + # Subscribes to Server Description Changed events and logs them. + # + # @since 2.3.0 + class ServerDescriptionChangedLogSubscriber < SDAMLogSubscriber + + private + + def log_event(event) + log_debug( + "Server description for #{event.address} changed from " + + "'#{event.previous_description.server_type}' to '#{event.new_description.server_type}'." + ) + end + end + end +end diff --git a/lib/mongo/monitoring/server_opening_log_subscriber.rb b/lib/mongo/monitoring/server_opening_log_subscriber.rb new file mode 100644 index 0000000000..9c4849da73 --- /dev/null +++ b/lib/mongo/monitoring/server_opening_log_subscriber.rb @@ -0,0 +1,30 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + + # Subscribes to Server Opening events and logs them. + # + # @since 2.3.0 + class ServerOpeningLogSubscriber < SDAMLogSubscriber + + private + + def log_event(event) + log_debug("Server #{event.address} initializing.") + end + end + end +end diff --git a/lib/mongo/monitoring/topology_changed_log_subscriber.rb b/lib/mongo/monitoring/topology_changed_log_subscriber.rb new file mode 100644 index 0000000000..48ca30a93e --- /dev/null +++ b/lib/mongo/monitoring/topology_changed_log_subscriber.rb @@ -0,0 +1,33 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + + # Subscribes to Topology Changed events and logs them. + # + # @since 2.3.0 + class TopologyChangedLogSubscriber < SDAMLogSubscriber + + private + + def log_event(event) + log_debug( + "Topology type '#{event.previous_topology.display_name.downcase}' changed to " + + "type '#{event.new_topology.display_name.downcase}'." + ) + end + end + end +end diff --git a/lib/mongo/monitoring/topology_opening_log_subscriber.rb b/lib/mongo/monitoring/topology_opening_log_subscriber.rb new file mode 100644 index 0000000000..29bb8e1871 --- /dev/null +++ b/lib/mongo/monitoring/topology_opening_log_subscriber.rb @@ -0,0 +1,30 @@ +# Copyright (C) 2016 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + class Monitoring + + # Subscribes to Topology Openeing events and logs them. + # + # @since 2.3.0 + class TopologyOpeningLogSubscriber < SDAMLogSubscriber + + private + + def log_event(event) + log_debug("Topology type '#{event.topology.display_name.downcase}' initializing.") + end + end + end +end diff --git a/lib/mongo/server.rb b/lib/mongo/server.rb index 6bda231fca..ae6cd61d63 100644 --- a/lib/mongo/server.rb +++ b/lib/mongo/server.rb @@ -27,6 +27,7 @@ module Mongo # @since 2.0.0 class Server extend Forwardable + include Monitoring::Publishable # @return [ String ] The configured address for the server. attr_reader :address @@ -155,6 +156,10 @@ def initialize(address, cluster, monitoring, event_listeners, options = {}) @cluster = cluster @monitoring = monitoring @options = options.freeze + publish_sdam_event( + Monitoring::SERVER_OPENING, + Monitoring::Event::ServerOpening.new(address, cluster.topology) + ) @monitor = Monitor.new(address, event_listeners, options) monitor.scan! monitor.run! diff --git a/lib/mongo/server/description.rb b/lib/mongo/server/description.rb index 59299918e3..8db0dc5737 100644 --- a/lib/mongo/server/description.rb +++ b/lib/mongo/server/description.rb @@ -458,6 +458,24 @@ def secondary? !!config[SECONDARY] && !replica_set_name.nil? end + # Returns the server type as a symbol. + # + # @example Get the server type. + # description.server_type + # + # @return [ Symbol ] The server type. + # + # @since 2.3.0 + def server_type + return :arbiter if arbiter? + return :ghost if ghost? + return :sharded if mongos? + return :primary if primary? + return :secondary if secondary? + return :standalone if standalone? + :unknown + end + # Is this server a standalone server? # # @example Is the server standalone? diff --git a/lib/mongo/server/description/inspector/description_changed.rb b/lib/mongo/server/description/inspector/description_changed.rb index c843919cd6..4ac8649702 100644 --- a/lib/mongo/server/description/inspector/description_changed.rb +++ b/lib/mongo/server/description/inspector/description_changed.rb @@ -47,7 +47,7 @@ def initialize(event_listeners) # @since 2.0.0 def run(description, updated) unless description == updated - publish(Event::DESCRIPTION_CHANGED, updated) + publish(Event::DESCRIPTION_CHANGED, description, updated) end end end diff --git a/lib/mongo/server_selector.rb b/lib/mongo/server_selector.rb index 921af8f221..359fd0b3fb 100644 --- a/lib/mongo/server_selector.rb +++ b/lib/mongo/server_selector.rb @@ -48,11 +48,11 @@ module ServerSelector # # @since 2.0.0 PREFERENCES = { - nearest: Nearest, - primary: Primary, - primary_preferred: PrimaryPreferred, - secondary: Secondary, - secondary_preferred: SecondaryPreferred + nearest: Nearest, + primary: Primary, + primary_preferred: PrimaryPreferred, + secondary: Secondary, + secondary_preferred: SecondaryPreferred }.freeze # Create a server selector object. diff --git a/lib/mongo/server_selector/selectable.rb b/lib/mongo/server_selector/selectable.rb index 6c35b3182e..e7851a10ac 100644 --- a/lib/mongo/server_selector/selectable.rb +++ b/lib/mongo/server_selector/selectable.rb @@ -40,6 +40,26 @@ def ==(other) name == other.name && tag_sets == other.tag_sets end + # Get the potential candidates to selecto from the cluster. + # + # @example Get the server candidates. + # selectable.candidates(cluster) + # + # @param [ Cluster ] cluster The cluster. + # + # @return [ Array ] The candidate servers. + # + # @since 2.3.0 + def candidates(cluster) + if cluster.single? + cluster.servers + elsif cluster.sharded? + near_servers(cluster.servers) + else + select(cluster.servers) + end + end + # Initialize the server selector. # # @example Initialize the selector. @@ -136,16 +156,6 @@ def local_threshold private - def candidates(cluster) - if cluster.single? - cluster.servers - elsif cluster.sharded? - near_servers(cluster.servers) - else - select(cluster.servers) - end - end - # Select the primary from a list of provided candidates. # # @param [ Array ] candidates List of candidate servers to select the diff --git a/spec/mongo/address/unix_spec.rb b/spec/mongo/address/unix_spec.rb index b033989519..ef9404dacc 100644 --- a/spec/mongo/address/unix_spec.rb +++ b/spec/mongo/address/unix_spec.rb @@ -24,7 +24,7 @@ end end - describe '#socket' do + pending '#socket' do let(:address) do '/tmp/mongodb-27017.sock' diff --git a/spec/mongo/auth/cr_spec.rb b/spec/mongo/auth/cr_spec.rb index 426735f828..d3e6980912 100644 --- a/spec/mongo/auth/cr_spec.rb +++ b/spec/mongo/auth/cr_spec.rb @@ -7,15 +7,23 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:listeners) do Mongo::Event::Listeners.new end + let(:topology) do + double('topology') + end + + let(:cluster) do + double('cluster', topology: topology) + end + let(:server) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:connection) do diff --git a/spec/mongo/auth/ldap_spec.rb b/spec/mongo/auth/ldap_spec.rb index 6d99e7dce4..4d3fc75037 100644 --- a/spec/mongo/auth/ldap_spec.rb +++ b/spec/mongo/auth/ldap_spec.rb @@ -7,15 +7,23 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:listeners) do Mongo::Event::Listeners.new end + let(:topology) do + double('topology') + end + + let(:cluster) do + double('cluster', topology: topology) + end + let(:server) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:connection) do diff --git a/spec/mongo/auth/scram_spec.rb b/spec/mongo/auth/scram_spec.rb index eeb94d45cd..f1afc60ad6 100644 --- a/spec/mongo/auth/scram_spec.rb +++ b/spec/mongo/auth/scram_spec.rb @@ -7,15 +7,23 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:listeners) do Mongo::Event::Listeners.new end + let(:topology) do + double('topology') + end + + let(:cluster) do + double('cluster', topology: topology) + end + let(:server) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:connection) do diff --git a/spec/mongo/auth/x509_spec.rb b/spec/mongo/auth/x509_spec.rb index 7c83413e55..21a51e2805 100644 --- a/spec/mongo/auth/x509_spec.rb +++ b/spec/mongo/auth/x509_spec.rb @@ -7,15 +7,23 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:listeners) do Mongo::Event::Listeners.new end + let(:topology) do + double('topology') + end + + let(:cluster) do + double('cluster', topology: topology) + end + let(:server) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:connection) do diff --git a/spec/mongo/cluster/topology/replica_set_spec.rb b/spec/mongo/cluster/topology/replica_set_spec.rb index 406a83bbec..6f5a52a84b 100644 --- a/spec/mongo/cluster/topology/replica_set_spec.rb +++ b/spec/mongo/cluster/topology/replica_set_spec.rb @@ -11,25 +11,29 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) + end + + let(:cluster) do + double('cluster', topology: topology) end describe '#servers' do let(:mongos) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:standalone) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:replica_set) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:replica_set_two) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:mongos_description) do @@ -58,7 +62,7 @@ context 'when no replica set name is provided' do let(:topology) do - described_class.new({}) + described_class.new({}, monitoring, []) end let(:servers) do @@ -73,7 +77,7 @@ context 'when a replica set name is provided' do let(:topology) do - described_class.new(:replica_set => 'testing') + described_class.new({ :replica_set => 'testing' }, monitoring) end let(:servers) do @@ -89,32 +93,215 @@ describe '.replica_set?' do it 'returns true' do - expect(described_class.new({})).to be_replica_set + expect(described_class.new({}, monitoring)).to be_replica_set end end describe '.sharded?' do it 'returns false' do - expect(described_class.new({})).to_not be_sharded + expect(described_class.new({}, monitoring)).to_not be_sharded end end describe '.single?' do it 'returns false' do - expect(described_class.new({})).to_not be_single + expect(described_class.new({}, monitoring)).to_not be_single + end + end + + describe '#has_readable_servers?' do + + let(:topology) do + described_class.new({}, monitoring, []) + end + + let(:cluster) do + double('cluster', servers: servers, single?: false, sharded?: false) + end + + context 'when the read preference is primary' do + + let(:selector) do + Mongo::ServerSelector.get(:mode => :primary) + end + + context 'when a primary exists' do + + let(:servers) do + [ double('server', primary?: true) ] + end + + it 'returns true' do + expect(topology).to have_readable_server(cluster, selector) + end + end + + context 'when a primary does not exist' do + + let(:servers) do + [ double('server', primary?: false) ] + end + + it 'returns false' do + expect(topology).to_not have_readable_server(cluster, selector) + end + end + end + + context 'when the read preference is primary preferred' do + + let(:selector) do + Mongo::ServerSelector.get(:mode => :primary_preferred) + end + + context 'when a primary exists' do + + let(:servers) do + [ double('server', primary?: true) ] + end + + it 'returns true' do + expect(topology).to have_readable_server(cluster, selector) + end + end + + context 'when a primary does not exist' do + + let(:servers) do + [ double('server', primary?: false, secondary?: true, average_round_trip_time: 0.01) ] + end + + it 'returns true' do + expect(topology).to have_readable_server(cluster, selector) + end + end + end + + context 'when the read preference is secondary' do + + let(:selector) do + Mongo::ServerSelector.get(:mode => :secondary) + end + + context 'when a secondary exists' do + + let(:servers) do + [ double('server', secondary?: true, average_round_trip_time: 0.01) ] + end + + it 'returns true' do + expect(topology).to have_readable_server(cluster, selector) + end + end + + context 'when a secondary does not exist' do + + let(:servers) do + [ double('server', secondary?: false) ] + end + + it 'returns false' do + expect(topology).to_not have_readable_server(cluster, selector) + end + end + end + + context 'when the read preference is secondary preferred' do + + let(:selector) do + Mongo::ServerSelector.get(:mode => :secondary_preferred) + end + + context 'when a secondary exists' do + + let(:servers) do + [ double('server', primary?: false, secondary?: true, average_round_trip_time: 0.01) ] + end + + it 'returns true' do + expect(topology).to have_readable_server(cluster, selector) + end + end + + context 'when a secondary does not exist' do + + let(:servers) do + [ double('server', secondary?: false, primary?: true) ] + end + + it 'returns true' do + expect(topology).to have_readable_server(cluster, selector) + end + end + end + + context 'when the read preference is nearest' do + + let(:selector) do + Mongo::ServerSelector.get(:mode => :nearest) + end + + let(:servers) do + [ double('server', primary?: false, secondary?: true, average_round_trip_time: 0.01) ] + end + + it 'returns true' do + expect(topology).to have_readable_server(cluster, selector) + end + end + end + + describe '#has_writable_servers?' do + + let(:topology) do + described_class.new({}, monitoring, []) + end + + context 'when a primary server exists' do + + let(:primary) do + double('server', :primary? => true) + end + + let(:secondary) do + double('server', :primary? => false) + end + + let(:cluster) do + double('cluster', servers: [ primary, secondary ]) + end + + it 'returns true' do + expect(topology).to have_writable_server(cluster) + end + end + + context 'when no primary server exists' do + + let(:server) do + double('server', :primary? => false) + end + + let(:cluster) do + double('cluster', servers: [ server ]) + end + + it 'returns false' do + expect(topology).to_not have_writable_server(cluster) + end end end describe '#add_hosts?' do let(:primary) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:secondary) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:primary_description) do @@ -127,7 +314,7 @@ end let(:topology) do - described_class.new(:replica_set => 'testing') + described_class.new({ :replica_set => 'testing' }, monitoring) end before do @@ -193,7 +380,7 @@ describe '#remove_hosts?' do let(:primary) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:primary_description) do @@ -201,7 +388,7 @@ end let(:topology) do - described_class.new(:replica_set => 'testing') + described_class.new({ :replica_set => 'testing' }, monitoring) end before do @@ -274,7 +461,7 @@ describe '#remove_server?' do let(:secondary) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:secondary_description) do @@ -283,7 +470,7 @@ end let(:topology) do - described_class.new(:replica_set => 'testing') + described_class.new({ :replica_set => 'testing' }, monitoring) end before do diff --git a/spec/mongo/cluster/topology/sharded_spec.rb b/spec/mongo/cluster/topology/sharded_spec.rb index bec1e2df13..5926188f0e 100644 --- a/spec/mongo/cluster/topology/sharded_spec.rb +++ b/spec/mongo/cluster/topology/sharded_spec.rb @@ -7,27 +7,31 @@ end let(:topology) do - described_class.new({}) + described_class.new({}, monitoring) end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:listeners) do Mongo::Event::Listeners.new end + let(:cluster) do + double('cluster', topology: topology) + end + let(:mongos) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:standalone) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:replica_set) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:mongos_description) do @@ -80,6 +84,20 @@ end end + describe '#has_readable_servers?' do + + it 'returns true' do + expect(topology).to have_readable_server(nil, nil) + end + end + + describe '#has_writable_servers?' do + + it 'returns true' do + expect(topology).to have_writable_server(nil) + end + end + describe '#add_hosts?' do it 'returns false' do diff --git a/spec/mongo/cluster/topology/single_spec.rb b/spec/mongo/cluster/topology/single_spec.rb index 3bd6bb407b..bbf89290fa 100644 --- a/spec/mongo/cluster/topology/single_spec.rb +++ b/spec/mongo/cluster/topology/single_spec.rb @@ -6,34 +6,38 @@ Mongo::Address.new('127.0.0.1:27017') end - let(:topology) do - described_class.new({}) + let(:monitoring) do + Mongo::Monitoring.new(monitoring: false) end - let(:monitoring) do - Mongo::Monitoring.new + let(:topology) do + described_class.new({}, monitoring) end let(:listeners) do Mongo::Event::Listeners.new end + let(:cluster) do + double('cluster', topology: topology) + end + describe '.servers' do let(:mongos) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:standalone) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:standalone_two) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:replica_set) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS) + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS) end let(:mongos_description) do @@ -85,6 +89,94 @@ end end + describe '#has_readable_servers?' do + + let(:cluster) do + double('cluster', servers: servers, single?: true) + end + + let(:selector) do + Mongo::ServerSelector.get(mode: :primary) + end + + context 'when using a direct connection to a primary' do + + let(:servers) do + [ double('server', primary?: true) ] + end + + it 'returns true' do + expect(topology).to have_readable_server(cluster, selector) + end + end + + context 'when using a direct connection to a secondary' do + + let(:servers) do + [ double('server', secondary?: true) ] + end + + it 'returns true' do + expect(topology).to have_readable_server(cluster, selector) + end + end + + context 'when using a direct connection to an arbiter' do + + let(:servers) do + [ double('server', secondary?: true) ] + end + + it 'returns true' do + expect(topology).to have_readable_server(cluster, selector) + end + end + + context 'when no servers have been scanned' do + + let(:servers) do + [] + end + + it 'returns false' do + expect(topology).to_not have_readable_server(cluster, selector) + end + end + end + + describe '#has_writable_servers?' do + + context 'when the server is a primary' do + + let(:server) do + double('server', :primary? => true) + end + + let(:cluster) do + double('cluster', servers: [ server ]) + end + + it 'returns true' do + expect(topology).to have_writable_server(cluster) + end + end + + context 'when the server is not a primary (e.g. direct connect to secondary)' do + + let(:server) do + double('server', :primary? => false) + end + + let(:cluster) do + double('cluster', servers: [ server ]) + end + + it 'returns false' do + expect(topology).to_not have_writable_server(cluster) + end + end + end + describe '#add_hosts?' do it 'returns false' do diff --git a/spec/mongo/cluster/topology/unknown_spec.rb b/spec/mongo/cluster/topology/unknown_spec.rb index 58c2845b1b..f3de452b46 100644 --- a/spec/mongo/cluster/topology/unknown_spec.rb +++ b/spec/mongo/cluster/topology/unknown_spec.rb @@ -2,8 +2,12 @@ describe Mongo::Cluster::Topology::Unknown do + let(:monitoring) do + Mongo::Monitoring.new(monitoring: false) + end + let(:topology) do - described_class.new({}) + described_class.new({}, monitoring) end describe '.servers' do @@ -45,6 +49,20 @@ end end + describe '#has_readable_servers?' do + + it 'returns false' do + expect(topology).to_not have_readable_server(nil, nil) + end + end + + describe '#has_writable_servers?' do + + it 'returns false' do + expect(topology).to_not have_writable_server(nil) + end + end + describe '#add_hosts?' do context 'when the description is from an unknown server' do diff --git a/spec/mongo/cluster/topology_spec.rb b/spec/mongo/cluster/topology_spec.rb index 6f3079b898..ac909cb9b9 100644 --- a/spec/mongo/cluster/topology_spec.rb +++ b/spec/mongo/cluster/topology_spec.rb @@ -2,12 +2,16 @@ describe Mongo::Cluster::Topology do + let(:monitoring) do + Mongo::Monitoring.new(monitoring: false) + end + describe '.initial' do context 'when provided a replica set option' do let(:topology) do - described_class.initial([ 'a' ], connect: :replica_set) + described_class.initial([ 'a' ], monitoring, connect: :replica_set) end it 'returns a replica set topology' do @@ -18,7 +22,7 @@ context 'when provided a single option' do let(:topology) do - described_class.initial([ 'a' ], connect: :direct) + described_class.initial([ 'a' ], monitoring, connect: :direct) end it 'returns a single topology' do @@ -33,7 +37,7 @@ context 'when provided a sharded option' do let(:topology) do - described_class.initial([ 'a' ], connect: :sharded) + described_class.initial([ 'a' ], monitoring, connect: :sharded) end it 'returns a sharded topology' do @@ -46,7 +50,7 @@ context 'when a set name is in the options' do let(:topology) do - described_class.initial([], replica_set: 'testing') + described_class.initial([], monitoring, replica_set: 'testing') end it 'returns a replica set topology' do @@ -57,7 +61,7 @@ context 'when no set name is in the options' do let(:topology) do - described_class.initial([], {}) + described_class.initial([], monitoring, {}) end it 'returns an unknown topology' do @@ -68,7 +72,7 @@ context 'when provided a single mongos', if: single_mongos? do let(:topology) do - described_class.initial(ADDRESSES, TEST_OPTIONS) + described_class.initial(ADDRESSES, monitoring, TEST_OPTIONS) end it 'returns a sharded topology' do @@ -79,7 +83,7 @@ context 'when provided a single replica set member', if: single_rs_member? do let(:topology) do - described_class.initial(ADDRESSES, TEST_OPTIONS) + described_class.initial(ADDRESSES, monitoring, TEST_OPTIONS) end it 'returns a single topology' do diff --git a/spec/mongo/cluster_spec.rb b/spec/mongo/cluster_spec.rb index 3c5f3a2858..41306fa0f1 100644 --- a/spec/mongo/cluster_spec.rb +++ b/spec/mongo/cluster_spec.rb @@ -3,7 +3,7 @@ describe Mongo::Cluster do let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:cluster) do @@ -59,6 +59,24 @@ end end + describe '#has_readable_server?' do + + let(:selector) do + Mongo::ServerSelector.get(mode: :primary) + end + + it 'delegates to the topology' do + expect(cluster).to have_readable_server(selector) + end + end + + describe '#has_writable_server?' do + + it 'delegates to the topology' do + expect(cluster).to_not have_writable_server + end + end + describe '#inspect' do let(:preference) do @@ -156,7 +174,7 @@ context 'when topology is Single' do let(:topology) do - Mongo::Cluster::Topology::Single.new({}) + Mongo::Cluster::Topology::Single.new({}, monitoring) end it 'returns an empty array' do @@ -167,7 +185,7 @@ context 'when topology is ReplicaSet' do let(:topology) do - Mongo::Cluster::Topology::ReplicaSet.new({}) + Mongo::Cluster::Topology::ReplicaSet.new({}, monitoring) end it 'returns an empty array' do @@ -178,7 +196,7 @@ context 'when topology is Sharded' do let(:topology) do - Mongo::Cluster::Topology::Sharded.new({}) + Mongo::Cluster::Topology::Sharded.new({}, monitoring) end it 'returns an empty array' do @@ -189,7 +207,7 @@ context 'when topology is Unknown' do let(:topology) do - Mongo::Cluster::Topology::Unknown.new({}) + Mongo::Cluster::Topology::Unknown.new({}, monitoring) end it 'returns an empty array' do @@ -258,7 +276,7 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:server_a) do @@ -351,7 +369,7 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:server) do diff --git a/spec/mongo/monitoring_spec.rb b/spec/mongo/monitoring_spec.rb index fac06d9d4f..e4bb1f9812 100644 --- a/spec/mongo/monitoring_spec.rb +++ b/spec/mongo/monitoring_spec.rb @@ -45,7 +45,7 @@ end it 'includes the global subscribers' do - expect(monitoring.subscribers.size).to eq(1) + expect(monitoring.subscribers.size).to eq(6) end end @@ -58,7 +58,7 @@ end it 'includes the global subscribers' do - expect(monitoring.subscribers.size).to eq(1) + expect(monitoring.subscribers.size).to eq(6) end end diff --git a/spec/mongo/sdam_monitoring_spec.rb b/spec/mongo/sdam_monitoring_spec.rb new file mode 100644 index 0000000000..e8f6134e27 --- /dev/null +++ b/spec/mongo/sdam_monitoring_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe 'SDAM Monitoring' do + include Mongo::SDAM + + SDAM_MONITORING_TESTS.each do |file| + + spec = Mongo::SDAM::Spec.new(file) + + context(spec.description) do + + before(:all) do + + module Mongo + # We monkey-patch the server here, so the monitors do not run and no + # real TCP connection is attempted. Thus we can control the server + # descriptions per-phase. + # + # @since 2.0.0 + class Server + + alias :original_initialize :initialize + def initialize(address, cluster, monitoring, event_listeners, options = {}) + @address = address + @cluster = cluster + @monitoring = monitoring + @options = options.freeze + @monitor = Monitor.new(address, event_listeners, options) + end + + alias :original_disconnect! :disconnect! + def disconnect!; true; end + end + end + + # Client is set as an instance variable inside the scope of the spec to + # retain its modifications across contexts/phases. Let is no good + # here as we have a clean slate for each context/phase. + @client = Mongo::Client.new(spec.uri_string) + end + + after(:all) do + @client.close + + # Return the server implementation to its original for the other + # tests in the suite. + module Mongo + class Server + alias :initialize :original_initialize + remove_method(:original_initialize) + + alias :disconnect! :original_disconnect! + remove_method(:original_disconnect!) + end + end + end + + spec.phases.each_with_index do |phase, index| + + context("Phase: #{index + 1}") do + + before(:all) do + @subscriber = Mongo::SDAMMonitoring::TestSubscriber.new + @client.subscribe(Mongo::Monitoring::SERVER_OPENING, @subscriber) + @client.subscribe(Mongo::Monitoring::SERVER_CLOSED, @subscriber) + @client.subscribe(Mongo::Monitoring::SERVER_DESCRIPTION_CHANGED, @subscriber) + @client.subscribe(Mongo::Monitoring::TOPOLOGY_OPENING, @subscriber) + @client.subscribe(Mongo::Monitoring::TOPOLOGY_CHANGED, @subscriber) + end + + phase.responses.each do |response| + + before do + # For each response in the phase, we need to change that server's + # description. + server = find_server(@client, response.address) + server = Mongo::Server.new( + Mongo::Address.new(response.address), + @client.cluster, + @client.instance_variable_get(:@monitoring), + @client.cluster.send(:event_listeners), + @client.cluster.options + ) unless server + monitor = server.instance_variable_get(:@monitor) + description = monitor.inspector.run(server.description, response.ismaster, 0.5) + monitor.instance_variable_set(:@description, description) + end + end + + phase.outcome.events.each do |expectation| + + it "expects a #{expectation.name} to be fired" do + fired_event = @subscriber.first_event(expectation.name) + expect(fired_event).to match_sdam_monitoring_event(expectation) + end + end + end + end + end + end +end diff --git a/spec/mongo/server/connection_pool_spec.rb b/spec/mongo/server/connection_pool_spec.rb index ffa79360a2..ce26d24c7a 100644 --- a/spec/mongo/server/connection_pool_spec.rb +++ b/spec/mongo/server/connection_pool_spec.rb @@ -11,15 +11,19 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:listeners) do Mongo::Event::Listeners.new end + let(:topology) do + double('topology') + end + let(:cluster) do - double('cluster') + double('cluster', topology: topology) end describe '#checkin' do diff --git a/spec/mongo/server/connection_spec.rb b/spec/mongo/server/connection_spec.rb index 4ee10bf6eb..da75639d3e 100644 --- a/spec/mongo/server/connection_spec.rb +++ b/spec/mongo/server/connection_spec.rb @@ -7,15 +7,19 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:listeners) do Mongo::Event::Listeners.new end + let(:topology) do + double('topology') + end + let(:cluster) do - double('cluster') + double('cluster', topology: topology) end let(:server) do diff --git a/spec/mongo/server/description_spec.rb b/spec/mongo/server/description_spec.rb index 477dd2ecac..9ad6715fde 100644 --- a/spec/mongo/server/description_spec.rb +++ b/spec/mongo/server/description_spec.rb @@ -32,7 +32,15 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) + end + + let(:topology) do + double('topology') + end + + let(:cluster) do + double('cluster', topology: topology) end describe '#arbiter?' do @@ -540,6 +548,90 @@ end end + describe '#server_type' do + + context 'when the server is an arbiter' do + + let(:description) do + described_class.new(address, { 'arbiterOnly' => true, 'setName' => 'test' }) + end + + it 'returns :arbiter' do + expect(description.server_type).to eq(:arbiter) + end + end + + context 'when the server is a ghost' do + + let(:description) do + described_class.new(address, { 'isreplicaset' => true }) + end + + it 'returns :ghost' do + expect(description.server_type).to eq(:ghost) + end + end + + context 'when the server is a mongos' do + + let(:config) do + { 'msg' => 'isdbgrid', 'ismaster' => true } + end + + let(:description) do + described_class.new(address, config) + end + + it 'returns :sharded' do + expect(description.server_type).to eq(:sharded) + end + end + + context 'when the server is a primary' do + + let(:description) do + described_class.new(address, replica) + end + + it 'returns :primary' do + expect(description.server_type).to eq(:primary) + end + end + + context 'when the server is a secondary' do + + let(:description) do + described_class.new(address, { 'secondary' => true, 'setName' => 'test' }) + end + + it 'returns :secondary' do + expect(description.server_type).to eq(:secondary) + end + end + + context 'when the server is standalone' do + + let(:description) do + described_class.new(address, { 'ismaster' => true, 'ok' => 1 }) + end + + it 'returns :standalone' do + expect(description.server_type).to eq(:standalone) + end + end + + context 'when the description has no configuration' do + + let(:description) do + described_class.new(address) + end + + it 'returns :unknown' do + expect(description.server_type).to eq(:unknown) + end + end + end + describe '#unknown?' do context 'when the description has no configuration' do @@ -587,7 +679,7 @@ end let(:server) do - Mongo::Server.new(address, double('cluster'), monitoring, listeners) + Mongo::Server.new(address, cluster, monitoring, listeners) end let(:description) do @@ -608,7 +700,7 @@ end let(:server) do - Mongo::Server.new(other_address, double('cluster'), monitoring, listeners) + Mongo::Server.new(other_address, cluster, monitoring, listeners) end it 'returns false' do @@ -674,7 +766,7 @@ end let(:server) do - Mongo::Server.new(server_address, double('cluster'), monitoring, listeners) + Mongo::Server.new(server_address, cluster, monitoring, listeners) end context 'when the server is included in the description hosts list' do diff --git a/spec/mongo/server_selection_spec.rb b/spec/mongo/server_selection_spec.rb index 24ef6fb2d2..069b35c9bf 100644 --- a/spec/mongo/server_selection_spec.rb +++ b/spec/mongo/server_selection_spec.rb @@ -10,12 +10,12 @@ context(spec.description) do - let(:topology) do - spec.type.new({}) + let(:monitoring) do + Mongo::Monitoring.new(monitoring: false) end - let(:monitoring) do - Mongo::Monitoring.new + let(:topology) do + spec.type.new({}, monitoring) end let(:listeners) do @@ -34,7 +34,7 @@ let(:candidate_servers) do spec.candidate_servers.collect do |server| address = Mongo::Address.new(server['address']) - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS).tap do |s| + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS).tap do |s| allow(s).to receive(:average_round_trip_time).and_return(server['avg_rtt_ms']) allow(s).to receive(:tags).and_return(server['tags']) allow(s).to receive(:secondary?).and_return(server['type'] == 'RSSecondary') @@ -47,7 +47,7 @@ let(:in_latency_window) do spec.in_latency_window.collect do |server| address = Mongo::Address.new(server['address']) - Mongo::Server.new(address, double('cluster'), monitoring, listeners, TEST_OPTIONS).tap do |s| + Mongo::Server.new(address, cluster, monitoring, listeners, TEST_OPTIONS).tap do |s| allow(s).to receive(:average_round_trip_time).and_return(server['avg_rtt_ms']) allow(s).to receive(:tags).and_return(server['tags']) allow(s).to receive(:connectable?).and_return(true) diff --git a/spec/mongo/server_spec.rb b/spec/mongo/server_spec.rb index 392b40175a..ff296afa29 100644 --- a/spec/mongo/server_spec.rb +++ b/spec/mongo/server_spec.rb @@ -2,8 +2,12 @@ describe Mongo::Server do + let(:topology) do + double('topology') + end + let(:cluster) do - double('cluster') + double('cluster', topology: topology) end let(:listeners) do @@ -11,7 +15,7 @@ end let(:monitoring) do - Mongo::Monitoring.new + Mongo::Monitoring.new(monitoring: false) end let(:address) do diff --git a/spec/mongo/socket/unix_spec.rb b/spec/mongo/socket/unix_spec.rb index d3b72e49c0..8d9b511f20 100644 --- a/spec/mongo/socket/unix_spec.rb +++ b/spec/mongo/socket/unix_spec.rb @@ -16,7 +16,7 @@ socket.close end - it 'connects to the server' do + pending 'connects to the server' do expect(socket).to be_alive end end @@ -33,7 +33,7 @@ socket.close end - it 'returns true' do + pending 'returns true' do expect(socket).to be_alive end end @@ -44,7 +44,7 @@ socket.close end - it 'raises error' do + pending 'raises error' do expect { socket.alive? }.to raise_error(IOError) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8f6dffdcc1..d32cfa3f64 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,7 @@ COVERAGE_MIN = 90 CURRENT_PATH = File.expand_path(File.dirname(__FILE__)) SERVER_DISCOVERY_TESTS = Dir.glob("#{CURRENT_PATH}/support/sdam/**/*.yml") +SDAM_MONITORING_TESTS = Dir.glob("#{CURRENT_PATH}/support/sdam_monitoring/**/*.yml") SERVER_SELECTION_RTT_TESTS = Dir.glob("#{CURRENT_PATH}/support/server_selection/rtt/*.yml") SERVER_SELECTION_TESTS = Dir.glob("#{CURRENT_PATH}/support/server_selection/selection/**/*.yml") CRUD_TESTS = Dir.glob("#{CURRENT_PATH}/support/crud_tests/**/*.yml") @@ -26,6 +27,7 @@ require 'support/server_discovery_and_monitoring' require 'support/server_selection_rtt' require 'support/server_selection' +require 'support/sdam_monitoring' require 'support/crud' require 'support/command_monitoring' require 'support/connection_string' diff --git a/spec/support/sdam_monitoring.rb b/spec/support/sdam_monitoring.rb new file mode 100644 index 0000000000..27fa739973 --- /dev/null +++ b/spec/support/sdam_monitoring.rb @@ -0,0 +1,144 @@ +# Copyright (C) 2014-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +RSpec::Matchers.define :match_topology_opening_event do |expectation| + + match do |event| + event.topology != nil + end +end + +RSpec::Matchers.define :match_topology_description_changed_event do |expectation| + include Mongo::SDAMMonitoring::Matchable + + match do |event| + topologies_match?(event, expectation) + end +end + +RSpec::Matchers.define :match_server_opening_event do |expectation| + + match do |event| + true + end +end + +RSpec::Matchers.define :match_server_description_changed_event do |expectation| + include Mongo::SDAMMonitoring::Matchable + + match do |event| + descriptions_match?(event, expectation) + end +end + +RSpec::Matchers.define :match_server_closed_event do |expectation| + + match do |event| + event.address.to_s == expectation.data['address'] + end +end + +RSpec::Matchers.define :match_sdam_monitoring_event do |expectation| + + match do |event| + expect(event).to send("match_#{expectation.name}", expectation) + end +end + +module Mongo + module SDAMMonitoring + module Matchable + + def descriptions_match?(event, expectation) + description_matches?(event.previous_description, expectation.data['previousDescription']) && + description_matches?(event.new_description, expectation.data['newDescription']) + end + + def topologies_match?(event, expectation) + topology_matches?(event.previous_topology, expectation.data['previousDescription']) && + topology_matches?(event.new_topology, expectation.data['newDescription']) + end + + def description_matches?(actual, expected) + case expected['type'] + when 'Standalone' then actual.standalone? + when 'RSPrimary' then actual.primary? + when 'RSSecondary' then actual.secondary? + when 'RSArbiter' then actual.arbiter? + when 'Mongos' then actual.mongos? + when 'Unknown' then actual.unknown? + when 'PossiblePrimary' then actual.unknown? + when 'RSGhost' then actual.ghost? + when 'RSOther' then actual.other? + end + end + + def topology_matches?(actual, expected) + case expected['topologyType'] + when 'ReplicaSetWithPrimary' then actual.replica_set? + when 'ReplicaSetNoPrimary' then actual.replica_set? + when 'Sharded' then actual.sharded? + when 'Single' then actual.single? + when 'Unknown' then actual.unknown? + end + end + end + + # Test subscriber for SDAM monitoring. + # + # @since 2.3.0 + class TestSubscriber + + # The mappings of event names to types. + # + # @since 2.3.0 + MAPPINGS = { + 'topology_opening_event' => Mongo::Monitoring::Event::TopologyOpening, + 'topology_description_changed_event' => Mongo::Monitoring::Event::TopologyChanged, + 'server_opening_event' => Mongo::Monitoring::Event::ServerOpening, + 'server_description_changed_event' => Mongo::Monitoring::Event::ServerDescriptionChanged, + 'server_closed_event' => Mongo::Monitoring::Event::ServerClosed + }.freeze + + # Implement the succeeded event. + # + # @param [ Event ] event The event. + # + # @since 2.3.0 + def succeeded(event) + events.push(event) + end + + # Get the first event fired for the name, and then delete it. + # + # @param [ String ] name The event name. + # + # @return [ Event ] The matching event. + def first_event(name) + matching = events.find do |event| + event.class == MAPPINGS[name] + end + events.delete(events.find_index(matching)) + matching + end + + private + + def events + @events ||= [] + end + end + end +end diff --git a/spec/support/sdam_monitoring/replica_set_with_removal.yml b/spec/support/sdam_monitoring/replica_set_with_removal.yml new file mode 100644 index 0000000000..203902c4ea --- /dev/null +++ b/spec/support/sdam_monitoring/replica_set_with_removal.yml @@ -0,0 +1,85 @@ +description: "Monitoring a replica set with non member" +uri: "mongodb://a,b/" +phases: + - + responses: + - + - "a:27017" + - { + ok: 1, + ismaster: true, + setName: "rs", + setVersion: 1.0, + primary: "a:27017", + hosts: [ "a:27017" ], + minWireVersion: 0, + maxWireVersion: 4 + } + - + - "b:27017" + - { ok: 1, ismaster: true } + outcome: + events: + - + topology_opening_event: + topologyId: "42" + - + server_opening_event: + topologyId: "42" + address: "a:27017" + - + server_opening_event: + topologyId: "42" + address: "b:27017" + - + server_description_changed_event: + topologyId: "42" + address: "a:27017" + previousDescription: + address: "a:27017" + arbiters: [] + hosts: [] + passives: [] + type: "Unknown" + newDescription: + address: "a:27017" + arbiters: [] + hosts: [ "a:27017" ] + passives: [] + primary: "a:27017" + setName: "rs" + type: "RSPrimary" + - + server_closed_event: + topologyId: "42" + address: "b:27017" + - + topology_description_changed_event: + topologyId: "42" + previousDescription: + topologyType: "Unknown" + servers: + - + address: "a:27017" + arbiters: [] + hosts: [] + passives: [] + type: "Unknown" + - + address: "b:27017" + arbiters: [] + hosts: [] + passives: [] + type: "Unknown" + newDescription: + topologyType: "ReplicaSetWithPrimary" + setName: "rs" + servers: + - + address: "a:27017" + arbiters: [] + hosts: [ "a:27017" ] + passives: [] + primary: "a:27017" + setName: "rs" + type: "RSPrimary" diff --git a/spec/support/sdam_monitoring/standalone.yml b/spec/support/sdam_monitoring/standalone.yml new file mode 100644 index 0000000000..a9bd9d2f68 --- /dev/null +++ b/spec/support/sdam_monitoring/standalone.yml @@ -0,0 +1,55 @@ +description: "Monitoring a standalone connection" +uri: "mongodb://a:27017" +phases: + - + responses: + - + - "a:27017" + - { ok: 1, ismaster: true, minWireVersion: 0, maxWireVersion: 4 } + + outcome: + events: + - + topology_opening_event: + topologyId: "42" + - + server_opening_event: + topologyId: "42" + address: "a:27017" + - + server_description_changed_event: + topologyId: "42" + address: "a:27017" + previousDescription: + address: "a:27017" + arbiters: [] + hosts: [] + passives: [] + type: "Unknown" + newDescription: + address: "a:27017" + arbiters: [] + hosts: [] + passives: [] + type: "Standalone" + - + topology_description_changed_event: + topologyId: "42" + previousDescription: + topologyType: "Unknown" + servers: + - + address: "a:27017" + arbiters: [] + hosts: [] + passives: [] + type: "Unknown" + newDescription: + topologyType: "Single" + servers: + - + address: "a:27017" + arbiters: [] + hosts: [] + passives: [] + type: "Standalone" diff --git a/spec/support/server_discovery_and_monitoring.rb b/spec/support/server_discovery_and_monitoring.rb index d4c84ae159..c2c75ebc68 100644 --- a/spec/support/server_discovery_and_monitoring.rb +++ b/spec/support/server_discovery_and_monitoring.rb @@ -150,6 +150,9 @@ def initialize(response, uri) # @since 2.0.0 class Outcome + # @return [ Array ] events The expected events. + attr_reader :events + # @return [ Hash ] servers The expecations for # server states. attr_reader :servers @@ -169,18 +172,48 @@ class Outcome # # @since 2.0.0 def initialize(outcome) - @servers = process_servers(outcome['servers']) + @servers = process_servers(outcome['servers']) if outcome['servers'] @set_name = outcome['setName'] @topology_type = outcome['topologyType'] + @events = process_events(outcome['events']) if outcome['events'] end private + def process_events(events) + events.map do |event| + Event.new(event.keys.first, event.values.first) + end + end + def process_servers(servers) servers.each do |s| SDAM::convert_election_ids([ s[1] ]) end end end + + class Event + + MAPPINGS = { + 'server_closed_event' => Mongo::Monitoring::Event::ServerClosed, + 'server_description_changed_event' => Mongo::Monitoring::Event::ServerDescriptionChanged, + 'server_opening_event' => Mongo::Monitoring::Event::ServerOpening, + 'topology_description_changed_event' => Mongo::Monitoring::Event::TopologyChanged, + 'topology_opening_event' => Mongo::Monitoring::Event::TopologyOpening + } + + attr_reader :name + attr_reader :data + + def initialize(name, data) + @name = name + @data = data + end + + def expected + MAPPINGS.fetch(name) + end + end end end