Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions jstests/core/explain_use_backup_plan.js
Original file line number Diff line number Diff line change
@@ -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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put the contents of this test in an immediately-invoked-function-expression (https://en.wikipedia.org/wiki/Immediately-invoked_function_expression)

(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");
})();
4 changes: 4 additions & 0 deletions jstests/libs/analyze_plan.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
8 changes: 8 additions & 0 deletions src/mongo/db/exec/multi_plan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ MultiPlanStage::MultiPlanStage(OperationContext* opCtx,
_query(cq),
_bestPlanIdx(kNoSuchPlan),
_backupPlanIdx(kNoSuchPlan),
_originalWinningPlanIdx(kNoSuchPlan),
_failure(false),
_failureCount(0),
_statusMemberId(WorkingSet::INVALID_ID) {
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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;
Expand Down
24 changes: 20 additions & 4 deletions src/mongo/db/exec/multi_plan.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -194,14 +206,18 @@ class MultiPlanStage final : public PlanStage {
// one-to-one with _candidates.
std::vector<CandidatePlan> _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:
Expand Down
69 changes: 56 additions & 13 deletions src/mongo/db/query/explain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::unique_ptr<PlanStageStats>> getRejectedPlansTrialStats(PlanExecutor* exec) {
std::vector<std::unique_ptr<PlanStageStats>> 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<std::unique_ptr<PlanStageStats>> 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<size_t>(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<size_t>(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<size_t>(mps->bestPlanIdx())) {
res.emplace_back(std::move(mpsStats->children[i]));
}
}
}
}

Expand All @@ -289,6 +299,17 @@ unique_ptr<PlanStageStats> 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<PlanStageStats> 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 {
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -680,14 +711,24 @@ void Explain::generatePlannerInfo(PlanExecutor* exec,
winningPlanBob.doneFast();

// Genenerate array of rejected plans.
const vector<unique_ptr<PlanStageStats>> rejectedStats = getRejectedPlansTrialStats(exec);
const vector<unique_ptr<PlanStageStats>> rejectedStats = getRejectedPlansTrialStats(exec, backupPlanUsed);
BSONArrayBuilder allPlansBob(plannerBob.subarrayStart("rejectedPlans"));
for (size_t i = 0; i < rejectedStats.size(); i++) {
BSONObjBuilder childBob(allPlansBob.subobjStart());
statsToBSON(*rejectedStats[i], &childBob, ExplainOptions::Verbosity::kQueryPlanner);
}
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();
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -798,7 +840,7 @@ void Explain::generateExecutionInfo(PlanExecutor* exec,
planBob.doneFast();
}

const vector<unique_ptr<PlanStageStats>> rejectedStats = getRejectedPlansTrialStats(exec);
const vector<unique_ptr<PlanStageStats>> rejectedStats = getRejectedPlansTrialStats(exec, backupPlanUsed);
for (size_t i = 0; i < rejectedStats.size(); ++i) {
BSONObjBuilder planBob(allPlansBob.subobjStart());
generateSinglePlanExecutionInfo(
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/mongo/db/query/explain.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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',
Expand Down