Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

RUBY-456 Read Semantics update for MongoDB 2.2

Updates read semantics for the Ruby driver in order to comply with the specification outlined here:

http://docs.mongodb.org/manual/applications/replication/#replica-set-read-preference

- Modified ReplSetConnection :read parameter to accept and respect the following read preference modes (Note :secondary behaves differently than before)

  :primary
  :primary_preferred
  :secondary
  :secondary_preferred
  :nearest

- Added :tag_sets parameter to ReplSetConnection in order for tag sets to be specified as part of read preference.

- Added :secondary_acceptable_latency_ms parameter to ReplSetConnection for setting maximum latency specification as part of read preference.
  • Loading branch information...
commit 4e40e74dbd94b338ad6a3b5de26f8f63db2cecca 1 parent 3f4254d
@TylerBrock TylerBrock authored
Showing with 786 additions and 490 deletions.
  1. +10 −8 lib/mongo/collection.rb
  2. +17 −23 lib/mongo/connection.rb
  3. +38 −23 lib/mongo/cursor.rb
  4. +10 −8 lib/mongo/db.rb
  5. +26 −49 lib/mongo/networking.rb
  6. +58 −121 lib/mongo/repl_set_connection.rb
  7. +5 −3 lib/mongo/util/node.rb
  8. +10 −3 lib/mongo/util/pool.rb
  9. +66 −36 lib/mongo/util/pool_manager.rb
  10. +0 −1  lib/mongo/util/sharding_pool_manager.rb
  11. +22 −2 lib/mongo/util/support.rb
  12. +3 −3 test/connection_test.rb
  13. +3 −3 test/replica_sets/basic_test.rb
  14. +231 −0 test/replica_sets/complex_read_preference_test.rb
  15. +9 −25 test/replica_sets/connect_test.rb
  16. +1 −1  test/replica_sets/count_test.rb
  17. +36 −33 test/replica_sets/cursor_test.rb
  18. +169 −118 test/replica_sets/read_preference_test.rb
  19. +3 −3 test/replica_sets/refresh_test.rb
  20. +1 −1  test/replica_sets/refresh_with_threads_test.rb
  21. +2 −2 test/replica_sets/rs_test_helper.rb
  22. +9 −9 test/tools/repl_set_manager.rb
  23. +1 −1  test/unit/collection_test.rb
  24. +11 −6 test/unit/cursor_test.rb
  25. +4 −0 test/unit/db_test.rb
  26. +2 −0  test/unit/grid_test.rb
  27. +39 −8 test/unit/read_test.rb
