Skip to content

Commit 5eeefa8

Browse files
authored
RUBY-2996 add feature flag for update/replace (#2515)
* RUBY-2996 define feature flag * unpend tests * fix spacing * RUBY-2996 use the feature flag * RUBY-2996 move config override * RUBY-2996 asnwer comments * RUBY-2996 set feature flag * RUBY-2996 include warning * RUBY-2996 answer comments * RUBY-2996 throttle the warning * RUBY-2996 add user docs * RUBY-2996 fix test
1 parent 198e46e commit 5eeefa8

18 files changed

+451
-16
lines changed

docs/reference/create-client.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2050,3 +2050,29 @@ driver in production:
20502050
<https://devcenter.heroku.com/articles/request-timeout>`_ is 30 seconds and
20512051
is not configurable; if deploying a Ruby application using MongoDB to Heroku,
20522052
consider lowering server selection timeout to 20 or 15 seconds.
2053+
2054+
Feature Flags
2055+
=============
2056+
2057+
The following is a list of feature flags that the Mongo Ruby Driver provides:
2058+
2059+
.. list-table::
2060+
:header-rows: 1
2061+
:widths: 30 60
2062+
2063+
* - Flag
2064+
- Description
2065+
* - ``validate_update_replace``
2066+
- Validates that there are no atomic operators (those that start with $)
2067+
in the root of a replacement document, and that there are only atomic
2068+
operators at the root of an update document. If this feature flag is on,
2069+
an error will be raised on an invalid update or replacement document,
2070+
if not, a warning will be output to the logs. (default: false)
2071+
2072+
These feature flags can be set directly on the ``Mongo`` module or using
2073+
the ``options`` method:
2074+
2075+
.. code::
2076+
2077+
Mongo.validate_update_replace = true
2078+
Mongo.options = { validate_update_replace: true }

lib/mongo.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,28 @@
7575
require 'mongo/version'
7676
require 'mongo/write_concern'
7777
require 'mongo/utils'
78+
require 'mongo/config'
7879

7980
module Mongo
81+
82+
class << self
83+
extend Forwardable
84+
85+
# Delegate the given option along with its = and ? methods to the given
86+
# object.
87+
#
88+
# @param [ Object ] obj The object to delegate to.
89+
# @param [ Symbol ] opt The method to delegate.
90+
def self.delegate_option(obj, opt)
91+
def_delegators obj, opt, "#{opt}=", "#{opt}?"
92+
end
93+
94+
# Take all the public instance methods from the Config singleton and allow
95+
# them to be accessed through the Mongo module directly.
96+
def_delegators Config, :options=
97+
delegate_option Config, :validate_update_replace
98+
end
99+
80100
# Clears the driver's OCSP response cache.
81101
module_function def clear_ocsp_cache
82102
Socket::OcspCache.clear

lib/mongo/bulk_write.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,14 +333,22 @@ def validate_requests!
333333
if doc = maybe_first(req.dig(op, :update))
334334
if key = doc.keys&.first
335335
unless key.to_s.start_with?("$")
336-
raise Error::InvalidUpdateDocument.new(key: key)
336+
if Mongo.validate_update_replace
337+
raise Error::InvalidUpdateDocument.new(key: key)
338+
else
339+
Error::InvalidUpdateDocument.warn(Logger.logger, key)
340+
end
337341
end
338342
end
339343
end
340344
elsif op == :replace_one
341345
if key = req.dig(op, :replacement)&.keys&.first
342346
if key.to_s.start_with?("$")
343-
raise Error::InvalidReplacementDocument.new(key: key)
347+
if Mongo.validate_update_replace
348+
raise Error::InvalidReplacementDocument.new(key: key)
349+
else
350+
Error::InvalidReplacementDocument.warn(Logger.logger, key)
351+
end
344352
end
345353
end
346354
end

lib/mongo/collection/view/writable.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,11 @@ def validate_update_documents!(spec)
556556
if update = spec.is_a?(Array) ? spec&.first : spec
557557
if key = update.keys&.first
558558
unless key.to_s.start_with?("$")
559-
raise Error::InvalidUpdateDocument.new(key: key)
559+
if Mongo.validate_update_replace
560+
raise Error::InvalidUpdateDocument.new(key: key)
561+
else
562+
Error::InvalidUpdateDocument.warn(Logger.logger, key)
563+
end
560564
end
561565
end
562566
end
@@ -574,7 +578,11 @@ def validate_replacement_documents!(spec)
574578
if replace = spec.is_a?(Array) ? spec&.first : spec
575579
if key = replace.keys&.first
576580
if key.to_s.start_with?("$")
577-
raise Error::InvalidReplacementDocument.new(key: key)
581+
if Mongo.validate_update_replace
582+
raise Error::InvalidReplacementDocument.new(key: key)
583+
else
584+
Error::InvalidReplacementDocument.warn(Logger.logger, key)
585+
end
578586
end
579587
end
580588
end

lib/mongo/config.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
require "mongo/config/options"
4+
require "mongo/config/validators/option"
5+
6+
module Mongo
7+
8+
# This module defines configuration options for Mongo.
9+
#
10+
# @api private
11+
module Config
12+
extend Forwardable
13+
extend Options
14+
extend self
15+
16+
option :validate_update_replace, default: false
17+
18+
# Set the configuration options.
19+
#
20+
# @example Set the options.
21+
# config.options = { validate_update_replace: true }
22+
#
23+
# @param [ Hash ] options The configuration options.
24+
def options=(options)
25+
options.each_pair do |option, value|
26+
Validators::Option.validate(option)
27+
send("#{option}=", value)
28+
end
29+
end
30+
end
31+
end

lib/mongo/config/options.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
module Mongo
4+
module Config
5+
6+
# Encapsulates logic for setting options.
7+
module Options
8+
9+
# Get the defaults or initialize a new empty hash.
10+
#
11+
# @return [ Hash ] The default options.
12+
def defaults
13+
@defaults ||= {}
14+
end
15+
16+
# Define a configuration option with a default.
17+
#
18+
# @param [ Symbol ] name The name of the configuration option.
19+
# @param [ Hash ] options Extras for the option.
20+
#
21+
# @option options [ Object ] :default The default value.
22+
def option(name, options = {})
23+
defaults[name] = settings[name] = options[:default]
24+
25+
class_eval do
26+
# log_level accessor is defined specially below
27+
define_method(name) do
28+
settings[name]
29+
end
30+
31+
define_method("#{name}=") do |value|
32+
settings[name] = value
33+
end
34+
35+
define_method("#{name}?") do
36+
!!send(name)
37+
end
38+
end
39+
end
40+
41+
# Reset the configuration options to the defaults.
42+
#
43+
# @example Reset the configuration options.
44+
# config.reset
45+
#
46+
# @return [ Hash ] The defaults.
47+
def reset
48+
settings.replace(defaults)
49+
end
50+
51+
# Get the settings or initialize a new empty hash.
52+
#
53+
# @example Get the settings.
54+
# options.settings
55+
#
56+
# @return [ Hash ] The setting options.
57+
def settings
58+
@settings ||= {}
59+
end
60+
end
61+
end
62+
end

lib/mongo/config/validators/option.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
module Mongo
4+
module Config
5+
module Validators
6+
7+
# Validator for configuration options.
8+
#
9+
# @api private
10+
module Option
11+
extend self
12+
13+
# Validate a configuration option.
14+
#
15+
# @example Validate a configuration option.
16+
#
17+
# @param [ String ] option The name of the option.
18+
def validate(option)
19+
unless Config.settings.keys.include?(option.to_sym)
20+
raise Mongo::Error::InvalidConfigOption.new(option)
21+
end
22+
end
23+
end
24+
end
25+
end
26+
end

lib/mongo/error.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ def write_concern_error_labels
151151
require 'mongo/error/invalid_bulk_operation'
152152
require 'mongo/error/invalid_bulk_operation_type'
153153
require 'mongo/error/invalid_collection_name'
154+
require 'mongo/error/invalid_config_option'
154155
require 'mongo/error/invalid_cursor_operation'
155156
require 'mongo/error/invalid_database_name'
156157
require 'mongo/error/invalid_document'
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
module Mongo
4+
class Error
5+
6+
# This error is raised when a bad configuration option is attempted to be
7+
# set.
8+
class InvalidConfigOption < Error
9+
10+
# Create the new error.
11+
#
12+
# @param [ Symbol, String ] name The attempted config option name.
13+
#
14+
# @api private
15+
def initialize(name)
16+
super("Invalid config option #{name}.")
17+
end
18+
end
19+
end
20+
end

lib/mongo/error/invalid_replacement_document.rb

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,34 @@ class InvalidReplacementDocument < Error
2626
# @deprecated
2727
MESSAGE = 'Invalid replacement document provided'.freeze
2828

29+
# Construct the error message.
30+
#
31+
# @param [ String ] key The invalid key.
32+
#
33+
# @return [ String ] The error message.
34+
#
35+
# @api private
36+
def self.message(key)
37+
message = "Invalid replacement document provided. Replacement documents "
38+
message += "must not contain atomic modifiers. The \"#{key}\" key is invalid."
39+
message
40+
end
41+
42+
# Send and cache the warning.
43+
#
44+
# @api private
45+
def self.warn(logger, key)
46+
@warned ||= begin
47+
logger.warn(message(key))
48+
true
49+
end
50+
end
51+
2952
# Instantiate the new exception.
3053
#
3154
# @param [ String ] :key The invalid key.
3255
def initialize(key: nil)
33-
message = "Invalid replacement document provided. Replacement documents "
34-
message += "must not contain atomic modifiers. The \"#{key}\" key is invalid."
35-
super(message)
56+
super(self.class.message(key))
3657
end
3758
end
3859
end

0 commit comments

Comments
 (0)