summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZixuan Zhuang <zixuan.zhuang@mongodb.com>2023-03-21 19:40:51 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2023-03-21 20:24:42 +0000
commit0f6baedfbb04cd0f1434d091b5ff9e23a6da15c9 (patch)
tree20a31b9623771a3281c69674fe25277551c9e08c
parenteb49b1fea5972be2345dddd9b8ab2274da701edf (diff)
downloadmongo-0f6baedfbb04cd0f1434d091b5ff9e23a6da15c9.tar.gz
SERVER-74264 Backport to 6.3
-rw-r--r--jstests/auth/sbe_plan_cache_user_roles.js56
-rw-r--r--jstests/noPassthroughWithMongod/now_variable.js97
-rw-r--r--src/mongo/db/pipeline/expression.cpp8
-rw-r--r--src/mongo/db/pipeline/expression_date_test.cpp3
-rw-r--r--src/mongo/db/pipeline/expression_field_path_test.cpp20
5 files changed, 171 insertions, 13 deletions
diff --git a/jstests/auth/sbe_plan_cache_user_roles.js b/jstests/auth/sbe_plan_cache_user_roles.js
new file mode 100644
index 00000000000..a42e36bf372
--- /dev/null
+++ b/jstests/auth/sbe_plan_cache_user_roles.js
@@ -0,0 +1,56 @@
+/**
+ * Test $$USER_ROLES works correctly with the SBE plan cache. The same query should return the
+ * updated user role info when a different user logs in.
+ * @tags: [
+ * featureFlagUserRoles,
+ * # Multiple servers can mess up the plan cache list.
+ * assumes_standalone_mongod,
+ * ]
+ */
+(function() {
+"use strict";
+load("jstests/libs/sbe_util.js"); // For checkSBEEnabled.
+
+const mongod = MongoRunner.runMongod();
+const dbName = "test";
+const db = mongod.getDB(dbName);
+
+// Create two users, each with different roles.
+assert.commandWorked(
+ db.runCommand({createUser: "user1", pwd: "pwd", roles: [{role: "read", db: dbName}]}));
+assert.commandWorked(
+ db.runCommand({createUser: "user2", pwd: "pwd", roles: [{role: "readWrite", db: dbName}]}));
+
+const coll = db.sbe_plan_cache_user_roles;
+coll.drop();
+
+const verifyPlanCache = function(role) {
+ if (checkSBEEnabled(db, ["featureFlagSbeFull"])) {
+ const caches = coll.getPlanCache().list();
+ assert.eq(1, caches.length, caches);
+ assert.eq(caches[0].cachedPlan.stages.includes(role), false, caches);
+ }
+};
+
+assert.commandWorked(coll.insert({_id: 1}));
+
+// While logged in as user1, we should see user1's roles.
+db.auth("user1", "pwd");
+let results = coll.find({}, {roles: "$$USER_ROLES"}).toArray();
+assert.eq(results.length, 1);
+assert.eq(results[0].roles, [{_id: "test.read", role: "read", db: "test"}]);
+// It can take two executions of a query for a plan to get cached.
+coll.find({}, {roles: "$$USER_ROLES"}).toArray();
+verifyPlanCache("test.read");
+db.logout();
+
+// While logged in as user2, we should see user2's roles.
+db.auth("user2", "pwd");
+results = coll.find({}, {roles: "$$USER_ROLES"}).toArray();
+assert.eq(results.length, 1);
+assert.eq(results[0].roles, [{_id: "test.readWrite", role: "readWrite", db: "test"}]);
+verifyPlanCache("test.readWrite");
+db.logout();
+
+MongoRunner.stopMongod(mongod);
+})(); \ No newline at end of file
diff --git a/jstests/noPassthroughWithMongod/now_variable.js b/jstests/noPassthroughWithMongod/now_variable.js
index 8b74602fe9d..341959956cb 100644
--- a/jstests/noPassthroughWithMongod/now_variable.js
+++ b/jstests/noPassthroughWithMongod/now_variable.js
@@ -4,6 +4,7 @@
(function() {
"use strict";
+load("jstests/libs/sbe_util.js"); // For checkSBEEnabled.
load("jstests/libs/sbe_assert_error_override.js"); // Override error-code-checking APIs.
const coll = db[jsTest.name()];
@@ -39,22 +40,37 @@ assert.commandWorked(db.createView(
const viewWithClusterTime = db["viewWithClusterTime"];
function runTests(query) {
- const results = query().toArray();
- assert.eq(results.length, numdocs);
+ const runAndCompare = function() {
+ const results = query().toArray();
+ assert.eq(results.length, numdocs);
- // Make sure the values are the same for all documents
- for (let i = 0; i < numdocs; ++i) {
- assert.eq(results[0].timeField, results[i].timeField);
- }
+ // Make sure the values are the same for all documents
+ for (let i = 0; i < numdocs; ++i) {
+ assert.eq(results[0].timeField, results[i].timeField);
+ }
+ return results;
+ };
+
+ const results = runAndCompare();
// Sleep for a while and then rerun.
sleep(3000);
- const resultsLater = query().toArray();
- assert.eq(resultsLater.length, numdocs);
+ const resultsLater = runAndCompare();
// Later results should be later in time.
- assert.lte(results[0].timeField, resultsLater[0].timeField);
+ assert.lt(results[0].timeField, resultsLater[0].timeField);
+
+ // Sleep for a while and then run for the third time.
+ //
+ // Test when the query is cached (it can take two executions of a query for a plan to get
+ // cached).
+ sleep(3000);
+
+ const resultsLast = runAndCompare();
+
+ // 'resultsLast' should be later in time than 'resultsLater'.
+ assert.lt(resultsLater[0].timeField, resultsLast[0].timeField);
}
function runTestsExpectFailure(query) {
@@ -64,7 +80,7 @@ function runTestsExpectFailure(query) {
}
function baseCollectionNowFind() {
- return otherColl.find({$expr: {$lte: ["$timeField", "$$NOW"]}});
+ return otherColl.find({$expr: {$lt: ["$timeField", "$$NOW"]}});
}
function baseCollectionClusterTimeFind() {
@@ -98,6 +114,18 @@ function withExprNow() {
return viewWithNow.find({$expr: {$eq: ["$timeField", "$$NOW"]}});
}
+function projWithNow() {
+ return coll.find({}, {timeField: "$$NOW"});
+}
+
+function aggWithNow() {
+ return coll.aggregate([{$project: {timeField: "$$NOW"}}]);
+}
+
+function aggWithNowNotPushedDown() {
+ return coll.aggregate([{$_internalInhibitOptimization: {}}, {$project: {timeField: "$$NOW"}}]);
+}
+
function withExprClusterTime() {
return db.runCommand({
find: viewWithClusterTime.getName(),
@@ -106,10 +134,16 @@ function withExprClusterTime() {
}
// Test that $$NOW is usable in all contexts.
-runTests(baseCollectionNowFind);
-runTests(baseCollectionNowAgg);
+//
+// $$NOW used at insertion time, it is expected values are not changed.
+assert.eq(baseCollectionNowFind().toArray().length, numdocs);
+// $$NOW used in the query, it is expected to update its time value for different runs.
runTests(fromViewWithNow);
runTests(withExprNow);
+runTests(baseCollectionNowAgg);
+runTests(projWithNow);
+runTests(aggWithNow);
+runTests(aggWithNowNotPushedDown);
// Test that $$NOW can be used in explain for both find and aggregate.
assert.commandWorked(coll.explain().find({$expr: {$lte: ["$timeField", "$$NOW"]}}).finish());
@@ -121,4 +155,43 @@ runTestsExpectFailure(baseCollectionClusterTimeFind);
runTestsExpectFailure(baseCollectionClusterTimeAgg);
runTestsExpectFailure(fromViewWithClusterTime);
runTestsExpectFailure(withExprClusterTime);
+
+if (checkSBEEnabled(db, ["featureFlagSbeFull"])) {
+ function verifyPlanCacheSize(query) {
+ coll.getPlanCache().clear();
+
+ query().toArray();
+ // It can take two executions of a query for a plan to get cached.
+ query().toArray();
+
+ const caches = coll.getPlanCache().list();
+ assert.eq(caches.length, 1, caches);
+ assert.eq(caches[0].cachedPlan.stages.includes("Date"), false, caches);
+ }
+
+ // Query with $$NOW will be cached.
+ verifyPlanCacheSize(projWithNow);
+ verifyPlanCacheSize(aggWithNow);
+ // $$NOW is not in SBE query.
+ verifyPlanCacheSize(fromViewWithNow);
+ verifyPlanCacheSize(withExprNow);
+ // $$NOW could not be pushed down into SBE.
+ verifyPlanCacheSize(baseCollectionNowAgg);
+ verifyPlanCacheSize(aggWithNowNotPushedDown);
+}
+
+// Insert an doc with a future time.
+const futureColl = db[coll.getName() + "_future"];
+futureColl.drop();
+const time = new Date();
+time.setSeconds(time.getSeconds() + 3);
+assert.commandWorked(futureColl.insert({timeField: time}));
+
+// The 'timeField' value is later than '$$NOW' in '$expr'.
+assert.eq(0, futureColl.find({$expr: {$lt: ["$timeField", "$$NOW"]}}).itcount());
+// The '$$NOW' in '$expr' should advance its value after sleeping for 3 second, the 'timeField'
+// value should be earlier than it now.
+assert.soon(() => {
+ return futureColl.find({$expr: {$lt: ["$timeField", "$$NOW"]}}).itcount() == 1;
+}, "$$NOW should catch up after 3 seconds");
}());
diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp
index 3a5e4107e27..fd430493e80 100644
--- a/src/mongo/db/pipeline/expression.cpp
+++ b/src/mongo/db/pipeline/expression.cpp
@@ -2454,6 +2454,14 @@ intrusive_ptr<Expression> ExpressionFieldPath::optimize() {
return ExpressionConstant::create(getExpressionContext(), Value());
}
+ if (_variable == Variables::kNowId || _variable == Variables::kClusterTimeId) {
+ // The agg system is allowed to replace the ExpressionFieldPath with an ExpressionConstant,
+ // which in turn would result in a plan cache entry that inlines the value of a system
+ // variable. However, the values of these system variables are not guaranteed to be constant
+ // across different executions of the same query shape, so we prohibit the optimization.
+ return intrusive_ptr<Expression>(this);
+ }
+
if (getExpressionContext()->variables.hasConstantValue(_variable)) {
return ExpressionConstant::create(
getExpressionContext(), evaluate(Document(), &(getExpressionContext()->variables)));
diff --git a/src/mongo/db/pipeline/expression_date_test.cpp b/src/mongo/db/pipeline/expression_date_test.cpp
index 1a76e1e1838..419028b9f06 100644
--- a/src/mongo/db/pipeline/expression_date_test.cpp
+++ b/src/mongo/db/pipeline/expression_date_test.cpp
@@ -2137,13 +2137,14 @@ TEST_F(ExpressionDateArithmeticsTest, OptimizesToConstant) {
dateAddExp = Expression::parseExpression(expCtx.get(), doc, expCtx->variablesParseState);
ASSERT(dynamic_cast<ExpressionConstant*>(dateAddExp->optimize().get()));
+ // Test that $$NOW will not be optimized as constant.
doc = BSON(expName << BSON("startDate"
<< "$$NOW"
<< "unit"
<< "day"
<< "amount" << 1));
dateAddExp = Expression::parseExpression(expCtx.get(), doc, expCtx->variablesParseState);
- ASSERT(dynamic_cast<ExpressionConstant*>(dateAddExp->optimize().get()));
+ ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateAddExp->optimize().get()));
// Test that expression does not optimize to constant if some of the parameters is not a
diff --git a/src/mongo/db/pipeline/expression_field_path_test.cpp b/src/mongo/db/pipeline/expression_field_path_test.cpp
index 13b59c1b4c4..1cd92cc2807 100644
--- a/src/mongo/db/pipeline/expression_field_path_test.cpp
+++ b/src/mongo/db/pipeline/expression_field_path_test.cpp
@@ -186,6 +186,26 @@ TEST(FieldPath, NoOptimizationOnVariableWithMissingValue) {
ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(optimizedExpr.get()));
}
+TEST(FieldPath, NoOptimizationOnCertainVariables) {
+ auto expCtx = ExpressionContextForTest{};
+
+ {
+ auto expr = ExpressionFieldPath::parse(&expCtx, "$$NOW", expCtx.variablesParseState);
+ ASSERT_TRUE(dynamic_cast<ExpressionFieldPath*>(expr.get()));
+
+ auto optimizedExpr = expr->optimize();
+ ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(optimizedExpr.get()));
+ }
+ {
+ auto expr =
+ ExpressionFieldPath::parse(&expCtx, "$$CLUSTER_TIME", expCtx.variablesParseState);
+ ASSERT_TRUE(dynamic_cast<ExpressionFieldPath*>(expr.get()));
+
+ auto optimizedExpr = expr->optimize();
+ ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(optimizedExpr.get()));
+ }
+}
+
TEST(FieldPath, ScalarVariableWithDottedFieldPathOptimizesToConstantMissingValue) {
auto expCtx = ExpressionContextForTest{};
auto varId = expCtx.variablesParseState.defineVariable("userVar");