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 # @param opts Any of the options available for Connection.new
# #
# @return [Mongo::Connection] # @return [Mongo::Connection, Mongo::ReplSetConnection]
def self.from_uri(uri, opts={}) def self.from_uri(string, extra_opts={})
nodes, auths = Mongo::URIParser.parse(uri) uri = URIParser.new(string)
opts.merge!({:auths => auths}) opts = uri.connection_options
if nodes.length == 1 opts.merge!(extra_opts)
Connection.new(nodes[0][0], nodes[0][1], opts)
elsif nodes.length > 1 if uri.nodes.length == 1
nodes << opts opts.merge!({:auths => uri.auths})
ReplSetConnection.new(*nodes) 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 else
raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node." raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node."
end 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 # @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. # 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 # @option options [Boolean, Hash] :safe (false) Set the default safe-mode options
# propogated to DB objects instantiated off of this Connection. This # 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 # default can be overridden upon instantiation of any DB by explicity setting a :safe value

View File

@ -17,28 +17,99 @@
# ++ # ++
module Mongo module Mongo
module URIParser class URIParser
DEFAULT_PORT = 27017 DEFAULT_PORT = 27017
MONGODB_URI_MATCHER = /(([-_.\w\d]+):([-_\w\d]+)@)?([-.\w\d]+)(:([\w\d]+))?(\/([-\d\w]+))?/ 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]" 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. # 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. # Returns an array of nodes and an array of db authorizations, if applicable.
# def initialize(string)
# @private
def parse(string)
if string =~ /^mongodb:\/\// if string =~ /^mongodb:\/\//
string = string[10..-1] string = string[10..-1]
else else
raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}" raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
end end
nodes = [] hosts, opts = string.split('?')
auths = [] parse_hosts(hosts)
specs = string.split(',') 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| specs.each do |spec|
matches = MONGODB_URI_MATCHER.match(spec) matches = MONGODB_URI_MATCHER.match(spec)
if !matches if !matches
@ -52,8 +123,8 @@ module Mongo
if !(port.to_s =~ /^\d+$/) if !(port.to_s =~ /^\d+$/)
raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits." raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits."
end end
port = port.to_i port = port.to_i
db = matches[8] db = matches[8]
if uname && pwd && db if uname && pwd && db
auths << {'db_name' => db, 'username' => uname, 'password' => pwd} auths << {'db_name' => db, 'username' => uname, 'password' => pwd}
@ -62,10 +133,47 @@ module Mongo
"and db if any one of these is specified." "and db if any one of these is specified."
end 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 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 end
end end

View File

@ -16,8 +16,8 @@ class ConnectTest < Test::Unit::TestCase
def test_connect_with_deprecated_multi def test_connect_with_deprecated_multi
@conn = Connection.multi([[RS.host, RS.ports[0]], [RS.host, RS.ports[1]]], :name => RS.name) @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.is_a?(ReplSetConnection)
assert @conn.connected?
end end
def test_connect_bad_name 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) TCPSocket.stubs(:new).returns(new_mock_socket)
admin_db = new_mock_db admin_db = new_mock_db
admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}) admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}).twice
@conn.expects(:[]).with('admin').returns(admin_db) @conn.expects(:[]).with('admin').returns(admin_db).twice
@conn.connect @conn.connect
end end
@ -65,8 +65,8 @@ class ConnectionTest < Test::Unit::TestCase
@conn = Connection.from_uri("mongodb://localhost", :connect => false) @conn = Connection.from_uri("mongodb://localhost", :connect => false)
admin_db = new_mock_db admin_db = new_mock_db
admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}) admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1}).twice
@conn.expects(:[]).with('admin').returns(admin_db) @conn.expects(:[]).with('admin').returns(admin_db).twice
@conn.expects(:apply_saved_authentication) @conn.expects(:apply_saved_authentication)
@conn.connect @conn.connect
end end

View File

@ -67,16 +67,6 @@ class ReplSetConnectionTest < Test::Unit::TestCase
assert_equal ['localhost', 27017], @conn.nodes[0] assert_equal ['localhost', 27017], @conn.nodes[0]
assert_equal ['mydb.com', 27018], @conn.nodes[1] assert_equal ['mydb.com', 27018], @conn.nodes[1]
end 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 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