Files
mongo/jstests/noPassthrough/plan_cache_memory_debug_info.js

224 lines
9.3 KiB
JavaScript

/**
* Tests that detailed debug information is excluded from new plan cache entries once the estimated
* cumulative size of the system's plan caches exceeds a pre-configured threshold.
*/
(function() {
"use strict";
load("jstests/libs/sbe_util.js"); // For checkSBEEnabled.
/**
* Creates two indexes for the given collection. In order for plans to be cached, there need to be
* at least two possible indexed plans.
*/
function createIndexesForColl(coll) {
assert.commandWorked(coll.createIndex({a: 1}));
assert.commandWorked(coll.createIndex({b: 1}));
}
function totalPlanCacheSize() {
const serverStatus = assert.commandWorked(db.serverStatus());
return serverStatus.metrics.query.planCacheTotalSizeEstimateBytes;
}
function planCacheContents(coll) {
return coll.aggregate([{$planCacheStats: {}}]).toArray();
}
/**
* Retrieve the cache entry associated with the query shape defined by the given 'filter' (assuming
* the query has no projection, sort, or collation). Asserts that a plan cache entry with the
* expected key is present in the $planCacheStats output, and returns the matching entry.
*/
function getPlanCacheEntryForFilter(coll, filter) {
// First, use explain to obtain the 'planCacheKey' associated with 'filter'.
const explain = coll.find(filter).explain();
const cacheKey = explain.queryPlanner.planCacheKey;
const allPlanCacheEntries =
coll.aggregate([{$planCacheStats: {}}, {$match: {planCacheKey: cacheKey}}]).toArray();
// There should be only one cache entry with the given key.
assert.eq(allPlanCacheEntries.length, 1, allPlanCacheEntries);
return allPlanCacheEntries[0];
}
function assertExistenceOfRequiredCacheEntryFields(entry) {
assert(entry.hasOwnProperty("version"), entry);
assert.eq(entry["version"], "1", entry);
assert(entry.hasOwnProperty("queryHash"), entry);
assert(entry.hasOwnProperty("planCacheKey"), entry);
assert(entry.hasOwnProperty("isActive"), entry);
assert(entry.hasOwnProperty("works"), entry);
assert(entry.hasOwnProperty("timeOfCreation"), entry);
assert(entry.hasOwnProperty("indexFilterSet"), entry);
assert(entry.hasOwnProperty("estimatedSizeBytes"), entry);
}
const debugInfoFields =
["createdFromQuery", "cachedPlan", "creationExecStats", "candidatePlanScores"];
function assertCacheEntryHasDebugInfo(entry) {
assertExistenceOfRequiredCacheEntryFields(entry);
for (const field of debugInfoFields) {
assert(entry.hasOwnProperty(field), entry);
}
}
function assertCacheEntryIsMissingDebugInfo(entry) {
assertExistenceOfRequiredCacheEntryFields(entry);
for (const field of debugInfoFields) {
assert(!entry.hasOwnProperty(field), entry);
}
// We expect cache entries to be reasonably small when their debug info is stripped. Although
// there are no strict guarantees on the size of the entry, we can expect that the size estimate
// should always remain under 2kb.
assert.lt(entry.estimatedSizeBytes, 2 * 1024, entry);
}
const conn = MongoRunner.runMongod({});
assert.neq(conn, null, "mongod failed to start");
const db = conn.getDB("test");
const coll = db.plan_cache_memory_debug_info;
if (checkSBEEnabled(db, ["featureFlagSbePlanCache", "featureFlagSbeFull"])) {
jsTest.log("Skipping test because SBE and SBE plan cache are both enabled.");
MongoRunner.stopMongod(conn);
return;
}
coll.drop();
createIndexesForColl(coll);
const smallQuery = {
a: 1,
b: 1,
};
// Create a plan cache entry, and verify that the estimated plan cache size has increased.
let oldPlanCacheSize = totalPlanCacheSize();
assert.eq(0, coll.find(smallQuery).itcount());
let newPlanCacheSize = totalPlanCacheSize();
assert.gt(newPlanCacheSize, oldPlanCacheSize);
// Verify that the cache now has a single entry whose estimated size explains the increase in the
// total plan cache size reported by serverStatus(). The cache entry should contain all expected
// debug info.
let cacheContents = planCacheContents(coll);
assert.eq(cacheContents.length, 1, cacheContents);
const cacheEntry = cacheContents[0];
assertCacheEntryHasDebugInfo(cacheEntry);
assert.eq(cacheEntry.estimatedSizeBytes, newPlanCacheSize - oldPlanCacheSize, cacheEntry);
// Configure the server so that new plan cache entries should not preserve debug info.
const setParamRes = assert.commandWorked(
db.adminCommand({setParameter: 1, internalQueryCacheMaxSizeBytesBeforeStripDebugInfo: 0}));
const stripDebugInfoThresholdDefault = setParamRes.was;
// Generate a query which includes a 10,000 element $in predicate.
const kNumInElements = 10 * 1000;
const largeQuery = {
a: 1,
b: 1,
c: {$in: Array.from({length: kNumInElements}, (_, i) => i)},
};
// Create a new cache entry using the query with the large $in predicate. Verify that the estimated
// total plan cache size has increased again, and check that there are now two entries in the cache.
oldPlanCacheSize = totalPlanCacheSize();
assert.eq(0, coll.find(largeQuery).itcount());
newPlanCacheSize = totalPlanCacheSize();
assert.gt(newPlanCacheSize, oldPlanCacheSize);
cacheContents = planCacheContents(coll);
assert.eq(cacheContents.length, 2, cacheContents);
// The cache entry associated with 'smallQuery' should retain its debug info, whereas the cache
// entry associated with 'largeQuery' should have had its debug info stripped.
const smallQueryCacheEntry = getPlanCacheEntryForFilter(coll, smallQuery);
assertCacheEntryHasDebugInfo(smallQueryCacheEntry);
let largeQueryCacheEntry = getPlanCacheEntryForFilter(coll, largeQuery);
assertCacheEntryIsMissingDebugInfo(largeQueryCacheEntry);
// The second cache entry should be smaller than the first, despite the query being much larger.
assert.lt(largeQueryCacheEntry.estimatedSizeBytes,
smallQueryCacheEntry.estimatedSizeBytes,
cacheContents);
// The new cache entry's size should account for the latest observed increase in total plan cache
// size.
assert.eq(
largeQueryCacheEntry.estimatedSizeBytes, newPlanCacheSize - oldPlanCacheSize, cacheContents);
// Verify that a new cache entry in a different collection also has its debug info stripped. This
// demonstrates that the size threshold applies on a server-wide basis as opposed to on a
// per-collection basis.
const secondColl = db.plan_cache_memory_debug_info_other;
secondColl.drop();
createIndexesForColl(secondColl);
// Introduce a new cache entry in the second collection's cache and verify that the cumulative plan
// cache size has increased.
oldPlanCacheSize = totalPlanCacheSize();
assert.eq(0, secondColl.find(smallQuery).itcount());
newPlanCacheSize = totalPlanCacheSize();
assert.gt(newPlanCacheSize, oldPlanCacheSize);
// Ensure that the second collection's cache now has one entry, and that entry's debug info is
// stripped.
cacheContents = planCacheContents(secondColl);
assert.eq(cacheContents.length, 1, cacheContents);
assertCacheEntryIsMissingDebugInfo(cacheContents[0]);
// Meanwhile, the contents of the original collection's plan cache should remain unchanged.
cacheContents = planCacheContents(coll);
assert.eq(cacheContents.length, 2, cacheContents);
assertCacheEntryHasDebugInfo(getPlanCacheEntryForFilter(coll, smallQuery));
assertCacheEntryIsMissingDebugInfo(getPlanCacheEntryForFilter(coll, largeQuery));
// Restore the threshold for stripping debug info to its default. Verify that if we add a third
// cache entry to the original collection 'coll', the plan cache size increases once again, and the
// new cache entry stores debug info.
assert.commandWorked(db.adminCommand({
setParameter: 1,
internalQueryCacheMaxSizeBytesBeforeStripDebugInfo: stripDebugInfoThresholdDefault,
}));
const smallQuery2 = {
a: 1,
b: 1,
c: 1,
};
oldPlanCacheSize = totalPlanCacheSize();
assert.eq(0, coll.find(smallQuery2).itcount());
newPlanCacheSize = totalPlanCacheSize();
assert.gt(newPlanCacheSize, oldPlanCacheSize);
// Verify that there are now three cache entries.
cacheContents = planCacheContents(coll);
assert.eq(cacheContents.length, 3, cacheContents);
// Make sure that the cache entries have or are missing debug info as expected.
assertCacheEntryHasDebugInfo(getPlanCacheEntryForFilter(coll, smallQuery));
assertCacheEntryHasDebugInfo(getPlanCacheEntryForFilter(coll, smallQuery2));
assertCacheEntryIsMissingDebugInfo(getPlanCacheEntryForFilter(coll, largeQuery));
assertCacheEntryIsMissingDebugInfo(getPlanCacheEntryForFilter(secondColl, smallQuery));
// Clear the cache entry for 'largeQuery' and regenerate it. The cache should grow larger, since the
// regenerated cache entry should now contain debug info. Also, check that the size of the new cache
// entry is estimated to be at least 10kb, since the query itself is known to be at least 10kb.
oldPlanCacheSize = totalPlanCacheSize();
assert.commandWorked(coll.runCommand("planCacheClear", {query: largeQuery}));
cacheContents = planCacheContents(coll);
assert.eq(cacheContents.length, 2, cacheContents);
assert.eq(0, coll.find(largeQuery).itcount());
cacheContents = planCacheContents(coll);
assert.eq(cacheContents.length, 3, cacheContents);
newPlanCacheSize = totalPlanCacheSize();
assert.gt(newPlanCacheSize, oldPlanCacheSize);
largeQueryCacheEntry = getPlanCacheEntryForFilter(coll, largeQuery);
assertCacheEntryHasDebugInfo(largeQueryCacheEntry);
assert.gt(largeQueryCacheEntry.estimatedSizeBytes, 10 * 1024, largeQueryCacheEntry);
MongoRunner.stopMongod(conn);
}());