Allow the setting of safe mode globally on the Connection,

DB, and Collection levels. The safe mode setting will
automatically be inherited down the hierarchy Connection ->
DB -> Collection -> (insert, update, remove). This default
can be overridden at any time. Connection#safe, DB#safe, and
Collection#safe will yield the current default value.
This commit is contained in:
Kyle Banker 2010-11-03 17:36:08 -04:00
parent f7d151c8dc
commit 68af3dbe8f
7 changed files with 224 additions and 20 deletions

View File

@ -21,13 +21,18 @@ module Mongo
# A named collection of documents in a database.
class Collection
attr_reader :db, :name, :pk_factory, :hint
attr_reader :db, :name, :pk_factory, :hint, :safe
# Initialize a collection object.
#
# @param [DB] db a MongoDB database instance.
# @param [String, Symbol] name the name of the collection.
#
# @option options [Boolean, Hash] :safe (false) Set the default safe-mode options
# for insert, update, and remove method called on this Collection instance. If no
# value is provided, the default value set on this instance's DB will be used. This
# default can be overridden for any invocation of insert, update, or remove.
#
# @raise [InvalidNSName]
# if collection name is empty, contains '$', or starts or ends with '.'
#
@ -37,7 +42,7 @@ module Mongo
# @return [Collection]
#
# @core collections constructor_details
def initialize(db, name, pk_factory=nil)
def initialize(db, name, pk_factory=nil, options={})
case name
when Symbol, String
else
@ -60,6 +65,7 @@ module Mongo
@connection = @db.connection
@logger = @connection.logger
@pk_factory = pk_factory || BSON::ObjectId
@safe = options.has_key?(:safe) ? options[:safe] : @db.safe
@hint = nil
end
@ -245,8 +251,10 @@ module Mongo
# @option opts [Boolean, Hash] :safe (+false+)
# run the operation in safe mode, which run a getlasterror command on the
# database to report any assertion. In addition, a hash can be provided to
# run an fsync and/or wait for replication of the insert (>= 1.5.1). See the options
# for DB#error.
# run an fsync and/or wait for replication of the insert (>= 1.5.1). Safe
# options provided here will override any safe options set on this collection,
# its database object, or the current connection. See the options on
# for DB#get_last_error.
#
# @see DB#remove for options that can be passed to :safe.
#
@ -254,7 +262,8 @@ module Mongo
def insert(doc_or_docs, options={})
doc_or_docs = [doc_or_docs] unless doc_or_docs.is_a?(Array)
doc_or_docs.collect! { |doc| @pk_factory.create_pk(doc) }
result = insert_documents(doc_or_docs, @name, true, options[:safe])
safe = options.has_key?(:safe) ? options[:safe] : @safe
result = insert_documents(doc_or_docs, @name, true, safe)
result.size > 1 ? result : result.first
end
alias_method :<<, :insert
@ -265,10 +274,11 @@ module Mongo
# If specified, only matching documents will be removed.
#
# @option opts [Boolean, Hash] :safe (+false+)
# run the operation in safe mode, which run a getlasterror command on the
# run the operation in safe mode, which will run a getlasterror command on the
# database to report any assertion. In addition, a hash can be provided to
# run an fsync and/or wait for replication of the remove (>= 1.5.1). See the options
# for DB#get_last_error.
# run an fsync and/or wait for replication of the remove (>= 1.5.1). Safe
# options provided here will override any safe options set on this collection,
# its database, or the current connection. See the options for DB#get_last_error for more details.
#
# @example remove all documents from the 'users' collection:
# users.remove
@ -287,14 +297,15 @@ module Mongo
# @core remove remove-instance_method
def remove(selector={}, opts={})
# Initial byte is 0.
safe = opts.has_key?(:safe) ? opts[:safe] : @safe
message = BSON::ByteBuffer.new("\0\0\0\0")
BSON::BSON_RUBY.serialize_cstr(message, "#{@db.name}.#{@name}")
message.put_int(0)
message.put_binary(BSON::BSON_CODER.serialize(selector, false, true).to_s)
@logger.debug("MONGODB #{@db.name}['#{@name}'].remove(#{selector.inspect})") if @logger
if opts[:safe]
@connection.send_message_with_safe_check(Mongo::Constants::OP_DELETE, message, @db.name, nil, opts[:safe])
if safe
@connection.send_message_with_safe_check(Mongo::Constants::OP_DELETE, message, @db.name, nil, safe)
# the return value of send_message_with_safe_check isn't actually meaningful --
# only the fact that it didn't raise an error is -- so just return true
true
@ -320,11 +331,14 @@ module Mongo
# @option opts [Boolean] :safe (+false+)
# If true, check that the save succeeded. OperationFailure
# will be raised on an error. Note that a safe check requires an extra
# round-trip to the database.
# round-trip to the database. Safe options provided here will override any safe
# options set on this collection, its database object, or the current collection.
# See the options for DB#get_last_error for details.
#
# @core update update-instance_method
def update(selector, document, options={})
# Initial byte is 0.
safe = options.has_key?(:safe) ? options[:safe] : @safe
message = BSON::ByteBuffer.new("\0\0\0\0")
BSON::BSON_RUBY.serialize_cstr(message, "#{@db.name}.#{@name}")
update_options = 0
@ -334,8 +348,8 @@ module Mongo
message.put_binary(BSON::BSON_CODER.serialize(selector, false, true).to_s)
message.put_binary(BSON::BSON_CODER.serialize(document, false, true).to_s)
@logger.debug("MONGODB #{@db.name}['#{@name}'].update(#{selector.inspect}, #{document.inspect})") if @logger
if options[:safe]
@connection.send_message_with_safe_check(Mongo::Constants::OP_UPDATE, message, @db.name, nil, options[:safe])
if safe
@connection.send_message_with_safe_check(Mongo::Constants::OP_UPDATE, message, @db.name, nil, safe)
else
@connection.send_message(Mongo::Constants::OP_UPDATE, message, nil)
end

