Merge branch 'repl_set_connection'
This commit is contained in:
commit
5860265cc9
38
Rakefile
38
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|
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -35,11 +35,8 @@ 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
|
||||
:safe, :primary_pool, :read_pool, :secondary_pools, :host_to_try
|
||||
|
||||
# Counter for generating unique request ids.
|
||||
@@current_request_id = 0
|
||||
|
@ -92,62 +89,19 @@ module Mongo
|
|||
#
|
||||
# @core connections
|
||||
def initialize(host=nil, port=nil, options={})
|
||||
@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
|
||||
|
||||
# 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]
|
||||
|
||||
# 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
|
||||
setup(options)
|
||||
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,20 +124,13 @@ 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."
|
||||
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:
|
||||
|
@ -195,8 +142,15 @@ 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)
|
||||
else
|
||||
raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node."
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -482,21 +436,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
|
||||
|
||||
|
@ -515,84 +460,6 @@ 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.
|
||||
#
|
||||
# @private
|
||||
def format_pair(pair_or_host, port)
|
||||
case pair_or_host
|
||||
when String
|
||||
[[pair_or_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
|
||||
|
||||
# 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).
|
||||
|
@ -629,25 +496,85 @@ module Mongo
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
protected
|
||||
|
||||
# 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)]
|
||||
# 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
|
||||
# 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
|
||||
|
@ -655,8 +582,9 @@ 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
|
||||
config && (config['ismaster'] == 1 || config['ismaster'] == true) || @slave_ok
|
||||
end
|
||||
|
||||
def check_is_master(node)
|
||||
|
@ -666,41 +594,15 @@ module Mongo
|
|||
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?
|
||||
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
|
||||
|
||||
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)
|
||||
|
@ -710,45 +612,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, expected_response)
|
||||
begin
|
||||
receive_header(sock, expected_response)
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
# 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 < Connection
|
||||
attr_reader :nodes, :secondaries, :arbiters, :read_pool, :secondary_pools
|
||||
|
||||
def initialize(*args)
|
||||
if args.last.is_a?(Hash)
|
||||
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 = opts[: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
|
||||
|
||||
# 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.
|
||||
# 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
|
||||
|
||||
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?
|
||||
@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.
|
||||
# TODO: what's the point of this method?
|
||||
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
|
||||
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']}")
|
||||
end
|
||||
end
|
||||
|
||||
socket.close if socket
|
||||
end
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
raise ReplicaSetConnectionError,
|
||||
"Attempting to connect to replica set '#{config['set']}' but expected '#{@replica_set}'"
|
||||
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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1,47 +1,78 @@
|
|||
$:.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 teardown
|
||||
RS.restart_killed_nodes
|
||||
end
|
||||
|
||||
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(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 = Mongo::Connection.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.sort, @conn.secondaries.sort
|
||||
assert_equal RS.arbiters.sort, @conn.arbiters.sort
|
||||
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 = Mongo::Connection.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 = Mongo::Connection.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 = Mongo::Connection.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
|
||||
|
|
|
@ -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,18 +7,22 @@ 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.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.restart_killed_nodes
|
||||
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
|
||||
|
||||
puts "Please disconnect the current master."
|
||||
gets
|
||||
# Kill the current master node
|
||||
@node = RS.kill_primary
|
||||
|
||||
rescue_connection_failure do
|
||||
@coll.insert({:a => 30}, :safe => true)
|
||||
|
|
|
@ -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,16 +7,20 @@ 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.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)
|
||||
|
@ -29,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
|
||||
|
|
|
@ -1,42 +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 = Mongo::Connection.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")
|
||||
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
|
||||
|
||||
puts "Please disconnect the current primary and reconnect so that it becomes secondary."
|
||||
gets
|
||||
|
||||
# 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
|
|
@ -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,18 +7,22 @@ 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.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")
|
||||
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)
|
||||
puts "Please disconnect the current master."
|
||||
gets
|
||||
|
||||
RS.kill_primary
|
||||
|
||||
threads = []
|
||||
10.times do |i|
|
||||
|
@ -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
|
||||
|
|
|
@ -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 = Mongo::Connection.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
|
||||
|
|
|
@ -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,24 +7,27 @@ 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.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
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
$:.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 = Mongo::Connection.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 = 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")
|
||||
|
@ -32,32 +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']
|
||||
|
|
|
@ -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 "Rescue attempt #{tries}\n"
|
||||
tries += 1
|
||||
sleep(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
#!/usr/bin/ruby
|
||||
|
||||
STDOUT.sync = true
|
||||
|
||||
unless defined? Mongo
|
||||
require File.join(File.dirname(__FILE__), '..', '..', 'lib', 'mongo')
|
||||
end
|
||||
|
||||
class ReplSetManager
|
||||
|
||||
attr_accessor :host, :start_port, :ports, :name, :mongods
|
||||
|
||||
def initialize(opts={})
|
||||
@start_port = opts[:start_port] || 30000
|
||||
@ports = []
|
||||
@name = opts[:name] || 'replica-set-foo'
|
||||
@host = opts[:host] || 'localhost'
|
||||
@retries = opts[:retries] || 60
|
||||
@config = {"_id" => @name, "members" => []}
|
||||
@path = File.join(File.expand_path(File.dirname(__FILE__)), "data")
|
||||
|
||||
@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
|
||||
if @count > 7
|
||||
raise StandardError, "Cannot create a replica set with #{node_count} nodes. 7 is the max."
|
||||
end
|
||||
|
||||
@mongods = {}
|
||||
end
|
||||
|
||||
def start_set
|
||||
puts "** Starting a replica set with #{@count} nodes"
|
||||
|
||||
system("killall mongod")
|
||||
|
||||
n = 0
|
||||
(@primary_count + @secondary_count).times do |n|
|
||||
init_node(n)
|
||||
n += 1
|
||||
end
|
||||
|
||||
@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)
|
||||
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)
|
||||
end
|
||||
|
||||
def kill_primary
|
||||
node = get_node_with_state(1)
|
||||
kill(node)
|
||||
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
|
||||
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..."
|
||||
|
||||
attempt(Mongo::OperationFailure) do
|
||||
con = get_connection
|
||||
status = con['admin'].command({'replSetGetStatus' => 1})
|
||||
print "."
|
||||
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
|
||||
else
|
||||
raise Mongo::OperationFailure
|
||||
end
|
||||
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 initiate
|
||||
con = get_connection
|
||||
|
||||
attempt(Mongo::OperationFailure) do
|
||||
con['admin'].command({'replSetInitiate' => @config})
|
||||
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_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
|
||||
end
|
||||
|
||||
def get_path(name)
|
||||
File.join(@path, name)
|
||||
end
|
||||
|
||||
def attempt(exception)
|
||||
raise "No block given!" unless block_given?
|
||||
count = 0
|
||||
|
||||
while count < @retries do
|
||||
begin
|
||||
return yield
|
||||
rescue exception
|
||||
sleep(1)
|
||||
count += 1
|
||||
end
|
||||
end
|
||||
|
||||
raise exception
|
||||
end
|
||||
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
require './test/test_helper'
|
||||
include Mongo
|
||||
|
||||
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
|
Loading…
Reference in New Issue