diff --git a/jstests/core/explain_use_backup_plan.js b/jstests/core/explain_use_backup_plan.js new file mode 100644 index 0000000000000..f96124b21e720 --- /dev/null +++ b/jstests/core/explain_use_backup_plan.js @@ -0,0 +1,31 @@ +// Test that the explain will use backup plan if the original winning plan ran out of memory in the +// "executionStats" mode +// This test was designed to reproduce SERVER-32721" + +load("jstests/libs/analyze_plan.js"); +(function() { + "use strict"; + + const explain_backup_plan_test = db.explain_backup_plan; + explain_backup_plan_test.drop(); + + let bulk = explain_backup_plan_test.initializeUnorderedBulkOp(); + + for (let i = 0; i < 100000; ++i) { + bulk.insert({_id: i, x: i, y: i}); + } + + bulk.execute(); + explain_backup_plan_test.ensureIndex({x: 1}); + + db.adminCommand({setParameter: 1, internalQueryExecMaxBlockingSortBytes: 100}); + + const test1 = explain_backup_plan_test.find({x: {$gte: 90}}).sort({_id: 1}).explain(true); + const test2 = explain_backup_plan_test.find({x: {$gte: 90000}}).sort({_id: 1}).explain(true); + // This query will not use the backup plan, hence it generates only two stages: winningPlan and + // rejectedPlans. + assert(!backupPlanUsed(test1), "test1 did not use a backup plan"); + // This query will use backup plan, the exaplin output for this query will generate three + // stages: winningPlan, rejectedPlans and originalWinningPlan. + assert(backupPlanUsed(test2), "backup plan invoked in test2"); +})(); \ No newline at end of file diff --git a/jstests/libs/analyze_plan.js b/jstests/libs/analyze_plan.js index 6a719d611f65e..935e6c1e9441c 100644 --- a/jstests/libs/analyze_plan.js +++ b/jstests/libs/analyze_plan.js @@ -287,3 +287,7 @@ function assertCoveredQueryAndCount({collection, query, project, count}) { "Winning plan for count was not covered: " + tojson(explain.queryPlanner.winningPlan)); assertExplainCount({explainResults: explain, expectedCount: count}); } + +function backupPlanUsed(root) { + return root.queryPlanner.backupPlanUsed; +} \ No newline at end of file diff --git a/src/mongo/db/exec/multi_plan.cpp b/src/mongo/db/exec/multi_plan.cpp index 5dc4bd0b86340..2c5e742de7da6 100644 --- a/src/mongo/db/exec/multi_plan.cpp +++ b/src/mongo/db/exec/multi_plan.cpp @@ -71,6 +71,7 @@ MultiPlanStage::MultiPlanStage(OperationContext* opCtx, _query(cq), _bestPlanIdx(kNoSuchPlan), _backupPlanIdx(kNoSuchPlan), + _originalWinningPlanIdx(kNoSuchPlan), _failure(false), _failureCount(0), _statusMemberId(WorkingSet::INVALID_ID) { @@ -128,6 +129,7 @@ PlanStage::StageState MultiPlanStage::doWork(WorkingSetID* out) { // cached plan runner to fall back on a different solution // if the best solution fails. Alternatively we could try to // defer cache insertion to be after the first produced result. + _originalWinningPlanIdx = _bestPlanIdx; _collection->infoCache()->getPlanCache()->remove(*_query).transitional_ignore(); @@ -452,6 +454,12 @@ void MultiPlanStage::doInvalidate(OperationContext* opCtx, bool MultiPlanStage::hasBackupPlan() const { return kNoSuchPlan != _backupPlanIdx; } +int MultiPlanStage::backupPlanIdx() const { + return _backupPlanIdx; +} +int MultiPlanStage::originalWinningPlanIdx() const { + return _originalWinningPlanIdx; +} bool MultiPlanStage::bestPlanChosen() const { return kNoSuchPlan != _bestPlanIdx; diff --git a/src/mongo/db/exec/multi_plan.h b/src/mongo/db/exec/multi_plan.h index 5d6369e3c0dc4..3b70cb9c43582 100644 --- a/src/mongo/db/exec/multi_plan.h +++ b/src/mongo/db/exec/multi_plan.h @@ -129,9 +129,21 @@ class MultiPlanStage final : public PlanStage { /** Return true if a best plan has been chosen */ bool bestPlanChosen() const; - /** Return the index of the best plan chosen, for testing */ + /* + * Return the index of the best plan chosen + */ int bestPlanIdx() const; + /** + * Return the index of the backup plan chosen + */ + int backupPlanIdx() const; + + /** + * Return the index of the original winning plan chosen + */ + int originalWinningPlanIdx() const; + /** * Returns the QuerySolution for the best plan, or NULL if no best plan * @@ -194,14 +206,18 @@ class MultiPlanStage final : public PlanStage { // one-to-one with _candidates. std::vector _candidates; - // index into _candidates, of the winner of the plan competition + // index into '_candidates' of the winner of the plan competition // uses -1 / kNoSuchPlan when best plan is not (yet) known int _bestPlanIdx; - // index into _candidates, of the backup plan for sort - // uses -1 / kNoSuchPlan when best plan is not (yet) known + // The index within '_candidates' of the non-blocking backup plan which can be used if a blocking plan fails. + // This is set to 'kNoSuchPlan' if there is no backup plan, or when it is not yet known. int _backupPlanIdx; + // Index into '_candidates' of the original winner of the plan which can be used if a blocking plan fails. + // This is set to 'kNoSuchPlan' if there is no backup plan, or when it is not (yet) known + int _originalWinningPlanIdx; + // Set if this MultiPlanStage cannot continue, and the query must fail. This can happen in // two ways. The first is that all candidate plans fail. Note that one plan can fail // during normal execution of the plan competition. Here is an example: diff --git a/src/mongo/db/query/explain.cpp b/src/mongo/db/query/explain.cpp index ae985487ffbb3..57a488aeb18d1 100644 --- a/src/mongo/db/query/explain.cpp +++ b/src/mongo/db/query/explain.cpp @@ -260,20 +260,30 @@ void appendMultikeyPaths(const BSONObj& keyPattern, /** * Gather the PlanStageStats for all of the losing plans. If exec doesn't have a MultiPlanStage * (or any losing plans), will return an empty vector. + * If a backup plan was used, includes the stats of the new winning plan instaed of the */ -std::vector> getRejectedPlansTrialStats(PlanExecutor* exec) { +std::vector> getRejectedPlansTrialStats(PlanExecutor* exec, bool backupPlanUsed) { // Inspect the tree to see if there is a MultiPlanStage. Plan selection has already happened at // this point, since we have a PlanExecutor. const auto mps = getMultiPlanStage(exec->getRootStage()); std::vector> res; - + // Get the stats from the trial period for all the plans. if (mps) { const auto mpsStats = mps->getStats(); - for (size_t i = 0; i < mpsStats->children.size(); ++i) { - if (i != static_cast(mps->bestPlanIdx())) { - res.emplace_back(std::move(mpsStats->children[i])); - } + + if (backupPlanUsed) { + for (size_t i = 0; i < mpsStats->children.size(); ++i) { + if (i != static_cast(mps->originalWinningPlanIdx())) { + res.emplace_back(std::move(mpsStats->children[i])); + } + } + } else { + for (size_t i = 0; i < mpsStats->children.size(); ++i) { + if (i != static_cast(mps->bestPlanIdx())) { + res.emplace_back(std::move(mpsStats->children[i])); + } + } } } @@ -289,6 +299,17 @@ unique_ptr getWinningPlanStatsTree(const PlanExecutor* exec) { : std::move(exec->getRootStage()->getStats()); } +/** + * Returns the root of the orginal winning plan used by 'exec'. + * This might be different than the final winning plan in the case that the MultiPlanStage selected a + * blocking plan which failed, and fell back to a non-blocking plan instead. + * If there is no MultiPlanStage in the tree, returns the root stage of 'exec'. + */ +unique_ptr getOriginalWinningPlanStatsTree(const PlanExecutor* exec) { + MultiPlanStage* mps = getMultiPlanStage(exec->getRootStage()); + return mps ? std::move(mps->getStats()->children[mps->originalWinningPlanIdx()]) + : std::move(exec->getRootStage()->getStats()); +} } // namespace namespace mongo { @@ -638,12 +659,22 @@ void Explain::getWinningPlanStats(const PlanExecutor* exec, BSONObjBuilder* bob) // static void Explain::generatePlannerInfo(PlanExecutor* exec, const Collection* collection, - BSONObjBuilder* out) { + BSONObjBuilder* out, bool backupPlanUsed) { CanonicalQuery* query = exec->getCanonicalQuery(); BSONObjBuilder plannerBob(out->subobjStart("queryPlanner")); plannerBob.append("plannerVersion", QueryPlanner::kPlannerVersion); + + const auto mps = getMultiPlanStage(exec->getRootStage()); + backupPlanUsed = ((mps->originalWinningPlanIdx()) > -1); + + if (backupPlanUsed) { + plannerBob.append("backupPlanUsed", true); + } else { + plannerBob.append("backupPlanUsed", false); + } + plannerBob.append("namespace", exec->nss().ns()); // Find whether there is an index filter set for the query shape. The 'indexFilterSet' @@ -680,7 +711,7 @@ void Explain::generatePlannerInfo(PlanExecutor* exec, winningPlanBob.doneFast(); // Genenerate array of rejected plans. - const vector> rejectedStats = getRejectedPlansTrialStats(exec); + const vector> rejectedStats = getRejectedPlansTrialStats(exec, backupPlanUsed); BSONArrayBuilder allPlansBob(plannerBob.subarrayStart("rejectedPlans")); for (size_t i = 0; i < rejectedStats.size(); i++) { BSONObjBuilder childBob(allPlansBob.subobjStart()); @@ -688,6 +719,16 @@ void Explain::generatePlannerInfo(PlanExecutor* exec, } allPlansBob.doneFast(); + if (backupPlanUsed) { + // Generate array of original winning plan + BSONObjBuilder originalWinningPlanBob(plannerBob.subobjStart("originalWinningPlan")); + const auto originalWinnerStats = getOriginalWinningPlanStatsTree(exec); + statsToBSON(*originalWinnerStats.get(), + &originalWinningPlanBob, + ExplainOptions::Verbosity::kQueryPlanner); + originalWinningPlanBob.doneFast(); + } + plannerBob.doneFast(); } @@ -758,13 +799,14 @@ void Explain::generateExecutionInfo(PlanExecutor* exec, ExplainOptions::Verbosity verbosity, Status executePlanStatus, PlanStageStats* winningPlanTrialStats, - BSONObjBuilder* out) { + BSONObjBuilder* out, bool backupPlanUsed) { invariant(verbosity >= ExplainOptions::Verbosity::kExecStats); if (verbosity >= ExplainOptions::Verbosity::kExecAllPlans && findStageOfType(exec->getRootStage(), STAGE_MULTI_PLAN) != nullptr) { invariant(winningPlanTrialStats, "winningPlanTrialStats must be non-null when requesting all execution stats"); } + BSONObjBuilder execBob(out->subobjStart("executionStats")); // If there is an execution error while running the query, the error is reported under @@ -798,7 +840,7 @@ void Explain::generateExecutionInfo(PlanExecutor* exec, planBob.doneFast(); } - const vector> rejectedStats = getRejectedPlansTrialStats(exec); + const vector> rejectedStats = getRejectedPlansTrialStats(exec, backupPlanUsed); for (size_t i = 0; i < rejectedStats.size(); ++i) { BSONObjBuilder planBob(allPlansBob.subobjStart()); generateSinglePlanExecutionInfo( @@ -821,13 +863,14 @@ void Explain::explainStages(PlanExecutor* exec, // // Use the stats trees to produce explain BSON. // + bool backupPlanUsed = false; if (verbosity >= ExplainOptions::Verbosity::kQueryPlanner) { - generatePlannerInfo(exec, collection, out); + generatePlannerInfo(exec, collection, out, backupPlanUsed); } if (verbosity >= ExplainOptions::Verbosity::kExecStats) { - generateExecutionInfo(exec, verbosity, executePlanStatus, winningPlanTrialStats, out); + generateExecutionInfo(exec, verbosity, executePlanStatus, winningPlanTrialStats, out, backupPlanUsed); } } @@ -855,8 +898,8 @@ void Explain::explainStages(PlanExecutor* exec, const Collection* collection, ExplainOptions::Verbosity verbosity, BSONObjBuilder* out) { + auto winningPlanTrialStats = Explain::getWinningPlanTrialStats(exec); - Status executePlanStatus = Status::OK(); // If we need execution stats, then run the plan in order to gather the stats. diff --git a/src/mongo/db/query/explain.h b/src/mongo/db/query/explain.h index cb177321f1c23..a6520f528c98a 100644 --- a/src/mongo/db/query/explain.h +++ b/src/mongo/db/query/explain.h @@ -178,7 +178,7 @@ class Explain { */ static void generatePlannerInfo(PlanExecutor* exec, const Collection* collection, - BSONObjBuilder* out); + BSONObjBuilder* out, bool backupPlanUsed); /** * Private helper that does the heavy-lifting for the public statsToBSON(...) functions @@ -204,7 +204,7 @@ class Explain { ExplainOptions::Verbosity verbosity, Status executePlanStatus, PlanStageStats* winningPlanTrialStats, - BSONObjBuilder* out); + BSONObjBuilder* out, bool backupPlanUsed); /** * Generates the execution stats section for the stats tree 'stats',