/**
 * 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();
}
Ejemplo n.º 4
0
    // 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();
    }