From 68af3dbe8f880f893eff8db853208818dcd7d494 Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Wed, 3 Nov 2010 17:36:08 -0400 Subject: [PATCH] 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. --- lib/mongo/collection.rb | 40 ++++++++----- lib/mongo/connection.rb | 12 +++- lib/mongo/db.rb | 21 +++++-- test/safe_test.rb | 42 ++++++++++++++ test/unit/db_test.rb | 2 + test/unit/grid_test.rb | 2 + test/unit/safe_test.rb | 125 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 test/safe_test.rb create mode 100644 test/unit/safe_test.rb diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index 5e4f2d2..0729360 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -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 diff --git a/lib/mongo/connection.rb b/lib/mongo/connection.rb index b307716..f6afadd 100644 --- a/lib/mongo/connection.rb +++ b/lib/mongo/connection.rb @@ -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. diff --git a/lib/mongo/db.rb b/lib/mongo/db.rb index 12e60ce..040799c 100644 --- a/lib/mongo/db.rb +++ b/lib/mongo/db.rb @@ -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 diff --git a/test/safe_test.rb b/test/safe_test.rb new file mode 100644 index 0000000..f528464 --- /dev/null +++ b/test/safe_test.rb @@ -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 diff --git a/test/unit/db_test.rb b/test/unit/db_test.rb index 08ae117..9e60102 100644 --- a/test/unit/db_test.rb +++ b/test/unit/db_test.rb @@ -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 diff --git a/test/unit/grid_test.rb b/test/unit/grid_test.rb index d35fa95..8e8ba3f 100644 --- a/test/unit/grid_test.rb +++ b/test/unit/grid_test.rb @@ -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 diff --git a/test/unit/safe_test.rb b/test/unit/safe_test.rb new file mode 100644 index 0000000..e2a473d --- /dev/null +++ b/test/unit/safe_test.rb @@ -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