Simpify replica set connection code.

This commit is contained in:
Kyle Banker 2011-08-16 16:47:07 -04:00
parent 2557a575eb
commit 3027e29f46
5 changed files with 216 additions and 147 deletions

View File

@ -35,7 +35,8 @@ module Mongo
STANDARD_HEADER_SIZE = 16 STANDARD_HEADER_SIZE = 16
RESPONSE_HEADER_SIZE = 20 RESPONSE_HEADER_SIZE = 20
attr_reader :logger, :size, :auths, :primary, :safe, :primary_pool, :host_to_try, :pool_size attr_reader :logger, :size, :auths, :primary, :safe, :primary_pool,
:host_to_try, :pool_size, :connect_timeout
# Counter for generating unique request ids. # Counter for generating unique request ids.
@@current_request_id = 0 @@current_request_id = 0

View File

@ -1,19 +1,133 @@
module Mongo module Mongo
class Node class Node
attr_accessor :host, :port, :address attr_accessor :host, :port, :address, :config, :repl_set_status, :connection, :socket
def initialize(data) def initialize(connection, data)
data = data.split(':') if data.is_a?(String) self.connection = connection
self.host = data[0] if data.is_a?(String)
self.port = data[1] ? data[1].to_i : Connection::DEFAULT_PORT self.host, self.port = split_nodes(data)
else
self.host, self.port = data
end
self.address = "#{host}:#{port}" self.address = "#{host}:#{port}"
end end
def eql?(other) def eql?(other)
other.is_a?(Node) && host == other.host && port == other.port other.is_a?(Node) && host == other.host && port == other.port
end end
alias :== :eql? alias :== :eql?
# Create a connection to the provided node,
# and, if successful, return the socket. Otherwise,
# return nil.
def connect
begin
if self.connection.connect_timeout
Mongo::TimeoutHandler.timeout(self.connection.connect_timeout, OperationTimeout) do
socket = TCPSocket.new(self.host, self.port)
end
else
socket = TCPSocket.new(self.host, self.port)
end
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
rescue OperationFailure, SocketError, SystemCallError, IOError => ex
return nil
end
self.socket = socket
end
def disconnect
if self.socket
self.socket.close
self.socket = nil
self.config = nil
end
end
def connected?
self.socket != nil
end
# Get the configuration for the provided node as returned by the
# ismaster command. Additionally, check that the replica set name
# matches with the name provided.
def set_config
begin
self.config = self.connection['admin'].command({:ismaster => 1}, :socket => self.socket)
if self.config['msg'] && @logger
self.connection.logger.warn("MONGODB #{config['msg']}")
end
check_set_name
rescue OperationFailure, SocketError, SystemCallError, IOError => ex
return nil
end
self.config
end
# Return a list of replica set nodes from the config.
# Note: this excludes arbiters.
def node_list
connect unless connected?
return [] unless self.config
nodes = []
nodes += config['hosts'] if config['hosts']
nodes += config['passives'] if config['passives']
nodes
end
def arbiters
connect unless connected?
config['arbiters'].map do |arbiter|
split_nodes(arbiter)
end
end
def primary?
self.config['ismaster'] == true || self.config['ismaster'] == 1
end
def secondary?
self.config['secondary'] == true || self.config['secondary'] == 1
end
def host_port
[self.host, self.port]
end
def hash def hash
address.hash address.hash
end end
private
def split_nodes(host_string)
data = host_string.split(":")
host = data[0]
port = data[1].to_i || Connection::DEFAULT_PORT
[host, port]
end
# Make sure that we're connected to the expected replica set.
def check_set_name
if self.connection.replica_set_name
if !self.config['setName']
self.connection.logger.warn("MONGODB [warning] could not verify replica set name " +
"because ismaster does not return name in this version of MongoDB")
elsif self.connection.replica_set_name != self.config['setName']
raise ReplicaSetConnectionError,
"Attempting to connect to replica set '#{config['setName']}' " +
"but expected '#{self.connection.replica_set_name}'"
end
end
end
end end
end end

View File

