diff options
-rw-r--r-- | buildscripts/resmokeconfig/suites/aggregation_mongos_passthrough.yml | 3 | ||||
-rw-r--r-- | jstests/aggregation/accumulators/internal_js_reduce_with_scope.js | 54 | ||||
-rw-r--r-- | jstests/aggregation/expressions/internal_js_emit_with_scope.js | 126 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_js_reduce.cpp | 12 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_context.h | 13 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_javascript.cpp | 10 | ||||
-rw-r--r-- | src/mongo/db/pipeline/javascript_execution.h | 9 | ||||
-rw-r--r-- | src/mongo/db/pipeline/mongo_process_interface.h | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/mongos_process_interface.h | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/process_interface_standalone.h | 4 | ||||
-rw-r--r-- | src/mongo/db/pipeline/runtime_constants.idl | 5 | ||||
-rw-r--r-- | src/mongo/db/pipeline/stub_mongo_process_interface.h | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/variables.cpp | 14 | ||||
-rw-r--r-- | src/mongo/db/pipeline/variables.h | 1 |
14 files changed, 237 insertions, 20 deletions
diff --git a/buildscripts/resmokeconfig/suites/aggregation_mongos_passthrough.yml b/buildscripts/resmokeconfig/suites/aggregation_mongos_passthrough.yml index e31bcece4f2..b06872ca536 100644 --- a/buildscripts/resmokeconfig/suites/aggregation_mongos_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/aggregation_mongos_passthrough.yml @@ -12,6 +12,9 @@ selector: - jstests/aggregation/bugs/lookup_unwind_killcursor.js # TODO: Remove when SERVER-23229 is fixed. - jstests/aggregation/bugs/groupMissing.js + # Mongos does not support runtimeConstants. + - jstests/aggregation/accumulators/internal_js_reduce_with_scope.js + - jstests/aggregation/expressions/internal_js_emit_with_scope.js exclude_with_any_tags: - requires_profiling # The following tests start their own ShardingTest or ReplSetTest, respectively. diff --git a/jstests/aggregation/accumulators/internal_js_reduce_with_scope.js b/jstests/aggregation/accumulators/internal_js_reduce_with_scope.js new file mode 100644 index 00000000000..956c1f62a25 --- /dev/null +++ b/jstests/aggregation/accumulators/internal_js_reduce_with_scope.js @@ -0,0 +1,54 @@ +// Tests the ability of the $_internalJsReduce accumulator to access javascript scope explicitly +// specified in runtimeConstants. +// +// Do not run in sharded passthroughs since 'runtimeConstants' is disallowed on mongos. +// @tags: [assumes_unsharded_collection] +(function() { +"use strict"; + +load('jstests/aggregation/extras/utils.js'); + +db.js_reduce_with_scope.drop(); + +for (const i of ["hello", "world", "world", "hello", "hi"]) { + db.js_reduce.insert({word: i, val: 1}); +} + +// Simple reduce function which calculates the word value based on weights defined in a local JS +// variable. +const weights = { + hello: 3, + world: 2, + hi: 1 +}; +function reduce(key, values) { + return Array.sum(values) * weights[key]; +} + +const command = { + aggregate: 'js_reduce', + cursor: {}, + runtimeConstants: + {localNow: new Date(), clusterTime: new Timestamp(0, 0), jsScope: {weights: weights}}, + pipeline: [{ + $group: { + _id: "$word", + wordCount: { + $_internalJsReduce: { + data: {k: "$word", v: "$val"}, + eval: reduce, + } + } + } + }], +}; + +const expectedResults = [ + {_id: "hello", wordCount: 6}, + {_id: "world", wordCount: 4}, + {_id: "hi", wordCount: 1}, +]; + +const res = assert.commandWorked(db.runCommand(command)); +assert(resultsEq(res.cursor.firstBatch, expectedResults, res.cursor)); +})(); diff --git a/jstests/aggregation/expressions/internal_js_emit_with_scope.js b/jstests/aggregation/expressions/internal_js_emit_with_scope.js new file mode 100644 index 00000000000..731a91717fa --- /dev/null +++ b/jstests/aggregation/expressions/internal_js_emit_with_scope.js @@ -0,0 +1,126 @@ +// Tests the ability of the $_internalJsEmit expression to access javascript scope explicitly +// specified in runtimeConstants. +// +// Do not run in sharded passthroughs since 'runtimeConstants' is disallowed on mongos. +// @tags: [assumes_unsharded_collection] +(function() { +"use strict"; + +load('jstests/aggregation/extras/utils.js'); + +const coll = db.js_emit_with_scope; +coll.drop(); + +const weights = { + wood: 5, + chuck: 2, + could: 0 +}; + +let constants = { + localNow: new Date(), + clusterTime: new Timestamp(0, 0), + jsScope: {weights: weights} +}; + +function fmap() { + for (let word of this.text.split(' ')) { + emit(word, weights[word]); + } +} + +let pipeline = [ + { + $project: { + emits: { + $_internalJsEmit: { + this: '$$ROOT', + eval: fmap, + }, + }, + _id: 0, + } + }, + {$unwind: '$emits'}, + {$replaceRoot: {newRoot: '$emits'}} +]; + +assert.commandWorked(coll.insert({text: 'wood chuck could chuck wood'})); + +let results = coll.aggregate(pipeline, {cursor: {}, runtimeConstants: constants}).toArray(); +assert(resultsEq(results, + [ + {k: "wood", v: weights["wood"]}, + {k: "chuck", v: weights["chuck"]}, + {k: "could", v: weights["could"]}, + {k: "chuck", v: weights["chuck"]}, + {k: "wood", v: weights["wood"]} + ], + results)); + +// +// Test that the scope variables are mutable from within a user-defined javascript function. +// +pipeline[0].$project.emits.$_internalJsEmit.eval = function() { + for (let word of this.text.split(' ')) { + emit(word, weights[word]); + weights[word] += 1; + } +}; + +results = coll.aggregate(pipeline, {cursor: {}, runtimeConstants: constants}).toArray(); +assert(resultsEq(results, + [ + {k: "wood", v: weights["wood"]}, + {k: "chuck", v: weights["chuck"]}, + {k: "could", v: weights["could"]}, + {k: "chuck", v: weights["chuck"] + 1}, + {k: "wood", v: weights["wood"] + 1} + ], + results)); + +// +// Test that the jsScope is allowed to have any number of fields. +// +constants.jsScope.multiplier = 5; +pipeline[0].$project.emits.$_internalJsEmit.eval = function() { + for (let word of this.text.split(' ')) { + emit(word, weights[word] * multiplier); + } +}; +results = coll.aggregate(pipeline, {cursor: {}, runtimeConstants: constants}).toArray(); +assert(resultsEq(results, + [ + {k: "wood", v: weights["wood"] * 5}, + {k: "chuck", v: weights["chuck"] * 5}, + {k: "could", v: weights["could"] * 5}, + {k: "chuck", v: weights["chuck"] * 5}, + {k: "wood", v: weights["wood"] * 5} + ], + results)); +constants.jsScope = {}; +pipeline[0].$project.emits.$_internalJsEmit.eval = function() { + for (let word of this.text.split(' ')) { + emit(word, 1); + } +}; +results = coll.aggregate(pipeline, {cursor: {}, runtimeConstants: constants}).toArray(); +assert(resultsEq(results, + [ + {k: "wood", v: 1}, + {k: "chuck", v: 1}, + {k: "could", v: 1}, + {k: "chuck", v: 1}, + {k: "wood", v: 1}, + ], + results)); + +// +// Test that the command fails if the jsScope is not an object. +// +constants.jsScope = "you cant do this"; +assert.commandFailedWithCode( + db.runCommand( + {aggregate: coll.getName(), pipeline: pipeline, cursor: {}, runtimeConstants: constants}), + ErrorCodes.TypeMismatch); +})(); diff --git a/src/mongo/db/pipeline/accumulator_js_reduce.cpp b/src/mongo/db/pipeline/accumulator_js_reduce.cpp index 8069128c0e0..ec5780a8de4 100644 --- a/src/mongo/db/pipeline/accumulator_js_reduce.cpp +++ b/src/mongo/db/pipeline/accumulator_js_reduce.cpp @@ -106,15 +106,17 @@ Value AccumulatorInternalJsReduce::getValue(bool toBeMerged) { bsonValues << val; } - // Function signature: reduce(key, values). - BSONObj params = BSON_ARRAY(_key << bsonValues.arr()); - auto [jsExec, newlyCreated] = getExpressionContext()->mongoProcessInterface->getJsExec(); + auto expCtx = getExpressionContext(); + auto [jsExec, newlyCreated] = expCtx->getJsExecWithScope(); ScriptingFunction func = jsExec->getScope()->createFunction(_funcSource.c_str()); uassert(31247, "The reduce function failed to parse in the javascript engine", func); - BSONObj thisObj; // For reduce, the key and values are both passed as 'params' so there's - // no need to set 'this'. + // Function signature: reduce(key, values). + BSONObj params = BSON_ARRAY(_key << bsonValues.arr()); + // For reduce, the key and values are both passed as 'params' so there's no need to set + // 'this'. + BSONObj thisObj; return jsExec->callFunction(func, params, thisObj); }(); diff --git a/src/mongo/db/pipeline/expression_context.h b/src/mongo/db/pipeline/expression_context.h index 4d8ca0052dd..d5f051bc6eb 100644 --- a/src/mongo/db/pipeline/expression_context.h +++ b/src/mongo/db/pipeline/expression_context.h @@ -197,6 +197,19 @@ public: auto getRuntimeConstants() const { return variables.getRuntimeConstants(); } + + /** + * Retrieves the Javascript Scope for the current thread or creates a new one if it has not been + * created yet. Initializes the Scope with the 'jsScope' variables from the runtimeConstants. + * + * Returns a JsExec and a boolean indicating whether the Scope was created as part of this call. + */ + auto getJsExecWithScope() const { + RuntimeConstants runtimeConstants = getRuntimeConstants(); + const boost::optional<mongo::BSONObj>& scope = runtimeConstants.getJsScope(); + return mongoProcessInterface->getJsExec(scope.get_value_or(BSONObj())); + } + // The explain verbosity requested by the user, or boost::none if no explain was requested. boost::optional<ExplainOptions::Verbosity> explain; diff --git a/src/mongo/db/pipeline/expression_javascript.cpp b/src/mongo/db/pipeline/expression_javascript.cpp index d3b3519c19f..e48879cbcce 100644 --- a/src/mongo/db/pipeline/expression_javascript.cpp +++ b/src/mongo/db/pipeline/expression_javascript.cpp @@ -114,10 +114,10 @@ Value ExpressionInternalJsEmit::evaluate(const Document& root, Variables* variab Value thisVal = _thisRef->evaluate(root, variables); uassert(31225, "'this' must be an object.", thisVal.getType() == BSONType::Object); - // If the scope does not exist and is created by the call to ExpressionContext::getJsExec(), - // then make sure to re-bind emit() and the given function to the new scope. + // If the scope does not exist and is created by the following call, then make sure to + // re-bind emit() and the given function to the new scope. auto expCtx = getExpressionContext(); - auto [jsExec, newlyCreated] = expCtx->mongoProcessInterface->getJsExec(); + auto [jsExec, newlyCreated] = expCtx->getJsExecWithScope(); if (newlyCreated) { jsExec->getScope()->loadStored(expCtx->opCtx, true); @@ -135,7 +135,7 @@ Value ExpressionInternalJsEmit::evaluate(const Document& root, Variables* variab std::vector<Value> output; for (const BSONObj& obj : _emittedObjects) { - output.push_back(Value(std::move(obj))); + output.push_back(Value(obj)); } // Need to const_cast here in order to clean out _emittedObjects which were added in the call to @@ -197,7 +197,7 @@ Value ExpressionInternalJs::evaluate(const Document& root, Variables* variables) << " can't be run on this process. Javascript is disabled.", getGlobalScriptEngine()); - auto [jsExec, newlyCreated] = expCtx->mongoProcessInterface->getJsExec(); + auto [jsExec, newlyCreated] = expCtx->getJsExecWithScope(); if (newlyCreated) { jsExec->getScope()->loadStored(expCtx->opCtx, true); diff --git a/src/mongo/db/pipeline/javascript_execution.h b/src/mongo/db/pipeline/javascript_execution.h index b8983d5f243..e94da67dae7 100644 --- a/src/mongo/db/pipeline/javascript_execution.h +++ b/src/mongo/db/pipeline/javascript_execution.h @@ -42,10 +42,12 @@ namespace mongo { class JsExecution { public: /** - * Construct with a thread-local scope. + * Construct with a thread-local scope and initialize with the given scope variables. */ - JsExecution() : _scope(nullptr) { - _scope.reset(getGlobalScriptEngine()->newScopeForCurrentThread()); + JsExecution(const BSONObj& scopeVars) + : _scope(getGlobalScriptEngine()->newScopeForCurrentThread()) { + _scopeVars = scopeVars.getOwned(); + _scope->init(&_scopeVars); } /** @@ -84,6 +86,7 @@ public: } private: + BSONObj _scopeVars; std::unique_ptr<Scope> _scope; }; } // namespace mongo diff --git a/src/mongo/db/pipeline/mongo_process_interface.h b/src/mongo/db/pipeline/mongo_process_interface.h index 1899cf08053..58958aced64 100644 --- a/src/mongo/db/pipeline/mongo_process_interface.h +++ b/src/mongo/db/pipeline/mongo_process_interface.h @@ -414,7 +414,7 @@ public: * Returns a pointer to a JsExecution and a boolean to indicate whether the JS Scope was newly * created. */ - virtual std::pair<JsExecution*, bool> getJsExec() = 0; + virtual std::pair<JsExecution*, bool> getJsExec(const BSONObj& scope) = 0; virtual void releaseJsExec() = 0; }; diff --git a/src/mongo/db/pipeline/mongos_process_interface.h b/src/mongo/db/pipeline/mongos_process_interface.h index 163743d591c..8fb8a38b838 100644 --- a/src/mongo/db/pipeline/mongos_process_interface.h +++ b/src/mongo/db/pipeline/mongos_process_interface.h @@ -239,7 +239,7 @@ public: boost::optional<ChunkVersion> targetCollectionVersion, const NamespaceString& outputNs) const override; - std::pair<JsExecution*, bool> getJsExec() override { + std::pair<JsExecution*, bool> getJsExec(const BSONObj&) override { // Javascript engine is not support on mongos. MONGO_UNREACHABLE; } diff --git a/src/mongo/db/pipeline/process_interface_standalone.h b/src/mongo/db/pipeline/process_interface_standalone.h index e367df574dd..7e45dd0eaa0 100644 --- a/src/mongo/db/pipeline/process_interface_standalone.h +++ b/src/mongo/db/pipeline/process_interface_standalone.h @@ -158,9 +158,9 @@ public: * If we are creating a new JsExecution (and therefore a new thread-local scope), make sure * we pass that information back to the caller. */ - std::pair<JsExecution*, bool> getJsExec() override { + std::pair<JsExecution*, bool> getJsExec(const BSONObj& scope) override { if (!_jsExec) { - _jsExec = std::make_unique<JsExecution>(); + _jsExec = std::make_unique<JsExecution>(scope); return {_jsExec.get(), true}; } return {_jsExec.get(), false}; diff --git a/src/mongo/db/pipeline/runtime_constants.idl b/src/mongo/db/pipeline/runtime_constants.idl index 4a68db64c75..e0b1858379e 100644 --- a/src/mongo/db/pipeline/runtime_constants.idl +++ b/src/mongo/db/pipeline/runtime_constants.idl @@ -48,3 +48,8 @@ structs: cpp_name: clusterTime type: timestamp description: A value of the $$CLUSTER_TIME variable. + jsScope: + cpp_name: jsScope + type: object + optional: true + description: Optional scope variables accessible from internal javacsript expressions. diff --git a/src/mongo/db/pipeline/stub_mongo_process_interface.h b/src/mongo/db/pipeline/stub_mongo_process_interface.h index 23a90059f13..1644d545366 100644 --- a/src/mongo/db/pipeline/stub_mongo_process_interface.h +++ b/src/mongo/db/pipeline/stub_mongo_process_interface.h @@ -242,7 +242,7 @@ public: return {fieldPaths, targetCollectionVersion}; } - std::pair<JsExecution*, bool> getJsExec() { + std::pair<JsExecution*, bool> getJsExec(const BSONObj&) { MONGO_UNREACHABLE; } void releaseJsExec() {} diff --git a/src/mongo/db/pipeline/variables.cpp b/src/mongo/db/pipeline/variables.cpp index 8a37fecc10f..95caa4769a8 100644 --- a/src/mongo/db/pipeline/variables.cpp +++ b/src/mongo/db/pipeline/variables.cpp @@ -40,8 +40,11 @@ namespace mongo { constexpr Variables::Id Variables::kRootId; constexpr Variables::Id Variables::kRemoveId; -const StringMap<Variables::Id> Variables::kBuiltinVarNameToId = { - {"ROOT", kRootId}, {"REMOVE", kRemoveId}, {"NOW", kNowId}, {"CLUSTER_TIME", kClusterTimeId}}; +const StringMap<Variables::Id> Variables::kBuiltinVarNameToId = {{"ROOT", kRootId}, + {"REMOVE", kRemoveId}, + {"NOW", kNowId}, + {"CLUSTER_TIME", kClusterTimeId}, + {"JS_SCOPE", kJsScopeId}}; void Variables::uassertValidNameForUserWrite(StringData varName) { // System variables users allowed to write to (currently just one) @@ -180,6 +183,9 @@ RuntimeConstants Variables::getRuntimeConstants() const { if (auto it = _runtimeConstants.find(kClusterTimeId); it != _runtimeConstants.end()) { constants.setClusterTime(it->second.getTimestamp()); } + if (auto it = _runtimeConstants.find(kJsScopeId); it != _runtimeConstants.end()) { + constants.setJsScope(it->second.getDocument().toBson()); + } return constants; } @@ -192,6 +198,10 @@ void Variables::setRuntimeConstants(const RuntimeConstants& constants) { if (!constants.getClusterTime().isNull()) { _runtimeConstants[kClusterTimeId] = Value(constants.getClusterTime()); } + + if (constants.getJsScope()) { + _runtimeConstants[kJsScopeId] = Value(constants.getJsScope().get()); + } } void Variables::setDefaultRuntimeConstants(OperationContext* opCtx) { diff --git a/src/mongo/db/pipeline/variables.h b/src/mongo/db/pipeline/variables.h index ddb76457cac..742c5757581 100644 --- a/src/mongo/db/pipeline/variables.h +++ b/src/mongo/db/pipeline/variables.h @@ -83,6 +83,7 @@ public: static constexpr Variables::Id kRemoveId = Id(-2); static constexpr Variables::Id kNowId = Id(-3); static constexpr Variables::Id kClusterTimeId = Id(-4); + static constexpr Variables::Id kJsScopeId = Id(-5); // Map from builtin var name to reserved id number. static const StringMap<Id> kBuiltinVarNameToId; |