View
18 lib/mongo/collection.rb
@@ -23,6 +23,9 @@ class Collection
attr_reader :db, :name, :pk_factory, :hint, :safe
+ # Read Preference
+ attr_accessor :read_preference, :tag_sets, :acceptable_latency
+
# Initialize a collection object.
#
# @param [String, Symbol] name the name of the collection.
@@ -97,6 +100,8 @@ def initialize(name, db, opts={})
value = @db.read_preference
end
@read_preference = value.is_a?(Hash) ? value.dup : value
+ @tag_sets = opts.fetch(:tag_sets, @db.tag_sets)
+ @acceptable_latency = opts.fetch(:acceptable_latency, @db.acceptable_latency)
end
@pk_factory = pk_factory || opts[:pk] || BSON::ObjectId
@hint = nil
@@ -220,6 +225,8 @@ def find(selector={}, opts={})
transformer = opts.delete(:transformer)
show_disk_loc = opts.delete(:show_disk_loc)
read = opts.delete(:read) || @read_preference
+ tag_sets = opts.delete(:tag_sets) || @tag_sets
+ acceptable_latency = opts.delete(:acceptable_latency) || @acceptable_latency
if timeout == false && !block_given?
raise ArgumentError, "Collection#find must be invoked with a block when timeout is disabled."
@@ -247,7 +254,9 @@ def find(selector={}, opts={})
:max_scan => max_scan,
:show_disk_loc => show_disk_loc,
:return_key => return_key,
- :read => read
+ :read => read,
+ :tag_sets => tag_sets,
+ :acceptable_latency => acceptable_latency
})
if block_given?
@@ -763,13 +772,6 @@ def group(opts, condition={}, initial={}, reduce=nil, finalize=nil)
end
end
- # The value of the read preference. This will be
- # either +:primary+, +:secondary+, or an object
- # representing the tags to be read from.
- def read_preference
- @read_preference
- end
-
private
def new_group(opts={})
View
40 lib/mongo/connection.rb
@@ -42,7 +42,7 @@ class Connection
attr_reader :logger, :size, :auths, :primary, :safe, :host_to_try,
:pool_size, :connect_timeout, :pool_timeout,
- :primary_pool, :socket_class, :op_timeout
+ :primary_pool, :socket_class, :op_timeout, :tag_sets, :acceptable_latency
# Create a connection to single MongoDB instance.
#
@@ -127,6 +127,10 @@ def initialize(host=nil, port=nil, opts={})
@primary = nil
@primary_pool = nil
+ # Not set for direct connection
+ @tag_sets = {}
+ @acceptable_latency = 15
+
check_opts(opts)
setup(opts)
end
@@ -334,6 +338,15 @@ def [](db_name)
DB.new(db_name, self)
end
+ def refresh
+ end
+
+ def pin_pool(pool)
+ end
+
+ def unpin_pool(pool)
+ end
+
# Drop a database.
#
# @param [String] name name of an existing database.
@@ -471,7 +484,7 @@ def read_pool
# The value of the read preference.
def read_preference
if slave_ok?
- :secondary
+ :secondary_preferred
else
:primary
end
@@ -492,15 +505,9 @@ def max_bson_size
@max_bson_size
end
- # Prefer primary pool but fall back to secondary
- def checkout_best
- connect unless connected?
- @primary_pool.checkout
- end
-
# Checkout a socket for reading (i.e., a secondary node).
# Note: this is overridden in ReplSetConnection.
- def checkout_reader
+ def checkout_reader(mode=:primary, tag_sets={}, acceptable_latency=15)
connect unless connected?
@primary_pool.checkout
end
@@ -515,24 +522,11 @@ def checkout_writer
# Check a socket back into its pool.
# Note: this is overridden in ReplSetConnection.
def checkin(socket)
- if @primary_pool && socket
+ if @primary_pool && socket && socket.pool
socket.pool.checkin(socket)
end
end
- # Excecutes block with the best available socket
- def best_available_socket
- socket = nil
- begin
- socket = checkout_best
- yield socket
- ensure
- if socket
- socket.pool.checkin(socket)
- end
- end
- end
-
protected
def valid_opts
View
61 lib/mongo/cursor.rb
@@ -77,6 +77,9 @@ def initialize(collection, opts={})
value = collection.read_preference
end
@read_preference = value.is_a?(Hash) ? value.dup : value
+ @tag_sets = opts.fetch(:tag_sets, @collection.tag_sets)
+ @acceptable_latency = opts.fetch(:acceptable_latency, @collection.acceptable_latency)
+
batch_size(opts[:batch_size] || 0)
@full_collection_name = "#{@collection.db.name}.#{@collection.name}"
@@ -335,7 +338,7 @@ def close
message.put_int(1)
message.put_long(@cursor_id)
log(:debug, "Cursor#close #{@cursor_id}")
- @connection.send_message(Mongo::Constants::OP_KILL_CURSORS, message)
+ @connection.send_message(Mongo::Constants::OP_KILL_CURSORS, message, :socket => @socket)
end
@cursor_id = 0
@closed = true
@@ -458,19 +461,37 @@ def refresh
end
end
+ # Sends initial query -- which is always a read unless it is a command
+ #
+ # Upon ConnectionFailure, tries query 3 times if socket was not provided
+ # and the query is either not a command or is a secondary_ok command.
+ #
+ # Pins pools upon successful read and unpins pool upon ConnectionFailure
+ #
def send_initial_query
- message = construct_query_message
- sock = @socket || checkout_socket_from_connection
+ tries = 0
instrument(:find, instrument_payload) do
begin
- results, @n_received, @cursor_id = @connection.receive_message(
- Mongo::Constants::OP_QUERY, message, nil, sock, @command,
- nil, @options & OP_QUERY_EXHAUST != 0)
- rescue ConnectionFailure, OperationFailure, OperationTimeout => ex
- checkin_socket(sock) unless @socket
+ tries += 1
+ message = construct_query_message
+ sock = @socket || checkout_socket_from_connection
+ results, @n_received, @cursor_id = @connection.receive_message(
+ Mongo::Constants::OP_QUERY, message, nil, sock, @command,
+ nil, @options & OP_QUERY_EXHAUST != 0)
+ rescue ConnectionFailure => ex
+ if tries < 3 && !@socket && (!@command || Mongo::Support.secondary_ok?(@selector))
+ @connection.unpin_pool(sock.pool) if sock
+ @connection.refresh
+ retry
+ else
+ raise ex
+ end
+ rescue OperationFailure, OperationTimeout => ex
raise ex
+ ensure
+ checkin_socket(sock) unless @socket
end
- checkin_socket(sock) unless @socket
+ @connection.pin_pool(sock.pool) if !@command && !@socket
@returned += @n_received
@cache += results
@query_run = true
@@ -498,38 +519,32 @@ def send_get_more
# Cursor id.
message.put_long(@cursor_id)
log(:debug, "cursor.refresh() for cursor #{@cursor_id}") if @logger
+
sock = @socket || checkout_socket_from_connection
begin
- results, @n_received, @cursor_id = @connection.receive_message(
- Mongo::Constants::OP_GET_MORE, message, nil, sock, @command, nil)
- rescue ConnectionFailure, OperationFailure, OperationTimeout => ex
+ results, @n_received, @cursor_id = @connection.receive_message(
+ Mongo::Constants::OP_GET_MORE, message, nil, sock, @command, nil)
+ ensure
checkin_socket(sock) unless @socket
- raise ex
end
- checkin_socket(sock) unless @socket
+
@returned += @n_received
@cache += results
close_cursor_if_query_complete
end
def checkout_socket_from_connection
- socket = nil
begin
- @checkin_connection = true
- if @command || @read_preference == :primary
- socket = @connection.checkout_writer
- elsif @read_preference == :secondary_only
- socket = @connection.checkout_secondary
+ if @command && !Mongo::Support::secondary_ok?(@selector)
+ @connection.checkout_reader(:primary)
else
- socket = @connection.checkout_reader
+ @connection.checkout_reader(@read_preference, @tag_sets, @acceptable_latency)
end
rescue SystemStackError, NoMemoryError, SystemCallError => ex
@connection.close
raise ex
end
-
- socket
end
def checkin_socket(sock)
View
18 lib/mongo/db.rb
@@ -53,6 +53,9 @@ def strict?; @strict; end
# The length of time that Collection.ensure_index should cache index calls
attr_accessor :cache_time
+ # Read Preference
+ attr_accessor :read_preference, :tag_sets, :acceptable_latency
+
# Instances of DB are normally obtained by calling Mongo#db.
#
# @param [String] name the database name.
@@ -72,6 +75,7 @@ def strict?; @strict; end
# value is provided, the default value set on this instance's Connection object will be used. This
# default can be overridden upon instantiation of any collection by explicity setting a :safe value
# on initialization
+ #
# @option opts [Integer] :cache_time (300) Set the time that all ensure_index calls should cache the command.
#
# @core databases constructor_details
@@ -87,6 +91,8 @@ def initialize(name, connection, opts={})
value = @connection.read_preference
end
@read_preference = value.is_a?(Hash) ? value.dup : value
+ @tag_sets = opts.fetch(:tag_sets, @connection.tag_sets)
+ @acceptable_latency = opts.fetch(:acceptable_latency, @connection.acceptable_latency)
@cache_time = opts[:cache_time] || 300 #5 minutes.
end
@@ -112,8 +118,11 @@ def authenticate(username, password, save_auth=true)
end
end
- @connection.best_available_socket do |socket|
+ begin
+ socket = @connection.checkout_reader(:primary_preferred)
issue_authentication(username, password, save_auth, :socket => socket)
+ ensure
+ socket.pool.checkin(socket) if socket
end
@connection.authenticate_pools
@@ -628,13 +637,6 @@ def validate_collection(name)
doc
end
- # The value of the read preference. This will be
- # either +:primary+, +:secondary+, or an object
- # representing the tags to be read from.
- def read_preference
- @read_preference
- end
-
private
def system_command_collection
View
75 lib/mongo/networking.rb
@@ -26,12 +26,12 @@ def send_message(operation, message, opts={})
add_message_headers(message, operation)
packed_message = message.to_s
- sock = nil
+ sock = opts.fetch(:socket, nil)
begin
if operation == Mongo::Constants::OP_KILL_CURSORS && read_preference != :primary
- sock = checkout_reader
+ sock ||= checkout_reader
else
- sock = checkout_writer
+ sock ||= checkout_writer
end
send_message_on_socket(packed_message, sock)
rescue SystemStackError, NoMemoryError, SystemCallError => ex
@@ -40,7 +40,6 @@ def send_message(operation, message, opts={})
ensure
if sock
sock.pool.checkin(sock)
- sync_refresh if self.class == "ReplSetConnection"
end
end
end
@@ -113,24 +112,13 @@ def receive_message(operation, message, log_message=nil, socket=nil, command=fal
packed_message = message.to_s
result = ''
- sock = nil
- begin
- if socket
- sock = socket
- should_checkin = false
- else
- if command || read == :primary
- sock = checkout_writer
- elsif read == :secondary
- sock = checkout_reader
- else
- sock = checkout_tagged(read)
- end
- should_checkin = true
- end
- send_message_on_socket(packed_message, sock)
- result = receive(sock, request_id, exhaust)
+ begin
+ send_message_on_socket(packed_message, socket)
+ result = receive(socket, request_id, exhaust)
+ rescue ConnectionFailure => ex
+ checkin(socket)
+ raise ex
rescue SystemStackError, NoMemoryError, SystemCallError => ex
close
raise ex
@@ -139,10 +127,6 @@ def receive_message(operation, message, log_message=nil, socket=nil, command=fal
close if ex.class == IRB::Abort
end
raise ex
- ensure
- if should_checkin
- checkin(sock)
- end
end
result
end
@@ -150,30 +134,25 @@ def receive_message(operation, message, log_message=nil, socket=nil, command=fal
private
def receive(sock, cursor_id, exhaust=false)
- begin
- if exhaust
- docs = []
- num_received = 0
-
- while(cursor_id != 0) do
- receive_header(sock, cursor_id, exhaust)
- number_received, cursor_id = receive_response_header(sock)
- new_docs, n = read_documents(number_received, sock)
- docs += new_docs
- num_received += n
- end
-
- return [docs, num_received, cursor_id]
- else
+ if exhaust
+ docs = []
+ num_received = 0
+
+ while(cursor_id != 0) do
receive_header(sock, cursor_id, exhaust)
number_received, cursor_id = receive_response_header(sock)
- docs, num_received = read_documents(number_received, sock)
-
- return [docs, num_received, cursor_id]
+ new_docs, n = read_documents(number_received, sock)
+ docs += new_docs
+ num_received += n
end
- rescue Mongo::ConnectionFailure => ex
- close
- raise ex
+
+ return [docs, num_received, cursor_id]
+ else
+ receive_header(sock, cursor_id, exhaust)
+ number_received, cursor_id = receive_response_header(sock)
+ docs, num_received = read_documents(number_received, sock)
+
+ return [docs, num_received, cursor_id]
end
end
@@ -182,7 +161,6 @@ def receive_header(sock, expected_response, exhaust=false)
# unpacks to size, request_id, response_to
response_to = header.unpack('VVV')[2]
-
if !exhaust && expected_response != response_to
raise Mongo::ConnectionFailure, "Expected response #{expected_response} but got #{response_to}"
end
@@ -304,7 +282,6 @@ def send_message_on_socket(packed_message, socket)
end
total_bytes_sent
rescue => ex
- close
raise ConnectionFailure, "Operation failed with the following exception: #{ex}:#{ex.message}"
end
end
@@ -315,7 +292,7 @@ def receive_message_on_socket(length, socket)
begin
message = receive_data(length, socket)
rescue OperationTimeout, ConnectionFailure => ex
- close
+ socket.close
if ex.class == OperationTimeout
raise OperationTimeout, "Timed out waiting on socket read."
View
179 lib/mongo/repl_set_connection.rb
@@ -21,11 +21,11 @@ module Mongo
# Instantiates and manages connections to a MongoDB replica set.
class ReplSetConnection < Connection
- REPL_SET_OPTS = [:read, :refresh_mode, :refresh_interval, :require_primary,
- :read_secondary, :rs_name, :name]
+ REPL_SET_OPTS = [:read, :refresh_mode, :refresh_interval, :read_secondary,
+ :rs_name, :name, :tag_sets, :secondary_acceptable_latency_ms]
attr_reader :replica_set_name, :seeds, :refresh_interval, :refresh_mode,
- :refresh_version, :manager
+ :refresh_version, :manager, :tag_sets, :acceptable_latency
# Create a connection to a MongoDB replica set.
#
@@ -43,10 +43,23 @@ class ReplSetConnection < Connection
# propagated to DB objects instantiated off of this Connection. This
# default can be overridden upon instantiation of any DB by explicitly setting a :safe value
# on initialization.
- # @option opts [:primary, :secondary, :secondary_only] :read (:primary) The default read preference for Mongo::DB
- # objects created from this connection object. If +:secondary+ is chosen, reads will be sent
- # to one of the closest available secondary nodes. If a secondary node cannot be located, the
- # read will be sent to the primary.
+ # @option opts [:primary, :primary_preferred, :secondary, :secondary_preferred, :nearest] :read_preference (:primary)
+ # A "read preference" determines the candidate replica set members to which a query or command can be sent.
+ # [:primary]
+ # * Read from primary only.
+ # * Cannot be combined with tags.
+ # [:primary_preferred]
+ # * Read from primary if available, otherwise read from a secondary.
+ # [:secondary]
+ # * Read from secondary if available.
+ # [:secondary_preferred]
+ # * Read from a secondary if available, otherwise read from the primary.
+ # [:nearest]
+ # * Read from any member.
+ # @option opts [Array<Hash{ String, Symbol => Tag Value }>] :tag_sets ([])
+ # Read from replica-set members with these tags.
+ # @option opts [Integer] :secondary_acceptable_latency_ms (15) The acceptable
+ # nearest available member for a member to be considered "near".
# @option opts [Logger] :logger (nil) Logger instance to receive driver operation log.
# @option opts [Integer] :pool_size (1) The maximum number of socket connections allowed per
# connection pool. Note: this setting is relevant only for multi-threaded applications.
@@ -62,9 +75,7 @@ class ReplSetConnection < Connection
# will always trigger a complete refresh. This option is useful when you want to add new nodes
# or remove replica set nodes not currently in use by the driver.
# @option opts [Integer] :refresh_interval (90) If :refresh_mode is enabled, this is the number of seconds
- # between calls to check the replica set's state.
- # @option opts [Boolean] :require_primary (true) If true, require a primary node for the connection
- # to succeed. Otherwise, connection will succeed as long as there's at least one secondary node.
+ # between calls to check the replica set's state.
# @note the number of seed nodes does not have to be equal to the number of replica set members.
# The purpose of seed nodes is to permit the driver to find at least one replica set member even if a member is down.
#
@@ -161,10 +172,7 @@ def connect
@manager.connect
@refresh_version += 1
- if @require_primary && @manager.primary.nil? #TODO: in v2.0, we'll let this be optional and do a lazy connect.
- close
- raise ConnectionFailure, "Failed to connect to primary node."
- elsif @manager.read_pool.nil?
+ if @manager.pools.empty?
close
raise ConnectionFailure, "Failed to connect to any node."
else
@@ -220,7 +228,7 @@ def hard_refresh!
end
def connected?
- @connected && (@manager.primary_pool || @manager.read_pool)
+ @connected && !@manager.pools.empty?
end
# @deprecated
@@ -288,116 +296,68 @@ def reset_connection
end
# Returns +true+ if it's okay to read from a secondary node.
- # Since this is a replica set, this must always be true.
#
# This method exist primarily so that Cursor objects will
# generate query messages with a slaveOkay value of +true+.
#
# @return [Boolean] +true+
def slave_ok?
- true
+ @read != :primary
end
def authenticate_pools
- if primary_pool
- primary_pool.authenticate_existing
- end
- secondary_pools.each do |pool|
- pool.authenticate_existing
- end
+ pools.each { |pool| pool.authenticate_existing }
end
def logout_pools(db)
- if primary_pool
- primary_pool.logout_existing(db)
- end
- secondary_pools.each do |pool|
- pool.logout_existing(db)
- end
+ pools.each { |pool| pool.logout_existing(db) }
end
# Generic socket checkout
# Takes a block that returns a socket from pool
- def checkout(&block)
- if connected?
- sync_refresh
- else
- connect
- end
-
+ def checkout
+ ensure_manager
+
+ connected? ? sync_refresh : connect
+
begin
- socket = block.call
+ socket = yield
rescue => ex
checkin(socket) if socket
raise ex
end
-
+
if socket
socket
else
@connected = false
raise ConnectionFailure.new("Could not checkout a socket.")
end
- end
-
- # Checkout best available socket by trying primary
- # pool first and then falling back to secondary.
- def checkout_best
- checkout do
- socket = get_socket_from_pool(:primary)
- if !socket
- connect
- socket = get_socket_from_pool(:secondary)
- end
- socket
- end
+ socket
end
- # Checkout a socket for reading (i.e., a secondary node).
- # Note that @read_pool might point to the primary pool
- # if no read pool has been defined.
- def checkout_reader
+ def checkout_reader(mode=@read, tag_sets=@tag_sets, acceptable_latency=@acceptable_latency)
checkout do
- socket = get_socket_from_pool(:read)
- if !socket
- connect
- socket = get_socket_from_pool(:primary)
- end
- socket
- end
- end
-
- # Checkout a socket from a secondary
- # For :read_preference => :secondary_only
- def checkout_secondary
- checkout do
- get_socket_from_pool(:secondary)
+ pool = read_pool(mode, tag_sets, acceptable_latency)
+ get_socket_from_pool(pool)
end
end
# Checkout a socket for writing (i.e., a primary node).
def checkout_writer
checkout do
- get_socket_from_pool(:primary)
+ get_socket_from_pool(primary_pool)
end
end
# Checkin a socket used for reading.
def checkin(socket)
- if socket
+ if socket && socket.pool
socket.pool.checkin(socket)
end
sync_refresh
end
- def close_socket(socket)
- begin
- socket.close if socket
- rescue IOError
- log(:info, "Tried to close socket #{socket} but already closed.")
- end
- end
-
def ensure_manager
Thread.current[:managers] ||= Hash.new
@@ -406,25 +366,19 @@ def ensure_manager
end
end
- def get_socket_from_pool(pool_type)
- ensure_manager
+ def pin_pool(pool)
+ @manager.pinned_pools[Thread.current] = pool if @manager
+ end
- pool = case pool_type
- when :primary
- primary_pool
- when :secondary
- secondary_pool
- when :read
- read_pool
- end
+ def unpin_pool(pool)
+ @manager.pinned_pools[Thread.current] = nil if @manager
+ end
+ def get_socket_from_pool(pool)
begin
- if pool
- pool.checkout
- end
- rescue ConnectionFailure => ex
- log(:info, "Failed to checkout from #{pool} with #{ex.class}; #{ex.message}")
- return nil
+ pool.checkout if pool
+ rescue ConnectionFailure
+ nil
end
end
@@ -453,8 +407,8 @@ def primary_pool
local_manager ? local_manager.primary_pool : nil
end
- def read_pool
- local_manager ? local_manager.read_pool : nil
+ def read_pool(mode=@read, tags=@tag_sets, acceptable_latency=@acceptable_latency)
+ local_manager ? local_manager.read_pool(mode, tags, acceptable_latency) : nil
end
def secondary_pool
@@ -481,9 +435,6 @@ def max_bson_size
# Parse option hash
def setup(opts)
- # Require a primary node to connect?
- @require_primary = opts.fetch(:require_primary, true)
-
# Refresh
@refresh_mode = opts.fetch(:refresh_mode, false)
@refresh_interval = opts.fetch(:refresh_interval, 90)
@@ -500,17 +451,20 @@ def setup(opts)
"Refresh mode must be either :sync or false."
end
- # Are we allowing reads from secondaries?
+ # Determine read preference
if opts[:read_secondary]
warn ":read_secondary options has now been deprecated and will " +
"be removed in driver v2.0. Use the :read option instead."
@read_secondary = opts.fetch(:read_secondary, false)
- @read = :secondary
+ @read = :secondary_preferred
else
@read = opts.fetch(:read, :primary)
Mongo::Support.validate_read_preference(@read)
end
+ @tag_sets = opts.fetch(:tag_sets, [])
+ @acceptable_latency = opts.fetch(:secondary_acceptable_latency_ms, 15)
+
# Replica set name
if opts[:rs_name]
warn ":rs_name option has been deprecated and will be removed in v2.0. " +
@@ -524,24 +478,6 @@ def setup(opts)
super opts
end
-
- # Checkout a socket connected to a node with one of
- # the provided tags. If no such node exists, raise
- # an exception.
- #
- # NOTE: will be available in driver release v2.0.
- def checkout_tagged(tags)
- tags.each do |k, v|
- pool = self.tag_map[{k.to_s => v}]
- if pool
- socket = pool.checkout
- return socket
- end
- end
-
- raise NodeWithTagsNotFound,
- "Could not find a connection tagged with #{tags}."
- end
def prune_managers
@old_managers.each do |manager|
@@ -558,8 +494,9 @@ def prune_managers
def sync_refresh
if @refresh_mode == :sync &&
((Time.now - @last_refresh) > @refresh_interval)
+
@last_refresh = Time.now
-
+
if @refresh_mutex.try_lock
begin
refresh
View
8 lib/mongo/util/node.rb
@@ -79,9 +79,7 @@ def set_config
"#{ex.class}: #{ex.message}")
# Socket may already be nil from issuing command
- if @socket && !@socket.closed?
- @socket.close
- end
+ close
end
@config
@@ -119,6 +117,10 @@ def secondary?
@config['secondary'] == true || @config['secondary'] == 1
end
+ def tags
+ @config['tags'] || {}
+ end
+
def host_port
[@host, @port]
end
View
13 lib/mongo/util/pool.rb
@@ -72,7 +72,13 @@ def close(opts={})
close_sockets(@sockets)
@closed = true
end
+ @node.close if @node
end
+ true
+ end
+
+ def tags
+ @node.tags
end
def closed?
@@ -81,7 +87,8 @@ def closed?
def inspect
"#<Mongo::Pool:0x#{self.object_id.to_s(16)} @host=#{@host} @port=#{port} " +
- "@ping_time=#{@ping_time} #{@checked_out.size}/#{@size} sockets available.>"
+ "@ping_time=#{@ping_time} #{@checked_out.size}/#{@size} sockets available " +
+ "up=#{!closed?}>"
end
def host_string
@@ -132,8 +139,8 @@ def refresh_ping_time
def ping
begin
- return self.connection['admin'].command({:ping => 1}, :socket => @node.socket)
- rescue OperationFailure, SocketError, SystemCallError, IOError
+ return self.connection['admin'].command({:ping => 1}, :socket => @node.socket, :timeout => 1)
+ rescue ConnectionFailure, OperationFailure, SocketError, SystemCallError, IOError
return false
end
end
View
102 lib/mongo/util/pool_manager.rb
@@ -2,8 +2,10 @@ module Mongo
class PoolManager
attr_reader :connection, :arbiters, :primary, :secondaries, :primary_pool,
- :read_pool, :secondary_pool, :read, :secondary_pools, :hosts, :nodes,
- :max_bson_size, :members, :seeds
+ :secondary_pool, :secondary_pools, :hosts, :nodes, :members, :seeds,
+ :max_bson_size
+
+ attr_accessor :pinned_pools
# Create a new set of connection pools.
#
@@ -13,6 +15,7 @@ class PoolManager
# time. The union of these lists will be used when attempting to connect,
# with the newly-discovered nodes being used first.
def initialize(connection, seeds=[])
+ @pinned_pools = {}
@connection = connection
@seeds = seeds
@previously_connected = false
@@ -29,7 +32,6 @@ def connect
members = connect_to_members
initialize_pools(members)
cache_discovered_seeds(members)
- set_read_pool
@members = members
@previously_connected = true
@@ -88,18 +90,54 @@ def closed?
def close(opts={})
begin
- pools.each { |pool| pool.close(opts) if pool }
- @members.each { |member| member.close }
+ pools.each { |pool| pool.close(opts) }
rescue ConnectionFailure
end
end
- private
+ def read
+ read_pool.host_port
+ end
+
+ def read_pool(mode=@connection.read_preference, tags=@connection.tag_sets,
+ acceptable_latency=@connection.acceptable_latency)
+
+ if mode == :primary && !tags.empty?
+ raise MongoArgumentError, "Read preferecy :primary cannot be combined with tags"
+ end
+
+ pinned = @pinned_pools[Thread.current]
+ if mode && pinned && select_pool([pinned], tags, acceptable_latency) && !pinned.closed?
+ pool = pinned
+ else
+ pool = case mode
+ when :primary
+ @primary_pool
+ when :primary_preferred
+ @primary_pool || select_pool(@secondary_pools, tags, acceptable_latency)
+ when :secondary
+ select_pool(@secondary_pools, tags, acceptable_latency)
+ when :secondary_preferred
+ select_pool(@secondary_pools, tags, acceptable_latency) || @primary_pool
+ when :nearest
+ select_pool(pools, tags, acceptable_latency)
+ end
+ end
+
+ unless pool
+ raise ConnectionFailure, "No replica set member available for query " +
+ "with read preference matching mode #{mode} and tags matching #{tags}."
+ end
+
+ pool
+ end
def pools
- [@primary_pool, *@secondary_pools]
+ [@primary_pool, *@secondary_pools].compact
end
+ private
+
def validate_existing_member(member)
config = member.set_config
if !config
@@ -132,6 +170,7 @@ def initialize_data
@hosts = Set.new
@members = Set.new
@refresh_required = false
+ @pinned_pools = {}
end
# Connect to each member of the replica set
@@ -195,40 +234,31 @@ def assign_secondary(member)
@secondary_pools << pool
end
- # Pick a node from the set of possible secondaries.
- # If more than one node is available, use the ping
- # time to figure out which nodes to choose from.
- def set_read_pool
- if @secondary_pools.empty?
- @read_pool = @primary_pool
- elsif @secondary_pools.size == 1
- @read_pool = @secondary_pools[0]
- @secondary_pool = @read_pool
- else
- @read_pool = nearby_pool_from_set(@secondary_pools)
- @secondary_pool = @read_pool
- end
- @read = [@read_pool.host, @read_pool.port]
- end
+ def select_pool(candidates, tag_sets, acceptable_latency)
+ tag_sets = [tag_sets] unless tag_sets.is_a?(Array)
- def nearby_pool_from_set(pool_set)
- ping_ranges = Array.new(3) { |i| Array.new }
- pool_set.each do |pool|
- case pool.ping_time
- when 0..150
- ping_ranges[0] << pool
- when 150..1000
- ping_ranges[1] << pool
- else
- ping_ranges[2] << pool
+ if !tag_sets.empty?
+ matches = []
+ tag_sets.detect do |tag_set|
+ matches = candidates.select do |candidate|
+ tag_set.none? { |k,v| candidate.tags[k.to_s] != v } &&
+ candidate.ping_time
end
+ !matches.empty?
end
+ else
+ matches = candidates
+ end
- for list in ping_ranges do
- break if !list.empty?
- end
+ matches.empty? ? nil : near_pool(matches, acceptable_latency)
+ end
- list[rand(list.length)]
+ def near_pool(pool_set, acceptable_latency)
+ nearest_pool = pool_set.min_by { |pool| pool.ping_time }
+ near_pools = pool_set.select do |pool|
+ (pool.ping_time - nearest_pool.ping_time) <= acceptable_latency
+ end
+ near_pools[ rand(near_pools.length) ]
end
# Iterate through the list of provided seed
View
1  lib/mongo/util/sharding_pool_manager.rb
@@ -80,7 +80,6 @@ def connect
# or the members have changed, set @refresh_required to true, and return.
# The config.mongos find can't be part of the connect call chain due to infinite recursion
def check_connection_health
- puts "check_connection_health"
begin
seeds = @connection['config']['mongos'].find.to_a.map{|doc| doc['_id']}
if @seeds != seeds
View
24 lib/mongo/util/support.rb
@@ -23,6 +23,13 @@ module Support
include Mongo::Conversions
extend self
+ READ_PREFERENCES = [:primary, :primary_preferred, :secondary, :secondary_preferred, :nearest]
+
+ # Commands that may be sent to replica-set secondaries, depending on
+ # read preference and tags. All other commands are always run on the primary.
+ SECONDARY_OK_COMMANDS = ['group', 'aggregate', 'collstats', 'dbstats', 'count', 'distinct',
+ 'geonear', 'geosearch', 'geowalk']
+
# Generate an MD5 for authentication.
#
# @param [String] username
@@ -58,12 +65,25 @@ def validate_db_name(db_name)
db_name
end
+ def secondary_ok?(selector)
+ command = selector.keys.first.to_s.downcase
+
+ if command == 'mapreduce'
+ map_reduce = selector[command]
+ if map_reduce && map_reduce.is_a?(Hash) && map_reduce.has_key?('out')
+ map_reduce['out'] == 'inline' ? false : true
+ end
+ else
+ SECONDARY_OK_COMMANDS.member?(command)
+ end
+ end
+
def validate_read_preference(value)
- if [:primary, :secondary, :secondary_only, nil].include?(value)
+ if READ_PREFERENCES.include?(value)
return true
else
raise MongoArgumentError, "#{value} is not a valid read preference. " +
- "Please specify either :primary or :secondary or :secondary_only."
+ "Please specify one of the following read preferences as a symbol: #{READ_PREFERENCES}"
end
end
View
6 test/connection_test.rb
@@ -385,7 +385,7 @@ def test_connection_activity
end
should "close the connection on receive_message for major exceptions" do
- @con.expects(:checkout_writer).raises(SystemStackError)
+ @con.expects(:checkout_reader).raises(SystemStackError)
@con.expects(:close)
begin
@coll.find.next
@@ -421,11 +421,11 @@ def test_connection_activity
should "release connection if an exception is raised on receive_message" do
@con.stubs(:receive).raises(ConnectionFailure)
- assert_equal 0, @con.primary_pool.checked_out.size
+ assert_equal 0, @con.read_pool.checked_out.size
assert_raise ConnectionFailure do
@coll.find.to_a
end
- assert_equal 0, @con.primary_pool.checked_out.size
+ assert_equal 0, @con.read_pool.checked_out.size
end
should "show a proper exception message if an IOError is raised while closing a socket" do
View
6 test/replica_sets/basic_test.rb
@@ -58,7 +58,7 @@ def test_accessors
assert_equal 0, @conn.arbiters.length
assert_equal 2, @conn.secondary_pools.length
assert_equal @rs.name, @conn.replica_set_name
- assert @conn.secondary_pools.include?(@conn.read_pool)
+ assert @conn.secondary_pools.include?(@conn.read_pool(:secondary))
assert_equal 90, @conn.refresh_interval
assert_equal @conn.refresh_mode, false
#assert_equal 5, @conn.tag_map.keys.length unless @major_version < 2
@@ -92,10 +92,10 @@ def test_accessors
end
should "close the connection on receive_message for major exceptions" do
- @con.expects(:checkout_writer).raises(SystemStackError)
+ @con.expects(:checkout_reader).raises(SystemStackError)
@con.expects(:close)
begin
- @coll.find.next
+ @coll.find({}, :read => :primary).next
rescue SystemStackError
end
end
View
231 test/replica_sets/complex_read_preference_test.rb
@@ -0,0 +1,231 @@
+$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+require './test/replica_sets/rs_test_helper'
+require 'logger'
+
+# Tags for members:
+# 0 => {"dc" => "ny", "rack" => "a", "db" => "main"}
+# 1 => {"dc" => "ny", "rack" => "b", "db" => "main"}
+# 2 => {"dc" => "sf", "rack" => "a", "db" => "main"}
+
+class ComplexReadPreferenceTest < Test::Unit::TestCase
+ def setup
+ ensure_rs
+
+ # Insert data
+ conn = Connection.new(@rs.host, @rs.primary[1])
+ db = conn.db(MONGO_TEST_DB)
+ coll = db.collection("test-sets")
+ coll.save({:a => 20}, :safe => {:w => 2})
+ end
+
+ def test_primary_with_tags
+ # Test specifying a tag set with default read preference of primary throws and error
+ conn = make_connection({:tag_sets => {"rack" => "a"}})
+ assert_raise_error MongoArgumentError, "Read preferecy :primary cannot be combined with tags" do
+ conn.read_pool
+ end
+ end
+
+ def test_tags
+ assert_read_pool(:primary, {}, 0)
+ assert_read_pool(:primary_preferred, {}, 0)
+ assert_read_pool(:secondary, {}, [1,2])
+ assert_read_pool(:secondary_preferred, {}, [1,2])
+
+ # Test tag_sets are ignored on primary
+ assert_read_pool(:primary_preferred,
+ {"rack" => "b"}, 0)
+
+ # Test single tag
+ assert_read_pool(:secondary,
+ {"rack" => "a"}, 2)
+ assert_read_pool(:secondary,
+ {"rack" => "b"}, 1)
+ assert_read_pool(:secondary,
+ {"db" => "main"}, [1, 2])
+
+ # Test multiple tags
+ assert_read_pool(:secondary,
+ {"db" => "main", "rack" => "a"}, 2)
+ assert_read_pool(:secondary,
+ {"dc" => "ny", "rack" => "b", "db" => "main"}, 1)
+
+ # Test multiple tags failing
+ assert_fail_pool(:secondary,
+ {"dc" => "ny", "rack" => "a"})
+ assert_fail_pool(:secondary,
+ {"dc" => "ny", "rack" => "b", "db" => "main", "xtra" => "?"})
+
+ # Test symbol is converted to string for key
+ assert_read_pool(:secondary,
+ {:db => "main", "rack" => "b"}, 1)
+ assert_read_pool(:secondary,
+ {:db => "main", :rack => "b"}, 1)
+ assert_read_pool(:secondary,
+ {"db" => "main", :rack => "b"}, 1)
+
+ # Test secondary_preferred
+ assert_read_pool(:secondary_preferred,
+ {"dc" => "ny"}, 1)
+ assert_read_pool(:secondary_preferred,
+ {"dc" => "sf"}, 2)
+ assert_read_pool(:secondary_preferred,
+ {"dc" => "china"}, 0)
+
+ # Test secondary_preferred with no matching member
+ assert_read_pool(:secondary_preferred,
+ {"dc" => "bad"}, 0)
+ assert_read_pool(:secondary_preferred,
+ {"db" => "main", "dc" => "china"}, 0)
+ assert_read_pool(:secondary_preferred,
+ {"db" => "ny", "rack" => "a"}, 0)
+ end
+
+ def test_tag_sets
+ # Test primary_preferred overrides any tags when primary is available
+ assert_read_pool(:primary_preferred, [
+ {"dc" => "sf"}
+ ], 0)
+
+ # Test first tag_set takes priority over the second
+ assert_read_pool(:secondary, [
+ {"dc" => "sf"},
+ {"dc" => "ny"}
+ ], 2)
+ assert_read_pool(:secondary, [
+ {"dc" => "ny"},
+ {"dc" => "sf"}
+ ], 1)
+ assert_read_pool(:secondary_preferred, [
+ {"dc" => "sf"},
+ {"dc" => "ny"}
+ ], 2)
+ assert_read_pool(:secondary_preferred, [
+ {"dc" => "ny"},
+ {"dc" => "sf"}
+ ], 1)
+
+ # Test tags not matching any member throw an error
+ assert_fail_pool(:secondary, [
+ {"dc" => "ny", "rack" => "a"},
+ {"dc" => "sf", "rack" => "b"},
+ ])
+
+ # Test bad tags get skipped over
+ assert_read_pool(:secondary_preferred, [
+ {"bad" => "tag"},
+ {"dc" => "sf"}
+ ], 2)
+
+ # Test less selective tags
+ assert_read_pool(:secondary, [
+ {"dc" => "ny", "rack" => "b", "db" => "alt"},
+ {"dc" => "ny", "rack" => "a"},
+ {"dc" => "sf"}
+ ], 2)
+ assert_read_pool(:secondary_preferred, [
+ {"dc" => "ny", "rack" => "b", "db" => "alt"},
+ {"dc" => "ny", "rack" => "a"},
+ {"dc" => "sf"}
+ ], 2)
+ assert_read_pool(:secondary_preferred, [
+ {"dc" => "ny", "rack" => "a"},
+ {"dc" => "sf", "rack" => "b"},
+ {"db" => "main"}
+ ], [1,2])
+
+ # Test secondary preferred gives primary if no tags match
+ assert_read_pool(:secondary_preferred, [
+ {"dc" => "ny", "rack" => "a"},
+ {"dc" => "sf", "rack" => "b"}
+ ], 0)
+ assert_read_pool(:secondary_preferred, [
+ {"dc" => "ny", "rack" => "a"},
+ {"dc" => "sf", "rack" => "b"},
+ {"dc" => "ny", "rack" => "b"},
+ ], 1)
+
+ # Basic nearest test
+ assert_read_pool(:nearest, [
+ {"dc" => "ny", "rack" => "a"},
+ {"dc" => "sf", "rack" => "b"},
+ {"db" => "main"}
+ ], [0,1,2])
+ end
+
+ def test_nearest
+ # Test refresh happens on connection after interval has passed
+ conn = make_connection(
+ :read => :secondary_preferred,
+ :refresh_mode => :sync,
+ :refresh_interval => 1,
+ :secondary_acceptable_latency_ms => 10
+ )
+ pools = conn.manager.pools
+
+ # Connection should select node with 110 ping every time
+ set_pings(pools, [100,110,130])
+ sleep(2)
+
+ assert conn.read_pool == pools[1]
+
+ # Connection should select node with 100 ping every time
+ set_pings(pools, [100,120,100])
+ sleep(2)
+
+ assert conn.read_pool == pools[2]
+ end
+
+ def test_tags_and_nearest
+ # Test connection's read pool matches tags
+ assert_read_pool(:secondary_preferred, {"dc" => "sf"}, 2, [100,110,130])
+
+ # Test connection's read pool picks near pool (both match tags)
+ assert_read_pool(:secondary_preferred, {"db" => "main"}, 1, [100,110,130])
+ assert_read_pool(:secondary_preferred, {"db" => "main"}, 2, [100,130,110])
+ assert_read_pool(:secondary_preferred, {"db" => "fake"}, 0, [100,130,110])
+ end
+
+ private
+
+ def set_pings(pools, pings)
+ pools.sort! { |a,b| a.port <=> b.port }
+ pools.each_with_index do |pool, index|
+ pool.stubs(:ping_time).returns(pings[index])
+ end
+ end
+
+ def make_connection(opts = {})
+ ReplSetConnection.new(build_seeds(3), opts)
+ end
+
+ def assert_read_pool(mode=:primary, tags=[], node_nums=[0], pings=[], latency=10)
+ if pings.empty?
+ conn = make_connection({:read => mode, :tag_sets => tags})
+ else
+ conn = make_connection({
+ :read => mode,
+ :tag_sets => tags,
+ :refresh_mode => :sync,
+ :refresh_interval => 1,
+ :secondary_acceptable_latency_ms => latency
+ })
+
+ set_pings(conn.manager.pools, pings)
+ sleep(2)
+ end
+
+ assert conn[MONGO_TEST_DB]['test-sets'].find_one
+
+ target_ports = [*node_nums].collect {|num| @rs.ports[num]}
+
+ assert target_ports.member?(conn.read_pool.port)
+ end
+
+ def assert_fail_pool(mode=:primary, tags={})
+ assert_raise_error ConnectionFailure, "No replica set member available for query " +
+ "with read preference matching mode #{mode} and tags matching #{tags}." do
+ make_connection({:read => mode, :tag_sets => tags}).read_pool
+ end
+ end
+end
View
34 test/replica_sets/connect_test.rb
@@ -13,6 +13,14 @@ def teardown
ENV['MONGODB_URI'] = @old_mongodb_uri
end
+ def step_down_command
+ # Adding force=true to avoid 'no secondaries within 10 seconds of my optime' errors
+ step_down_command = BSON::OrderedHash.new
+ step_down_command[:replSetStepDown] = 60
+ step_down_command[:force] = true
+ step_down_command
+ end
+
# TODO: test connect timeout.
def test_connect_with_deprecated_multi
@@ -29,21 +37,6 @@ def test_connect_bad_name
end
end
- def test_connect_with_primary_node_killed
- @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 build_seeds(3)
- end
-
- # This allows the secondary to come up as a primary
- rescue_connection_failure do
- @conn = ReplSetConnection.new build_seeds(3)
- end
- end
-
def test_connect_with_secondary_node_killed
@rs.kill_secondary
@@ -69,13 +62,9 @@ def test_connect_with_primary_stepped_down
primary = Mongo::Connection.new(@conn.primary_pool.host, @conn.primary_pool.port)
assert_raise Mongo::ConnectionFailure do
- primary['admin'].command({:replSetStepDown => 60})
+ primary['admin'].command(step_down_command)
end
assert @conn.connected?
- assert_raise Mongo::ConnectionFailure do
- @conn[MONGO_TEST_DB]['bar'].find_one
- end
- assert !@conn.connected?
rescue_connection_failure do
@conn[MONGO_TEST_DB]['bar'].find_one
@@ -86,11 +75,6 @@ def test_save_with_primary_stepped_down
@conn = ReplSetConnection.new build_seeds(3)
primary = Mongo::Connection.new(@conn.primary_pool.host, @conn.primary_pool.port)
-
- # Adding force=true to avoid 'no secondaries within 10 seconds of my optime' errors
- step_down_command = BSON::OrderedHash.new
- step_down_command[:replSetStepDown] = 60
- step_down_command[:force] = true
assert_raise Mongo::ConnectionFailure do
primary['admin'].command(step_down_command)
end
View
2  test/replica_sets/count_test.rb
@@ -5,7 +5,7 @@ class ReplicaSetCountTest < Test::Unit::TestCase
def setup
ensure_rs
- @conn = ReplSetConnection.new(build_seeds(3), :read => :secondary)
+ @conn = ReplSetConnection.new(build_seeds(3), :read => :primary_preferred)
assert @conn.primary_pool
@primary = Connection.new(@conn.primary_pool.host, @conn.primary_pool.port)
@db = @conn.db(MONGO_TEST_DB)
View
69 test/replica_sets/cursor_test.rb
@@ -6,62 +6,65 @@ def setup
ensure_rs
end
+ def test_cursors_get_closed
+ setup_connection
+ assert_cursor_count
+ end
+
+ def test_cursors_get_closed_secondary
+ setup_connection(:secondary)
+ assert_cursor_count
+ end
+
+ private
+
def setup_connection(read=:primary)
# Setup ReplicaSet Connection
@replconn = Mongo::ReplSetConnection.new(
- build_seeds(2),
+ build_seeds(3),
:read => read
)
-
- # Setup Direct Connections
- @primary = Mongo::Connection.new(*@replconn.manager.primary)
- @secondary = Mongo::Connection.new(*@replconn.manager.read)
- end
-
- def setup_collection
+
@db = @replconn.db(MONGO_TEST_DB)
@db.drop_collection("cursor_tests")
@coll = @db.collection("cursor_tests")
- @coll.insert({:a => 1}, :safe => true)
- @coll.insert({:b => 2}, :safe => true)
- @coll.insert({:c => 3}, :safe => true)
+ @coll.insert({:a => 1}, :safe => true, :w => 3)
+ @coll.insert({:b => 2}, :safe => true, :w => 3)
+ @coll.insert({:c => 3}, :safe => true, :w => 3)
+
+ # Pin reader
+ @coll.find_one
+
+ # Setup Direct Connections
+ @primary = Mongo::Connection.new(*@replconn.manager.primary)
+ @read = Mongo::Connection.new(*@replconn.manager.read)
end
def cursor_count(connection)
connection['cursor_tests'].command({:cursorInfo => 1})['totalOpen']
end
+ def query_count(connection)
+ connection['admin'].command({:serverStatus => 1})['opcounters']['query']
+ end
+
def assert_cursor_count
- before_primary = cursor_count(@primary)
- before_secondary = cursor_count(@secondary)
+ before_primary = cursor_count(@primary)
+ before_read = cursor_count(@read)
+ before_query = query_count(@read)
@coll.find.limit(2).to_a
sleep(1)
- after_primary = cursor_count(@primary)
- after_secondary = cursor_count(@secondary)
+ after_primary = cursor_count(@primary)
+ after_read = cursor_count(@read)
+ after_query = query_count(@read)
assert_equal before_primary, after_primary
- assert_equal before_secondary, after_secondary
+ assert_equal before_read, after_read
+ assert_equal 1, after_query - before_query
end
- def test_cursors_get_closed
- setup_connection
- setup_collection
- assert_cursor_count
- end
-
- def test_cursors_get_closed_secondary
- setup_connection(:secondary)
- setup_collection
- assert_cursor_count
- end
-
- def test_cursors_get_closed_secondary_only
- setup_connection(:secondary_only)
- setup_collection
- assert_cursor_count
- end
end
View
287 test/replica_sets/read_preference_test.rb
@@ -5,80 +5,167 @@
class ReadPreferenceTest < Test::Unit::TestCase
def setup
- ensure_rs
- log = Logger.new("test.log")
- seeds = build_seeds(2)
- args = {
- :read => :secondary,
- :pool_size => 50,
- :refresh_mode => false,
- :refresh_interval => 5,
- :logger => log
- }
- @conn = ReplSetConnection.new(seeds, args)
- @db = @conn.db(MONGO_TEST_DB)
- @db.drop_collection("test-sets")
- end
+ ensure_rs(:secondary_count => 1, :arbiter_count => 1)
- def teardown
- @rs.restart_killed_nodes
+ # Insert data
+ conn = Connection.new(@rs.host, @rs.primary[1])
+ db = conn.db(MONGO_TEST_DB)
+ coll = db.collection("test-sets")
+ coll.save({:a => 20}, :safe => {:w => 2})
end
def test_read_primary
+ conn = make_connection
+ rescue_connection_failure do
+ assert conn.read_primary?
+ assert conn.primary?
+ end
+
+ conn = make_connection(:primary_preferred)
+ rescue_connection_failure do
+ assert conn.read_primary?
+ assert conn.primary?
+ end
+
+ conn = make_connection(:secondary)
rescue_connection_failure do
- assert !@conn.read_primary?
- assert !@conn.primary?
+ assert !conn.read_primary?
+ assert !conn.primary?
+ end
+
+ conn = make_connection(:secondary_preferred)
+ rescue_connection_failure do
+ assert !conn.read_primary?
+ assert !conn.primary?
end
end
- def test_con
- assert @conn.primary_pool, "No primary pool!"
- assert @conn.read_pool, "No read pool!"
- assert @conn.primary_pool.port != @conn.read_pool.port,
- "Primary port and read port at the same!"
+ def test_connection_pools
+ conn = make_connection
+ assert conn.primary_pool, "No primary pool!"
+ assert conn.read_pool, "No read pool!"
+ assert conn.primary_pool.port == conn.read_pool.port,
+ "Primary port and read port are not the same!"
+
+ conn = make_connection(:primary_preferred)
+ assert conn.primary_pool, "No primary pool!"
+ assert conn.read_pool, "No read pool!"
+ assert conn.primary_pool.port == conn.read_pool.port,
+ "Primary port and read port are not the same!"
+
+ conn = make_connection(:secondary)
+ assert conn.primary_pool, "No primary pool!"
+ assert conn.read_pool, "No read pool!"
+ assert conn.primary_pool.port != conn.read_pool.port,
+ "Primary port and read port are the same!"
+
+ conn = make_connection(:secondary_preferred)
+ assert conn.primary_pool, "No primary pool!"
+ assert conn.read_pool, "No read pool!"
+ assert conn.primary_pool.port != conn.read_pool.port,
+ "Primary port and read port are the same!"
end
- def test_read_secondary_only
- @rs.add_arbiter
- @rs.remove_secondary_node
-
- @conn = ReplSetConnection.new(build_seeds(3), :read => :secondary_only)
+ def test_read_routing
+ prepare_routing_test
+
+ # Test that reads are going to the right members
+ assert_query_route(@primary, @primary_direct)
+ assert_query_route(@primary_preferred, @primary_direct)
+ assert_query_route(@secondary, @secondary_direct)
+ assert_query_route(@secondary_preferred, @secondary_direct)
+ end
- @db = @conn.db(MONGO_TEST_DB)
- @coll = @db.collection("test-sets")
-
- @coll.save({:a => 20}, :safe => {:w => 2})
+ def test_read_routing_with_primary_down
+ prepare_routing_test
- # Test that reads are going to secondary on ReplSetConnection
- @secondary = Connection.new(@rs.host, @conn.read_pool.port, :slave_ok => true)
- queries_before = @secondary['admin'].command({:serverStatus => 1})['opcounters']['query']
- @coll.find_one
- queries_after = @secondary['admin'].command({:serverStatus => 1})['opcounters']['query']
- assert_equal 1, queries_after - queries_before
+ # Test that reads are going to the right members
+ assert_query_route(@primary, @primary_direct)
+ assert_query_route(@primary_preferred, @primary_direct)
+ assert_query_route(@secondary, @secondary_direct)
+ assert_query_route(@secondary_preferred, @secondary_direct)
+ # Kill the primary so only a single secondary exists
+ @rs.kill_primary
+
+ # Test that reads are going to the right members
+ assert_raise_error ConnectionFailure do
+ @primary[MONGO_TEST_DB]['test-sets'].find_one
+ end
+ assert_query_route(@primary_preferred, @secondary_direct)
+ assert_query_route(@secondary, @secondary_direct)
+ assert_query_route(@secondary_preferred, @secondary_direct)
+
+ # Restore set
+ @rs.restart_killed_nodes
+ @repl_cons.each { |con| con.refresh }
+ sleep(1)
+ @primary_direct = Connection.new(
+ @rs.host,
+ @primary.read_pool.port
+ )
+
+ # Test that reads are going to the right members
+ assert_query_route(@primary, @primary_direct)
+ assert_query_route(@primary_preferred, @primary_direct)
+ assert_query_route(@secondary, @secondary_direct)
+ assert_query_route(@secondary_preferred, @secondary_direct)
+ end
+
+ def test_read_routing_with_secondary_down
+ prepare_routing_test
+
+ # Test that reads are going to the right members
+ assert_query_route(@primary, @primary_direct)
+ assert_query_route(@primary_preferred, @primary_direct)
+ assert_query_route(@secondary, @secondary_direct)
+ assert_query_route(@secondary_preferred, @secondary_direct)
+
+ # Kill the secondary so that only primary exists
@rs.kill_secondary
- @conn.refresh
-
- # Test that reads are only allowed from secondaries
- assert_raise ConnectionFailure.new("Could not checkout a socket.") do
- @coll.find_one
+
+ # Test that reads are going to the right members
+ assert_query_route(@primary, @primary_direct)
+ assert_query_route(@primary_preferred, @primary_direct)
+ assert_raise_error ConnectionFailure do
+ @secondary[MONGO_TEST_DB]['test-sets'].find_one
end
-
- @rs = ReplSetManager.new
- @rs.start_set
+ assert_query_route(@secondary_preferred, @primary_direct)
+
+ # Restore set
+ @rs.restart_killed_nodes
+ @repl_cons.each { |con| con.refresh }
+ sleep(1)
+ @secondary_direct = Connection.new(
+ @rs.host,
+ @secondary.read_pool.port,
+ :slave_ok => true
+ )
+
+ # Test that reads are going to the right members
+ assert_query_route(@primary, @primary_direct)
+ assert_query_route(@primary_preferred, @primary_direct)
+ assert_query_route(@secondary, @secondary_direct)
+ assert_query_route(@secondary_preferred, @secondary_direct)
end
- def test_query_secondaries
- @secondary = Connection.new(@rs.host, @conn.read_pool.port, :slave_ok => true)
- @coll = @db.collection("test-sets", :safe => {:w => 3, :wtimeout => 20000})
+ def test_write_conecern
+ @conn = make_connection(:secondary_preferred)
+ @db = @conn[MONGO_TEST_DB]
+ @coll = @db.collection("test-sets", :safe => {
+ :w => 2, :wtimeout => 20000
+ })
@coll.save({:a => 20})
@coll.save({:a => 30})
@coll.save({:a => 40})
+
+ # pin the read pool
+ @coll.find_one
+ @secondary = Connection.new(@rs.host, @conn.read_pool.port, :slave_ok => true)
+
results = []
- queries_before = @secondary['admin'].command({:serverStatus => 1})['opcounters']['query']
@coll.find.each {|r| results << r["a"]}
- queries_after = @secondary['admin'].command({:serverStatus => 1})['opcounters']['query']
- assert_equal 1, queries_after - queries_before
+
assert results.include?(20)
assert results.include?(30)
assert results.include?(40)
@@ -87,57 +174,17 @@ def test_query_secondaries
results = []
rescue_connection_failure do
- #puts "@coll.find().each"
@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
end
- end
-
- def test_kill_primary
- @coll = @db.collection("test-sets", :safe => {:w => 3, :wtimeout => 10000})
- @coll.save({:a => 20})
- @coll.save({:a => 30})
- assert_equal 2, @coll.find.to_a.length
-
- # Should still be able to read immediately after killing master node
- @rs.kill_primary
- assert_equal 2, @coll.find.to_a.length
- rescue_connection_failure do
- @coll.save({:a => 50}, :safe => {:w => 2, :wtimeout => 10000})
- end
@rs.restart_killed_nodes
- sleep(1)
- @coll.save({:a => 50}, :safe => {:w => 2, :wtimeout => 10000})
- assert_equal 4, @coll.find.to_a.length
- end
-
- def test_kill_secondary
- @coll = @db.collection("test-sets", {:safe => {:w => 3, :wtimeout => 20000}})
- @coll.save({:a => 20})
- @coll.save({:a => 30})
- assert_equal 2, @coll.find.to_a.length
-
- read_node = @rs.get_node_from_port(@conn.read_pool.port)
- @rs.kill(read_node)
-
- # Should fail immediately on next read
- old_read_pool_port = @conn.read_pool.port
- assert_raise ConnectionFailure do
- @coll.find.to_a.length
- end
-
- # Should eventually reconnect and be able to read
- rescue_connection_failure do
- length = @coll.find.to_a.length
- assert_equal 2, length
- end
- new_read_pool_port = @conn.read_pool.port
- assert old_read_pool_port != new_read_pool_port
end
def test_write_lots_of_data
+ @conn = make_connection(:secondary_preferred)
+ @db = @conn[MONGO_TEST_DB]
@coll = @db.collection("test-sets", {:safe => {:w => 2}})
6000.times do |n|
@@ -149,33 +196,37 @@ def test_write_lots_of_data
cursor.close
end
- # TODO: enable this once we enable reads from tags.
- # def test_query_tagged
- # col = @db['mongo-test']
+ private
- # col.insert({:a => 1}, :safe => {:w => 3})
- # col.find_one({}, :read => {:db => "main"})
- # col.find_one({}, :read => {:dc => "ny"})
- # col.find_one({}, :read => {:dc => "sf"})
+ def prepare_routing_test
+ # Setup replica set connections
+ @primary = make_connection(:primary)
+ @primary_preferred = make_connection(:primary_preferred)
+ @secondary = make_connection(:secondary)
+ @secondary_preferred = make_connection(:secondary_preferred)
+ @repl_cons = [@primary, @primary_preferred, @secondary, @secondary_preferred]
- # assert_raise Mongo::NodeWithTagsNotFound do
- # col.find_one({}, :read => {:foo => "bar"})
- # end
-
- # threads = []
- # 100.times do
- # threads << Thread.new do
- # col.find_one({}, :read => {:dc => "sf"})
- # end
- # end
-
- # threads.each {|t| t.join }
+ # Setup direct connections
+ @primary_direct = Connection.new(@rs.host, @primary.read_pool.port)
+ @secondary_direct = Connection.new(@rs.host, @secondary.read_pool.port, :slave_ok => true)
+ end
- # col.remove
- # end
+ def make_connection(mode = :primary, opts = {})
+ opts.merge!({:read => mode})
+ ReplSetConnection.new(build_seeds(3), opts)
+ end
- #def teardown
- # @rs.restart_killed_nodes
- #end
+ def query_count(connection)
+ connection['admin'].command({:serverStatus => 1})['opcounters']['query']
+ end
+ def assert_query_route(test_connection, expected_target)
+ #puts "#{test_connection.read_pool.port} #{expected_target.read_pool.port}"
+ queries_before = query_count(expected_target)
+ assert_nothing_raised do
+ test_connection['MONGO_TEST_DB']['test-sets'].find_one
+ end
+ queries_after = query_count(expected_target)
+ assert_equal 1, queries_after - queries_before
+ end
end
View
6 test/replica_sets/refresh_test.rb
@@ -69,12 +69,12 @@ def test_automated_refresh_with_secondaries_down
rescue_connection_failure do
@conn = ReplSetConnection.new(build_seeds(3),
- :refresh_interval => 2, :refresh_mode => :sync)
+ :refresh_interval => 2, :refresh_mode => :sync, :read => :secondary_preferred)
end
assert_equal [], @conn.secondaries
assert @conn.connected?
- assert_equal @conn.read_pool, @conn.primary_pool
+ assert_equal @conn.manager.read, @conn.manager.primary
old_refresh_version = @conn.refresh_version
@rs.restart_killed_nodes
@@ -86,7 +86,7 @@ def test_automated_refresh_with_secondaries_down
"Refresh version hasn't changed."
assert @conn.secondaries.length > 0,
"No secondaries have been added."
- assert @conn.read_pool != @conn.primary_pool,
+ assert @conn.manager.read != @conn.manager.primary,
"Read pool and primary pool are identical."
end
View
2  test/replica_sets/refresh_with_threads_test.rb
@@ -21,7 +21,7 @@ def test_read_write_load_with_added_nodes
args = {
:refresh_interval => 5,
:refresh_mode => :sync,
- :read => :secondary
+ :read => :secondary_preferred
}
@conn = ReplSetConnection.new(seeds, args)
@duplicate = @conn[MONGO_TEST_DB]['duplicate']
View
4 test/replica_sets/rs_test_helper.rb
@@ -5,10 +5,10 @@
class Test::Unit::TestCase
# Ensure replica set is available as an instance variable and that
# a new set is spun up for each TestCase class
- def ensure_rs
+ def ensure_rs(opts={})
unless defined?(@@current_class) and @@current_class == self.class
@@current_class = self.class
- @@rs = ReplSetManager.new
+ @@rs = ReplSetManager.new(opts)
@@rs.start_set
end
@rs = @@rs
View
18 test/tools/repl_set_manager.rb
@@ -138,13 +138,13 @@ def start_cmd(n)
def remove_secondary_node
primary = get_node_with_state(1)
con = get_connection(primary)
- config = con['local']['system.replset'].find_one
+ @config = con['local']['system.replset'].find_one
secondary = get_node_with_state(2)
host_port = "#{@host}:#{@mongods[secondary]['port']}"
kill(secondary)
@mongods.delete(secondary)
@config['members'].reject! {|m| m['host'] == host_port}
- @config['version'] = config['version'] + 1
+ @config['version'] += 1
begin
con['admin'].command({'replSetReconfig' => @config})
@@ -161,9 +161,9 @@ def add_node(n=nil, &block)
primary = get_node_with_state(1)
con = get_connection(primary)
+ @config = con['local']['system.replset'].find_one
init_node(n || @mongods.length, &block)
- config = con['local']['system.replset'].find_one
- @config['version'] = config['version'] + 1
+ @config['version'] += 1
# We expect a connection failure on reconfigure here.
begin
@@ -175,8 +175,8 @@ def add_node(n=nil, &block)
ensure_up
end
- def add_arbiter
- add_node do |attrs|
+ def add_arbiter(n=nil)
+ add_node(n) do |attrs|
attrs['arbiterOnly'] = true
end
end
@@ -184,14 +184,14 @@ def add_arbiter
def wait_for_death(pid)
@retries.times do
if `ps a | grep mongod`.include?("#{pid}")
- puts "waiting for mongod @ pid #{pid} to die..."
+ #puts "waiting for mongod @ pid #{pid} to die..."
sleep(1)
else
#puts "mongod @ pid #{pid} was killed successfully"
return true
end
end
- puts "mongod never died"
+ #puts "mongod never died"
return false
end
@@ -334,7 +334,7 @@ def shard_string
str
end
- private
+ #private
def initiate
#puts "Initiating replica set..."
View
2  test/unit/collection_test.rb
@@ -37,7 +37,7 @@ class CollectionTest < Test::Unit::TestCase
@conn = Connection.new('localhost', 27017, :logger => @logger, :connect => false)
@db = @conn['testing']
@coll = @db.collection('books')
- @conn.expects(:checkout_writer).returns(mock())
+ @conn.expects(:checkout_reader).returns(mock(:pool))
@conn.expects(:receive_message).with do |op, msg, log, sock|
op == 2004
end.returns([[], 0, 0])
View
17 test/unit/cursor_test.rb
@@ -6,10 +6,13 @@ class CursorTest < Test::Unit::TestCase
@logger = mock()
@logger.stubs(:debug)
@connection = stub(:class => Connection, :logger => @logger,
- :slave_ok? => false, :read_preference => :primary, :log_duration => false)
+ :slave_ok? => false, :read_preference => :primary, :log_duration => false,
+ :tag_sets => {}, :acceptable_latency => 10)
@db = stub(:name => "testing", :slave_ok? => false,
- :connection => @connection, :read_preference => :primary)
- @collection = stub(:db => @db, :name => "items", :read_preference => :primary)
+ :connection => @connection, :read_preference => :primary,
+ :tag_sets => {}, :acceptable_latency => 10)
+ @collection = stub(:db => @db, :name => "items", :read_preference => :primary,
+ :tag_sets => {}, :acceptable_latency => 10)
@cursor = Cursor.new(@collection)
end
@@ -103,9 +106,11 @@ class CursorTest < Test::Unit::TestCase
@logger = mock()
@logger.stubs(:debug)
@connection = stub(:class => Connection, :logger => @logger, :slave_ok? => false,
- :log_duration => false)
- @db = stub(:slave_ok? => true, :name => "testing", :connection => @connection)
- @collection = stub(:db => @db, :name => "items", :read_preference => :primary)
+ :log_duration => false, :tag_sets =>{}, :acceptable_latency => 10)
+ @db = stub(:slave_ok? => true, :name => "testing", :connection => @connection,
+ :tag_sets => {}, :acceptable_latency => 10)
+ @collection = stub(:db => @db, :name => "items", :read_preference => :primary,
+ :tag_sets => {}, :acceptable_latency => 10)
end
should "when an array should return a hash with each key" do
View
4 test/unit/db_test.rb
@@ -16,9 +16,13 @@ class DBTest < Test::Unit::TestCase
@conn = stub()
@conn.stubs(:safe)
@conn.stubs(:read_preference)
+ @conn.stubs(:tag_sets)
+ @conn.stubs(:acceptable_latency)
@db = DB.new("testing", @conn)
@db.stubs(:safe)
@db.stubs(:read_preference)
+ @db.stubs(:tag_sets)
+ @db.stubs(:acceptable_latency)
@collection = mock()
@db.stubs(:system_command_collection).returns(@collection)
end
View
2  test/unit/grid_test.rb
@@ -7,6 +7,8 @@ class GridTest < Test::Unit::TestCase
@conn = stub()
@conn.stubs(:safe)
@conn.stubs(:read_preference)
+ @conn.stubs(:tag_sets)
+ @conn.stubs(:acceptable_latency)
@db = DB.new("testing", @conn)
@files = mock()
@chunks = mock()
View
47 test/unit/read_test.rb
@@ -10,33 +10,54 @@ class ReadTest < Test::Unit::TestCase
end
- context "Read mode on replica set connection: " do
+ context "Read preferences on replica set connection: " do
setup do
- @read_preference = :secondary
- @con = Mongo::ReplSetConnection.new(['localhost:27017'], :read => @read_preference, :connect => false)
+ @read_preference = :secondary_preferred
+ @acceptable_latency = 100
+ @tags = {"dc" => "Tyler", "rack" => "Brock"}
+ @bad_tags = {"wow" => "cool"}
+ @con = Mongo::ReplSetConnection.new(
+ ['localhost:27017'],
+ :read => @read_preference,
+ :tag_sets => @tags,
+ :secondary_acceptable_latency_ms => @acceptable_latency,
+ :connect => false
+ )
end