Use ENV["MONGODB_URI"] if available.

This commit is contained in:
Seamus Abshere 2012-05-18 11:17:21 -05:00 committed by Tyler Brock
parent 351eeb65a2
commit 1454210d9f
8 changed files with 296 additions and 84 deletions

View File

@ -173,6 +173,14 @@ Certain Ruby application servers work by forking, and it has long been necessary
re-establish the child process's connection to the database after fork. But with the release
of v1.3.0, the Ruby driver detects forking and reconnects automatically.
## Environment variable `MONGODB_URI`
`Mongo::Connection.new` and `Mongo::ReplSetConnection.new` will use <code>ENV["MONGODB_URI"]</code> if no other args are provided.
The URI must fit this specification:
mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
## String Encoding
The BSON ("Binary JSON") format used to communicate with Mongo requires that

View File

@ -32,6 +32,7 @@ module Mongo
Thread.abort_on_exception = true
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 27017
GENERIC_OPTS = [:ssl, :auths, :pool_size, :pool_timeout, :timeout, :op_timeout, :connect_timeout, :safe, :logger, :connect]
CONNECTION_OPTS = [:slave_ok]
@ -44,6 +45,8 @@ module Mongo
# Create a connection to single MongoDB instance.
#
# If no args are provided, it will check <code>ENV["MONGODB_URI"]</code>.
#
# You may specify whether connection to slave is permitted.
# In all cases, the default host is "localhost" and the default port is 27017.
#
@ -76,7 +79,7 @@ module Mongo
# connection attempt.
# @option opts [Boolean] :ssl (false) If true, create the connection to the server using SSL.
#
# @example localhost, 27017
# @example localhost, 27017 (or <code>ENV["MONGODB_URI"]</code> if available)
# Mongo::Connection.new
#
# @example localhost, 27017
@ -93,9 +96,22 @@ module Mongo
# @raise [ReplicaSetConnectionError] This is raised if a replica set name is specified and the
# driver fails to connect to a replica set with that name.
#
# @raise [MongoArgumentError] If called with no arguments and <code>ENV["MONGODB_URI"]</code> implies a replica set.
#
# @core self.connections
def initialize(host=nil, port=nil, opts={})
@host_to_try = format_pair(host, port)
if host.nil? and ENV.has_key?('MONGODB_URI')
parser = URIParser.new ENV['MONGODB_URI'], opts
if parser.replicaset?
raise MongoArgumentError, "Mongo::Connection.new called with no arguments, but ENV['MONGODB_URI'] implies a replica set."
end
opts = parser.connection_options
@host_to_try = [parser.host, parser.port]
elsif host.is_a?(String)
@host_to_try = [host, (port || DEFAULT_PORT).to_i]
else
@host_to_try = [DEFAULT_HOST, DEFAULT_PORT]
end
# Host and port of current master.
@host = @port = nil
@ -143,8 +159,7 @@ module Mongo
def self.multi(nodes, opts={})
warn "Connection.multi is now deprecated and will be removed in v2.0. Please use ReplSetConnection.new instead."
nodes << opts
ReplSetConnection.new(*nodes)
ReplSetConnection.new(*(nodes+[opts]))
end
# Initialize a connection to MongoDB using the MongoDB URI spec:
@ -155,21 +170,9 @@ module Mongo
# @param opts Any of the options available for Connection.new
#
# @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
def self.from_uri(uri, extra_opts={})
parser = URIParser.new uri, extra_opts
parser.connection
end
# The host name used for this connection.
@ -337,7 +340,7 @@ module Mongo
# @param [String] from_host host of the 'from' database.
# @param [String] username username for authentication against from_db (>=1.3.x).
# @param [String] password password for authentication against from_db (>=1.3.x).
def copy_database(from, to, from_host="localhost", username=nil, password=nil)
def copy_database(from, to, from_host=DEFAULT_HOST, username=nil, password=nil)
oh = BSON::OrderedHash.new
oh[:copydb] = 1
oh[:fromhost] = from_host
@ -585,23 +588,8 @@ module Mongo
write_logging_startup_message
end
should_connect = opts.fetch(:connect, true)
connect if should_connect
end
## Configuration helper methods
# Returns a host-port pair.
#
# @return [Array]
#
# @private
def format_pair(host, port)
case host
when String
[host, port ? port.to_i : DEFAULT_PORT]
when nil
['localhost', DEFAULT_PORT]
if opts.fetch(:connect, true)
connect
end
end

