RUBY-205 RUBY-150 Support new connection URI options

This commit is contained in:
Kyle Banker 2010-12-30 15:40:50 -05:00
parent 9da68bb3db
commit 4e5b1a7d23
8 changed files with 247 additions and 36 deletions

View File

@ -138,15 +138,19 @@ module Mongo
#
# @param opts Any of the options available for Connection.new
#
# @return [Mongo::Connection]
def self.from_uri(uri, opts={})
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)
# @return [Mongo::Connection, Mongo::ReplSetConnection]
def self.from_uri(string, extra_opts={})
uri = URIParser.new(string)
opts = uri.connection_options
opts.merge!(extra_opts)
if uri.nodes.length == 1
opts.merge!({:auths => uri.auths})
Connection.new(uri.nodes[0][0], uri.nodes[0][1], opts)
elsif uri.nodes.length > 1
nodes = uri.nodes.clone
nodes_with_opts = nodes << opts
ReplSetConnection.new(*nodes_with_opts)
else
raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node."
end

View File

@ -32,6 +32,8 @@ module Mongo
# @param [Array] args A list of host-port pairs ending with a hash containing any options. See
# the examples below for exactly how to use the constructor.
#
# @option options [String] :rs_name (nil) The name of the replica set to connect to. You
# can use this option to verify that you're connecting to the right replica set.
# @option options [Boolean, Hash] :safe (false) Set the default safe-mode options
# propogated to DB objects instantiated off of this Connection. This
# default can be overridden upon instantiation of any DB by explicity setting a :safe value

View File

@ -17,28 +17,99 @@
# ++
module Mongo
module URIParser
class 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]"
SPEC_ATTRS = [:nodes, :auths]
OPT_ATTRS = [:connect, :replicaset, :slaveok, :safe, :w, :wtimeout, :fsync]
extend self
OPT_VALID = {:connect => lambda {|arg| ['direct', 'replicaset'].include?(arg)},
:replicaset => lambda {|arg| arg.length > 0},
:slaveok => lambda {|arg| ['true', 'false'].include?(arg)},
:safe => lambda {|arg| ['true', 'false'].include?(arg)},
:w => lambda {|arg| arg =~ /^\d+$/ },
:wtimeout => lambda {|arg| arg =~ /^\d+$/ },
:fsync => lambda {|arg| ['true', 'false'].include?(arg)}
}
OPT_ERR = {:connect => "must be 'direct' or 'replicaset'",
:replicaset => "must be a string containing the name of the replica set to connect to",
:slaveok => "must be 'true' or 'false'",
:safe => "must be 'true' or 'false'",
:w => "must be an integer specifying number of nodes to replica to",
:wtimeout => "must be an integer specifying milliseconds",
:fsync => "must be 'true' or 'false'"
}
OPT_CONV = {:connect => lambda {|arg| arg},
:replicaset => lambda {|arg| arg},
:slaveok => lambda {|arg| arg == 'true' ? true : false},
:safe => lambda {|arg| arg == 'true' ? true : false},
:w => lambda {|arg| arg.to_i},
:wtimeout => lambda {|arg| arg.to_i},
:fsync => lambda {|arg| arg == 'true' ? true : false}
}
ATTRS = SPEC_ATTRS + OPT_ATTRS
attr_reader *ATTRS
# 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)
def initialize(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(',')
hosts, opts = string.split('?')
parse_hosts(hosts)
parse_options(opts)
configure_connect
end
def connection_options
opts = {}
if (@w || @wtimeout || @fsync) && !@safe
raise MongoArgumentError, "Safe must be true if w, wtimeout, or fsync is specified"
end
if @safe
if @w || @wtimeout || @fsync
safe_opts = {}
safe_opts[:w] = @w if @w
safe_opts[:wtimeout] = @wtimeout if @wtimeout
safe_opts[:fsync] = @fsync if @fsync
else
safe_opts = true
end
opts[:safe] = safe_opts
end
if @slaveok
if @connect == 'direct'
opts[:slave_ok] = true
else
opts[:read_secondary] = true
end
end
opts[:rs_name] = @replicaset if @replicaset
opts
end
private
def parse_hosts(hosts)
@nodes = []
@auths = []
specs = hosts.split(',')
specs.each do |spec|
matches = MONGODB_URI_MATCHER.match(spec)
if !matches
@ -52,8 +123,8 @@ module Mongo
if !(port.to_s =~ /^\d+$/)
raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits."
end
port = port.to_i
db = matches[8]
port = port.to_i
db = matches[8]
if uname && pwd && db
auths << {'db_name' => db, 'username' => uname, 'password' => pwd}
@ -62,10 +133,47 @@ module Mongo
"and db if any one of these is specified."
end
nodes << [host, port]
@nodes << [host, port]
end
end
# This method uses the lambdas defined in OPT_VALID and OPT_CONV to validate
# and convert the given options.
def parse_options(opts)
return unless opts
separator = opts.include?('&') ? '&' : ';'
opts.split(separator).each do |attr|
key, value = attr.split('=')
key = key.to_sym
value = value.strip.downcase
if !OPT_ATTRS.include?(key)
raise MongoArgumentError, "Invalid Mongo URI option #{key}"
end
if OPT_VALID[key].call(value)
instance_variable_set("@#{key}", OPT_CONV[key].call(value))
else
raise MongoArgumentError, "Invalid value for #{key}: #{OPT_ERR[key]}"
end
end
end
def configure_connect
if @nodes.length > 1 && !@connect
@connect = 'replicaset'
end
[nodes, auths]
if !@connect
if @nodes.length > 1
@connect = 'replicaset'
else
@connect = 'direct'
end
end
if @connect == 'direct' && @replicaset
raise MongoArgumentError, "If specifying a replica set name, please also specify that connect=replicaset"
end
end
end
end

View File

@ -16,8 +16,8 @@ class ConnectTest < Test::Unit::TestCase
def test_connect_with_deprecated_multi
@conn = Connection.multi([[RS.host, RS.ports[0]], [RS.host, RS.ports[1]]], :name => RS.name)
assert @conn.connected?
assert @conn.is_a?(ReplSetConnection)
assert @conn.connected?
end
def test_connect_bad_name

View File

@ -0,0 +1,32 @@
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
require './test/replica_sets/rs_test_helper'
# 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 ConnectionStringTest < Test::Unit::TestCase
include Mongo
def setup
RS.restart_killed_nodes
end
def teardown
RS.restart_killed_nodes
end
def test_connect_with_connection_string
@conn = Connection.from_uri("mongodb://#{RS.host}:#{RS.ports[0]},#{RS.host}:#{RS.ports[1]}?replicaset=#{RS.name}")
assert @conn.is_a?(ReplSetConnection)
assert @conn.connected?
end
def test_connect_with_full_connection_string
@conn = Connection.from_uri("mongodb://#{RS.host}:#{RS.ports[0]},#{RS.host}:#{RS.ports[1]}?replicaset=#{RS.name};safe=true;w=2;fsync=true;slaveok=true")
assert @conn.is_a?(ReplSetConnection)
assert @conn.connected?
assert_equal 2, @conn.safe[:w]
assert @conn.safe[:fsync]
assert @conn.read_pool
end
end

View File

@ -22,8 +22,8 @@ class ConnectionTest < Test::Unit::TestCase
TCPSocket.stubs(:new).returns(new_mock_socket)
admin_db = new_mock_db
admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1})
@conn.expects(:[]).with('admin').returns(admin_db)
admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}).twice
@conn.expects(:[]).with('admin').returns(admin_db).twice
@conn.connect
end
@ -65,8 +65,8 @@ class ConnectionTest < Test::Unit::TestCase
@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)
admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}).twice
@conn.expects(:[]).with('admin').returns(admin_db).twice
@conn.expects(:apply_saved_authentication)
@conn.connect
end

