summaryrefslogtreecommitdiff
path: root/jstests/noPassthrough/plan_cache_memory_debug_info.js
diff options
context:
space:
mode:
Diffstat (limited to 'jstests/noPassthrough/plan_cache_memory_debug_info.js')
-rw-r--r--jstests/noPassthrough/plan_cache_memory_debug_info.js270
1 files changed, 270 insertions, 0 deletions
diff --git a/jstests/noPassthrough/plan_cache_memory_debug_info.js b/jstests/noPassthrough/plan_cache_memory_debug_info.js
new file mode 100644
index 00000000000..31dab5aeba9
--- /dev/null
+++ b/jstests/noPassthrough/plan_cache_memory_debug_info.js
@@ -0,0 +1,270 @@
+/**
+ * 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";
+
+ /**
+ * 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) {
+ const result =
+ assert.commandWorked(db.runCommand({planCacheListQueryShapes: coll.getName()}));
+ assert(result.hasOwnProperty("shapes"), tojson(result));
+ return result.shapes;
+ }
+
+ /**
+ * Retrieve the cache entry associated with the query shape defined by the given 'filter'
+ * (assuming the query has no projection, sort, or collation) using the 'planCacheListPlans'
+ * command. Asserts that the plan cache entry exists, and returns it.
+ */
+ function getPlanCacheEntryForFilter(coll, filter) {
+ const cmdResult = assert.commandWorked(
+ db.runCommand({planCacheListPlans: coll.getName(), query: filter}));
+ // Ensure that an entry actually exists in the cache for this query shape.
+ assert.gt(cmdResult.plans.length, 0, tojson(cmdResult));
+ return cmdResult;
+ }
+
+ function assertExistenceOfRequiredCacheEntryFields(entry) {
+ assert(entry.hasOwnProperty("works"), tojson(entry));
+ assert(entry.hasOwnProperty("timeOfCreation"), tojson(entry));
+ assert(entry.hasOwnProperty("estimatedSizeBytes"), tojson(entry));
+
+ assert(entry.hasOwnProperty("plans"), tojson(entry));
+ for (let plan of entry.plans) {
+ assert(plan.hasOwnProperty("details"), tojson(plan));
+ assert(plan.details.hasOwnProperty("solution"), tojson(plan));
+ assert(plan.hasOwnProperty("filterSet"), tojson(plan));
+ }
+ }
+
+ function assertCacheEntryHasDebugInfo(entry) {
+ assertExistenceOfRequiredCacheEntryFields(entry);
+
+ // Check that fields deriving from debug info are present.
+ for (let i = 0; i < entry.plans.length; ++i) {
+ const plan = entry.plans[i];
+ assert(plan.hasOwnProperty("reason"), tojson(plan));
+ assert(plan.reason.hasOwnProperty("score"), tojson(plan));
+ assert(plan.reason.hasOwnProperty("stats"), tojson(plan));
+ }
+ assert(entry.plans[0].hasOwnProperty("feedback"), tojson(entry));
+ assert(entry.plans[0].feedback.hasOwnProperty("nfeedback"), tojson(entry));
+ assert(entry.plans[0].feedback.hasOwnProperty("scores"), tojson(entry));
+ }
+
+ function assertCacheEntryIsMissingDebugInfo(entry) {
+ assertExistenceOfRequiredCacheEntryFields(entry);
+
+ // Verify that fields deriving from debug info are missing for the legacy format.
+ for (let i = 0; i < entry.plans.length; ++i) {
+ const plan = entry.plans[i];
+ assert(!plan.hasOwnProperty("reason"), tojson(plan));
+ assert(!plan.hasOwnProperty("feedback"), tojson(plan));
+ }
+
+ // 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 4kb.
+ assert.lt(entry.estimatedSizeBytes, 4 * 1024, tojson(entry));
+ }
+
+ /**
+ * Given a match expression 'filter' describing a query shape, obtains the associated plan cache
+ * information using 'planCacheListPlans'. Verifies that the plan cache entry exists and
+ * contains the expected debug info.
+ */
+ function assertQueryShapeHasDebugInfoInCache(coll, filter) {
+ const cacheEntry = getPlanCacheEntryForFilter(coll, filter);
+ assertCacheEntryHasDebugInfo(cacheEntry);
+ }
+
+ /**
+ * Given a match expression 'filter' describing a query shape, obtains the associated plan cache
+ * information using 'planCacheListPlans'. Verifies that the plan cache entry exists but has had
+ * its debug info stripped.
+ */
+ function assertQueryShapeIsMissingDebugInfoInCache(coll, filter) {
+ const cacheEntry = getPlanCacheEntryForFilter(coll, filter);
+ assertCacheEntryIsMissingDebugInfo(cacheEntry);
+ }
+
+ /**
+ * Given a previous total plan cache size, 'oldCacheSize', and a newer observation of the plan
+ * cache size, 'newCacheSize', asserts that this growth is consistent with the size reported by
+ * 'cacheEntry'.
+ */
+ function assertChangeInCacheSizeIsDueToEntry(cacheEntry, oldCacheSize, newCacheSize) {
+ const cacheSizeGrowth = newCacheSize - oldCacheSize;
+ // Instead of asserting that the cache size growth is exactly equal to the cache entry's
+ // size, we assert that the difference between them is sufficiently small. This wiggle room
+ // is necessary since the reported cache size is an estimate and may not be precise.
+ assert.lt(Math.abs(cacheEntry.estimatedSizeBytes - cacheSizeGrowth), 50);
+ }
+
+ 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;
+ 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 = getPlanCacheEntryForFilter(coll, cacheContents[0].query);
+ assertCacheEntryHasDebugInfo(cacheEntry);
+ assertQueryShapeHasDebugInfoInCache(coll, smallQuery);
+ assertChangeInCacheSizeIsDueToEntry(cacheEntry, oldPlanCacheSize, newPlanCacheSize);
+
+ // 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.
+ assertQueryShapeHasDebugInfoInCache(coll, smallQuery);
+ assertQueryShapeIsMissingDebugInfoInCache(coll, largeQuery);
+
+ // The second cache entry should be smaller than the first, despite the query being much larger.
+ const smallQueryCacheEntry = getPlanCacheEntryForFilter(coll, smallQuery);
+ let largeQueryCacheEntry = getPlanCacheEntryForFilter(coll, largeQuery);
+ 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.
+ assertChangeInCacheSizeIsDueToEntry(largeQueryCacheEntry, oldPlanCacheSize, newPlanCacheSize);
+
+ // 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);
+ assertQueryShapeIsMissingDebugInfoInCache(secondColl, smallQuery);
+
+ // Meanwhile, the contents of the original collection's plan cache should remain unchanged.
+ cacheContents = planCacheContents(coll);
+ assert.eq(cacheContents.length, 2, cacheContents);
+ assertQueryShapeHasDebugInfoInCache(coll, smallQuery);
+ assertQueryShapeIsMissingDebugInfoInCache(coll, largeQuery);
+
+ // Ensure that 'planCacheListQueryShapes' works when debug info has been stripped. For a cache
+ // entry which is missing debug info, we expect 'planCacheListQueryShapes' to display an empty
+ // object.
+ const listQueryShapesResult =
+ assert.commandWorked(db.runCommand({planCacheListQueryShapes: secondColl.getName()}));
+ assert(listQueryShapesResult.hasOwnProperty("shapes"), tojson(listQueryShapesResult));
+ assert.eq(listQueryShapesResult.shapes.length, 1, listQueryShapesResult);
+ const listedShape = listQueryShapesResult.shapes[0];
+ assert.eq(listedShape, {}, listQueryShapesResult);
+
+ // 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.
+ assertQueryShapeHasDebugInfoInCache(coll, smallQuery);
+ assertQueryShapeHasDebugInfoInCache(coll, smallQuery2);
+ assertQueryShapeIsMissingDebugInfoInCache(coll, largeQuery);
+ assertQueryShapeIsMissingDebugInfoInCache(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);
+
+ assertQueryShapeHasDebugInfoInCache(coll, largeQuery);
+ largeQueryCacheEntry = getPlanCacheEntryForFilter(coll, largeQuery);
+ assert.gt(largeQueryCacheEntry.estimatedSizeBytes, 10 * 1024, largeQueryCacheEntry);
+
+ MongoRunner.stopMongod(conn);
+}());