/** * Test the way cache entries are added (either "active" or "inactive") to the plan cache. */ TEST_F(QueryStageCachedPlan, QueryStageCachedPlanAddsActiveCacheEntries) { AutoGetCollectionForReadCommand ctx(&_opCtx, nss); Collection* collection = ctx.getCollection(); ASSERT(collection); // Never run - just used as a key for the cache's get() functions, since all of the other // CanonicalQueries created in this test will have this shape. const auto shapeCq = canonicalQueryFromFilterObj(opCtx(), nss, fromjson("{a: {$gte: 123}, b: {$gte: 123}}")); // Query can be answered by either index on "a" or index on "b". const auto noResultsCq = canonicalQueryFromFilterObj(opCtx(), nss, fromjson("{a: {$gte: 11}, b: {$gte: 11}}")); // We shouldn't have anything in the plan cache for this shape yet. PlanCache* cache = collection->infoCache()->getPlanCache(); ASSERT(cache); ASSERT_EQ(cache->get(*shapeCq).state, PlanCache::CacheEntryState::kNotPresent); // Run the CachedPlanStage with a long-running child plan. Replanning should be // triggered and an inactive entry will be added. forceReplanning(collection, noResultsCq.get()); // Check for an inactive cache entry. ASSERT_EQ(cache->get(*shapeCq).state, PlanCache::CacheEntryState::kPresentInactive); // The works should be 1 for the entry since the query we ran should not have any results. auto entry = assertGet(cache->getEntry(*shapeCq)); size_t works = 1U; ASSERT_EQ(entry->works, works); const size_t kExpectedNumWorks = 10; for (int i = 0; i < std::ceil(std::log(kExpectedNumWorks) / std::log(2)); ++i) { works *= 2; // Run another query of the same shape, which is less selective, and therefore takes // longer). auto someResultsCq = canonicalQueryFromFilterObj(opCtx(), nss, fromjson("{a: {$gte: 1}, b: {$gte: 0}}")); forceReplanning(collection, someResultsCq.get()); ASSERT_EQ(cache->get(*shapeCq).state, PlanCache::CacheEntryState::kPresentInactive); // The works on the cache entry should have doubled. entry = assertGet(cache->getEntry(*shapeCq)); ASSERT_EQ(entry->works, works); } // Run another query which takes less time, and be sure an active entry is created. auto fewResultsCq = canonicalQueryFromFilterObj(opCtx(), nss, fromjson("{a: {$gte: 6}, b: {$gte: 0}}")); forceReplanning(collection, fewResultsCq.get()); // Now there should be an active cache entry. ASSERT_EQ(cache->get(*shapeCq).state, PlanCache::CacheEntryState::kPresentActive); entry = assertGet(cache->getEntry(*shapeCq)); // This will query will match {a: 6} through {a:9} (4 works), plus one for EOF = 5 works. ASSERT_EQ(entry->works, 5U); }
TEST_F(QueryStageCachedPlan, DeactivatesEntriesOnReplan) { AutoGetCollectionForReadCommand ctx(&_opCtx, nss); Collection* collection = ctx.getCollection(); ASSERT(collection); // Never run - just used as a key for the cache's get() functions, since all of the other // CanonicalQueries created in this test will have this shape. const auto shapeCq = canonicalQueryFromFilterObj(opCtx(), nss, fromjson("{a: {$gte: 123}, b: {$gte: 123}}")); // Query can be answered by either index on "a" or index on "b". const auto noResultsCq = canonicalQueryFromFilterObj(opCtx(), nss, fromjson("{a: {$gte: 11}, b: {$gte: 11}}")); // We shouldn't have anything in the plan cache for this shape yet. PlanCache* cache = collection->infoCache()->getPlanCache(); ASSERT(cache); ASSERT_EQ(cache->get(*shapeCq).state, PlanCache::CacheEntryState::kNotPresent); // Run the CachedPlanStage with a long-running child plan. Replanning should be // triggered and an inactive entry will be added. forceReplanning(collection, noResultsCq.get()); // Check for an inactive cache entry. ASSERT_EQ(cache->get(*shapeCq).state, PlanCache::CacheEntryState::kPresentInactive); // Run the plan again, to create an active entry. forceReplanning(collection, noResultsCq.get()); // The works should be 1 for the entry since the query we ran should not have any results. ASSERT_EQ(cache->get(*noResultsCq.get()).state, PlanCache::CacheEntryState::kPresentActive); auto entry = assertGet(cache->getEntry(*shapeCq)); size_t works = 1U; ASSERT_EQ(entry->works, works); // Run another query which takes long enough to evict the active cache entry. The current // cache entry's works value is a very low number. When replanning is triggered, the cache // entry will be deactivated, but the new plan will not overwrite it, since the new plan will // have a higher works. Therefore, we will be left in an inactive entry which has had its works // value doubled from 1 to 2. auto highWorksCq = canonicalQueryFromFilterObj(opCtx(), nss, fromjson("{a: {$gte: 0}, b: {$gte:0}}")); forceReplanning(collection, highWorksCq.get()); ASSERT_EQ(cache->get(*shapeCq).state, PlanCache::CacheEntryState::kPresentInactive); ASSERT_EQ(assertGet(cache->getEntry(*shapeCq))->works, 2U); // Again, force replanning. This time run the initial query which finds no results. The multi // planner will choose a plan with works value lower than the existing inactive // entry. Replanning will thus deactivate the existing entry (it's already // inactive so this is a noop), then create a new entry with a works value of 1. forceReplanning(collection, noResultsCq.get()); ASSERT_EQ(cache->get(*shapeCq).state, PlanCache::CacheEntryState::kPresentActive); ASSERT_EQ(assertGet(cache->getEntry(*shapeCq))->works, 1U); }
// static Status PlanCacheListPlans::list(OperationContext* opCtx, const PlanCache& planCache, const std::string& ns, const BSONObj& cmdObj, BSONObjBuilder* bob) { auto statusWithCQ = canonicalize(opCtx, ns, cmdObj); if (!statusWithCQ.isOK()) { return statusWithCQ.getStatus(); } if (!internalQueryCacheListPlansNewOutput.load()) return listPlansOriginalFormat(std::move(statusWithCQ.getValue()), planCache, bob); unique_ptr<CanonicalQuery> cq = std::move(statusWithCQ.getValue()); auto entry = uassertStatusOK(planCache.getEntry(*cq)); // internalQueryCacheDisableInactiveEntries is True and we should use the new output format. Explain::planCacheEntryToBSON(*entry, bob); return Status::OK(); }
// static Status PlanCacheListPlans::list(OperationContext* txn, const PlanCache& planCache, const std::string& ns, const BSONObj& cmdObj, BSONObjBuilder* bob) { CanonicalQuery* cqRaw; Status status = canonicalize(txn, ns, cmdObj, &cqRaw); if (!status.isOK()) { return status; } scoped_ptr<CanonicalQuery> cq(cqRaw); if (!planCache.contains(*cq)) { // Return empty plans in results if query shape does not // exist in plan cache. BSONArrayBuilder plansBuilder(bob->subarrayStart("plans")); plansBuilder.doneFast(); return Status::OK(); } PlanCacheEntry* entryRaw; Status result = planCache.getEntry(*cq, &entryRaw); if (!result.isOK()) { return result; } scoped_ptr<PlanCacheEntry> entry(entryRaw); BSONArrayBuilder plansBuilder(bob->subarrayStart("plans")); size_t numPlans = entry->plannerData.size(); invariant(numPlans == entry->decision->stats.size()); invariant(numPlans == entry->decision->scores.size()); for (size_t i = 0; i < numPlans; ++i) { BSONObjBuilder planBob(plansBuilder.subobjStart()); // Create plan details field. // Currently, simple string representationg of // SolutionCacheData. Need to revisit format when we // need to parse user-provided plan details for planCacheAddPlan. SolutionCacheData* scd = entry->plannerData[i]; BSONObjBuilder detailsBob(planBob.subobjStart("details")); detailsBob.append("solution", scd->toString()); detailsBob.doneFast(); // reason is comprised of score and initial stats provided by // multi plan runner. BSONObjBuilder reasonBob(planBob.subobjStart("reason")); reasonBob.append("score", entry->decision->scores[i]); BSONObjBuilder statsBob(reasonBob.subobjStart("stats")); PlanStageStats* stats = entry->decision->stats.vector()[i]; if (stats) { Explain::statsToBSON(*stats, &statsBob); } statsBob.doneFast(); reasonBob.doneFast(); // BSON object for 'feedback' field is created from query executions // and shows number of executions since this cached solution was // created as well as score data (average and standard deviation). BSONObjBuilder feedbackBob(planBob.subobjStart("feedback")); if (i == 0U) { feedbackBob.append("nfeedback", int(entry->feedback.size())); feedbackBob.append("averageScore", entry->averageScore.get_value_or(0)); feedbackBob.append("stdDevScore",entry->stddevScore.get_value_or(0)); BSONArrayBuilder scoresBob(feedbackBob.subarrayStart("scores")); for (size_t i = 0; i < entry->feedback.size(); ++i) { BSONObjBuilder scoreBob(scoresBob.subobjStart()); scoreBob.append("score", entry->feedback[i]->score); } scoresBob.doneFast(); } feedbackBob.doneFast(); planBob.append("filterSet", scd->indexFilterApplied); } plansBuilder.doneFast(); return Status::OK(); }