View File

@ -29,6 +29,8 @@ module Mongo
# Create a connection to a MongoDB replica set.
#
# If no args are provided, it will check <code>ENV["MONGODB_URI"]</code>.
#
# Once connected to a replica set, you can find out which nodes are primary, secondary, and
# arbiters with the corresponding accessors: Connection#primary, Connection#secondaries, and
# Connection#arbiters. This is useful if your application needs to connect manually to nodes other
@ -78,6 +80,8 @@ module Mongo
#
# @see http://api.mongodb.org/ruby/current/file.REPLICA_SETS.html Replica sets in Ruby
#
# @raise [MongoArgumentError] If called with no arguments and <code>ENV["MONGODB_URI"]</code> implies a direct connection.
#
# @raise [ReplicaSetConnectionError] This is raised if a replica set name is specified and the
# driver fails to connect to a replica set with that name.
def initialize(*args)
@ -87,21 +91,30 @@ module Mongo
opts = {}
end
unless args.length > 0
nodes = args
if nodes.empty? and ENV.has_key?('MONGODB_URI')
parser = URIParser.new ENV['MONGODB_URI'], opts
if parser.direct?
raise MongoArgumentError, "Mongo::ReplSetConnection.new called with no arguments, but ENV['MONGODB_URI'] implies a direct connection."
end
opts = parser.connection_options
nodes = parser.nodes
end
unless nodes.length > 0
raise MongoArgumentError, "A ReplSetConnection requires at least one seed node."
end
# This is temporary until support for the old format is dropped
@seeds = []
if args.first.last.is_a?(Integer)
if nodes.first.last.is_a?(Integer)
warn "Initiating a ReplSetConnection with seeds passed as individual [host, port] array arguments is deprecated."
warn "Please specify hosts as an array of 'host:port' strings; the old format will be removed in v2.0"
@seeds = args
@seeds = nodes
else
args.first.map do |host_port|
seed = host_port.split(":")
seed[1] = seed[1].to_i
seeds << seed
@seeds = nodes.first.map do |host_port|
host, port = host_port.split(":")
[ host, port.to_i ]
end
end

View File

