diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index 7fde81c..00f54b4 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -130,7 +130,7 @@ module Mongo end raise RuntimeError, "Unknown options [#{options.inspect}]" unless options.empty? - cursor = @db.query(self, Query.new(selector, fields, skip, limit, sort, hint, snapshot, timeout)) + cursor = @db.query(self, Query.new(selector, fields, skip, limit, sort, hint, snapshot, timeout, @db.slave_ok?)) if block_given? yield cursor cursor.close() diff --git a/lib/mongo/db.rb b/lib/mongo/db.rb index 22b49c5..feeec2b 100644 --- a/lib/mongo/db.rb +++ b/lib/mongo/db.rb @@ -160,6 +160,9 @@ module Mongo is_master = master? @semaphore.lock if semaphore_is_locked + if !@slave_ok && !is_master + raise ConfigurationError, "Trying to connect directly to slave; if this is what you want, specify :slave_ok => true." + end @slave_ok || is_master rescue SocketError, SystemCallError, IOError => ex close if @socket diff --git a/lib/mongo/errors.rb b/lib/mongo/errors.rb index 3eefbe6..7fd6b43 100644 --- a/lib/mongo/errors.rb +++ b/lib/mongo/errors.rb @@ -15,6 +15,12 @@ # Exceptions raised by the MongoDB driver. module Mongo + # Generic Mongo Ruby Driver exception class. + class MongoRubyError < StandardError; end + + # Raised when configuration options cause connections, queries, etc., to fail. + class ConfigurationError < MongoRubyError; end + # Raised when a database operation fails. class OperationFailure < RuntimeError; end diff --git a/lib/mongo/message/opcodes.rb b/lib/mongo/message/opcodes.rb index 235ecd7..4a19bc9 100644 --- a/lib/mongo/message/opcodes.rb +++ b/lib/mongo/message/opcodes.rb @@ -25,5 +25,6 @@ module Mongo OP_DELETE = 2006 OP_KILL_CURSORS = 2007 + OP_QUERY_SLAVE_OK = 4 OP_QUERY_NO_CURSOR_TIMEOUT = 16 end diff --git a/lib/mongo/query.rb b/lib/mongo/query.rb index e8392d1..af056c3 100644 --- a/lib/mongo/query.rb +++ b/lib/mongo/query.rb @@ -67,9 +67,9 @@ module Mongo # the normal cursor timeout behavior of the mongod process. # When +false+, the returned cursor will never timeout. Care should # be taken to ensure that cursors with timeout disabled are properly closed. - def initialize(sel={}, return_fields=nil, number_to_skip=0, number_to_return=0, order_by=nil, hint=nil, snapshot=nil, timeout=true) - @number_to_skip, @number_to_return, @order_by, @hint, @snapshot, @timeout = - number_to_skip, number_to_return, order_by, hint, snapshot, timeout + def initialize(sel={}, return_fields=nil, number_to_skip=0, number_to_return=0, order_by=nil, hint=nil, snapshot=nil, timeout=true, slave_ok=false) + @number_to_skip, @number_to_return, @order_by, @hint, @snapshot, @timeout, @slave_ok = + number_to_skip, number_to_return, order_by, hint, snapshot, timeout, slave_ok @explain = nil self.selector = sel self.fields = return_fields @@ -121,7 +121,9 @@ module Mongo # Returns an integer indicating which query options have been selected. # See http://www.mongodb.org/display/DOCS/Mongo+Wire+Protocol#MongoWireProtocol-OPQUERY def query_opts - @timeout ? 0 : OP_QUERY_NO_CURSOR_TIMEOUT + timeout = @timeout ? 0 : OP_QUERY_NO_CURSOR_TIMEOUT + slave_ok = @slave_ok ? OP_QUERY_SLAVE_OK : 0 + slave_ok + timeout end def to_s diff --git a/test/messages/test_query_message.rb b/test/messages/test_query_message.rb index ea481ef..572a1f9 100644 --- a/test/messages/test_query_message.rb +++ b/test/messages/test_query_message.rb @@ -37,4 +37,22 @@ class TestQueryMessage < Test::Unit::TestCase assert_equal 16, buf[16] end + def test_timeout_opcodes + @timeout = true + @slave_ok = true + @query = Query.new({}, nil, 0, 0, nil, nil, nil, @timeout, @slave_ok) + @query_message = QueryMessage.new('db', 'collection', @query) + buf = @query_message.buf.instance_variable_get(:@buf) + assert_equal 4, buf[16] + + + @timeout = false + @slave_ok = true + @query = Query.new({}, nil, 0, 0, nil, nil, nil, @timeout, @slave_ok) + @query_message = QueryMessage.new('db', 'collection', @query) + buf = @query_message.buf.instance_variable_get(:@buf) + assert_equal 20, buf[16] + end + + end diff --git a/test/test_query.rb b/test/test_query.rb index 119c411..0210dfa 100644 --- a/test/test_query.rb +++ b/test/test_query.rb @@ -33,4 +33,22 @@ class TestQuery < Test::Unit::TestCase assert_equal 16, @query.query_opts end + def test_slave_ok_opcodes + @slave_ok = true + @query = Query.new({}, nil, 0, 0, nil, nil, nil, true, @slave_ok) + assert_equal 4, @query.query_opts + + + @slave_ok = false + @query = Query.new({}, nil, 0, 0, nil, nil, nil, true, @slave_ok) + assert_equal 0, @query.query_opts + end + + def test_combined_opcodes + @timeout = false + @slave_ok = true + @query = Query.new({}, nil, 0, 0, nil, nil, nil, @timeout, @slave_ok) + assert_equal 20, @query.query_opts + end + end diff --git a/test/test_slave_connection.rb b/test/test_slave_connection.rb new file mode 100644 index 0000000..1863c87 --- /dev/null +++ b/test/test_slave_connection.rb @@ -0,0 +1,42 @@ +$LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') +require 'mongo' +require 'test/unit' + +# NOTE: these tests are run only if we can connect to a single MongoDB in slave mode. +class SlaveConnectionTest < Test::Unit::TestCase + include Mongo + + def self.connect_to_slave + @@host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' + @@port = ENV['MONGO_RUBY_DRIVER_PORT'] || Connection::DEFAULT_PORT + db = Connection.new(@@host, @@port, :slave_ok => true).db('ruby-mongo-demo') + !db.master? + end + + if self.connect_to_slave + puts "Connected to slave; running slave tests." + + def test_connect_to_slave + assert_raise Mongo::ConfigurationError do + @db = Connection.new(@@host, @@port, :slave_ok => false).db('ruby-mongo-demo') + end + end + + def test_slave_ok_sent_to_queries + @db = Connection.new(@@host, @@port, :slave_ok => true).db('ruby-mongo-demo') + @coll = @db['test-collection'] + @cursor = @coll.find({}) + assert_equal true, @cursor.query.instance_variable_get(:@slave_ok) + end + else + puts "Not connected to slave; skipping slave connection tests." + + def test_slave_ok_false_on_queries + @db = Connection.new(@@host, @@port).db('ruby-mongo-demo') + @coll = @db['test-collection'] + @cursor = @coll.find({}) + assert_nil @cursor.query.instance_variable_get(:@slave_ok) + end + end + +end