From 08b7cddc814fff8cf48684c05481e5d6389f8538 Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Fri, 10 Dec 2010 16:00:35 -0500 Subject: [PATCH 1/8] Initial ReplSetConnection checking. Refactoring. --- lib/mongo.rb | 2 + lib/mongo/connection.rb | 195 +++++--------------------- lib/mongo/repl_set_connection.rb | 191 +++++++++++++++++++++++++ lib/mongo/util/uri_parser.rb | 71 ++++++++++ test/test_helper.rb | 2 +- test/unit/connection_test.rb | 88 +----------- test/unit/repl_set_connection_test.rb | 181 ++++++++++++++++++++++++ 7 files changed, 484 insertions(+), 246 deletions(-) create mode 100644 lib/mongo/repl_set_connection.rb create mode 100644 lib/mongo/util/uri_parser.rb create mode 100644 test/unit/repl_set_connection_test.rb diff --git a/lib/mongo.rb b/lib/mongo.rb index 754c424..b76ee23 100644 --- a/lib/mongo.rb +++ b/lib/mongo.rb @@ -42,9 +42,11 @@ require 'mongo/util/support' require 'mongo/util/core_ext' require 'mongo/util/pool' require 'mongo/util/server_version' +require 'mongo/util/uri_parser' require 'mongo/collection' require 'mongo/connection' +require 'mongo/repl_set_connection' require 'mongo/cursor' require 'mongo/db' require 'mongo/exceptions' diff --git a/lib/mongo/connection.rb b/lib/mongo/connection.rb index e71ce86..bb5cacb 100644 --- a/lib/mongo/connection.rb +++ b/lib/mongo/connection.rb @@ -39,7 +39,7 @@ module Mongo MONGODB_URI_SPEC = "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/database]" attr_reader :logger, :size, :nodes, :auths, :primary, :secondaries, :arbiters, - :safe, :primary_pool, :read_pool, :secondary_pools + :safe, :primary_pool, :read_pool, :secondary_pools, :host_to_try # Counter for generating unique request ids. @@current_request_id = 0 @@ -92,20 +92,13 @@ module Mongo # # @core connections def initialize(host=nil, port=nil, options={}) - @auths = [] + @auths = options.fetch(:auths, []) - if block_given? - @nodes = yield self - else - @nodes = format_pair(host, port) - end + @host_to_try = format_pair(host, port) # Host and port of current master. @host = @port = nil - # Replica set name - @replica_set_name = options[:rs_name] - # Lock for request ids. @id_lock = Mutex.new @@ -129,25 +122,19 @@ module Mongo # slave_ok can be true only if one node is specified @slave_ok = options[:slave_ok] - # Cache the various node types - # when connecting to a replica set. @primary = nil - @secondaries = [] - @arbiters = [] # Connection pool for primay node @primary_pool = nil - # Connection pools for each secondary node - @secondary_pools = [] - @read_pool = nil - @logger = options[:logger] || nil should_connect = options.fetch(:connect, true) connect if should_connect end + # DEPRECATED + # # Initialize a connection to a MongoDB replica set using an array of seed nodes. # # The seed nodes specified will be used on the initial connection to the replica set, but note @@ -170,6 +157,8 @@ module Mongo # :read_secondary => true) # # @return [Mongo::Connection] + # + # @deprecated def self.multi(nodes, opts={}) unless nodes.length > 0 && nodes.all? {|n| n.is_a? Array} raise MongoArgumentError, "Connection.multi requires at least one node to be specified." @@ -195,8 +184,13 @@ module Mongo # # @return [Mongo::Connection] def self.from_uri(uri, opts={}) - new(nil, nil, opts) do |con| - con.parse_uri(uri) + nodes, auths = Mongo::URIParser.parse(uri) + opts.merge!({:auths => auths}) + if nodes.length == 1 + Connection.new(nodes[0][0], nodes[0][1], opts) + elsif nodes.length > 1 + nodes << opts + ReplSetConnection.new(*nodes) end end @@ -480,21 +474,12 @@ module Mongo # @raise [ConnectionFailure] if unable to connect to any host or port. def connect reset_connection - @nodes_to_try = @nodes.clone - while connecting? - node = @nodes_to_try.shift - config = check_is_master(node) - - if is_primary?(config) - set_primary(node) - else - set_auxillary(node, config) - end + config = check_is_master(@host_to_try) + if is_primary?(config) + set_primary(@host_to_try) end - pick_secondary_for_read if @read_secondary - raise ConnectionFailure, "failed to connect to any given host:port" unless connected? end @@ -513,23 +498,21 @@ module Mongo def close @primary_pool.close if @primary_pool @primary_pool = nil - @read_pool = nil - @secondary_pools.each do |pool| - pool.close - end end ## Configuration helper methods - # Returns an array of host-port pairs. + # Returns a host-port pair. + # + # @return [Array] # # @private - def format_pair(pair_or_host, port) - case pair_or_host + def format_pair(host, port) + case host when String - [[pair_or_host, port ? port.to_i : DEFAULT_PORT]] + [host, port ? port.to_i : DEFAULT_PORT] when nil - [['localhost', DEFAULT_PORT]] + ['localhost', DEFAULT_PORT] end end @@ -550,50 +533,7 @@ module Mongo end end - # Parse a MongoDB URI. This method is used by Connection.from_uri. - # Returns an array of nodes and an array of db authorizations, if applicable. - # - # @private - def parse_uri(string) - if string =~ /^mongodb:\/\// - string = string[10..-1] - else - raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}" - end - - nodes = [] - auths = [] - specs = string.split(',') - specs.each do |spec| - matches = MONGODB_URI_MATCHER.match(spec) - if !matches - raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}" - end - - uname = matches[2] - pwd = matches[3] - host = matches[4] - port = matches[6] || DEFAULT_PORT - if !(port.to_s =~ /^\d+$/) - raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits." - end - port = port.to_i - db = matches[8] - - if uname && pwd && db - add_auth(db, uname, pwd) - elsif uname || pwd || db - raise MongoArgumentError, "MongoDB URI must include all three of username, password, " + - "and db if any one of these is specified." - end - - nodes << [host, port] - end - - nodes - end - - # Checkout a socket for reading (i.e., a secondary node). + # Checkout a socket for reading (i.e., a secondary node). def checkout_reader connect unless connected? @@ -629,23 +569,12 @@ module Mongo private - # Pick a node randomly from the set of possible secondaries. - def pick_secondary_for_read - if (size = @secondary_pools.size) > 0 - @read_pool = @secondary_pools[rand(size)] - end - end - # If a ConnectionFailure is raised, this method will be called # to close the connection and reset connection values. + # TODO: evaluate whether this method is actually necessary def reset_connection close @primary = nil - @secondaries = [] - @secondary_pools = [] - @arbiters = [] - @nodes_tried = [] - @nodes_to_try = [] end # Primary is defined as either a master node or a slave if @@ -653,6 +582,7 @@ module Mongo # # If a primary node is discovered, we set the the @host and @port and # apply any saved authentication. + # TODO: simplify def is_primary?(config) config && (config['ismaster'] == 1 || config['ismaster'] == true) || !@replica_set && @slave_ok end @@ -665,18 +595,17 @@ module Mongo config = self['admin'].command({:ismaster => 1}, :sock => socket) - check_set_name(config, socket) rescue OperationFailure, SocketError, SystemCallError, IOError => ex - close unless connected? + close# unless connected? ensure - @nodes_tried << node - if config - update_node_list(config['hosts']) if config['hosts'] + # @nodes_tried << node + # if config + # update_node_list(config['hosts']) if config['hosts'] - if config['msg'] && @logger - @logger.warn("MONGODB #{config['msg']}") - end - end + # if config['msg'] && @logger + # @logger.warn("MONGODB #{config['msg']}") + # end + # end socket.close if socket end @@ -684,21 +613,6 @@ module Mongo config end - # Make sure that we're connected to the expected replica set. - def check_set_name(config, socket) - if @replica_set_name - config = self['admin'].command({:replSetGetStatus => 1}, - :sock => socket, :check_response => false) - - if !Mongo::Support.ok?(config) - raise ReplicaSetConnectionError, config['errmsg'] - elsif config['set'] != @replica_set_name - raise ReplicaSetConnectionError, - "Attempting to connect to replica set '#{config['set']}' but expected '#{@replica_set_name}'" - end - end - end - # Set the specified node as primary, and # apply any saved authentication credentials. def set_primary(node) @@ -708,45 +622,6 @@ module Mongo apply_saved_authentication end - # Determines what kind of node we have and caches its host - # and port so that users can easily connect manually. - def set_auxillary(node, config) - if config - if config['secondary'] - host, port = *node - @secondaries << node unless @secondaries.include?(node) - @secondary_pools << Pool.new(self, host, port, :size => @pool_size, :timeout => @timeout) - elsif config['arbiterOnly'] - @arbiters << node unless @arbiters.include?(node) - end - end - end - - # Update the list of known nodes. Only applies to replica sets, - # where the response to the ismaster command will return a list - # of known hosts. - # - # @param hosts [Array] a list of hosts, specified as string-encoded - # host-port values. Example: ["myserver-1.org:27017", "myserver-1.org:27017"] - # - # @return [Array] the updated list of nodes - def update_node_list(hosts) - new_nodes = hosts.map do |host| - if !host.respond_to?(:split) - warn "Could not parse host #{host.inspect}." - next - end - - host, port = host.split(':') - [host, port.to_i] - end - - # Replace the list of seed nodes with the canonical list. - @nodes = new_nodes.clone - - @nodes_to_try = new_nodes - @nodes_tried - end - def receive(sock) receive_and_discard_header(sock) number_received, cursor_id = receive_response_header(sock) diff --git a/lib/mongo/repl_set_connection.rb b/lib/mongo/repl_set_connection.rb new file mode 100644 index 0000000..d07558d --- /dev/null +++ b/lib/mongo/repl_set_connection.rb @@ -0,0 +1,191 @@ +# encoding: UTF-8 + +# -- +# Copyright (C) 2008-2010 10gen 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 + + # Instantiates and manages connections to MongoDB. + class ReplSetConnection + attr_reader :nodes, :secondaries, :arbiters, :read_pool, :secondary_pools + + def initialize(*args) + + if args.last.is_a?(Hash) + options = args.pop + end + + @nodes = args + + # Replica set name + @replica_set_name = options[:rs_name] + + # Cache the various node types when connecting to a replica set. + @secondaries = [] + @arbiters = [] + + # Connection pools for each secondary node + @secondary_pools = [] + @read_pool = nil + + super + end + + # Create a new socket and attempt to connect to master. + # If successful, sets host and port to master and returns the socket. + # + # If connecting to a replica set, this method will replace the + # initially-provided seed list with any nodes known to the set. + # + # @raise [ConnectionFailure] if unable to connect to any host or port. + def connect + reset_connection + @nodes_to_try = @nodes.clone + + while connecting? + node = @nodes_to_try.shift + config = check_is_master(node) + + if is_primary?(config) + set_primary(node) + else + set_auxillary(node, config) + end + end + + pick_secondary_for_read if @read_secondary + + raise ConnectionFailure, "failed to connect to any given host:port" unless connected? + end + + def connecting? + @nodes_to_try.length > 0 + end + + # Close the connection to the database. + def close + super + @read_pool = nil + @secondary_pools.each do |pool| + pool.close + end + end + + # If a ConnectionFailure is raised, this method will be called + # to close the connection and reset connection values. + def reset_connection + super + @secondaries = [] + @secondary_pools = [] + @arbiters = [] + @nodes_tried = [] + @nodes_to_try = [] + end + + private + + def check_is_master(node) + begin + host, port = *node + socket = TCPSocket.new(host, port) + socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) + + config = self['admin'].command({:ismaster => 1}, :sock => socket) + + check_set_name(config, socket) + rescue OperationFailure, SocketError, SystemCallError, IOError => ex + close unless connected? + ensure + @nodes_tried << node + if config + update_node_list(config['hosts']) if config['hosts'] + + if config['msg'] && @logger + @logger.warn("MONGODB #{config['msg']}") + end + end + + socket.close if socket + end + + config + end + + + + # Pick a node randomly from the set of possible secondaries. + def pick_secondary_for_read + if (size = @secondary_pools.size) > 0 + @read_pool = @secondary_pools[rand(size)] + end + end + + # Make sure that we're connected to the expected replica set. + def check_set_name(config, socket) + if @replica_set_name + config = self['admin'].command({:replSetGetStatus => 1}, + :sock => socket, :check_response => false) + + if !Mongo::Support.ok?(config) + raise ReplicaSetConnectionError, config['errmsg'] + elsif config['set'] != @replica_set_name + raise ReplicaSetConnectionError, + "Attempting to connect to replica set '#{config['set']}' but expected '#{@replica_set_name}'" + end + end + end + + # Determines what kind of node we have and caches its host + # and port so that users can easily connect manually. + def set_auxillary(node, config) + if config + if config['secondary'] + host, port = *node + @secondaries << node unless @secondaries.include?(node) + @secondary_pools << Pool.new(self, host, port, :size => @pool_size, :timeout => @timeout) + elsif config['arbiterOnly'] + @arbiters << node unless @arbiters.include?(node) + end + end + end + + # Update the list of known nodes. Only applies to replica sets, + # where the response to the ismaster command will return a list + # of known hosts. + # + # @param hosts [Array] a list of hosts, specified as string-encoded + # host-port values. Example: ["myserver-1.org:27017", "myserver-1.org:27017"] + # + # @return [Array] the updated list of nodes + def update_node_list(hosts) + new_nodes = hosts.map do |host| + if !host.respond_to?(:split) + warn "Could not parse host #{host.inspect}." + next + end + + host, port = host.split(':') + [host, port.to_i] + end + + # Replace the list of seed nodes with the canonical list. + @nodes = new_nodes.clone + + @nodes_to_try = new_nodes - @nodes_tried + end + + end +end diff --git a/lib/mongo/util/uri_parser.rb b/lib/mongo/util/uri_parser.rb new file mode 100644 index 0000000..e413beb --- /dev/null +++ b/lib/mongo/util/uri_parser.rb @@ -0,0 +1,71 @@ +# encoding: UTF-8 + +# -- +# Copyright (C) 2008-2010 10gen 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 + module URIParser + + DEFAULT_PORT = 27017 + MONGODB_URI_MATCHER = /(([-_.\w\d]+):([-_\w\d]+)@)?([-.\w\d]+)(:([\w\d]+))?(\/([-\d\w]+))?/ + MONGODB_URI_SPEC = "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/database]" + + extend self + + # Parse a MongoDB URI. This method is used by Connection.from_uri. + # Returns an array of nodes and an array of db authorizations, if applicable. + # + # @private + def parse(string) + if string =~ /^mongodb:\/\// + string = string[10..-1] + else + raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}" + end + + nodes = [] + auths = [] + specs = string.split(',') + specs.each do |spec| + matches = MONGODB_URI_MATCHER.match(spec) + if !matches + raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}" + end + + uname = matches[2] + pwd = matches[3] + host = matches[4] + port = matches[6] || DEFAULT_PORT + if !(port.to_s =~ /^\d+$/) + raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits." + end + port = port.to_i + db = matches[8] + + if uname && pwd && db + auths << {'db_name' => db, 'username' => uname, 'password' => pwd} + elsif uname || pwd || db + raise MongoArgumentError, "MongoDB URI must include all three of username, password, " + + "and db if any one of these is specified." + end + + nodes << [host, port] + end + + [nodes, auths] + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 0e13370..56d2888 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -26,7 +26,7 @@ unless defined? MONGO_TEST_DB end unless defined? TEST_PORT - TEST_PORT = ENV['MONGO_RUBY_DRIVER_PORT'].to_i || Connection::DEFAULT_PORT + TEST_PORT = ENV['MONGO_RUBY_DRIVER_PORT'] ? ENV['MONGO_RUBY_DRIVER_PORT'].to_i : Mongo::Connection::DEFAULT_PORT end unless defined? TEST_HOST diff --git a/test/unit/connection_test.rb b/test/unit/connection_test.rb index 4edc2b0..d0d4627 100644 --- a/test/unit/connection_test.rb +++ b/test/unit/connection_test.rb @@ -41,103 +41,21 @@ class ConnectionTest < Test::Unit::TestCase end end - context "connecting to a replica set" do - setup do - TCPSocket.stubs(:new).returns(new_mock_socket('localhost', 27017)) - @conn = Connection.multi([['localhost', 27017]], :connect => false, :read_secondary => true) - - admin_db = new_mock_db - @hosts = ['localhost:27018', 'localhost:27019', 'localhost:27020'] - - admin_db.stubs(:command).returns({'ok' => 1, 'ismaster' => 1, 'hosts' => @hosts}). - then.returns({'ok' => 1, 'ismaster' => 0, 'hosts' => @hosts, 'secondary' => 1}). - then.returns({'ok' => 1, 'ismaster' => 0, 'hosts' => @hosts, 'secondary' => 1}). - then.returns({'ok' => 1, 'ismaster' => 0, 'arbiterOnly' => 1}) - - @conn.stubs(:[]).with('admin').returns(admin_db) - @conn.connect - end - - should "store the hosts returned from the ismaster command" do - assert_equal 'localhost', @conn.primary_pool.host - assert_equal 27017, @conn.primary_pool.port - - assert_equal 'localhost', @conn.secondary_pools[0].host - assert_equal 27018, @conn.secondary_pools[0].port - - assert_equal 'localhost', @conn.secondary_pools[1].host - assert_equal 27019, @conn.secondary_pools[1].port - - assert_equal 2, @conn.secondary_pools.length - end - end - - context "connecting to a replica set and providing seed nodes" do - setup do - TCPSocket.stubs(:new).returns(new_mock_socket) - @conn = Connection.multi([['localhost', 27017], ['localhost', 27019]], :connect => false) - - admin_db = new_mock_db - @hosts = ['localhost:27017', 'localhost:27018', 'localhost:27019'] - admin_db.stubs(:command).returns({'ok' => 1, 'ismaster' => 1, 'hosts' => @hosts}) - @conn.stubs(:[]).with('admin').returns(admin_db) - @conn.connect - end - - should "not store any hosts redundantly" do - end - end - - context "initializing a paired connection" do - should "require left and right nodes" do - assert_raise MongoArgumentError do - Connection.multi(['localhost', 27018], :connect => false) - end - - assert_raise MongoArgumentError do - Connection.multi(['localhost', 27018], :connect => false) - end - end - - should "store both nodes" do - @conn = Connection.multi([['localhost', 27017], ['localhost', 27018]], :connect => false) - - assert_equal ['localhost', 27017], @conn.nodes[0] - assert_equal ['localhost', 27018], @conn.nodes[1] - end - end - context "initializing with a mongodb uri" do should "parse a simple uri" do @conn = Connection.from_uri("mongodb://localhost", :connect => false) - assert_equal ['localhost', 27017], @conn.nodes[0] + assert_equal ['localhost', 27017], @conn.host_to_try end should "allow a complex host names" do host_name = "foo.bar-12345.org" @conn = Connection.from_uri("mongodb://#{host_name}", :connect => false) - assert_equal [host_name, 27017], @conn.nodes[0] - end - - should "parse a uri specifying multiple nodes" do - @conn = Connection.from_uri("mongodb://localhost:27017,mydb.com:27018", :connect => false) - assert_equal ['localhost', 27017], @conn.nodes[0] - assert_equal ['mydb.com', 27018], @conn.nodes[1] - end - - should "parse a uri specifying multiple nodes with auth" do - @conn = Connection.from_uri("mongodb://kyle:s3cr3t@localhost:27017/app,mickey:m0u5e@mydb.com:27018/dsny", :connect => false) - assert_equal ['localhost', 27017], @conn.nodes[0] - assert_equal ['mydb.com', 27018], @conn.nodes[1] - auth_hash = {'username' => 'kyle', 'password' => 's3cr3t', 'db_name' => 'app'} - assert_equal auth_hash, @conn.auths[0] - auth_hash = {'username' => 'mickey', 'password' => 'm0u5e', 'db_name' => 'dsny'} - assert_equal auth_hash, @conn.auths[1] + assert_equal [host_name, 27017], @conn.host_to_try end should "parse a uri with a hyphen & underscore in the username or password" do @conn = Connection.from_uri("mongodb://hyphen-user_name:p-s_s@localhost:27017/db", :connect => false) - assert_equal ['localhost', 27017], @conn.nodes[0] + assert_equal ['localhost', 27017], @conn.host_to_try auth_hash = { 'db_name' => 'db', 'username' => 'hyphen-user_name', "password" => 'p-s_s' } assert_equal auth_hash, @conn.auths[0] end diff --git a/test/unit/repl_set_connection_test.rb b/test/unit/repl_set_connection_test.rb new file mode 100644 index 0000000..1fe14ce --- /dev/null +++ b/test/unit/repl_set_connection_test.rb @@ -0,0 +1,181 @@ +require './test/test_helper' +include Mongo + +#class ConnectionTest < Test::Unit::TestCase +# context "Initialization: " do +# setup do +# def new_mock_socket(host='localhost', port=27017) +# socket = Object.new +# socket.stubs(:setsockopt).with(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) +# socket.stubs(:close) +# socket +# end +# +# def new_mock_db +# db = Object.new +# end +# end +# +# context "given a single node" do +# setup do +# @conn = Connection.new('localhost', 27017, :connect => false) +# TCPSocket.stubs(:new).returns(new_mock_socket) +# +# admin_db = new_mock_db +# admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}) +# @conn.expects(:[]).with('admin').returns(admin_db) +# @conn.connect +# end +# +# should "set localhost and port to master" do +# assert_equal 'localhost', @conn.primary_pool.host +# assert_equal 27017, @conn.primary_pool.port +# end +# +# should "set connection pool to 1" do +# assert_equal 1, @conn.primary_pool.size +# end +# +# should "default slave_ok to false" do +# assert !@conn.slave_ok? +# end +# end +# +# context "connecting to a replica set" do +# setup do +# TCPSocket.stubs(:new).returns(new_mock_socket('localhost', 27017)) +# @conn = Connection.multi([['localhost', 27017]], :connect => false, :read_secondary => true) +# +# admin_db = new_mock_db +# @hosts = ['localhost:27018', 'localhost:27019', 'localhost:27020'] +# +# admin_db.stubs(:command).returns({'ok' => 1, 'ismaster' => 1, 'hosts' => @hosts}). +# then.returns({'ok' => 1, 'ismaster' => 0, 'hosts' => @hosts, 'secondary' => 1}). +# then.returns({'ok' => 1, 'ismaster' => 0, 'hosts' => @hosts, 'secondary' => 1}). +# then.returns({'ok' => 1, 'ismaster' => 0, 'arbiterOnly' => 1}) +# +# @conn.stubs(:[]).with('admin').returns(admin_db) +# @conn.connect +# end +# +# should "store the hosts returned from the ismaster command" do +# assert_equal 'localhost', @conn.primary_pool.host +# assert_equal 27017, @conn.primary_pool.port +# +# assert_equal 'localhost', @conn.secondary_pools[0].host +# assert_equal 27018, @conn.secondary_pools[0].port +# +# assert_equal 'localhost', @conn.secondary_pools[1].host +# assert_equal 27019, @conn.secondary_pools[1].port +# +# assert_equal 2, @conn.secondary_pools.length +# end +# end +# +# context "connecting to a replica set and providing seed nodes" do +# setup do +# TCPSocket.stubs(:new).returns(new_mock_socket) +# @conn = Connection.multi([['localhost', 27017], ['localhost', 27019]], :connect => false) +# +# admin_db = new_mock_db +# @hosts = ['localhost:27017', 'localhost:27018', 'localhost:27019'] +# admin_db.stubs(:command).returns({'ok' => 1, 'ismaster' => 1, 'hosts' => @hosts}) +# @conn.stubs(:[]).with('admin').returns(admin_db) +# @conn.connect +# end +# +# should "not store any hosts redundantly" do +# end +# end +# +# context "initializing a paired connection" do +# should "require left and right nodes" do +# assert_raise MongoArgumentError do +# Connection.multi(['localhost', 27018], :connect => false) +# end +# +# assert_raise MongoArgumentError do +# Connection.multi(['localhost', 27018], :connect => false) +# end +# end +# +# should "store both nodes" do +# @conn = Connection.multi([['localhost', 27017], ['localhost', 27018]], :connect => false) +# +# assert_equal ['localhost', 27017], @conn.nodes[0] +# assert_equal ['localhost', 27018], @conn.nodes[1] +# end +# end +# +# context "initializing with a mongodb uri" do +# should "parse a simple uri" do +# @conn = Connection.from_uri("mongodb://localhost", :connect => false) +# assert_equal ['localhost', 27017], @conn.primary +# end +# +# should "allow a complex host names" do +# host_name = "foo.bar-12345.org" +# @conn = Connection.from_uri("mongodb://#{host_name}", :connect => false) +# assert_equal [host_name, 27017], @conn.primary +# end +# +# should "parse a uri specifying multiple nodes" do +# #@conn = Connection.from_uri("mongodb://localhost:27017,mydb.com:27018", :connect => false) +# #assert_equal ['localhost', 27017], @conn.nodes[0] +# #assert_equal ['mydb.com', 27018], @conn.nodes[1] +# end +# +# should "parse a uri specifying multiple nodes with auth" do +# #@conn = Connection.from_uri("mongodb://kyle:s3cr3t@localhost:27017/app,mickey:m0u5e@mydb.com:27018/dsny", :connect => false) +# #assert_equal ['localhost', 27017], @conn.nodes[0] +# #assert_equal ['mydb.com', 27018], @conn.nodes[1] +# #auth_hash = {'username' => 'kyle', 'password' => 's3cr3t', 'db_name' => 'app'} +# #assert_equal auth_hash, @conn.auths[0] +# #auth_hash = {'username' => 'mickey', 'password' => 'm0u5e', 'db_name' => 'dsny'} +# #assert_equal auth_hash, @conn.auths[1] +# end +# +# should "parse a uri with a hyphen & underscore in the username or password" do +# @conn = Connection.from_uri("mongodb://hyphen-user_name:p-s_s@localhost:27017/db", :connect => false) +# assert_equal ['localhost', 27017], @conn.nodes[0] +# auth_hash = { 'db_name' => 'db', 'username' => 'hyphen-user_name', "password" => 'p-s_s' } +# assert_equal auth_hash, @conn.auths[0] +# end +# +# should "attempt to connect" do +# TCPSocket.stubs(:new).returns(new_mock_socket) +# @conn = Connection.from_uri("mongodb://localhost", :connect => false) +# +# admin_db = new_mock_db +# admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}) +# @conn.expects(:[]).with('admin').returns(admin_db) +# @conn.expects(:apply_saved_authentication) +# @conn.connect +# end +# +# should "raise an error on invalid uris" do +# assert_raise MongoArgumentError do +# Connection.from_uri("mongo://localhost", :connect => false) +# end +# +# assert_raise MongoArgumentError do +# Connection.from_uri("mongodb://localhost:abc", :connect => false) +# end +# +# assert_raise MongoArgumentError do +# Connection.from_uri("mongodb://localhost:27017, my.db.com:27018, ", :connect => false) +# end +# end +# +# should "require all of username, password, and database if any one is specified" do +# assert_raise MongoArgumentError do +# Connection.from_uri("mongodb://localhost/db", :connect => false) +# end +# +# assert_raise MongoArgumentError do +# Connection.from_uri("mongodb://kyle:password@localhost", :connect => false) +# end +# end +# end +# end +#end From 27b410f869ac1913d2f67aad0624b712738c6bd9 Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Mon, 13 Dec 2010 14:07:32 -0500 Subject: [PATCH 2/8] ReplSetConnection updates --- lib/mongo/connection.rb | 159 ++++++------- lib/mongo/repl_set_connection.rb | 37 +++- test/replica_sets/connect_test.rb | 19 +- test/replica_sets/count_test.rb | 3 +- test/replica_sets/insert_test.rb | 3 +- test/replica_sets/node_type_test.rb | 3 +- test/replica_sets/pooled_insert_test.rb | 4 +- test/replica_sets/query_secondaries.rb | 2 +- test/replica_sets/query_test.rb | 3 +- test/replica_sets/replication_ack_test.rb | 7 +- test/unit/repl_set_connection_test.rb | 257 +++++++--------------- 11 files changed, 212 insertions(+), 285 deletions(-) diff --git a/lib/mongo/connection.rb b/lib/mongo/connection.rb index bb5cacb..26c755e 100644 --- a/lib/mongo/connection.rb +++ b/lib/mongo/connection.rb @@ -92,47 +92,18 @@ module Mongo # # @core connections def initialize(host=nil, port=nil, options={}) - @auths = options.fetch(:auths, []) - @host_to_try = format_pair(host, port) # Host and port of current master. @host = @port = nil - # Lock for request ids. - @id_lock = Mutex.new - - # Pool size and timeout. - @pool_size = options[:pool_size] || 1 - @timeout = options[:timeout] || 5.0 - - # Mutex for synchronizing pool access - @connection_mutex = Mutex.new - - # Global safe option. This is false by default. - @safe = options[:safe] || false - - # Create a mutex when a new key, in this case a socket, - # is added to the hash. - @safe_mutexes = Hash.new { |h, k| h[k] = Mutex.new } - - # Condition variable for signal and wait - @queue = ConditionVariable.new - # slave_ok can be true only if one node is specified @slave_ok = options[:slave_ok] - @primary = nil - - # Connection pool for primay node - @primary_pool = nil - - @logger = options[:logger] || nil - - should_connect = options.fetch(:connect, true) - connect if should_connect + setup(options) end + # DEPRECATED # # Initialize a connection to a MongoDB replica set using an array of seed nodes. @@ -160,19 +131,10 @@ module Mongo # # @deprecated def self.multi(nodes, opts={}) - unless nodes.length > 0 && nodes.all? {|n| n.is_a? Array} - raise MongoArgumentError, "Connection.multi requires at least one node to be specified." - end + warn "Connection.multi is now deprecated. Please use ReplSetConnection.new instead." - # Block returns an array, the first element being an array of nodes and the second an array - # of authorizations for the database. - new(nil, nil, opts) do |con| - nodes.map do |node| - con.instance_variable_set(:@replica_set, true) - con.instance_variable_set(:@read_secondary, true) if opts[:read_secondary] - con.pair_val_to_connection(node) - end - end + nodes << opts + ReplSetConnection.new(*nodes) end # Initialize a connection to MongoDB using the MongoDB URI spec: @@ -191,6 +153,8 @@ module Mongo elsif nodes.length > 1 nodes << opts ReplSetConnection.new(*nodes) + else + raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node." end end @@ -500,40 +464,8 @@ module Mongo @primary_pool = nil end - ## Configuration helper methods - - # Returns a host-port pair. - # - # @return [Array] - # - # @private - def format_pair(host, port) - case host - when String - [host, port ? port.to_i : DEFAULT_PORT] - when nil - ['localhost', DEFAULT_PORT] - end - end - - # Convert an argument containing a host name string and a - # port number integer into a [host, port] pair array. - # - # @private - def pair_val_to_connection(a) - case a - when nil - ['localhost', DEFAULT_PORT] - when String - [a, DEFAULT_PORT] - when Integer - ['localhost', a] - when Array - a - end - end - - # Checkout a socket for reading (i.e., a secondary node). + + # Checkout a socket for reading (i.e., a secondary node). def checkout_reader connect unless connected? @@ -567,6 +499,77 @@ module Mongo end end + protected + + # Generic initialization code. + # @protected + def setup(options) + # Authentication objects + @auths = options.fetch(:auths, []) + + # Lock for request ids. + @id_lock = Mutex.new + + # Pool size and timeout. + @pool_size = options[:pool_size] || 1 + @timeout = options[:timeout] || 5.0 + + # Mutex for synchronizing pool access + @connection_mutex = Mutex.new + + # Global safe option. This is false by default. + @safe = options[:safe] || false + + # Create a mutex when a new key, in this case a socket, + # is added to the hash. + @safe_mutexes = Hash.new { |h, k| h[k] = Mutex.new } + + # Condition variable for signal and wait + @queue = ConditionVariable.new + + # Connection pool for primay node + @primary = nil + @primary_pool = nil + + @logger = options[:logger] || nil + + should_connect = options.fetch(:connect, true) + connect if should_connect + end + + ## Configuration helper methods + + # Returns a host-port pair. + # + # @return [Array] + # + # @private + def format_pair(host, port) + case host + when String + [host, port ? port.to_i : DEFAULT_PORT] + when nil + ['localhost', DEFAULT_PORT] + end + end + + # Convert an argument containing a host name string and a + # port number integer into a [host, port] pair array. + # + # @private + def pair_val_to_connection(a) + case a + when nil + ['localhost', DEFAULT_PORT] + when String + [a, DEFAULT_PORT] + when Integer + ['localhost', a] + when Array + a + end + end + private # If a ConnectionFailure is raised, this method will be called @@ -584,7 +587,7 @@ module Mongo # apply any saved authentication. # TODO: simplify def is_primary?(config) - config && (config['ismaster'] == 1 || config['ismaster'] == true) || !@replica_set && @slave_ok + config && (config['ismaster'] == 1 || config['ismaster'] == true) || @slave_ok end def check_is_master(node) diff --git a/lib/mongo/repl_set_connection.rb b/lib/mongo/repl_set_connection.rb index d07558d..5b8ca35 100644 --- a/lib/mongo/repl_set_connection.rb +++ b/lib/mongo/repl_set_connection.rb @@ -19,19 +19,25 @@ module Mongo # Instantiates and manages connections to MongoDB. - class ReplSetConnection + class ReplSetConnection < Connection attr_reader :nodes, :secondaries, :arbiters, :read_pool, :secondary_pools def initialize(*args) - if args.last.is_a?(Hash) - options = args.pop + opts = args.pop + else + opts = {} end + unless args.length > 0 + raise MongoArgumentError, "A ReplSetConnection requires at least one node." + end + + # Get seed nodes @nodes = args # Replica set name - @replica_set_name = options[:rs_name] + @replica_set = opts[:rs_name] # Cache the various node types when connecting to a replica set. @secondaries = [] @@ -41,7 +47,10 @@ module Mongo @secondary_pools = [] @read_pool = nil - super + # Are we allowing reads from secondaries? + @read_secondary = opts.fetch(:read_secondary, false) + + setup(opts) end # Create a new socket and attempt to connect to master. @@ -86,13 +95,14 @@ module Mongo # If a ConnectionFailure is raised, this method will be called # to close the connection and reset connection values. + # TODO: what's the point of this method? def reset_connection super @secondaries = [] @secondary_pools = [] @arbiters = [] - @nodes_tried = [] - @nodes_to_try = [] + @nodes_tried = [] + @nodes_to_try = [] end private @@ -124,7 +134,12 @@ module Mongo config end - + # Primary, when connecting to a replica can, can only be a true primary node. + # (And not a slave, which is possible when connecting with the standard + # Connection class. + def is_primary?(config) + config && (config['ismaster'] == 1 || config['ismaster'] == true) + end # Pick a node randomly from the set of possible secondaries. def pick_secondary_for_read @@ -135,15 +150,15 @@ module Mongo # Make sure that we're connected to the expected replica set. def check_set_name(config, socket) - if @replica_set_name + if @replica_set config = self['admin'].command({:replSetGetStatus => 1}, :sock => socket, :check_response => false) if !Mongo::Support.ok?(config) raise ReplicaSetConnectionError, config['errmsg'] - elsif config['set'] != @replica_set_name + elsif config['set'] != @replica_set raise ReplicaSetConnectionError, - "Attempting to connect to replica set '#{config['set']}' but expected '#{@replica_set_name}'" + "Attempting to connect to replica set '#{config['set']}' but expected '#{@replica_set}'" end end end diff --git a/test/replica_sets/connect_test.rb b/test/replica_sets/connect_test.rb index df229d9..8b7cf3c 100644 --- a/test/replica_sets/connect_test.rb +++ b/test/replica_sets/connect_test.rb @@ -9,15 +9,15 @@ class ConnectTest < Test::Unit::TestCase include Mongo def test_connect_bad_name - assert_raise_error(ReplicaSetConnectionError, "expected 'wrong-repl-set-name'") do - Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]], - :rs_name => "wrong-repl-set-name") + assert_raise_error(ReplicaSetReplSetConnectionError, "expected 'wrong-repl-set-name'") do + ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2], :rs_name => "wrong-repl-set-name") end end def test_connect - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]], - :name => "foo") + @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2], :name => "foo") assert @conn.connected? end @@ -25,7 +25,8 @@ class ConnectTest < Test::Unit::TestCase puts "Please kill the node at #{TEST_PORT}." gets - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2]]) assert @conn.connected? end @@ -33,7 +34,8 @@ class ConnectTest < Test::Unit::TestCase puts "Please kill the node at #{TEST_PORT + 1}." gets - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2]]) assert @conn.connected? end @@ -41,7 +43,8 @@ class ConnectTest < Test::Unit::TestCase puts "Please kill the node at #{TEST_PORT + 2}." gets - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2]]) assert @conn.connected? end end diff --git a/test/replica_sets/count_test.rb b/test/replica_sets/count_test.rb index 8587876..b96e82e 100644 --- a/test/replica_sets/count_test.rb +++ b/test/replica_sets/count_test.rb @@ -9,7 +9,8 @@ class ReplicaSetCountTest < Test::Unit::TestCase include Mongo def setup - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2]) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets") diff --git a/test/replica_sets/insert_test.rb b/test/replica_sets/insert_test.rb index baa4fd6..f015719 100644 --- a/test/replica_sets/insert_test.rb +++ b/test/replica_sets/insert_test.rb @@ -9,7 +9,8 @@ class ReplicaSetInsertTest < Test::Unit::TestCase include Mongo def setup - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2]]) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets") diff --git a/test/replica_sets/node_type_test.rb b/test/replica_sets/node_type_test.rb index 6c72988..248fe22 100644 --- a/test/replica_sets/node_type_test.rb +++ b/test/replica_sets/node_type_test.rb @@ -9,7 +9,8 @@ class ReplicaSetNodeTypeTest < Test::Unit::TestCase include Mongo def setup - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2]) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets") diff --git a/test/replica_sets/pooled_insert_test.rb b/test/replica_sets/pooled_insert_test.rb index 8ad0dee..cdacb39 100644 --- a/test/replica_sets/pooled_insert_test.rb +++ b/test/replica_sets/pooled_insert_test.rb @@ -9,8 +9,8 @@ class ReplicaSetPooledInsertTest < Test::Unit::TestCase include Mongo def setup - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]], - :pool_size => 10, :timeout => 5) + @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2], :pool_size => 10, :timeout => 5) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets") diff --git a/test/replica_sets/query_secondaries.rb b/test/replica_sets/query_secondaries.rb index 9ccba77..ce7e1a9 100644 --- a/test/replica_sets/query_secondaries.rb +++ b/test/replica_sets/query_secondaries.rb @@ -9,7 +9,7 @@ class ReplicaSetQuerySecondariesTest < Test::Unit::TestCase include Mongo def setup - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT]], :read_secondary => true) + @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], :read_secondary => true) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets", :safe => {:w => 2, :wtimeout => 100}) diff --git a/test/replica_sets/query_test.rb b/test/replica_sets/query_test.rb index d0f4105..371f3b8 100644 --- a/test/replica_sets/query_test.rb +++ b/test/replica_sets/query_test.rb @@ -9,7 +9,8 @@ class ReplicaSetQueryTest < Test::Unit::TestCase include Mongo def setup - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2]) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets") diff --git a/test/replica_sets/replication_ack_test.rb b/test/replica_sets/replication_ack_test.rb index aa15bd1..78e30f7 100644 --- a/test/replica_sets/replication_ack_test.rb +++ b/test/replica_sets/replication_ack_test.rb @@ -8,11 +8,13 @@ class ReplicaSetAckTest < Test::Unit::TestCase include Mongo def setup - @conn = Mongo::Connection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], + [TEST_HOST, TEST_PORT + 2]) master = [@conn.primary_pool.host, @conn.primary_pool.port] - @slave1 = Mongo::Connection.new(@conn.secondary_pools[0].host, @conn.secondary_pools[0].port, :slave_ok => true) + @slave1 = Connection.new(@conn.secondary_pools[0].host, + @conn.secondary_pools[0].port, :slave_ok => true) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @@ -37,7 +39,6 @@ class ReplicaSetAckTest < Test::Unit::TestCase assert @col.insert({:foo => "0" * 10000}, :safe => {:w => 2, :wtimeout => 1000}) assert_equal 2, @slave1[MONGO_TEST_DB]["test-sets"].count - assert @col.update({:baz => "bar"}, {:baz => "foo"}, :safe => {:w => 2, :wtimeout => 1000}) assert @slave1[MONGO_TEST_DB]["test-sets"].find_one({:baz => "foo"}) diff --git a/test/unit/repl_set_connection_test.rb b/test/unit/repl_set_connection_test.rb index 1fe14ce..ebf8759 100644 --- a/test/unit/repl_set_connection_test.rb +++ b/test/unit/repl_set_connection_test.rb @@ -1,181 +1,82 @@ require './test/test_helper' include Mongo -#class ConnectionTest < Test::Unit::TestCase -# context "Initialization: " do -# setup do -# def new_mock_socket(host='localhost', port=27017) -# socket = Object.new -# socket.stubs(:setsockopt).with(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) -# socket.stubs(:close) -# socket -# end -# -# def new_mock_db -# db = Object.new -# end -# end -# -# context "given a single node" do -# setup do -# @conn = Connection.new('localhost', 27017, :connect => false) -# TCPSocket.stubs(:new).returns(new_mock_socket) -# -# admin_db = new_mock_db -# admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}) -# @conn.expects(:[]).with('admin').returns(admin_db) -# @conn.connect -# end -# -# should "set localhost and port to master" do -# assert_equal 'localhost', @conn.primary_pool.host -# assert_equal 27017, @conn.primary_pool.port -# end -# -# should "set connection pool to 1" do -# assert_equal 1, @conn.primary_pool.size -# end -# -# should "default slave_ok to false" do -# assert !@conn.slave_ok? -# end -# end -# -# context "connecting to a replica set" do -# setup do -# TCPSocket.stubs(:new).returns(new_mock_socket('localhost', 27017)) -# @conn = Connection.multi([['localhost', 27017]], :connect => false, :read_secondary => true) -# -# admin_db = new_mock_db -# @hosts = ['localhost:27018', 'localhost:27019', 'localhost:27020'] -# -# admin_db.stubs(:command).returns({'ok' => 1, 'ismaster' => 1, 'hosts' => @hosts}). -# then.returns({'ok' => 1, 'ismaster' => 0, 'hosts' => @hosts, 'secondary' => 1}). -# then.returns({'ok' => 1, 'ismaster' => 0, 'hosts' => @hosts, 'secondary' => 1}). -# then.returns({'ok' => 1, 'ismaster' => 0, 'arbiterOnly' => 1}) -# -# @conn.stubs(:[]).with('admin').returns(admin_db) -# @conn.connect -# end -# -# should "store the hosts returned from the ismaster command" do -# assert_equal 'localhost', @conn.primary_pool.host -# assert_equal 27017, @conn.primary_pool.port -# -# assert_equal 'localhost', @conn.secondary_pools[0].host -# assert_equal 27018, @conn.secondary_pools[0].port -# -# assert_equal 'localhost', @conn.secondary_pools[1].host -# assert_equal 27019, @conn.secondary_pools[1].port -# -# assert_equal 2, @conn.secondary_pools.length -# end -# end -# -# context "connecting to a replica set and providing seed nodes" do -# setup do -# TCPSocket.stubs(:new).returns(new_mock_socket) -# @conn = Connection.multi([['localhost', 27017], ['localhost', 27019]], :connect => false) -# -# admin_db = new_mock_db -# @hosts = ['localhost:27017', 'localhost:27018', 'localhost:27019'] -# admin_db.stubs(:command).returns({'ok' => 1, 'ismaster' => 1, 'hosts' => @hosts}) -# @conn.stubs(:[]).with('admin').returns(admin_db) -# @conn.connect -# end -# -# should "not store any hosts redundantly" do -# end -# end -# -# context "initializing a paired connection" do -# should "require left and right nodes" do -# assert_raise MongoArgumentError do -# Connection.multi(['localhost', 27018], :connect => false) -# end -# -# assert_raise MongoArgumentError do -# Connection.multi(['localhost', 27018], :connect => false) -# end -# end -# -# should "store both nodes" do -# @conn = Connection.multi([['localhost', 27017], ['localhost', 27018]], :connect => false) -# -# assert_equal ['localhost', 27017], @conn.nodes[0] -# assert_equal ['localhost', 27018], @conn.nodes[1] -# end -# end -# -# context "initializing with a mongodb uri" do -# should "parse a simple uri" do -# @conn = Connection.from_uri("mongodb://localhost", :connect => false) -# assert_equal ['localhost', 27017], @conn.primary -# end -# -# should "allow a complex host names" do -# host_name = "foo.bar-12345.org" -# @conn = Connection.from_uri("mongodb://#{host_name}", :connect => false) -# assert_equal [host_name, 27017], @conn.primary -# end -# -# should "parse a uri specifying multiple nodes" do -# #@conn = Connection.from_uri("mongodb://localhost:27017,mydb.com:27018", :connect => false) -# #assert_equal ['localhost', 27017], @conn.nodes[0] -# #assert_equal ['mydb.com', 27018], @conn.nodes[1] -# end -# -# should "parse a uri specifying multiple nodes with auth" do -# #@conn = Connection.from_uri("mongodb://kyle:s3cr3t@localhost:27017/app,mickey:m0u5e@mydb.com:27018/dsny", :connect => false) -# #assert_equal ['localhost', 27017], @conn.nodes[0] -# #assert_equal ['mydb.com', 27018], @conn.nodes[1] -# #auth_hash = {'username' => 'kyle', 'password' => 's3cr3t', 'db_name' => 'app'} -# #assert_equal auth_hash, @conn.auths[0] -# #auth_hash = {'username' => 'mickey', 'password' => 'm0u5e', 'db_name' => 'dsny'} -# #assert_equal auth_hash, @conn.auths[1] -# end -# -# should "parse a uri with a hyphen & underscore in the username or password" do -# @conn = Connection.from_uri("mongodb://hyphen-user_name:p-s_s@localhost:27017/db", :connect => false) -# assert_equal ['localhost', 27017], @conn.nodes[0] -# auth_hash = { 'db_name' => 'db', 'username' => 'hyphen-user_name', "password" => 'p-s_s' } -# assert_equal auth_hash, @conn.auths[0] -# end -# -# should "attempt to connect" do -# TCPSocket.stubs(:new).returns(new_mock_socket) -# @conn = Connection.from_uri("mongodb://localhost", :connect => false) -# -# admin_db = new_mock_db -# admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}) -# @conn.expects(:[]).with('admin').returns(admin_db) -# @conn.expects(:apply_saved_authentication) -# @conn.connect -# end -# -# should "raise an error on invalid uris" do -# assert_raise MongoArgumentError do -# Connection.from_uri("mongo://localhost", :connect => false) -# end -# -# assert_raise MongoArgumentError do -# Connection.from_uri("mongodb://localhost:abc", :connect => false) -# end -# -# assert_raise MongoArgumentError do -# Connection.from_uri("mongodb://localhost:27017, my.db.com:27018, ", :connect => false) -# end -# end -# -# should "require all of username, password, and database if any one is specified" do -# assert_raise MongoArgumentError do -# Connection.from_uri("mongodb://localhost/db", :connect => false) -# end -# -# assert_raise MongoArgumentError do -# Connection.from_uri("mongodb://kyle:password@localhost", :connect => false) -# end -# end -# end -# end -#end +class ReplSetConnectionTest < Test::Unit::TestCase + context "Initialization: " do + setup do + def new_mock_socket(host='localhost', port=27017) + socket = Object.new + socket.stubs(:setsockopt).with(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) + socket.stubs(:close) + socket + end + + def new_mock_db + db = Object.new + end + end + + context "connecting to a replica set" do + setup do + TCPSocket.stubs(:new).returns(new_mock_socket('localhost', 27017)) + @conn = ReplSetConnection.new(['localhost', 27017], :connect => false, :read_secondary => true) + + admin_db = new_mock_db + @hosts = ['localhost:27018', 'localhost:27019', 'localhost:27020'] + + admin_db.stubs(:command).returns({'ok' => 1, 'ismaster' => 1, 'hosts' => @hosts}). + then.returns({'ok' => 1, 'ismaster' => 0, 'hosts' => @hosts, 'secondary' => 1}). + then.returns({'ok' => 1, 'ismaster' => 0, 'hosts' => @hosts, 'secondary' => 1}). + then.returns({'ok' => 1, 'ismaster' => 0, 'arbiterOnly' => 1}) + + @conn.stubs(:[]).with('admin').returns(admin_db) + @conn.connect + end + + should "store the hosts returned from the ismaster command" do + assert_equal 'localhost', @conn.primary_pool.host + assert_equal 27017, @conn.primary_pool.port + + assert_equal 'localhost', @conn.secondary_pools[0].host + assert_equal 27018, @conn.secondary_pools[0].port + + assert_equal 'localhost', @conn.secondary_pools[1].host + assert_equal 27019, @conn.secondary_pools[1].port + + assert_equal 2, @conn.secondary_pools.length + end + end + + context "connecting to a replica set and providing seed nodes" do + setup do + TCPSocket.stubs(:new).returns(new_mock_socket) + @conn = ReplSetConnection.new(['localhost', 27017], ['localhost', 27019], :connect => false) + + admin_db = new_mock_db + @hosts = ['localhost:27017', 'localhost:27018', 'localhost:27019'] + admin_db.stubs(:command).returns({'ok' => 1, 'ismaster' => 1, 'hosts' => @hosts}) + @conn.stubs(:[]).with('admin').returns(admin_db) + @conn.connect + end + end + + context "initializing with a mongodb uri" do + + should "parse a uri specifying multiple nodes" do + @conn = Connection.from_uri("mongodb://localhost:27017,mydb.com:27018", :connect => false) + assert_equal ['localhost', 27017], @conn.nodes[0] + assert_equal ['mydb.com', 27018], @conn.nodes[1] + end + + should "parse a uri specifying multiple nodes with auth" do + @conn = Connection.from_uri("mongodb://kyle:s3cr3t@localhost:27017/app,mickey:m0u5e@mydb.com:27018/dsny", :connect => false) + assert_equal ['localhost', 27017], @conn.nodes[0] + assert_equal ['mydb.com', 27018], @conn.nodes[1] + auth_hash = {'username' => 'kyle', 'password' => 's3cr3t', 'db_name' => 'app'} + assert_equal auth_hash, @conn.auths[0] + auth_hash = {'username' => 'mickey', 'password' => 'm0u5e', 'db_name' => 'dsny'} + assert_equal auth_hash, @conn.auths[1] + end + end + end +end From d69b0df717a02474ef8923cf7f2f4800c467637a Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Mon, 13 Dec 2010 15:22:51 -0500 Subject: [PATCH 3/8] Initial replica set manager commit --- test/tools/repl_set_manager.rb | 126 +++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 test/tools/repl_set_manager.rb diff --git a/test/tools/repl_set_manager.rb b/test/tools/repl_set_manager.rb new file mode 100644 index 0000000..64f24bb --- /dev/null +++ b/test/tools/repl_set_manager.rb @@ -0,0 +1,126 @@ +#!/usr/bin/ruby + +require 'rubygems' +require 'mongo' + +class ReplSetManager + + def initialize(opts={}) + @start_port = opts[:start_port] || 30000 + @name = opts[:name] || 'replica-set-foo' + @count = opts[:count] || 3 + @host = opts[:host] || 'localhost' + @retries = opts[:retries] || 60 + @config = {"_id" => @name, "members" => []} + @path = File.join(File.expand_path(File.dirname(__FILE__)), "data") + + @mongods = {} + end + + def start_set + puts "Starting a replica set with #{@count} nodes" + + system("killall mongod") + @count.times do |n| + @mongods[n] ||= {} + @mongods[n]['db_path'] = get_path("rs-#{n}") + @mongods[n]['log_path'] = get_path("log-#{n}") + system("rm -rf #{@mongods[n]['db_path']}") + system("mkdir -p #{@mongods[n]['db_path']}") + + @mongods[n]['port'] = @start_port + n + @mongods[n]['start'] = "mongod --replSet #{@name} --logpath '#{@mongods[n]['log_path']}' " + + " --dbpath #{@mongods[n]['db_path']} --port #{@mongods[n]['port']} --fork" + + start(n) + + member = {'_id' => n, 'host' => "#{@host}:#{@mongods[n]['port']}"} + if n == @count-1 + @mongods[n]['arbiter'] = true + member['arbiterOnly'] = true + end + + @config['members'] << member + end + + p @mongods + init + ensure_up + end + + def kill(node) + system("kill -2 #{@mongods[node]['pid']}") + @mongods[node]['up'] = false + sleep(1) + end + + def start(node) + system(@mongods[node]['start']) + @mongods[node]['up'] = true + sleep(1) + @mongods[node]['pid'] = File.open(File.join(@mongods[node]['db_path'], 'mongod.lock')).read.strip + end + alias :restart :start + + def ensure_up + @con = get_connection + p @con + + attempt(Mongo::OperationFailure) do + status = @con['admin'].command({'replSetGetStatus' => 1}) + p status + if status['members'].all? { |m| [1, 2, 7].include?(m['state']) } + puts "All members up!" + return + else + raise Mongo::OperationFailure + end + end + end + + private + + def init + get_connection + + attempt(Mongo::OperationFailure) do + @con['admin'].command({'replSetInitiate' => @config}) + end + end + + def get_connection + attempt(Mongo::ConnectionFailure) do + node = @mongods.keys.detect {|key| !@mongods[key]['arbiter'] && @mongods[key]['up'] } + p @mongods[node]['port'] + p node + @con = Mongo::Connection.new(@host, @mongods[node]['port'], :slave_ok => true) + end + + return @con + end + + def get_path(name) + p @path + j = File.join(@path, name) + p j + j + end + + def attempt(exception) + raise "No block given!" unless block_given? + count = 0 + + while count < @retries do + begin + yield + return + rescue exception + sleep(1) + count += 1 + end + end + + raise exception + end + +end From 0a47b76fca87e0ed30509b837c919953a8fc6014 Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Mon, 13 Dec 2010 16:25:23 -0500 Subject: [PATCH 4/8] Toward automated rs tests --- test/replica_sets/count_test.rb | 13 +++++--- test/test_helper.rb | 16 +-------- test/tools/repl_set_manager.rb | 58 ++++++++++++++++++++++++--------- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/test/replica_sets/count_test.rb b/test/replica_sets/count_test.rb index b96e82e..7ad2e41 100644 --- a/test/replica_sets/count_test.rb +++ b/test/replica_sets/count_test.rb @@ -1,7 +1,7 @@ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'mongo' require 'test/unit' -require './test/test_helper' +require './test/replica_sets/rs_test_helper' # NOTE: This test expects a replica set of three nodes to be running # on the local host. @@ -9,19 +9,22 @@ class ReplicaSetCountTest < Test::Unit::TestCase include Mongo def setup - @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2]) + @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], [RS.host, RS.ports[2]]) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets") end + def teardown + RS.start(@node) + end + def test_correct_count_after_insertion_reconnect @coll.insert({:a => 20})#, :safe => {:w => 3, :wtimeout => 10000}) assert_equal 1, @coll.count - puts "Please disconnect the current master." - gets + # Disconnecting the current master node + @node = RS.kill_primary rescue_connection_failure do @coll.insert({:a => 30}, :safe => true) diff --git a/test/test_helper.rb b/test/test_helper.rb index 56d2888..395ce04 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -69,21 +69,7 @@ class Test::Unit::TestCase self.class.mongo_port end - # Generic code for rescuing connection failures and retrying operations. - # This could be combined with some timeout functionality. - def rescue_connection_failure - success = false - while !success - begin - yield - success = true - rescue Mongo::ConnectionFailure - puts "Rescuing" - sleep(1) - end - end - end - + def assert_raise_error(klass, message) begin yield diff --git a/test/tools/repl_set_manager.rb b/test/tools/repl_set_manager.rb index 64f24bb..ed7599f 100644 --- a/test/tools/repl_set_manager.rb +++ b/test/tools/repl_set_manager.rb @@ -1,12 +1,18 @@ #!/usr/bin/ruby -require 'rubygems' -require 'mongo' +STDOUT.sync = true + +unless defined? Mongo + require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'mongo') +end class ReplSetManager + attr_accessor :host, :start_port, :ports + def initialize(opts={}) @start_port = opts[:start_port] || 30000 + @ports = [] @name = opts[:name] || 'replica-set-foo' @count = opts[:count] || 3 @host = opts[:host] || 'localhost' @@ -21,14 +27,17 @@ class ReplSetManager puts "Starting a replica set with #{@count} nodes" system("killall mongod") + @count.times do |n| @mongods[n] ||= {} - @mongods[n]['db_path'] = get_path("rs-#{n}") - @mongods[n]['log_path'] = get_path("log-#{n}") + port = @start_port + n + @ports << port + @mongods[n]['port'] = port + @mongods[n]['db_path'] = get_path("rs-#{port}") + @mongods[n]['log_path'] = get_path("log-#{port}") system("rm -rf #{@mongods[n]['db_path']}") system("mkdir -p #{@mongods[n]['db_path']}") - @mongods[n]['port'] = @start_port + n @mongods[n]['start'] = "mongod --replSet #{@name} --logpath '#{@mongods[n]['log_path']}' " + " --dbpath #{@mongods[n]['db_path']} --port #{@mongods[n]['port']} --fork" @@ -43,7 +52,6 @@ class ReplSetManager @config['members'] << member end - p @mongods init ensure_up end @@ -54,6 +62,18 @@ class ReplSetManager sleep(1) end + def kill_primary + node = get_node_with_state(1) + kill(node) + return node + end + + def kill_secondary + node = get_node_with_state(2) + kill(node) + return node + end + def start(node) system(@mongods[node]['start']) @mongods[node]['up'] = true @@ -63,15 +83,15 @@ class ReplSetManager alias :restart :start def ensure_up + print "Ensuring members are up..." @con = get_connection - p @con attempt(Mongo::OperationFailure) do status = @con['admin'].command({'replSetGetStatus' => 1}) - p status + print "." if status['members'].all? { |m| [1, 2, 7].include?(m['state']) } puts "All members up!" - return + return status else raise Mongo::OperationFailure end @@ -88,11 +108,22 @@ class ReplSetManager end end + def get_node_with_state(state) + status = ensure_up + node = status['members'].detect {|m| m['state'] == state} + if node + host_port = node['name'].split(':') + port = host_port[1] ? host_port[1].to_i : 27017 + key = @mongods.keys.detect {|key| @mongods[key]['port'] == port} + return key + else + return false + end + end + def get_connection attempt(Mongo::ConnectionFailure) do node = @mongods.keys.detect {|key| !@mongods[key]['arbiter'] && @mongods[key]['up'] } - p @mongods[node]['port'] - p node @con = Mongo::Connection.new(@host, @mongods[node]['port'], :slave_ok => true) end @@ -100,10 +131,7 @@ class ReplSetManager end def get_path(name) - p @path - j = File.join(@path, name) - p j - j + File.join(@path, name) end def attempt(exception) From 236d4a821fb49c3804d1da866fe6676093efb1ee Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Tue, 14 Dec 2010 13:14:45 -0500 Subject: [PATCH 5/8] More connection refactoring. Updates to repl_set_manager. --- Rakefile | 38 ++++--- lib/mongo/connection.rb | 17 +-- lib/mongo/repl_set_connection.rb | 14 ++- test/replica_sets/connect_test.rb | 74 ++++++++----- test/replica_sets/count_test.rb | 6 +- test/replica_sets/rs_test_helper.rb | 29 +++++ test/tools/repl_set_manager.rb | 161 +++++++++++++++++++++------- 7 files changed, 241 insertions(+), 98 deletions(-) create mode 100644 test/replica_sets/rs_test_helper.rb diff --git a/Rakefile b/Rakefile index c95e73d..7ec359d 100644 --- a/Rakefile +++ b/Rakefile @@ -38,9 +38,8 @@ end desc "Test the MongoDB Ruby driver." task :test do - puts "\nThis option has changed." - puts "\nTo test the driver with the c-extensions:\nrake test:c\n" - puts "To test the pure ruby driver: \nrake test:ruby" + puts "\nTo test the driver with the C-extensions:\nrake test:c\n\n" + puts "To test the pure ruby driver: \nrake test:ruby\n\n" end namespace :test do @@ -48,22 +47,35 @@ namespace :test do desc "Test the driver with the C extension enabled." task :c do ENV['C_EXT'] = 'TRUE' - Rake::Task['test:unit'].invoke - Rake::Task['test:functional'].invoke - Rake::Task['test:bson'].invoke - Rake::Task['test:pooled_threading'].invoke - Rake::Task['test:drop_databases'].invoke + if ENV['TEST'] + Rake::Task['test:functional'].invoke + else + Rake::Task['test:unit'].invoke + Rake::Task['test:functional'].invoke + Rake::Task['test:bson'].invoke + Rake::Task['test:pooled_threading'].invoke + Rake::Task['test:drop_databases'].invoke + end ENV['C_EXT'] = nil end desc "Test the driver using pure ruby (no C extension)" task :ruby do ENV['C_EXT'] = nil - Rake::Task['test:unit'].invoke - Rake::Task['test:functional'].invoke - Rake::Task['test:bson'].invoke - Rake::Task['test:pooled_threading'].invoke - Rake::Task['test:drop_databases'].invoke + if ENV['TEST'] + Rake::Task['test:functional'].invoke + else + Rake::Task['test:unit'].invoke + Rake::Task['test:functional'].invoke + Rake::Task['test:bson'].invoke + Rake::Task['test:pooled_threading'].invoke + Rake::Task['test:drop_databases'].invoke + end + end + + Rake::TestTask.new(:rs) do |t| + t.test_files = FileList['test/replica_sets/*_test.rb'] + t.verbose = true end Rake::TestTask.new(:unit) do |t| diff --git a/lib/mongo/connection.rb b/lib/mongo/connection.rb index 26c755e..7701185 100644 --- a/lib/mongo/connection.rb +++ b/lib/mongo/connection.rb @@ -35,9 +35,6 @@ module Mongo STANDARD_HEADER_SIZE = 16 RESPONSE_HEADER_SIZE = 20 - MONGODB_URI_MATCHER = /(([-_.\w\d]+):([-_\w\d]+)@)?([-.\w\d]+)(:([\w\d]+))?(\/([-\d\w]+))?/ - MONGODB_URI_SPEC = "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/database]" - attr_reader :logger, :size, :nodes, :auths, :primary, :secondaries, :arbiters, :safe, :primary_pool, :read_pool, :secondary_pools, :host_to_try @@ -103,7 +100,6 @@ module Mongo setup(options) end - # DEPRECATED # # Initialize a connection to a MongoDB replica set using an array of seed nodes. @@ -464,7 +460,6 @@ module Mongo @primary_pool = nil end - # Checkout a socket for reading (i.e., a secondary node). def checkout_reader connect unless connected? @@ -597,19 +592,9 @@ module Mongo socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) config = self['admin'].command({:ismaster => 1}, :sock => socket) - rescue OperationFailure, SocketError, SystemCallError, IOError => ex - close# unless connected? + close ensure - # @nodes_tried << node - # if config - # update_node_list(config['hosts']) if config['hosts'] - - # if config['msg'] && @logger - # @logger.warn("MONGODB #{config['msg']}") - # end - # end - socket.close if socket end diff --git a/lib/mongo/repl_set_connection.rb b/lib/mongo/repl_set_connection.rb index 5b8ca35..d443c45 100644 --- a/lib/mongo/repl_set_connection.rb +++ b/lib/mongo/repl_set_connection.rb @@ -77,7 +77,13 @@ module Mongo pick_secondary_for_read if @read_secondary - raise ConnectionFailure, "failed to connect to any given host:port" unless connected? + if !connected? + if @secondary_pools.empty? + raise ConnectionFailure, "Failed to connect any given host:port" + else + raise ConnectionFailure, "Failed to connect to primary node." + end + end end def connecting? @@ -121,7 +127,11 @@ module Mongo ensure @nodes_tried << node if config - update_node_list(config['hosts']) if config['hosts'] + nodes = [] + nodes += config['hosts'] if config['hosts'] + nodes += config['arbiters'] if config['arbiters'] + nodes += config['passives'] if config['passives'] + update_node_list(nodes) if config['msg'] && @logger @logger.warn("MONGODB #{config['msg']}") diff --git a/test/replica_sets/connect_test.rb b/test/replica_sets/connect_test.rb index 8b7cf3c..66a87fc 100644 --- a/test/replica_sets/connect_test.rb +++ b/test/replica_sets/connect_test.rb @@ -1,50 +1,74 @@ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -require 'mongo' -require 'test/unit' -require './test/test_helper' +require './test/replica_sets/rs_test_helper' -# NOTE: This test expects a replica set of three nodes to be running on TEST_HOST, -# on ports TEST_PORT, TEST_PORT + 1, and TEST + 2. +# NOTE: This test expects a replica set of three nodes to be running on RS.host, +# on ports TEST_PORT, RS.ports[1], and TEST + 2. class ConnectTest < Test::Unit::TestCase include Mongo + def setup + RS.restart_killed_nodes + end + def test_connect_bad_name - assert_raise_error(ReplicaSetReplSetConnectionError, "expected 'wrong-repl-set-name'") do - ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2], :rs_name => "wrong-repl-set-name") + assert_raise_error(ReplicaSetConnectionError, "-wrong") do + ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], + [RS.host, RS.ports[2]], :rs_name => RS.name + "-wrong") end end def test_connect - @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2], :name => "foo") + @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], + [RS.host, RS.ports[2]], :name => RS.name) + assert @conn.connected? + + assert_equal RS.primary, @conn.primary + assert_equal RS.secondaries, @conn.secondaries + assert_equal RS.arbiters, @conn.arbiters + end + + def test_connect_with_primary_node_killed + node = RS.kill_primary + + # Becuase we're killing the primary and trying to connect right away, + # this is going to fail right away. + assert_raise_error(ConnectionFailure, "Failed to connect to primary node") do + @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], + [RS.host, RS.ports[2]]) + end + + # This allows the secondary to come up as a primary + rescue_connection_failure do + @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], + [RS.host, RS.ports[2]]) + end assert @conn.connected? end - def test_connect_with_first_node_down - puts "Please kill the node at #{TEST_PORT}." - gets + def test_connect_with_secondary_node_killed + node = RS.kill_secondary - @conn = ReplSetConnection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], + [RS.host, RS.ports[2]]) assert @conn.connected? end - def test_connect_with_second_node_down - puts "Please kill the node at #{TEST_PORT + 1}." - gets + def test_connect_with_third_node_killed + RS.kill(RS.get_node_from_port(RS.ports[2])) - @conn = ReplSetConnection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], + [RS.host, RS.ports[2]]) assert @conn.connected? end - def test_connect_with_third_node_down - puts "Please kill the node at #{TEST_PORT + 2}." - gets + def test_connect_with_primary_stepped_down + RS.step_down_primary - @conn = ReplSetConnection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2]]) + rescue_connection_failure do + @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], + [RS.host, RS.ports[2]]) + end assert @conn.connected? end + end diff --git a/test/replica_sets/count_test.rb b/test/replica_sets/count_test.rb index 7ad2e41..1476a2f 100644 --- a/test/replica_sets/count_test.rb +++ b/test/replica_sets/count_test.rb @@ -1,6 +1,4 @@ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -require 'mongo' -require 'test/unit' require './test/replica_sets/rs_test_helper' # NOTE: This test expects a replica set of three nodes to be running @@ -20,10 +18,10 @@ class ReplicaSetCountTest < Test::Unit::TestCase end def test_correct_count_after_insertion_reconnect - @coll.insert({:a => 20})#, :safe => {:w => 3, :wtimeout => 10000}) + @coll.insert({:a => 20}, :safe => {:w => 2, :wtimeout => 10000}) assert_equal 1, @coll.count - # Disconnecting the current master node + # Kill the current master node @node = RS.kill_primary rescue_connection_failure do diff --git a/test/replica_sets/rs_test_helper.rb b/test/replica_sets/rs_test_helper.rb new file mode 100644 index 0000000..8fd02db --- /dev/null +++ b/test/replica_sets/rs_test_helper.rb @@ -0,0 +1,29 @@ +$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +require './test/test_helper' +require './test/tools/repl_set_manager' + +unless defined? RS + RS = ReplSetManager.new + RS.start_set +end + +class Test::Unit::TestCase + + # Generic code for rescuing connection failures and retrying operations. + # This could be combined with some timeout functionality. + def rescue_connection_failure(max_retries=60) + success = false + tries = 0 + while !success && tries < max_retries + begin + yield + success = true + rescue Mongo::ConnectionFailure + puts "Rescuing attempt #{tries}" + tries += 1 + sleep(1) + end + end + end + +end diff --git a/test/tools/repl_set_manager.rb b/test/tools/repl_set_manager.rb index ed7599f..cdb7a59 100644 --- a/test/tools/repl_set_manager.rb +++ b/test/tools/repl_set_manager.rb @@ -8,18 +8,27 @@ end class ReplSetManager - attr_accessor :host, :start_port, :ports + attr_accessor :host, :start_port, :ports, :name, :mongods def initialize(opts={}) @start_port = opts[:start_port] || 30000 @ports = [] @name = opts[:name] || 'replica-set-foo' - @count = opts[:count] || 3 @host = opts[:host] || 'localhost' @retries = opts[:retries] || 60 @config = {"_id" => @name, "members" => []} @path = File.join(File.expand_path(File.dirname(__FILE__)), "data") + @passive_count = opts[:secondary_count] || 1 + @arbiter_count = opts[:arbiter_count] || 1 + @secondary_count = opts[:passive_count] || 1 + @primary_count = 1 + + @count = @primary_count + @passive_count + @arbiter_count + @secondary_count + if @count > 7 + raise StandardError, "Cannot create a replica set with #{node_count} nodes. 7 is the max." + end + @mongods = {} end @@ -28,34 +37,57 @@ class ReplSetManager system("killall mongod") - @count.times do |n| - @mongods[n] ||= {} - port = @start_port + n - @ports << port - @mongods[n]['port'] = port - @mongods[n]['db_path'] = get_path("rs-#{port}") - @mongods[n]['log_path'] = get_path("log-#{port}") - system("rm -rf #{@mongods[n]['db_path']}") - system("mkdir -p #{@mongods[n]['db_path']}") - - @mongods[n]['start'] = "mongod --replSet #{@name} --logpath '#{@mongods[n]['log_path']}' " + - " --dbpath #{@mongods[n]['db_path']} --port #{@mongods[n]['port']} --fork" - - start(n) - - member = {'_id' => n, 'host' => "#{@host}:#{@mongods[n]['port']}"} - if n == @count-1 - @mongods[n]['arbiter'] = true - member['arbiterOnly'] = true - end - - @config['members'] << member + n = 0 + (@primary_count + @secondary_count).times do |n| + init_node(n) + n += 1 end - init + @passive_count.times do + init_node(n) do |attrs| + attrs['priority'] = 0 + end + n += 1 + end + + @arbiter_count.times do + init_node(n) do |attrs| + attrs['arbiterOnly'] = true + end + n += 1 + end + + initiate ensure_up end + def init_node(n) + @mongods[n] ||= {} + port = @start_port + n + @ports << port + @mongods[n]['port'] = port + @mongods[n]['db_path'] = get_path("rs-#{port}") + @mongods[n]['log_path'] = get_path("log-#{port}") + system("rm -rf #{@mongods[n]['db_path']}") + system("mkdir -p #{@mongods[n]['db_path']}") + + @mongods[n]['start'] = "mongod --replSet #{@name} --logpath '#{@mongods[n]['log_path']}' " + + " --dbpath #{@mongods[n]['db_path']} --port #{@mongods[n]['port']} --fork" + + start(n) + + member = {'_id' => n, 'host' => "#{@host}:#{@mongods[n]['port']}"} + + if block_given? + custom_attrs = {} + yield custom_attrs + member.merge!(custom_attrs) + @mongods[n].merge!(custom_attrs) + end + + @config['members'] << member + end + def kill(node) system("kill -2 #{@mongods[node]['pid']}") @mongods[node]['up'] = false @@ -68,12 +100,40 @@ class ReplSetManager return node end + # Note that we have to rescue a connection failure + # when we run the StepDown command because that + # command will close the connection. + def step_down_primary + primary = get_node_with_state(1) + con = get_connection(primary) + begin + con['admin'].command({'replSetStepDown' => 90}) + rescue Mongo::ConnectionFailure + end + end + def kill_secondary node = get_node_with_state(2) kill(node) return node end + def restart_killed_nodes + nodes = @mongods.keys.select do |key| + @mongods[key]['up'] == false + end + + nodes.each do |node| + start(node) + end + + ensure_up + end + + def get_node_from_port(port) + @mongods.keys.detect { |key| @mongods[key]['port'] == port } + end + def start(node) system(@mongods[node]['start']) @mongods[node]['up'] = true @@ -84,12 +144,13 @@ class ReplSetManager def ensure_up print "Ensuring members are up..." - @con = get_connection attempt(Mongo::OperationFailure) do - status = @con['admin'].command({'replSetGetStatus' => 1}) + con = get_connection + status = con['admin'].command({'replSetGetStatus' => 1}) print "." - if status['members'].all? { |m| [1, 2, 7].include?(m['state']) } + if status['members'].all? { |m| [1, 2, 7].include?(m['state']) } && + status['members'].any? { |m| m['state'] == 1 } puts "All members up!" return status else @@ -98,13 +159,26 @@ class ReplSetManager end end + def primary + nodes = get_all_host_pairs_with_state(1) + nodes.empty? ? nil : nodes[0] + end + + def secondaries + get_all_host_pairs_with_state(2) + end + + def arbiters + get_all_host_pairs_with_state(7) + end + private - def init - get_connection + def initiate + con = get_connection attempt(Mongo::OperationFailure) do - @con['admin'].command({'replSetInitiate' => @config}) + con['admin'].command({'replSetInitiate' => @config}) end end @@ -121,13 +195,25 @@ class ReplSetManager end end - def get_connection - attempt(Mongo::ConnectionFailure) do - node = @mongods.keys.detect {|key| !@mongods[key]['arbiter'] && @mongods[key]['up'] } - @con = Mongo::Connection.new(@host, @mongods[node]['port'], :slave_ok => true) + def get_all_host_pairs_with_state(state) + status = ensure_up + nodes = status['members'].select {|m| m['state'] == state} + nodes.map do |node| + host_port = node['name'].split(':') + port = host_port[1] ? host_port[1].to_i : 27017 + [host, port] + end + end + + def get_connection(node=nil) + con = attempt(Mongo::ConnectionFailure) do + if !node + node = @mongods.keys.detect {|key| !@mongods[key]['arbiterOnly'] && @mongods[key]['up'] } + end + con = Mongo::Connection.new(@host, @mongods[node]['port'], :slave_ok => true) end - return @con + return con end def get_path(name) @@ -140,8 +226,7 @@ class ReplSetManager while count < @retries do begin - yield - return + return yield rescue exception sleep(1) count += 1 From 34b6f023eb240abbc01608f4018cb1abe9101a01 Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Tue, 14 Dec 2010 15:47:18 -0500 Subject: [PATCH 6/8] Replica set automated tests --- Rakefile | 2 +- test/replica_sets/connect_test.rb | 4 ++-- test/replica_sets/count_test.rb | 2 +- test/replica_sets/insert_test.rb | 21 +++++++++++---------- test/replica_sets/node_type_test.rb | 6 ++---- test/replica_sets/pooled_insert_test.rb | 15 +++++++-------- test/replica_sets/rs_test_helper.rb | 2 +- test/tools/repl_set_manager.rb | 16 +++++++++------- 8 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Rakefile b/Rakefile index 7ec359d..898d0e9 100644 --- a/Rakefile +++ b/Rakefile @@ -74,7 +74,7 @@ namespace :test do end Rake::TestTask.new(:rs) do |t| - t.test_files = FileList['test/replica_sets/*_test.rb'] + t.test_files = ['test/replica_sets/count_test.rb', 'test/replica_sets/connect_test.rb', 'test/replica_sets/insert_test.rb'] t.verbose = true end diff --git a/test/replica_sets/connect_test.rb b/test/replica_sets/connect_test.rb index 66a87fc..b4237f0 100644 --- a/test/replica_sets/connect_test.rb +++ b/test/replica_sets/connect_test.rb @@ -23,8 +23,8 @@ class ConnectTest < Test::Unit::TestCase assert @conn.connected? assert_equal RS.primary, @conn.primary - assert_equal RS.secondaries, @conn.secondaries - assert_equal RS.arbiters, @conn.arbiters + assert_equal RS.secondaries.sort, @conn.secondaries.sort + assert_equal RS.arbiters.sort, @conn.arbiters.sort end def test_connect_with_primary_node_killed diff --git a/test/replica_sets/count_test.rb b/test/replica_sets/count_test.rb index 1476a2f..04c0422 100644 --- a/test/replica_sets/count_test.rb +++ b/test/replica_sets/count_test.rb @@ -14,7 +14,7 @@ class ReplicaSetCountTest < Test::Unit::TestCase end def teardown - RS.start(@node) + RS.restart_killed_nodes end def test_correct_count_after_insertion_reconnect diff --git a/test/replica_sets/insert_test.rb b/test/replica_sets/insert_test.rb index f015719..13f9525 100644 --- a/test/replica_sets/insert_test.rb +++ b/test/replica_sets/insert_test.rb @@ -1,7 +1,5 @@ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -require 'mongo' -require 'test/unit' -require './test/test_helper' +require './test/replica_sets/rs_test_helper' # NOTE: This test expects a replica set of three nodes to be running # on the local host. @@ -9,17 +7,20 @@ class ReplicaSetInsertTest < Test::Unit::TestCase include Mongo def setup - @conn = ReplSetConnection.multi([[TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2]]) + @conn = ReplSetConnection.new([TEST_HOST, RS.ports[0]], [TEST_HOST, RS.ports[1]], [TEST_HOST, RS.ports[2]]) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets") end + def teardown + RS.restart_killed_nodes + end + def test_insert @coll.save({:a => 20}, :safe => true) - puts "Please disconnect the current master." - gets + + RS.kill_primary rescue_connection_failure do @coll.save({:a => 30}, :safe => true) @@ -30,9 +31,9 @@ class ReplicaSetInsertTest < Test::Unit::TestCase @coll.save({:a => 60}, :safe => true) @coll.save({:a => 70}, :safe => true) - puts "Please reconnect the old master to make sure that the new master " + - "has synced with the previous master. Note: this may have happened already." - gets + # Restart the old master and wait for sync + RS.restart_killed_nodes + sleep(1) results = [] rescue_connection_failure do diff --git a/test/replica_sets/node_type_test.rb b/test/replica_sets/node_type_test.rb index 248fe22..4c46d0a 100644 --- a/test/replica_sets/node_type_test.rb +++ b/test/replica_sets/node_type_test.rb @@ -9,8 +9,7 @@ class ReplicaSetNodeTypeTest < Test::Unit::TestCase include Mongo def setup - @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2]) + @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], [RS.host, RS.ports[2]]) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets") @@ -26,8 +25,7 @@ class ReplicaSetNodeTypeTest < Test::Unit::TestCase old_secondary = @conn.secondaries.first old_primary = @conn.primary - puts "Please disconnect the current primary and reconnect so that it becomes secondary." - gets + RS.step_down_primary # Insert something to rescue the connection failure. rescue_connection_failure do diff --git a/test/replica_sets/pooled_insert_test.rb b/test/replica_sets/pooled_insert_test.rb index cdacb39..c8eb99f 100644 --- a/test/replica_sets/pooled_insert_test.rb +++ b/test/replica_sets/pooled_insert_test.rb @@ -9,8 +9,8 @@ class ReplicaSetPooledInsertTest < Test::Unit::TestCase include Mongo def setup - @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2], :pool_size => 10, :timeout => 5) + @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], + [RS.host, RS.ports[2]], :pool_size => 10, :timeout => 5) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets") @@ -19,6 +19,8 @@ class ReplicaSetPooledInsertTest < Test::Unit::TestCase def test_insert expected_results = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] @coll.save({:a => -1}, :safe => true) + + RS.kill_primary puts "Please disconnect the current master." gets @@ -31,12 +33,9 @@ class ReplicaSetPooledInsertTest < Test::Unit::TestCase end end - puts "Please reconnect the old master to make sure that the new master " + - "has synced with the previous master. Note: this may have happened already." + - "Note also that when connection with multiple threads, you may need to wait a few seconds" + - "after restarting the old master so that all the data has had a chance to sync." + - "This is a case of eventual consistency." - gets + # Restart the old master and wait for sync + RS.restart_killed_nodes + sleep(1) results = [] rescue_connection_failure do diff --git a/test/replica_sets/rs_test_helper.rb b/test/replica_sets/rs_test_helper.rb index 8fd02db..64378a9 100644 --- a/test/replica_sets/rs_test_helper.rb +++ b/test/replica_sets/rs_test_helper.rb @@ -19,7 +19,7 @@ class Test::Unit::TestCase yield success = true rescue Mongo::ConnectionFailure - puts "Rescuing attempt #{tries}" + puts "Rescue attempt #{tries}" tries += 1 sleep(1) end diff --git a/test/tools/repl_set_manager.rb b/test/tools/repl_set_manager.rb index cdb7a59..bf31f80 100644 --- a/test/tools/repl_set_manager.rb +++ b/test/tools/repl_set_manager.rb @@ -19,9 +19,9 @@ class ReplSetManager @config = {"_id" => @name, "members" => []} @path = File.join(File.expand_path(File.dirname(__FILE__)), "data") - @passive_count = opts[:secondary_count] || 1 - @arbiter_count = opts[:arbiter_count] || 1 - @secondary_count = opts[:passive_count] || 1 + @arbiter_count = opts[:arbiter_count] || 2 + @secondary_count = opts[:secondary_count] || 1 + @passive_count = opts[:passive_count] || 1 @primary_count = 1 @count = @primary_count + @passive_count + @arbiter_count + @secondary_count @@ -33,7 +33,7 @@ class ReplSetManager end def start_set - puts "Starting a replica set with #{@count} nodes" + puts "** Starting a replica set with #{@count} nodes" system("killall mongod") @@ -89,6 +89,8 @@ class ReplSetManager end def kill(node) + pid = @mongods[node]['pid'] + puts "** Killing node with pid #{pid} at port #{@mongods[node]['port']}" system("kill -2 #{@mongods[node]['pid']}") @mongods[node]['up'] = false sleep(1) @@ -137,13 +139,13 @@ class ReplSetManager def start(node) system(@mongods[node]['start']) @mongods[node]['up'] = true - sleep(1) + sleep(0.5) @mongods[node]['pid'] = File.open(File.join(@mongods[node]['db_path'], 'mongod.lock')).read.strip end alias :restart :start def ensure_up - print "Ensuring members are up..." + print "** Ensuring members are up..." attempt(Mongo::OperationFailure) do con = get_connection @@ -151,7 +153,7 @@ class ReplSetManager print "." if status['members'].all? { |m| [1, 2, 7].include?(m['state']) } && status['members'].any? { |m| m['state'] == 1 } - puts "All members up!" + print "all members up!\n\n" return status else raise Mongo::OperationFailure From 550db8f6714296239cee6730094cf7fe3b808863 Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Tue, 14 Dec 2010 15:56:11 -0500 Subject: [PATCH 7/8] Removed redundant test --- test/replica_sets/node_type_test.rb | 41 ----------------------------- 1 file changed, 41 deletions(-) delete mode 100644 test/replica_sets/node_type_test.rb diff --git a/test/replica_sets/node_type_test.rb b/test/replica_sets/node_type_test.rb deleted file mode 100644 index 4c46d0a..0000000 --- a/test/replica_sets/node_type_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -require 'mongo' -require 'test/unit' -require './test/test_helper' - -# NOTE: This test expects a replica set of three nodes, one of which is an arbiter, to be running -# on the local host. -class ReplicaSetNodeTypeTest < Test::Unit::TestCase - include Mongo - - def setup - @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], [RS.host, RS.ports[2]]) - @db = @conn.db(MONGO_TEST_DB) - @db.drop_collection("test-sets") - @coll = @db.collection("test-sets") - end - - def test_correct_node_types - p @conn.primary - p @conn.secondaries - p @conn.arbiters - assert_equal 1, @conn.secondaries.length - assert_equal 1, @conn.arbiters.length - - old_secondary = @conn.secondaries.first - old_primary = @conn.primary - - RS.step_down_primary - - # Insert something to rescue the connection failure. - rescue_connection_failure do - @coll.insert({:a => 30}, :safe => true) - end - - assert_equal 1, @conn.secondaries.length - assert_equal 1, @conn.arbiters.length - assert_equal old_primary, @conn.secondaries.first - assert_equal old_secondary, @conn.primary - end - -end From 95c0fe088fe9943b145db1391feed9f758576b34 Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Tue, 14 Dec 2010 17:38:52 -0500 Subject: [PATCH 8/8] Fully-automated replica set tests. --- Rakefile | 2 +- lib/mongo/util/pool.rb | 7 ++++++- test/replica_sets/connect_test.rb | 4 ++++ test/replica_sets/pooled_insert_test.rb | 10 +++++----- test/replica_sets/query_secondaries.rb | 13 +++++++------ test/replica_sets/query_test.rb | 20 ++++++++++---------- test/replica_sets/replication_ack_test.rb | 23 ++++++++++------------- test/replica_sets/rs_test_helper.rb | 2 +- test/tools/repl_set_manager.rb | 2 +- 9 files changed, 45 insertions(+), 38 deletions(-) diff --git a/Rakefile b/Rakefile index 898d0e9..7ec359d 100644 --- a/Rakefile +++ b/Rakefile @@ -74,7 +74,7 @@ namespace :test do end Rake::TestTask.new(:rs) do |t| - t.test_files = ['test/replica_sets/count_test.rb', 'test/replica_sets/connect_test.rb', 'test/replica_sets/insert_test.rb'] + t.test_files = FileList['test/replica_sets/*_test.rb'] t.verbose = true end diff --git a/lib/mongo/util/pool.rb b/lib/mongo/util/pool.rb index 2bb0d22..c4f98d3 100644 --- a/lib/mongo/util/pool.rb +++ b/lib/mongo/util/pool.rb @@ -50,7 +50,12 @@ module Mongo def close @sockets.each do |sock| - sock.close + begin + sock.close + rescue IOError => ex + warn "IOError when attempting to close socket connected " + + "to #{@host}:#{@port}: #{ex.inspect}" + end end @host = @port = nil @sockets.clear diff --git a/test/replica_sets/connect_test.rb b/test/replica_sets/connect_test.rb index b4237f0..1fc937f 100644 --- a/test/replica_sets/connect_test.rb +++ b/test/replica_sets/connect_test.rb @@ -10,6 +10,10 @@ class ConnectTest < Test::Unit::TestCase RS.restart_killed_nodes end + def teardown + RS.restart_killed_nodes + end + def test_connect_bad_name assert_raise_error(ReplicaSetConnectionError, "-wrong") do ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], diff --git a/test/replica_sets/pooled_insert_test.rb b/test/replica_sets/pooled_insert_test.rb index c8eb99f..009f95b 100644 --- a/test/replica_sets/pooled_insert_test.rb +++ b/test/replica_sets/pooled_insert_test.rb @@ -1,7 +1,5 @@ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -require 'mongo' -require 'test/unit' -require './test/test_helper' +require './test/replica_sets/rs_test_helper' # NOTE: This test expects a replica set of three nodes to be running # on the local host. @@ -16,13 +14,15 @@ class ReplicaSetPooledInsertTest < Test::Unit::TestCase @coll = @db.collection("test-sets") end + def teardown + RS.restart_killed_nodes + end + def test_insert expected_results = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] @coll.save({:a => -1}, :safe => true) RS.kill_primary - puts "Please disconnect the current master." - gets threads = [] 10.times do |i| diff --git a/test/replica_sets/query_secondaries.rb b/test/replica_sets/query_secondaries.rb index ce7e1a9..33760e0 100644 --- a/test/replica_sets/query_secondaries.rb +++ b/test/replica_sets/query_secondaries.rb @@ -1,7 +1,5 @@ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -require 'mongo' -require 'test/unit' -require './test/test_helper' +require './test/replica_sets/rs_test_helper' # NOTE: This test expects a replica set of three nodes to be running # on the local host. @@ -9,12 +7,16 @@ class ReplicaSetQuerySecondariesTest < Test::Unit::TestCase include Mongo def setup - @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], :read_secondary => true) + @conn = ReplSetConnection.new([RS.host, RS.ports[0]], :read_secondary => true) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets", :safe => {:w => 2, :wtimeout => 100}) end + def teardown + RS.restart_killed_nodes + end + def test_con assert @conn.primary_pool, "No primary pool!" assert @conn.read_pool, "No read pool!" @@ -32,8 +34,7 @@ class ReplicaSetQuerySecondariesTest < Test::Unit::TestCase assert results.include?(30) assert results.include?(40) - puts "Please disconnect the current master." - gets + RS.kill_primary results = [] rescue_connection_failure do diff --git a/test/replica_sets/query_test.rb b/test/replica_sets/query_test.rb index 371f3b8..5713e66 100644 --- a/test/replica_sets/query_test.rb +++ b/test/replica_sets/query_test.rb @@ -1,7 +1,5 @@ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -require 'mongo' -require 'test/unit' -require './test/test_helper' +require './test/replica_sets/rs_test_helper' # NOTE: This test expects a replica set of three nodes to be running # on the local host. @@ -9,25 +7,27 @@ class ReplicaSetQueryTest < Test::Unit::TestCase include Mongo def setup - @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2]) + @conn = ReplSetConnection.new([RS.host, RS.ports[0]]) @db = @conn.db(MONGO_TEST_DB) @db.drop_collection("test-sets") @coll = @db.collection("test-sets") end + def teardown + RS.restart_killed_nodes + end + def test_query - @coll.save({:a => 20}) - @coll.save({:a => 30}) - @coll.save({:a => 40}) + @coll.save({:a => 20}, :safe => {:w => 3}) + @coll.save({:a => 30}, :safe => {:w => 3}) + @coll.save({:a => 40}, :safe => {:w => 3}) results = [] @coll.find.each {|r| results << r} [20, 30, 40].each do |a| assert results.any? {|r| r['a'] == a}, "Could not find record for a => #{a}" end - puts "Please disconnect the current master." - gets + RS.kill_primary results = [] rescue_connection_failure do diff --git a/test/replica_sets/replication_ack_test.rb b/test/replica_sets/replication_ack_test.rb index 78e30f7..53441fd 100644 --- a/test/replica_sets/replication_ack_test.rb +++ b/test/replica_sets/replication_ack_test.rb @@ -1,17 +1,14 @@ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -require 'mongo' -require 'test/unit' -require './test/test_helper' +require './test/replica_sets/rs_test_helper' # NOTE: This test expects a replica set of three nodes to be running on local host. class ReplicaSetAckTest < Test::Unit::TestCase include Mongo def setup - @conn = ReplSetConnection.multi([TEST_HOST, TEST_PORT], [TEST_HOST, TEST_PORT + 1], - [TEST_HOST, TEST_PORT + 2]) + RS.ensure_up - master = [@conn.primary_pool.host, @conn.primary_pool.port] + @conn = ReplSetConnection.new([RS.host, RS.ports[0]]) @slave1 = Connection.new(@conn.secondary_pools[0].host, @conn.secondary_pools[0].port, :slave_ok => true) @@ -34,31 +31,31 @@ class ReplicaSetAckTest < Test::Unit::TestCase end def test_safe_mode_replication_ack - @col.insert({:baz => "bar"}, :safe => {:w => 2, :wtimeout => 1000}) + @col.insert({:baz => "bar"}, :safe => {:w => 2, :wtimeout => 5000}) - assert @col.insert({:foo => "0" * 10000}, :safe => {:w => 2, :wtimeout => 1000}) + assert @col.insert({:foo => "0" * 5000}, :safe => {:w => 2, :wtimeout => 5000}) assert_equal 2, @slave1[MONGO_TEST_DB]["test-sets"].count - assert @col.update({:baz => "bar"}, {:baz => "foo"}, :safe => {:w => 2, :wtimeout => 1000}) + assert @col.update({:baz => "bar"}, {:baz => "foo"}, :safe => {:w => 2, :wtimeout => 5000}) assert @slave1[MONGO_TEST_DB]["test-sets"].find_one({:baz => "foo"}) - assert @col.remove({}, :safe => {:w => 2, :wtimeout => 1000}) + assert @col.remove({}, :safe => {:w => 2, :wtimeout => 5000}) assert_equal 0, @slave1[MONGO_TEST_DB]["test-sets"].count end def test_last_error_responses 20.times { @col.insert({:baz => "bar"}) } - response = @db.get_last_error(:w => 2, :wtimeout => 10000) + response = @db.get_last_error(:w => 2, :wtimeout => 5000) assert response['ok'] == 1 assert response['lastOp'] @col.update({}, {:baz => "foo"}, :multi => true) - response = @db.get_last_error(:w => 2, :wtimeout => 1000) + response = @db.get_last_error(:w => 2, :wtimeout => 5000) assert response['ok'] == 1 assert response['lastOp'] @col.remove({}) - response = @db.get_last_error(:w => 2, :wtimeout => 1000) + response = @db.get_last_error(:w => 2, :wtimeout => 5000) assert response['ok'] == 1 assert response['n'] == 20 assert response['lastOp'] diff --git a/test/replica_sets/rs_test_helper.rb b/test/replica_sets/rs_test_helper.rb index 64378a9..869e721 100644 --- a/test/replica_sets/rs_test_helper.rb +++ b/test/replica_sets/rs_test_helper.rb @@ -19,7 +19,7 @@ class Test::Unit::TestCase yield success = true rescue Mongo::ConnectionFailure - puts "Rescue attempt #{tries}" + puts "Rescue attempt #{tries}\n" tries += 1 sleep(1) end diff --git a/test/tools/repl_set_manager.rb b/test/tools/repl_set_manager.rb index bf31f80..5f84cdf 100644 --- a/test/tools/repl_set_manager.rb +++ b/test/tools/repl_set_manager.rb @@ -151,7 +151,7 @@ class ReplSetManager con = get_connection status = con['admin'].command({'replSetGetStatus' => 1}) print "." - if status['members'].all? { |m| [1, 2, 7].include?(m['state']) } && + if status['members'].all? { |m| m['health'] == 1 && [1, 2, 7].include?(m['state']) } && status['members'].any? { |m| m['state'] == 1 } print "all members up!\n\n" return status