@ -16,11 +16,11 @@
# limitations under the License.
# ++
require 'uri'
module Mongo
class URIParser
DEFAULT_PORT = 27017
USER_REGEX = /([-.\w:]+)/
PASS_REGEX = /([^@,]+)/
AUTH_REGEX = /(#{USER_REGEX}:#{PASS_REGEX}@)?/
@ -37,7 +37,7 @@ module Mongo
SPEC_ATTRS = [:nodes, :auths]
OPT_ATTRS = [:connect, :replicaset, :slaveok, :safe, :w, :wtimeout, :fsync, :journal, :connecttimeoutms, :sockettimeoutms, :wtimeoutms]
OPT_VALID = {:connect => lambda {|arg| ['direct', 'replicaset'].include?(arg)},
OPT_VALID = {:connect => lambda {|arg| ['direct', 'replicaset', 'true', 'false', true, false].include?(arg)},
:replicaset => lambda {|arg| arg.length > 0},
:slaveok => lambda {|arg| ['true', 'false'].include?(arg)},
:safe => lambda {|arg| ['true', 'false'].include?(arg)},
@ -50,7 +50,7 @@ module Mongo
:wtimeoutms => lambda {|arg| arg =~ /^\d+$/ }
}
OPT_ERR = {:connect => "must be 'direct' or 'replicaset'",
OPT_ERR = {:connect => "must be 'direct', 'replicaset', 'true', or 'false'",
: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'",
@ -63,7 +63,7 @@ module Mongo
:wtimeoutms => "must be an integer specifying milliseconds"
}
OPT_CONV = {:connect => lambda {|arg| arg},
OPT_CONV = {:connect => lambda {|arg| arg == 'false' ? false : arg}, # be sure to convert 'false' to FalseClass
:replicaset => lambda {|arg| arg},
:slaveok => lambda {|arg| arg == 'true' ? true : false},
:safe => lambda {|arg| arg == 'true' ? true : false},
@ -81,22 +81,73 @@ module Mongo
# 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.
#
# Note: passwords can contain any character except for a ','.
# @note Passwords can contain any character except for ','
#
# @param [String] uri The MongoDB URI string.
# @param [Hash,nil] extra_opts Extra options. Will override anything already specified in the URI.
#
# @core connections
def initialize(string)
if string =~ /^mongodb:\/\//
string = string[10..-1]
def initialize(uri, extra_opts={})
if uri.start_with?('mongodb://')
uri = uri[10..-1]
else
raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
end
hosts, opts = string.split('?')
hosts, opts = uri.split('?')
parse_hosts(hosts)
parse_options(opts)
configure_connect
parse_options(opts, extra_opts)
validate_connect
end
# Create a Mongo::Connection or a Mongo::ReplSetConnection based on the URI.
#
# @note Don't confuse this with attribute getter method #connect.
#
# @return [Connection,ReplSetConnection]
def connection
if replicaset?
ReplSetConnection.new(*(nodes+[connection_options]))
else
Connection.new(host, port, connection_options)
end
end
# Whether this represents a replica set.
# @return [true,false]
def replicaset?
replicaset.is_a?(String) || nodes.length > 1
end
# Whether to immediately connect to the MongoDB node[s]. Defaults to true.
# @return [true, false]
def connect?
connect != false
end
# Whether this represents a direct connection.
#
# @note Specifying :connect => 'direct' has no effect... other than to raise an exception if other variables suggest a replicaset.
#
# @return [true,false]
def direct?
!replicaset?
end
# For direct connections, the host of the (only) node.
# @return [String]
def host
nodes[0][0]
end
# For direct connections, the port of the (only) node.
# @return [Integer]
def port
nodes[0][1].to_i
end
# Options that can be passed to Mongo::Connection.new or Mongo::ReplSetConnection.new
# @return [Hash]
def connection_options
opts = {}
@ -136,14 +187,22 @@ module Mongo
end
if @slaveok
if @connect == 'direct'
if direct?
opts[:slave_ok] = true
else
opts[:read] = :secondary
end
end
opts[:name] = @replicaset if @replicaset
if direct?
opts[:auths] = auths
end
if replicaset.is_a?(String)
opts[:name] = replicaset
end
opts[:connect] = connect?
opts
end
@ -167,7 +226,7 @@ module Mongo
hosturis.each do |hosturi|
# If port is present, use it, otherwise use default port
host, port = hosturi.split(':') + [DEFAULT_PORT]
host, port = hosturi.split(':') + [Connection::DEFAULT_PORT]
if !(port.to_s =~ /^\d+$/)
raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits."
@ -178,6 +237,10 @@ module Mongo
@nodes << [host, port]
end
if @nodes.empty?
raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node."
end
if uname && pwd && db
auths << {'db_name' => db, 'username' => uname, 'password' => pwd}
elsif uname || pwd
@ -191,40 +254,37 @@ module Mongo
# This method uses the lambdas defined in OPT_VALID and OPT_CONV to validate
# and convert the given options.
def parse_options(opts)
def parse_options(string_opts, extra_opts={})
# initialize instance variables for available options
OPT_VALID.keys.each { |k| instance_variable_set("@#{k}", nil) }
return unless opts
string_opts ||= ''
separator = opts.include?('&') ? '&' : ';'
opts.split(separator).each do |attr|
key, value = attr.split('=')
key = key.downcase.to_sym
value = value.strip.downcase
return if string_opts.empty? && extra_opts.empty?
opts = URI.decode_www_form(string_opts).inject({}) do |memo, (key, value)|
memo[key.downcase.to_sym] = value.strip.downcase
memo
end
opts.merge! extra_opts
opts.each do |key, value|
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]}"
raise MongoArgumentError, "Invalid value #{value.inspect} for #{key}: #{OPT_ERR[key]}"
end
end
end
def configure_connect
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"
def validate_connect
if replicaset? and @connect == 'direct'
# Make sure the user doesn't specify something contradictory
raise MongoArgumentError, "connect=direct conflicts with setting a replicaset name"
end
end
end

View File

@ -58,6 +58,18 @@ class TestConnection < Test::Unit::TestCase
assert_equal mongo_port, con.primary_pool.port
end
def test_env_mongodb_uri
begin
old_mongodb_uri = ENV['MONGODB_URI']
ENV['MONGODB_URI'] = "mongodb://#{host_port}"
con = Connection.new
assert_equal mongo_host, con.primary_pool.host
assert_equal mongo_port, con.primary_pool.port
ensure
ENV['MONGODB_URI'] = old_mongodb_uri
end
end
def test_server_version
assert_match(/\d\.\d+(\.\d+)?/, @conn.server_version.to_s)
end

View File

@ -106,6 +106,20 @@ class ConnectTest < Test::Unit::TestCase
assert @conn.connected?
end
def test_connect_with_connection_string_in_env_var
begin
old_mongodb_uri = ENV['MONGODB_URI']
ENV['MONGODB_URI'] = "mongodb://#{@rs.host}:#{@rs.ports[0]},#{@rs.host}:#{@rs.ports[1]}?replicaset=#{@rs.name}"
silently do
@conn = ReplSetConnection.new
end
assert @conn.is_a?(ReplSetConnection)
assert @conn.connected?
ensure
ENV['MONGODB_URI'] = old_mongodb_uri
end
end
def test_connect_with_new_seed_format
@conn = ReplSetConnection.new build_seeds(3)
assert @conn.connected?
@ -128,4 +142,21 @@ class ConnectTest < Test::Unit::TestCase
assert @conn.safe[:fsync]
assert @conn.read_pool
end
def test_connect_with_full_connection_string_in_env_var
begin
old_mongodb_uri = ENV['MONGODB_URI']
ENV['MONGODB_URI'] = "mongodb://#{@rs.host}:#{@rs.ports[0]},#{@rs.host}:#{@rs.ports[1]}?replicaset=#{@rs.name};safe=true;w=2;fsync=true;slaveok=true"
silently do
@conn = ReplSetConnection.new
end
assert @conn.is_a?(ReplSetConnection)
assert @conn.connected?
assert_equal 2, @conn.safe[:w]
assert @conn.safe[:fsync]
assert @conn.read_pool
ensure
ENV['MONGODB_URI'] = old_mongodb_uri
end
end
end

View File

@ -130,5 +130,93 @@ class ConnectionTest < Test::Unit::TestCase
end
end
end
context "initializing with ENV['MONGODB_URI']" do
setup do
@old_mongodb_uri = ENV['MONGODB_URI']
end
teardown do
ENV['MONGODB_URI'] = @old_mongodb_uri
end
should "parse a simple uri" do
ENV['MONGODB_URI'] = "mongodb://localhost?connect=false"
@conn = Connection.new
assert_equal ['localhost', 27017], @conn.host_to_try
end
should "allow a complex host names" do
host_name = "foo.bar-12345.org"
ENV['MONGODB_URI'] = "mongodb://#{host_name}?connect=false"
@conn = Connection.new
assert_equal [host_name, 27017], @conn.host_to_try
end
should "allow db without username and password" do
host_name = "foo.bar-12345.org"
ENV['MONGODB_URI'] = "mongodb://#{host_name}/foo?connect=false"
@conn = Connection.new
assert_equal [host_name, 27017], @conn.host_to_try
end
should "set safe options on connection" do
host_name = "localhost"
opts = "safe=true&w=2&wtimeoutMS=1000&fsync=true&journal=true&connect=false"
ENV['MONGODB_URI'] = "mongodb://#{host_name}/foo?#{opts}"
@conn = Connection.new
assert_equal({:w => 2, :wtimeout => 1000, :fsync => true, :j => true}, @conn.safe)
end
should "set timeout options on connection" do
host_name = "localhost"
opts = "connectTimeoutMS=1000&socketTimeoutMS=5000&connect=false"
ENV['MONGODB_URI'] = "mongodb://#{host_name}/foo?#{opts}"
@conn = Connection.new
assert_equal 1, @conn.connect_timeout
assert_equal 5, @conn.op_timeout
end
should "parse a uri with a hyphen & underscore in the username or password" do
ENV['MONGODB_URI'] = "mongodb://hyphen-user_name:p-s_s@localhost:27017/db?connect=false"
@conn = Connection.new
assert_equal ['localhost', 27017], @conn.host_to_try
auth_hash = { 'db_name' => 'db', 'username' => 'hyphen-user_name', "password" => 'p-s_s' }
assert_equal auth_hash, @conn.auths[0]
end
should "attempt to connect" do
TCPSocket.stubs(:new).returns(new_mock_socket)
ENV['MONGODB_URI'] = "mongodb://localhost?connect=false" # connect=false ??
@conn = Connection.new
admin_db = new_mock_db
admin_db.expects(:command).returns({'ok' => 1, 'ismaster' => 1})
@conn.expects(:[]).with('admin').returns(admin_db)
@conn.connect
end
should "raise an error on invalid uris" do
ENV['MONGODB_URI'] = "mongo://localhost"
assert_raise MongoArgumentError do
Connection.new
end
ENV['MONGODB_URI'] = "mongodb://localhost:abc"
assert_raise MongoArgumentError do
Connection.new
end
end
should "require all of username, if password and db are specified" do
ENV['MONGODB_URI'] = "mongodb://kyle:jones@localhost/db?connect=false"
assert Connection.new
ENV['MONGODB_URI'] = "mongodb://kyle:password@localhost"
assert_raise MongoArgumentError do
Connection.new
end
end
end
end
end

View File

@ -63,9 +63,10 @@ class URITest < Test::Unit::TestCase
assert_equal "test", parser.auths[1]["db_name"]
end
def test_opts_basic
def test_opts_with_semincolon_separator
parser = Mongo::URIParser.new('mongodb://localhost:27018?connect=direct;slaveok=true;safe=true')
assert_equal 'direct', parser.connect
assert parser.direct?
assert parser.slaveok
assert parser.safe
end
@ -73,10 +74,17 @@ class URITest < Test::Unit::TestCase
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.direct?
assert parser.slaveok
assert parser.safe
end
def test_opts_made_invalid_by_mixed_separators
assert_raise_error ArgumentError, "invalid data of application/x-www-form-urlencoded (replicaset=foo;bar&slaveok=true&safe=true)" do
Mongo::URIParser.new('mongodb://localhost:27018?replicaset=foo;bar&slaveok=true&safe=true')
end
end
def test_opts_safe
parser = Mongo::URIParser.new('mongodb://localhost:27018?safe=true;w=2;journal=true;fsync=true;wtimeoutMS=200')
assert parser.safe
@ -93,12 +101,16 @@ class URITest < Test::Unit::TestCase
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
assert parser.replicaset?
end
def test_opts_conflicting_replica_set
assert_raise_error MongoArgumentError, "connect=direct conflicts with setting a replicaset name" do
Mongo::URIParser.new('mongodb://localhost:27018?connect=direct;replicaset=foo')
end
end
def test_case_insensitivity