@ -20,7 +20,8 @@ module Mongo
# Instantiates and manages connections to a MongoDB replica set. # Instantiates and manages connections to a MongoDB replica set.
class ReplSetConnection < Connection class ReplSetConnection < Connection
attr_reader :nodes, :secondaries, :arbiters, :read_pool, :secondary_pools attr_reader :nodes, :secondaries, :arbiters, :read_pool, :secondary_pools,
:replica_set_name, :members
# Create a connection to a MongoDB replica set. # Create a connection to a MongoDB replica set.
# #
@ -77,66 +78,68 @@ module Mongo
raise MongoArgumentError, "A ReplSetConnection requires at least one node." raise MongoArgumentError, "A ReplSetConnection requires at least one node."
end end
# Get seed nodes # Get the list of seed nodes
@nodes = args @seeds = args
# Replica set name # The members of the replica set, stored as instances of Mongo::Node.
@replica_set = opts[:rs_name] @members = []
# Cache the various node types when connecting to a replica set. # Connection pool for primay node
@secondaries = [] @primary = nil
@arbiters = [] @primary_pool = nil
# Connection pools for each secondary node # Connection pools for each secondary node
@secondaries = []
@secondary_pools = [] @secondary_pools = []
# The secondary pool to which we'll be sending reads.
@read_pool = nil @read_pool = nil
# A list of arbiter address (for client information only)
@arbiters = []
# Are we allowing reads from secondaries? # Are we allowing reads from secondaries?
@read_secondary = opts.fetch(:read_secondary, false) @read_secondary = opts.fetch(:read_secondary, false)
# Replica set name
if opts[:rs_name]
warn ":rs_name option has been deprecated and will be removed in 2.0. " +
"Please use :name instead."
@replica_set_name = opts[:rs_name]
else
@replica_set_name = opts[:name]
end
setup(opts) setup(opts)
end end
# Create a new socket and attempt to connect to master. # Use the provided seed nodes to initiate a connection
# If successful, sets host and port to master and returns the socket. # to the replica set.
#
# 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 def connect
close connect_to_members
@nodes_to_try = @nodes.clone initialize_pools
pick_secondary_for_read
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 connected?
BSON::BSON_CODER.update_max_bson_size(self) BSON::BSON_CODER.update_max_bson_size(self)
else else
if @secondary_pools.empty? close
close # close any existing pools and sockets
raise ConnectionFailure, "Failed to connect any given host:port" if @primary.nil?
else
close # close any existing pools and sockets
raise ConnectionFailure, "Failed to connect to primary node." raise ConnectionFailure, "Failed to connect to primary node."
else
raise ConnectionFailure, "Failed to connect to any given member."
end end
end end
end end
alias :reconnect :connect
def connected?
@primary_pool || (@read_pool && @read_secondary)
end
# @deprecated
def connecting? def connecting?
@nodes_to_try.length > 0 false
end end
# The replica set primary's host name. # The replica set primary's host name.
@ -165,6 +168,10 @@ module Mongo
# Close the connection to the database. # Close the connection to the database.
def close def close
super super
@members.each do |member|
member.disconnect
end
@members = []
@read_pool = nil @read_pool = nil
@secondary_pools.each do |pool| @secondary_pools.each do |pool|
pool.close pool.close
@ -172,8 +179,6 @@ module Mongo
@secondaries = [] @secondaries = []
@secondary_pools = [] @secondary_pools = []
@arbiters = [] @arbiters = []
@nodes_tried = []
@nodes_to_try = []
end end
# If a ConnectionFailure is raised, this method will be called # If a ConnectionFailure is raised, this method will be called
@ -208,115 +213,64 @@ module Mongo
private private
def check_is_master(node) # Iterate through the list of provided seed
begin # nodes until we've gotten a response from the
host, port = *node # replica set we're trying to connect to.
#
if @connect_timeout # If we don't get a response, raise an exception.
Mongo::TimeoutHandler.timeout(@connect_timeout, OperationTimeout) do def get_valid_seed_node
socket = TCPSocket.new(host, port) @seeds.each do |seed|
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) node = Mongo::Node.new(self, seed)
end if node.connect && node.set_config
else return node
socket = TCPSocket.new(host, port)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
end
config = self['admin'].command({:ismaster => 1}, :socket => socket)
check_set_name(config, socket)
rescue OperationFailure, SocketError, SystemCallError, IOError => ex
# It's necessary to rescue here. The #connect method will keep trying
# until it has no more nodes to try and raise a ConnectionFailure if
# it can't connect to a primary.
ensure
socket.close if socket
@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 end
end end
config raise ConnectionFailure, "Cannot connect to a replica set with name using seed nodes " +
"#{@seeds.map {|s| "#{s[0]}:#{s[1]}" }.join(',')}"
end end
# Primary, when connecting to a replica can, can only be a true primary node. # Connect to each member of the replica set
# (And not a slave, which is possible when connecting with the standard # as reported by the given seed node, and cache
# Connection class. # those connections in the @members array.
def is_primary?(config) def connect_to_members
config && (config['ismaster'] == 1 || config['ismaster'] == true) seed = get_valid_seed_node
seed.node_list.each do |host|
node = Mongo::Node.new(self, host)
if node.connect && node.set_config
@members << node
end
end
end
# Initialize the connection pools to the primary and secondary nodes.
def initialize_pools
if @members.empty?
raise ConnectionFailure, "Failed to connect to any given member."
end
@arbiters = @members.first.arbiters
@members.each do |member|
if member.primary?
@primary = member.host_port
@primary_pool = Pool.new(self, member.host, member.port, :size => @pool_size, :timeout => @timeout)
elsif member.secondary? && !@secondaries.include?(member.host_port)
@secondaries << member.host_port
@secondary_pools << Pool.new(self, member.host, member.port, :size => @pool_size, :timeout => @timeout)
end
end
end end
# Pick a node randomly from the set of possible secondaries. # Pick a node randomly from the set of possible secondaries.
def pick_secondary_for_read def pick_secondary_for_read
return unless @read_secondary
if (size = @secondary_pools.size) > 0 if (size = @secondary_pools.size) > 0
@read_pool = @secondary_pools[rand(size)] @read_pool = @secondary_pools[rand(size)]
end end
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},
:socket => 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 ? port.to_i : Connection::DEFAULT_PORT]
end
# Replace the list of seed nodes with the canonical list.
@nodes = new_nodes.clone
@nodes_to_try = new_nodes - @nodes_tried
end
# Checkout a socket for reading (i.e., a secondary node). # Checkout a socket for reading (i.e., a secondary node).
def checkout_reader def checkout_reader
connect unless connected? connect unless connected?

