From 79cfcdd83eb6f64e164a588d0daf9bb873328b45 Mon Sep 17 00:00:00 2001 From: Davis Haupt Date: Fri, 26 Aug 2022 19:05:37 +0000 Subject: SERVER-68766 add server parameter to enable single value reduce optimization --- jstests/core/mr_single_reduce.js | 18 ++++++++++ .../noPassthrough/mr_single_reduce_optimization.js | 29 +++++++++++++++ src/mongo/db/pipeline/SConscript | 6 +++- src/mongo/db/pipeline/accumulator_js_reduce.cpp | 5 +++ src/mongo/db/pipeline/accumulator_js_test.cpp | 33 +++++++++++++++++ src/mongo/db/pipeline/map_reduce_options.idl | 42 ++++++++++++++++++++++ 6 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 jstests/core/mr_single_reduce.js create mode 100644 jstests/noPassthrough/mr_single_reduce_optimization.js create mode 100644 src/mongo/db/pipeline/map_reduce_options.idl diff --git a/jstests/core/mr_single_reduce.js b/jstests/core/mr_single_reduce.js new file mode 100644 index 00000000000..28e66c71c72 --- /dev/null +++ b/jstests/core/mr_single_reduce.js @@ -0,0 +1,18 @@ +(function() { +"use strict"; +const coll = db.bar; + +assert.commandWorked(coll.insert({x: 1})); + +const map = function() { + emit(0, "mapped value"); +}; + +const reduce = function(key, values) { + return "reduced value"; +}; + +const res = assert.commandWorked( + db.runCommand({mapReduce: 'bar', map: map, reduce: reduce, out: {inline: 1}})); +assert.eq(res.results[0], {_id: 0, value: "reduced value"}); +}()); diff --git a/jstests/noPassthrough/mr_single_reduce_optimization.js b/jstests/noPassthrough/mr_single_reduce_optimization.js new file mode 100644 index 00000000000..71ab924836b --- /dev/null +++ b/jstests/noPassthrough/mr_single_reduce_optimization.js @@ -0,0 +1,29 @@ +(function() { +"use strict"; +// See SERVER-68766. Verify that the reduce function is not run on a single value if the relevant +// flag is enabled. + +const conn = MongoRunner.runMongod({setParameter: {mrEnableSingleReduceOptimization: true}}); +const testDB = conn.getDB('foo'); +const coll = testDB.bar; + +assert.commandWorked(coll.insert({x: 1})); + +const map = function() { + emit(0, "mapped value"); +}; + +const reduce = function(key, values) { + return "reduced value"; +}; + +let res = assert.commandWorked( + testDB.runCommand({mapReduce: 'bar', map: map, reduce: reduce, out: {inline: 1}})); +assert.eq(res.results[0], {_id: 0, value: "mapped value"}); +assert.commandWorked(coll.insert({x: 2})); +res = assert.commandWorked( + testDB.runCommand({mapReduce: 'bar', map: map, reduce: reduce, out: {inline: 1}})); +assert.eq(res.results[0], {_id: 0, value: "reduced value"}); + +MongoRunner.stopMongod(conn); +}()); diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index 440cbfc80f9..a5e6bf0234f 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -133,6 +133,7 @@ env.Library( 'accumulator_rank.cpp', 'accumulator_std_dev.cpp', 'accumulator_sum.cpp', + 'map_reduce_options.idl', 'window_function/window_bounds.cpp', 'window_function/window_function_covariance.cpp', 'window_function/window_function_count.cpp', @@ -148,7 +149,10 @@ env.Library( '$BUILD_DIR/mongo/util/summation', 'expression_context', 'field_path', - ] + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/idl/idl_parser', + ], ) env.Library( diff --git a/src/mongo/db/pipeline/accumulator_js_reduce.cpp b/src/mongo/db/pipeline/accumulator_js_reduce.cpp index e7192465d7c..acd77254b9d 100644 --- a/src/mongo/db/pipeline/accumulator_js_reduce.cpp +++ b/src/mongo/db/pipeline/accumulator_js_reduce.cpp @@ -32,6 +32,7 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/db/pipeline/accumulator_js_reduce.h" #include "mongo/db/pipeline/make_js_function.h" +#include "mongo/db/pipeline/map_reduce_options_gen.h" namespace mongo { @@ -116,6 +117,10 @@ void AccumulatorInternalJsReduce::processInternal(const Value& input, bool mergi Value AccumulatorInternalJsReduce::getValue(bool toBeMerged) { if (_values.size() < 1) { return Value{}; + } else if (mrSingleReduceOptimizationEnabled && _values.size() == 1) { + // This optimization existed in the old Pre-4.4 MapReduce implementation. If the flag is + // set, then we should replicate the optimization. See SERVER-68766 for more details. + return _values[0]; } const auto keySize = _key.getApproximateSize(); diff --git a/src/mongo/db/pipeline/accumulator_js_test.cpp b/src/mongo/db/pipeline/accumulator_js_test.cpp index 953b4ce475a..2a44e8e967c 100644 --- a/src/mongo/db/pipeline/accumulator_js_test.cpp +++ b/src/mongo/db/pipeline/accumulator_js_test.cpp @@ -38,6 +38,7 @@ #include "mongo/db/pipeline/process_interface/standalone_process_interface.h" #include "mongo/db/service_context_d_test_fixture.h" #include "mongo/dbtests/dbtests.h" +#include "mongo/idl/server_parameter_test_util.h" #include "mongo/scripting/engine.h" namespace mongo { @@ -197,6 +198,38 @@ TEST_F(MapReduceFixture, InternalJsReduceFailsWhenEvalContainsInvalidJavascript) } } +TEST_F( + MapReduceFixture, + InternalJsReduceFailsDependentOnDocumentCountWhenEvalIsInvalidJavascriptWithSingleReduceOpt) { + RAIIServerParameterControllerForTest flag("mrEnableSingleReduceOptimization", true); + std::string eval("INVALID_JAVASCRIPT"); + // Multiple source documents should evaluate the passed in function and return an error with + // invalid javascript. + { + auto accum = AccumulatorInternalJsReduce::create(getExpCtx(), "INVALID_JAVASCRIPT"); + auto input = Value(DOC("k" << Value(1) << "v" << Value(2))); + accum->process(input, false); + accum->process(input, false); + + ASSERT_THROWS_CODE(accum->getValue(false), DBException, ErrorCodes::JSInterpreterFailure); + } + + // Single source document. With the reduce optimization, we simply return this document rather + // than executing the JS engine at all, so no error is thrown. + { + auto accum = AccumulatorInternalJsReduce::create(getExpCtx(), "INVALID_JAVASCRIPT"); + + auto input = Value(DOC("k" << Value(1) << "v" << Value(2))); + auto expectedResult = Value(2); + + accum->process(input, false); + Value result = accum->getValue(false); + + ASSERT_VALUE_EQ(expectedResult, result); + ASSERT_EQUALS(expectedResult.getType(), result.getType()); + } +} + TEST_F(MapReduceFixture, InternalJsReduceFailsIfArgumentNotDocument) { auto argument = Value(2); assertProcessFailsWithCode( diff --git a/src/mongo/db/pipeline/map_reduce_options.idl b/src/mongo/db/pipeline/map_reduce_options.idl new file mode 100644 index 00000000000..55d5e133b6f --- /dev/null +++ b/src/mongo/db/pipeline/map_reduce_options.idl @@ -0,0 +1,42 @@ +# Copyright (C) 2022-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# . +# +# As a special exception, the copyright holders give permission to link the +# code of portions of this program with the OpenSSL library under certain +# conditions as described in each individual source file and distribute +# linked combinations including the program with the OpenSSL library. You +# must comply with the Server Side Public License in all respects for +# all of the code used other than as permitted herein. If you modify file(s) +# with this exception, you may extend this exception to your version of the +# file(s), but you are not obligated to do so. If you do not wish to do so, +# delete this exception statement from your version. If you delete this +# exception statement from all source files in the program, then also delete +# it in the license file. +# + +global: + cpp_namespace: "mongo" + +server_parameters: + mrEnableSingleReduceOptimization: + description: > + In version 4.2 and before, MongoDB MapReduce will not call the reduce function + for a key that has only a single value. In version 4.4 and later, the reduce + function is still called in order to validate the JavaScript reduce function + even when there is only one value. This setting will re-enable the old optimization. + set_at: startup + cpp_vartype: bool + cpp_varname: mrSingleReduceOptimizationEnabled + default: false -- cgit v1.2.1