View File

@ -38,7 +38,8 @@ module Mongo
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, :primary, :secondaries, :arbiters
attr_reader :logger, :size, :host, :port, :nodes, :auths, :sockets, :checked_out, :primary, :secondaries, :arbiters,
:safe
# Counter for generating unique request ids.
@@current_request_id = 0
@ -61,6 +62,10 @@ module Mongo
# @param [String, Hash] host.
# @param [Integer] port specify a port number here if only one host is being specified.
#
# @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
# on initialization.
# @option options [Boolean] :slave_ok (false) Must be set to +true+ when connecting
# to a single, slave node.
# @option options [Logger, #debug] :logger (nil) Logger instance to receive driver operation log.
@ -114,6 +119,9 @@ module Mongo
# Mutex for synchronizing pool access
@connection_mutex = Mutex.new
# Global safe option. This is false by default.
@safe = options[:safe] || false
# Create a mutex when a new key, in this case a socket,
# is added to the hash.
@safe_mutexes = Hash.new { |h, k| h[k] = Mutex.new }
@ -312,7 +320,7 @@ module Mongo
#
# @core databases []-instance_method
def [](db_name)
DB.new(db_name, self)
DB.new(db_name, self, :safe => @safe)
end
# Drop a database.

View File

@ -45,8 +45,8 @@ module Mongo
# Returns the value of the +strict+ flag.
def strict?; @strict; end
# The name of the database.
attr_reader :name
# The name of the database and the local safe option.
attr_reader :name, :safe
# The Mongo::Connection instance connecting to the MongoDB server.
attr_reader :connection
@ -65,12 +65,19 @@ module Mongo
# fields the factory wishes to inject. (NOTE: if the object already has a primary key,
# the factory should not inject a new key).
#
# @option options [Boolean, Hash] :safe (false) Set the default safe-mode options
# propogated to Collection objects instantiated off of this DB. If no
# value is provided, the default value set on this instance's Connection object will be used. This
# default can be overridden upon instantiation of any collection by explicity setting a :safe value
# on initialization
#
# @core databases constructor_details
def initialize(db_name, connection, options={})
@name = Mongo::Support.validate_db_name(db_name)
@connection = connection
@strict = options[:strict]
@pk_factory = options[:pk]
@safe = options.has_key?(:safe) ? options[:safe] : @connection.safe
end
# Authenticate with the given username and password. Note that mongod
@ -259,9 +266,13 @@ module Mongo
# @raise [MongoDBError] if collection does not already exist and we're in +strict+ mode.
#
# @return [Mongo::Collection]
def collection(name)
return Collection.new(self, name, @pk_factory) if !strict? || collection_names.include?(name)
raise Mongo::MongoDBError, "Collection #{name} doesn't exist. Currently in strict mode."
def collection(name, options={})
if strict? && !collection_names.include?(name)
raise Mongo::MongoDBError, "Collection #{name} doesn't exist. Currently in strict mode."
else
options[:safe] = options.has_key?(:safe) ? options[:safe] : @safe
Collection.new(self, name, @pk_factory, options)
end
end
alias_method :[], :collection

42
test/safe_test.rb Normal file
View File