View File

@ -7,7 +7,7 @@ class ReplicaSetQueryTest < Test::Unit::TestCase
include Mongo include Mongo
def setup def setup
@conn = ReplSetConnection.new([RS.host, RS.ports[0]]) @conn = ReplSetConnection.new([RS.host, RS.ports[0], RS.ports[1]])
@db = @conn.db(MONGO_TEST_DB) @db = @conn.db(MONGO_TEST_DB)
@db.drop_collection("test-sets") @db.drop_collection("test-sets")
@coll = @db.collection("test-sets") @coll = @db.collection("test-sets")

View File

@ -33,15 +33,15 @@ class ReplicaSetAckTest < Test::Unit::TestCase
end end
def test_safe_mode_replication_ack def test_safe_mode_replication_ack
@col.insert({:baz => "bar"}, :safe => {:w => 2, :wtimeout => 5000}) @col.insert({:baz => "bar"}, :safe => {:w => 3, :wtimeout => 5000})
assert @col.insert({:foo => "0" * 5000}, :safe => {:w => 2, :wtimeout => 5000}) assert @col.insert({:foo => "0" * 5000}, :safe => {:w => 3, :wtimeout => 5000})
assert_equal 2, @slave1[MONGO_TEST_DB]["test-sets"].count assert_equal 2, @slave1[MONGO_TEST_DB]["test-sets"].count
assert @col.update({:baz => "bar"}, {:baz => "foo"}, :safe => {:w => 2, :wtimeout => 5000}) assert @col.update({:baz => "bar"}, {:baz => "foo"}, :safe => {:w => 3, :wtimeout => 5000})
assert @slave1[MONGO_TEST_DB]["test-sets"].find_one({:baz => "foo"}) assert @slave1[MONGO_TEST_DB]["test-sets"].find_one({:baz => "foo"})
assert @col.remove({}, :safe => {:w => 2, :wtimeout => 5000}) assert @col.remove({}, :safe => {:w => 3, :wtimeout => 5000})
assert_equal 0, @slave1[MONGO_TEST_DB]["test-sets"].count assert_equal 0, @slave1[MONGO_TEST_DB]["test-sets"].count
end end