Connection.from_uri and Connection.paired. Connection API enhancement.

This commit is contained in:
Kyle Banker 2010-02-17 15:15:07 -05:00
parent fc2ddf3bbd
commit f176a45a20
3 changed files with 230 additions and 38 deletions

View File

@ -30,7 +30,10 @@ module Mongo
STANDARD_HEADER_SIZE = 16
RESPONSE_HEADER_SIZE = 20
attr_reader :logger, :size, :host, :port, :nodes, :sockets, :checked_out
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, :host, :port, :nodes, :auths, :sockets, :checked_out
# Counter for generating unique request ids.
@@current_request_id = 0
@ -74,21 +77,24 @@ module Mongo
# @example localhost, 3000, where this node may be a slave
# Connection.new("localhost", 3000, :slave_ok => true)
#
# @example A pair of servers. The driver will always talk to master.
# # On connection errors, Mongo::ConnectionFailure will be raised.
# @example DEPRECATED. To initialize a paired connection, use Connection.paired instead.
# Connection.new({:left => ["db1.example.com", 27017],
# :right => ["db2.example.com", 27017]})
#
# @example A pair of servers with connection pooling enabled. Note the nil param placeholder for port.
# @example DEPRECATED. To initialize a paired connection, use Connection.paired instead.
# Connection.new({:left => ["db1.example.com", 27017],
# :right => ["db2.example.com", 27017]}, nil,
# :pool_size => 20, :timeout => 5)
#
# @see http://www.mongodb.org/display/DOCS/Replica+Pairs+in+Ruby Replica pairs in Ruby
#
# @core connections constructor_details
# @core connections
def initialize(pair_or_host=nil, port=nil, options={})
@nodes = format_pair(pair_or_host, port)
if block_given?
@nodes, @auths = yield self
else
@nodes = format_pair(pair_or_host, port)
end
# Host and port of current master.
@host = @port = nil
@ -120,7 +126,50 @@ module Mongo
@options = options
should_connect = options[:connect].nil? ? true : options[:connect]
connect_to_master if should_connect
if should_connect
connect_to_master
authenticate_databases if @auths
end
end
# Initialize a paired connection to MongoDB.
#
# @param nodes [Array] An array of arrays, each of which specified a host and port.
# @param opts Takes the same options as Connection.new
#
# @example
# Connection.new([["db1.example.com", 27017],
# ["db2.example.com", 27017]])
#
# @example
# Connection.new([["db1.example.com", 27017],
# ["db2.example.com", 27017]],
# :pool_size => 20, :timeout => 5)
#
# @return [Mongo::Connection]
def self.paired(nodes, opts={})
unless nodes.length == 2 && nodes.all? {|n| n.is_a? Array}
raise MongoArgumentError, "Connection.paired requires that exactly two nodes be specified."
end
# 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|
[[con.pair_val_to_connection(nodes[0]), con.pair_val_to_connection(nodes[1])], []]
end
end
# Initialize a connection to MongoDB using the MongoDB URI spec:
#
# @param uri [String]
# A string of the format mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/database]
#
# @param opts Any of the options available for Connection.new
#
# @return [Mongo::Connection]
def self.from_uri(uri, opts={})
new(nil, nil, opts) do |con|
con.parse_uri(uri)
end
end
# Return a hash with all database names
@ -350,6 +399,86 @@ module Mongo
@checked_out.clear
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 Hash
warn "Initializing a paired connection with Connection.new is deprecated. Use Connection.pair instead."
connections = []
connections << pair_val_to_connection(pair_or_host[:left])
connections << pair_val_to_connection(pair_or_host[:right])
connections
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) && !(uname && pwd && db)
raise MongoArgumentError, "MongoDB URI must include all three of username, password, " +
"and db if any one of these is specified."
else
auths << [uname, pwd, db]
end
nodes << [host, port]
end
[nodes, auths]
end
private
# Return a socket to the pool.
@ -526,38 +655,18 @@ module Mongo
message
end
## Private helper methods
# Returns an array of host-port pairs.
def format_pair(pair_or_host, port)
case pair_or_host
when String
[[pair_or_host, port ? port.to_i : DEFAULT_PORT]]
when Hash
connections = []
connections << pair_val_to_connection(pair_or_host[:left])
connections << pair_val_to_connection(pair_or_host[:right])
connections
when nil
[['localhost', DEFAULT_PORT]]
# Authenticate for any auth info provided on instantiating the connection.
# Only called when a MongoDB URI has been used to instantiate the connection, and
# when that connection specifies databases and authentication credentials.
#
# @raise [MongoDBError]
def authenticate_databases
@auths.each do |auth|
user = auth[0]
pwd = auth[1]
db_name = auth[2]
self.db(db_name).authenticate(user, pwd) || raise(MongoDBError, "Failed to authenticate db #{db_name}.")
end
end
# Turns an array containing a host name string and a
# port number integer into a [host, port] pair array.
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
end
end

View File

@ -135,6 +135,15 @@ class DBTest < Test::Unit::TestCase
@@db.remove_user('spongebob')
end
def test_authenticate_with_connection_uri
@@db.add_user('spongebob', 'squarepants')
assert Mongo::Connection.from_uri("mongodb://spongebob:squarepants@localhost/#{@@db.name}")
assert_raise MongoDBError do
Mongo::Connection.from_uri("mongodb://wrong:info@localhost/#{@@db.name}")
end
end
def test_logout
assert @@db.logout
end

View File

@ -38,6 +38,80 @@ class ConnectionTest < Test::Unit::TestCase
assert !@conn.slave_ok?
end
end
context "initializing a paired connection" do
should "require left and right nodes" do
assert_raise MongoArgumentError do
Connection.paired(['localhost', 27018], :connect => false)
end
assert_raise MongoArgumentError do
Connection.paired(['localhost', 27018], :connect => false)
end
end
should "store both nodes" do
@conn = Connection.paired([['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]
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]
assert_equal ['kyle', 's3cr3t', 'app'], @conn.auths[0]
assert_equal ['mickey', 'm0u5e', 'dsny'], @conn.auths[1]
end
should "attempt to connect" do
TCPSocket.stubs(:new).returns(new_mock_socket)
@conn = Connection.from_uri("mongodb://localhost", :connect => false)
admin_db = new_mock_db
admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1})
@conn.expects(:[]).with('admin').returns(admin_db)
@conn.connect_to_master
end
should "raise an error on invalid uris" do
assert_raise MongoArgumentError do
Connection.from_uri("mongo://localhost", :connect => false)
end
assert_raise MongoArgumentError do
Connection.from_uri("mongodb://localhost:abc", :connect => false)
end
assert_raise MongoArgumentError do
Connection.from_uri("mongodb://localhost:27017, my.db.com:27018, ", :connect => false)
end
end
should "require all of username, password, and database if any one is specified" do
assert_raise MongoArgumentError do
Connection.from_uri("mongodb://localhost/db", :connect => false)
end
assert_raise MongoArgumentError do
Connection.from_uri("mongodb://kyle:password@localhost", :connect => false)
end
end
end
end
context "with a nonstandard port" do