@ -0,0 +1,42 @@
require './test/test_helper'
include Mongo
class SafeTest < Test::Unit::TestCase
context "Safe tests: " do
setup do
@con = standard_connection(:safe => {:w => 1})
@db = @con[MONGO_TEST_DB]
@col = @db['test-safe']
@col.create_index([[:a, 1]], :unique => true)
@col.remove
end
should "propogate safe option on insert" do
@col.insert({:a => 1})
assert_raise_error(OperationFailure, "duplicate key") do
@col.insert({:a => 1})
end
end
should "allow safe override on insert" do
@col.insert({:a => 1})
@col.insert({:a => 1}, :safe => false)
end
should "propogate safe option on update" do
@col.insert({:a => 1})
@col.insert({:a => 2})
assert_raise_error(OperationFailure, "duplicate key") do
@col.update({:a => 2}, {:a => 1})
end
end
should "allow safe override on update" do
@col.insert({:a => 1})
@col.insert({:a => 2})
@col.update({:a => 2}, {:a => 1}, :safe => false)
end
end
end

View File

@ -16,7 +16,9 @@ class DBTest < Test::Unit::TestCase
context "DB commands" do
setup do
@conn = stub()
@conn.stubs(:safe)
@db = DB.new("testing", @conn)
@db.stubs(:safe)
@collection = mock()
@db.stubs(:system_command_collection).returns(@collection)
end

View File

@ -5,12 +5,14 @@ class GridTest < Test::Unit::TestCase
context "GridFS: " do
setup do
@conn = stub()
@conn.stubs(:safe)
@db = DB.new("testing", @conn)
@files = mock()
@chunks = mock()
@db.expects(:[]).with('fs.files').returns(@files)
@db.expects(:[]).with('fs.chunks').returns(@chunks)
@db.stubs(:safe)
end
context "Grid classe with standard connections" do

125
test/unit/safe_test.rb Normal file
View File

@ -0,0 +1,125 @@
require File.expand_path('./test/test_helper.rb')
class SafeTest < Test::Unit::TestCase
context "Safe mode on connection: " do
setup do
@safe_value = {:w => 7}
@con = Mongo::Connection.new('localhost', 27017, :safe => @safe_value, :connect => false)
end
should "propogate to DB" do
db = @con['foo']
assert_equal @safe_value, db.safe
db = @con.db('foo')
assert_equal @safe_value, db.safe
db = DB.new('foo', @con)
assert_equal @safe_value, db.safe
end
should "allow db override" do
db = DB.new('foo', @con, :safe => false)
assert_equal false, db.safe
db = @con.db('foo', :safe => false)
assert_equal false, db.safe
end
context "on DB: " do
setup do
@db = @con['foo']
end
should "propogate to collection" do
col = @db.collection('bar')
assert_equal @safe_value, col.safe
col = @db['bar']
assert_equal @safe_value, col.safe
col = Collection.new(@db, 'bar')
assert_equal @safe_value, col.safe
end
should "allow override on collection" do
col = @db.collection('bar', :safe => false)
assert_equal false, col.safe
col = Collection.new(@db, 'bar', nil, :safe => false)
assert_equal false, col.safe
end
end
context "on operations supporting safe mode" do
setup do
@col = @con['foo']['bar']
end
should "use default value on insert" do
@con.expects(:send_message_with_safe_check).with do |op, msg, log, n, safe|
safe == @safe_value
end
@col.insert({:a => 1})
end
should "allow override alternate value on insert" do
@con.expects(:send_message_with_safe_check).with do |op, msg, log, n, safe|
safe == {:w => 100}
end
@col.insert({:a => 1}, :safe => {:w => 100})
end
should "allow override to disable on insert" do
@con.expects(:send_message)
@col.insert({:a => 1}, :safe => false)
end
should "use default value on update" do
@con.expects(:send_message_with_safe_check).with do |op, msg, log, n, safe|
safe == @safe_value
end
@col.update({:a => 1}, {:a => 2})
end
should "allow override alternate value on update" do
@con.expects(:send_message_with_safe_check).with do |op, msg, log, n, safe|
safe == {:w => 100}
end
@col.update({:a => 1}, {:a => 2}, :safe => {:w => 100})
end
should "allow override to disable on update" do
@con.expects(:send_message)
@col.update({:a => 1}, {:a => 2}, :safe => false)
end
should "use default value on remove" do
@con.expects(:send_message_with_safe_check).with do |op, msg, log, n, safe|
safe == @safe_value
end
@col.remove
end
should "allow override alternate value on remove" do
@con.expects(:send_message_with_safe_check).with do |op, msg, log, n, safe|
safe == {:w => 100}
end
@col.remove({}, :safe => {:w => 100})
end
should "allow override to disable on remove" do
@con.expects(:send_message)
@col.remove({}, :safe => false)
end
end
end
end