From 59217e73260289054bcb91fea3f104d5f6cf34e6 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 | 26 ++++++++++++++ .../noPassthrough/mr_single_reduce_optimization.js | 37 +++++++++++++++++++ src/mongo/db/pipeline/SConscript | 8 +++-- src/mongo/db/pipeline/accumulator_js_reduce.cpp | 5 +++ src/mongo/db/pipeline/accumulator_js_test.cpp | 35 ++++++++++++++++++ src/mongo/db/pipeline/map_reduce_options.idl | 42 ++++++++++++++++++++++ 6 files changed, 151 insertions(+), 2 deletions(-) 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..574bf42c1b8 --- /dev/null +++ b/jstests/core/mr_single_reduce.js @@ -0,0 +1,26 @@ +/* + * See SERVER-68766. Verify that the reduce function is run on a single value. + * + * @tags: [ + * requires_fcv_44, + * multiversion_incompatible, + * ] + */ +(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..ec3a59074df --- /dev/null +++ b/jstests/noPassthrough/mr_single_reduce_optimization.js @@ -0,0 +1,37 @@ +/* + * See SERVER-68766. Verify that the reduce function is not run on a single value if the relevant + * flag is enabled. + * + * @tags: [ + * requires_fcv_44, + * multiversion_incompatible, + * ] + */ + +(function() { +"use strict"; + +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 dc5c7fcf624..b6c45fcaf37 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -131,7 +131,8 @@ env.Library( 'accumulator_push.cpp', 'accumulator_std_dev.cpp', 'accumulator_sum.cpp', - ], + 'map_reduce_options.idl', + ], LIBDEPS=[ '$BUILD_DIR/mongo/db/exec/document_value/document_value', '$BUILD_DIR/mongo/db/query/query_knobs', @@ -139,7 +140,10 @@ env.Library( '$BUILD_DIR/mongo/util/summation', 'expression', '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 66059c3d6da..7f02bca0726 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 { @@ -119,6 +120,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 f177ba31bac..2ba9899da27 100644 --- a/src/mongo/db/pipeline/accumulator_js_test.cpp +++ b/src/mongo/db/pipeline/accumulator_js_test.cpp @@ -197,6 +197,41 @@ TEST_F(MapReduceFixture, InternalJsReduceFailsWhenEvalContainsInvalidJavascript) } } +TEST_F( + MapReduceFixture, + InternalJsReduceFailsDependentOnDocumentCountWhenEvalIsInvalidJavascriptWithSingleReduceOpt) { + std::string optionName = "mrEnableSingleReduceOptimization"; + auto param = ServerParameterSet::getGlobal()->get(optionName); + uassertStatusOK(param->set(BSON(optionName << true).firstElement())); + 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()); + } + uassertStatusOK(param->set(BSON(optionName << false).firstElement())); +} + 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