View File

@ -67,16 +67,6 @@ class ReplSetConnectionTest < Test::Unit::TestCase
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

75
test/uri_test.rb Normal file
View File

@ -0,0 +1,75 @@
require './test/test_helper'
class TestThreading < Test::Unit::TestCase
include Mongo
def test_uri_without_port
parser = Mongo::URIParser.new('mongodb://localhost')
assert_equal 1, parser.nodes.length
assert_equal 'localhost', parser.nodes[0][0]
assert_equal 27017, parser.nodes[0][1]
end
def test_basic_uri
parser = Mongo::URIParser.new('mongodb://localhost:27018')
assert_equal 1, parser.nodes.length
assert_equal 'localhost', parser.nodes[0][0]
assert_equal 27018, parser.nodes[0][1]
end
def test_multiple_uris
parser = Mongo::URIParser.new('mongodb://a.example.com:27018,b.example.com')
assert_equal 2, parser.nodes.length
assert_equal 'a.example.com', parser.nodes[0][0]
assert_equal 27018, parser.nodes[0][1]
assert_equal 'b.example.com', parser.nodes[1][0]
assert_equal 27017, parser.nodes[1][1]
end
def test_multiple_uris_with_auths
parser = Mongo::URIParser.new('mongodb://bob:secret@a.example.com:27018/test,joe:secret2@b.example.com/test2')
assert_equal 2, parser.nodes.length
assert_equal 'a.example.com', parser.nodes[0][0]
assert_equal 27018, parser.nodes[0][1]
assert_equal 'b.example.com', parser.nodes[1][0]
assert_equal 27017, parser.nodes[1][1]
assert_equal 2, parser.auths.length
assert_equal "bob", parser.auths[0]["username"]
assert_equal "secret", parser.auths[0]["password"]
assert_equal "test", parser.auths[0]["db_name"]
assert_equal "joe", parser.auths[1]["username"]
assert_equal "secret2", parser.auths[1]["password"]
assert_equal "test2", parser.auths[1]["db_name"]
end
def test_opts_basic
parser = Mongo::URIParser.new('mongodb://localhost:27018?connect=direct;slaveok=true;safe=true')
assert_equal 'direct', parser.connect
assert parser.slaveok
assert parser.safe
end
def test_opts_with_amp_separator
parser = Mongo::URIParser.new('mongodb://localhost:27018?connect=direct&slaveok=true&safe=true')
assert_equal 'direct', parser.connect
assert parser.slaveok
assert parser.safe
end
def test_opts_safe
parser = Mongo::URIParser.new('mongodb://localhost:27018?safe=true;w=2;wtimeout=200;fsync=true')
assert parser.safe
assert_equal 2, parser.w
assert_equal 200, parser.wtimeout
assert parser.fsync
end
def test_opts_replica_set
assert_raise_error MongoArgumentError, "specify that connect=replicaset" do
Mongo::URIParser.new('mongodb://localhost:27018?replicaset=foo')
end
parser = Mongo::URIParser.new('mongodb://localhost:27018?connect=replicaset;replicaset=foo')
assert_equal 'foo', parser.replicaset
assert_equal 'replicaset', parser.connect
end
end