summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--buildscripts/resmokeconfig/suites/aggregation_mongos_passthrough.yml3
-rw-r--r--jstests/aggregation/accumulators/internal_js_reduce_with_scope.js54
-rw-r--r--jstests/aggregation/expressions/internal_js_emit_with_scope.js126
-rw-r--r--src/mongo/db/pipeline/accumulator_js_reduce.cpp12
-rw-r--r--src/mongo/db/pipeline/expression_context.h13
-rw-r--r--src/mongo/db/pipeline/expression_javascript.cpp10
-rw-r--r--src/mongo/db/pipeline/javascript_execution.h9
-rw-r--r--src/mongo/db/pipeline/mongo_process_interface.h2
-rw-r--r--src/mongo/db/pipeline/mongos_process_interface.h2
-rw-r--r--src/mongo/db/pipeline/process_interface_standalone.h4
-rw-r--r--src/mongo/db/pipeline/runtime_constants.idl5
-rw-r--r--src/mongo/db/pipeline/stub_mongo_process_interface.h2
-rw-r--r--src/mongo/db/pipeline/variables.cpp14
-rw-r--r--src/mongo/db/pipeline/variables.h1
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;