diff options
author | David Storch <david.storch@mongodb.com> | 2021-03-11 14:31:02 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-03-26 15:19:39 +0000 |
commit | 7e996c56ac576f8ef548a52c84aa0edf506344b7 (patch) | |
tree | 0245a452a49eebaa9c903d09c83110604c98d7f7 | |
parent | 7d1cf0943da755745baf61d86a20507594b6931d (diff) | |
download | mongo-7e996c56ac576f8ef548a52c84aa0edf506344b7.tar.gz |
SERVER-54710 Improve checks for overlarge BSON when generating explain output
(cherry picked from commit 4fc3991bf64b33ca5f5237722bc563f8eb1a552a)
(cherry picked from commit 754b0efe8549fe2d5fa2847676985709c60412a7)
(cherry picked from commit fc232126bd406e6c85ec45b869c88d7a238e60c5)
-rw-r--r-- | jstests/noPassthrough/explain_output_truncation.js | 58 | ||||
-rw-r--r-- | src/mongo/db/query/explain.cpp | 19 | ||||
-rw-r--r-- | src/mongo/db/query/query_knobs.cpp | 13 | ||||
-rw-r--r-- | src/mongo/db/query/query_knobs.h | 3 |
4 files changed, 83 insertions, 10 deletions
diff --git a/jstests/noPassthrough/explain_output_truncation.js b/jstests/noPassthrough/explain_output_truncation.js new file mode 100644 index 00000000000..83c4f6412c7 --- /dev/null +++ b/jstests/noPassthrough/explain_output_truncation.js @@ -0,0 +1,58 @@ +/** + * Test that explain output is correctly truncated when it grows too large. + */ +(function() { + "use strict"; + + load("jstests/libs/analyze_plan.js"); + + const dbName = "test"; + const collName = jsTestName(); + const explainSizeParam = "internalQueryExplainSizeThresholdBytes"; + + const conn = MongoRunner.runMongod({}); + assert.neq(conn, null, "mongod failed to start up"); + + const testDb = conn.getDB(dbName); + const coll = testDb[collName]; + coll.drop(); + + assert.commandWorked(coll.createIndex({a: 1})); + + // Explain output should show a simple IXSCAN => FETCH => SORT_KEY_GENERATOR => SORT plan with + // no truncation. + let explain = coll.find({a: 1, b: 1}).sort({c: 1}).explain(); + let sortStage = getPlanStage(explain.queryPlanner.winningPlan, "SORT"); + assert.neq(sortStage, null, explain); + let sortKeyGenStage = getPlanStage(explain.queryPlanner.winningPlan, "SORT_KEY_GENERATOR"); + assert.neq(sortKeyGenStage, null, explain); + let fetchStage = getPlanStage(sortKeyGenStage, "FETCH"); + assert.neq(fetchStage, null, explain); + let ixscanStage = getPlanStage(sortStage, "IXSCAN"); + assert.neq(ixscanStage, null, explain); + + // Calculate the size of explain output without the index scan. If the explain size threshold is + // set near this amount, then the IXSCAN stage will need to be truncated. + assert.neq(ixscanStage, null, explain); + const newExplainSize = + Object.bsonsize(explain.queryPlanner) - Object.bsonsize(ixscanStage) - 10; + assert.gt(newExplainSize, 0); + + // Reduce the size at which we start truncating explain output. If we explain the same query + // again, then the FETCH stage should be present, but the IXSCAN stage should be truncated. + assert.commandWorked( + testDb.adminCommand({setParameter: 1, [explainSizeParam]: newExplainSize})); + + explain = coll.find({a: 1, b: 1}).sort({c: 1}).explain(); + assert(planHasStage(testDb, explain.queryPlanner.winningPlan, "SORT"), explain); + assert(planHasStage(testDb, explain.queryPlanner.winningPlan, "SORT_KEY_GENERATOR"), explain); + fetchStage = getPlanStage(explain.queryPlanner.winningPlan, "FETCH"); + assert.neq(fetchStage, null, explain); + assert(fetchStage.hasOwnProperty("inputStage"), explain); + assert(fetchStage.inputStage.hasOwnProperty("warning"), explain); + assert.eq( + fetchStage.inputStage.warning, "stats tree exceeded BSON size limit for explain", explain); + assert(!planHasStage(testDb, explain, "IXSCAN"), explain); + + MongoRunner.stopMongod(conn); +}()); diff --git a/src/mongo/db/query/explain.cpp b/src/mongo/db/query/explain.cpp index 0627cd78912..0067182a3df 100644 --- a/src/mongo/db/query/explain.cpp +++ b/src/mongo/db/query/explain.cpp @@ -307,10 +307,9 @@ void Explain::statsToBSON(const PlanStageStats& stats, invariant(bob); invariant(topLevelBob); - // Stop as soon as the BSON object we're building exceeds 10 MB. - static const int kMaxStatsBSONSize = 10 * 1024 * 1024; - if (topLevelBob->len() > kMaxStatsBSONSize) { - bob->append("warning", "stats tree exceeded 10 MB"); + // Stop as soon as the BSON object we're building exceeds the limit. + if (topLevelBob->len() > internalQueryExplainSizeThresholdBytes.load()) { + bob->append("warning", "stats tree exceeded BSON size limit for explain"); return; } @@ -397,12 +396,11 @@ void Explain::statsToBSON(const PlanStageStats& stats, bob->appendBool("isPartial", spec->isPartial); bob->append("indexVersion", spec->indexVersion); - BSONObjBuilder indexBoundsBob; + BSONObjBuilder indexBoundsBob(bob->subobjStart("indexBounds")); indexBoundsBob.append("startKey", spec->startKey); indexBoundsBob.append("startKeyInclusive", spec->startKeyInclusive); indexBoundsBob.append("endKey", spec->endKey); indexBoundsBob.append("endKeyInclusive", spec->endKeyInclusive); - bob->append("indexBounds", indexBoundsBob.obj()); } else if (STAGE_DELETE == stats.stageType) { DeleteStats* spec = static_cast<DeleteStats*>(stats.specific.get()); @@ -428,7 +426,8 @@ void Explain::statsToBSON(const PlanStageStats& stats, bob->append("indexVersion", spec->indexVersion); bob->append("direction", spec->direction > 0 ? "forward" : "backward"); - if ((topLevelBob->len() + spec->indexBounds.objsize()) > kMaxStatsBSONSize) { + if ((topLevelBob->len() + spec->indexBounds.objsize()) > + internalQueryExplainSizeThresholdBytes.load()) { bob->append("warning", "index bounds omitted due to BSON size limit"); } else { bob->append("indexBounds", spec->indexBounds); @@ -499,7 +498,8 @@ void Explain::statsToBSON(const PlanStageStats& stats, bob->append("indexVersion", spec->indexVersion); bob->append("direction", spec->direction > 0 ? "forward" : "backward"); - if ((topLevelBob->len() + spec->indexBounds.objsize()) > kMaxStatsBSONSize) { + if ((topLevelBob->len() + spec->indexBounds.objsize()) > + internalQueryExplainSizeThresholdBytes.load()) { bob->append("warning", "index bounds omitted due to BSON size limit"); } else { bob->append("indexBounds", spec->indexBounds); @@ -595,9 +595,8 @@ void Explain::statsToBSON(const PlanStageStats& stats, // the output more readable by saving a level of nesting. Name the field 'inputStage' // rather than 'inputStages'. if (1 == stats.children.size()) { - BSONObjBuilder childBob; + BSONObjBuilder childBob(bob->subobjStart("inputStage")); statsToBSON(*stats.children[0], verbosity, &childBob, topLevelBob); - bob->append("inputStage", childBob.obj()); return; } diff --git a/src/mongo/db/query/query_knobs.cpp b/src/mongo/db/query/query_knobs.cpp index 4245820cc6b..2cd2c61f022 100644 --- a/src/mongo/db/query/query_knobs.cpp +++ b/src/mongo/db/query/query_knobs.cpp @@ -136,4 +136,17 @@ MONGO_EXPORT_SERVER_PARAMETER(internalQueryMaxAddToSetBytes, int, 100 * 1024 * 1 } return Status::OK(); }); + +MONGO_EXPORT_SERVER_PARAMETER(internalQueryExplainSizeThresholdBytes, int, 10 * 1024 * 1024) + ->withValidator([](const int& newVal) { + if (newVal <= 0) { + return Status(ErrorCodes::BadValue, + "internalQueryExplainSizeThresholdBytes must be positive"); + } else if (newVal > BSONObjMaxInternalSize) { + return Status(ErrorCodes::BadValue, + "internalQueryExplainSizeThresholdBytes cannot exceed max BSON size"); + } else { + return Status::OK(); + } + }); } // namespace mongo diff --git a/src/mongo/db/query/query_knobs.h b/src/mongo/db/query/query_knobs.h index f95f0aa82bf..6165be149f8 100644 --- a/src/mongo/db/query/query_knobs.h +++ b/src/mongo/db/query/query_knobs.h @@ -156,4 +156,7 @@ extern AtomicBool internalQueryProhibitBlockingMergeOnMongoS; extern AtomicInt32 internalQueryMaxPushBytes; extern AtomicInt32 internalQueryMaxAddToSetBytes; + +// The number of bytes after which explain should start truncating portions of its output. +extern AtomicInt32 internalQueryExplainSizeThresholdBytes; } // namespace mongo |