summaryrefslogtreecommitdiff
path: root/jstests/libs/query_stats_utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'jstests/libs/query_stats_utils.js')
-rw-r--r--jstests/libs/query_stats_utils.js490
1 files changed, 0 insertions, 490 deletions
diff --git a/jstests/libs/query_stats_utils.js b/jstests/libs/query_stats_utils.js
deleted file mode 100644
index 8597e4cfd11..00000000000
--- a/jstests/libs/query_stats_utils.js
+++ /dev/null
@@ -1,490 +0,0 @@
-load("jstests/libs/fixture_helpers.js"); // For FixtureHelpers
-const kShellApplicationName = "MongoDB Shell";
-const kDefaultQueryStatsHmacKey = BinData(8, "MjM0NTY3ODkxMDExMTIxMzE0MTUxNjE3MTgxOTIwMjE=");
-
-/**
- * Utility for checking that the aggregated queryStats metrics are logical (follows sum >= max >=
- * min, and sum = max = min if only one execution).
- */
-function verifyMetrics(batch) {
- batch.forEach(element => {
- if (element.metrics.execCount === 1) {
- for (const [metricName, summaryValues] of Object.entries(element.metrics)) {
- // Skip over fields that aren't aggregated metrics with sum/min/max (execCount,
- // lastExecutionMicros).
- if (summaryValues.sum === undefined) {
- continue;
- }
- const debugInfo = {[metricName]: summaryValues};
- // If there has only been one execution, all metrics should have min, max, and sum
- // equal to each other.
- assert.eq(summaryValues.sum, summaryValues.min, debugInfo);
- assert.eq(summaryValues.sum, summaryValues.max, debugInfo);
- assert.eq(summaryValues.min, summaryValues.max, debugInfo);
- }
- } else {
- for (const [metricName, summaryValues] of Object.entries(element.metrics)) {
- // Skip over fields that aren't aggregated metrics with sum/min/max (execCount,
- // lastExecutionMicros).
- if (summaryValues.sum === undefined) {
- continue;
- }
- const debugInfo = {[metricName]: summaryValues};
- assert.gte(summaryValues.sum, summaryValues.min, debugInfo);
- assert.gte(summaryValues.sum, summaryValues.max, debugInfo);
- assert.lte(summaryValues.min, summaryValues.max, debugInfo);
- }
- }
- });
-}
-
-/**
- * Return the latest query stats entry from the given collection. Only returns query shapes
- * generated by the shell that is running tests.
- *
- * @param conn - connection to database
- * @param {object} options {
- * {String} collName - name of collection
- * {object} - extraMatch - optional argument that can be used to filter the pipeline
- * }
- */
-function getLatestQueryStatsEntry(conn, options = {
- collName: ""
-}) {
- let sortedEntries = getQueryStats(
- conn, Object.merge({customSort: {"metrics.latestSeenTimestamp": -1}}, options));
- assert.neq([], sortedEntries);
- return sortedEntries[0];
-}
-
-/**
- * Collect query stats from a given collection. Only include query shapes generated by the shell
- * that is running tests.
- *
- * @param conn - connection to database
- * @param {object} options {
- * {String} collName - name of collection
- * {object} - extraMatch - optional argument that can be used to filter the pipeline
- * {object} - customSort - optional custom sort order - otherwise sorted by 'key' just to be
- * deterministic.
- * }
- */
-function getQueryStats(conn, options = {
- collName: ""
-}) {
- let match =
- Object.assign({"key.client.application.name": kShellApplicationName}, options.extraMatch);
- if (options.collName && options.collName) {
- match["key.queryShape.cmdNs.coll"] = options.collName;
- }
- const result = conn.adminCommand({
- aggregate: 1,
- pipeline: [{$queryStats: {}}, {$sort: (options.customSort || {key: 1})}, {$match: match}],
- cursor: {}
- });
- assert.commandWorked(result);
- return result.cursor.firstBatch;
-}
-
-/**
- * @param {object} conn - connection to database
- * @param {object} options {
- * {BinData} hmacKey
- * {String} collName - name of collection
- * {boolean} transformIdentifiers - whether to include transform identifiers
- * }
- */
-function getQueryStatsFindCmd(conn, options = {
- collName: "",
- transformIdentifiers: false,
- hmacKey: kDefaultQueryStatsHmacKey
-}) {
- let matchExpr = {
- "key.queryShape.command": "find",
- "key.client.application.name": kShellApplicationName
- };
- if (options.collName) {
- matchExpr["key.queryShape.cmdNs.coll"] = options.collName;
- }
- // Filter out agg queries, including $queryStats.
- var pipeline;
- if (options.transformIdentifiers) {
- pipeline = [
- {
- $queryStats: {
- transformIdentifiers: {
- algorithm: "hmac-sha-256",
- hmacKey: options.hmacKey ? options.hmacKey : kDefaultQueryStatsHmacKey
- }
- }
- },
- {$match: matchExpr},
- // Sort on queryStats key so entries are in a deterministic order.
- {$sort: {key: 1}},
- ];
- } else {
- pipeline = [
- {$queryStats: {}},
- {$match: matchExpr},
- // Sort on queryStats key so entries are in a deterministic order.
- {$sort: {key: 1}},
- ];
- }
- const result = conn.adminCommand({aggregate: 1, pipeline: pipeline, cursor: {}});
- assert.commandWorked(result);
- return result.cursor.firstBatch;
-}
-
-/**
- * Collects query stats from any aggregate command query shapes (with $queryStats requests filtered
- * out) that were generated by the shell that is running tests.
- *
- * /**
- * @param {object} conn - connection to database
- * @param {object} options {
- * {BinData} hmacKey
- * {boolean} transformIdentifiers - whether to include transform identifiers
- * }
- */
-function getQueryStatsAggCmd(db, options = {
- transformIdentifiers: false,
- hmacKey: kDefaultQueryStatsHmacKey
-}) {
- var pipeline;
- let queryStatsStage = {$queryStats: {}};
- if (options.transformIdentifiers) {
- queryStatsStage = {
- $queryStats: {
- transformIdentifiers: {
- algorithm: "hmac-sha-256",
- hmacKey: options.hmacKey ? options.hmacKey : kDefaultQueryStatsHmacKey
- }
- }
- };
- }
-
- pipeline = [
- queryStatsStage,
- // Filter out find queries and $queryStats aggregations.
- {
- $match: {
- "key.queryShape.command": "aggregate",
- "key.queryShape.pipeline.0.$queryStats": {$exists: false},
- "key.client.application.name": kShellApplicationName
- }
- },
- // Sort on key so entries are in a deterministic order.
- {$sort: {key: 1}},
- ];
- return db.getSiblingDB("admin").aggregate(pipeline).toArray();
-}
-
-function confirmAllExpectedFieldsPresent(expectedKey, resultingKey) {
- let fieldsCounter = 0;
- for (const field in resultingKey) {
- fieldsCounter++;
- if (field === "client") {
- // client meta data is environment/machine dependent, so do not
- // assert on fields or specific fields other than the application name.
- assert.eq(resultingKey.client.application.name, kShellApplicationName);
- // SERVER-83926 We should never report the "mongos" section, since it is too specific
- // and would result in too many different query shapes.
- assert(!resultingKey.client.hasOwnProperty("mongos"), resultingKey.client);
- continue;
- }
- if (!expectedKey.hasOwnProperty(field)) {
- print("Field present in actual object but missing from expected: " + field);
- print("Expected " + tojson(expectedKey));
- print("Actual " + tojson(resultingKey));
- }
- assert(expectedKey.hasOwnProperty(field), field);
- assert.eq(expectedKey[field], resultingKey[field]);
- }
- // Make sure the resulting key isn't missing any fields.
- assert.eq(fieldsCounter, Object.keys(expectedKey).length, resultingKey);
-}
-
-function assertExpectedResults(results,
- expectedQueryStatsKey,
- expectedExecCount,
- expectedDocsReturnedSum,
- expectedDocsReturnedMax,
- expectedDocsReturnedMin,
- expectedDocsReturnedSumOfSq,
- getMores) {
- const {key, keyHash, metrics, asOf} = results;
- confirmAllExpectedFieldsPresent(expectedQueryStatsKey, key);
- assert.eq(expectedExecCount, metrics.execCount);
- assert.docEq({
- sum: NumberLong(expectedDocsReturnedSum),
- max: NumberLong(expectedDocsReturnedMax),
- min: NumberLong(expectedDocsReturnedMin),
- sumOfSquares: NumberLong(expectedDocsReturnedSumOfSq)
- },
- metrics.docsReturned);
-
- const {
- firstSeenTimestamp,
- latestSeenTimestamp,
- lastExecutionMicros,
- totalExecMicros,
- firstResponseExecMicros
- } = metrics;
-
- // The tests can't predict exact timings, so just assert these three fields have been set (are
- // non-zero).
- assert.neq(lastExecutionMicros, NumberLong(0));
- assert.neq(firstSeenTimestamp.getTime(), 0);
- assert.neq(latestSeenTimestamp.getTime(), 0);
- assert.neq(asOf.getTime(), 0);
- assert.neq(keyHash.length, 0);
-
- const distributionFields = ['sum', 'max', 'min', 'sumOfSquares'];
- for (const field of distributionFields) {
- assert.neq(totalExecMicros[field], NumberLong(0));
- assert.neq(firstResponseExecMicros[field], NumberLong(0));
- if (getMores) {
- // If there are getMore calls, totalExecMicros fields should be greater than or equal to
- // firstResponseExecMicros.
- if (field == 'min' || field == 'max') {
- // In the case that we've executed multiple queries with the same shape, it is
- // possible for the min or max to be equal.
- assert.gte(totalExecMicros[field], firstResponseExecMicros[field]);
- } else {
- assert.gt(totalExecMicros[field], firstResponseExecMicros[field]);
- }
- } else {
- // If there are no getMore calls, totalExecMicros fields should be equal to
- // firstResponseExecMicros.
- assert.eq(totalExecMicros[field], firstResponseExecMicros[field]);
- }
- }
-}
-
-function asFieldPath(str) {
- return "$" + str;
-}
-
-function asVarRef(str) {
- return "$$" + str;
-}
-
-function resetQueryStatsStore(conn, queryStatsStoreSize) {
- // Set the cache size to 0MB to clear the queryStats store, and then reset to
- // queryStatsStoreSize.
- assert.commandWorked(conn.adminCommand({setParameter: 1, internalQueryStatsCacheSize: "0MB"}));
- assert.commandWorked(
- conn.adminCommand({setParameter: 1, internalQueryStatsCacheSize: queryStatsStoreSize}));
-}
-
-/**
- * Checks that the given object contains the dottedPath.
- * @param {object} object
- * @param {string} dottedPath
- */
-function hasValueAtPath(object, dottedPath) {
- let nestedFields = dottedPath.split(".");
- for (const nestedField of nestedFields) {
- if (!object.hasOwnProperty(nestedField)) {
- return false;
- }
- object = object[nestedField];
- }
- return true;
-}
-
-/**
- * Returns the object's value at the dottedPath.
- * @param {object} object
- * @param {string} dottedPath
- */
-function getValueAtPath(object, dottedPath) {
- let nestedFields = dottedPath.split(".");
- for (const nestedField of nestedFields) {
- if (!object.hasOwnProperty(nestedField)) {
- return false;
- }
- object = object[nestedField];
- }
- return object;
-}
-
-/**
- * Runs an assertion callback function on a node with query stats enabled - once with a mongod, and
- * once with a mongos.
- * @param {String} collName - The desired collection name to use. The db will be "test".
- * @param {Function} callbackFn - The function to make the assertion on each connection.
- */
-function withQueryStatsEnabled(collName, callbackFn) {
- const options = {
- setParameter: {internalQueryStatsRateLimit: -1},
- };
-
- {
- const conn = MongoRunner.runMongod(options);
- const testDB = conn.getDB("test");
- var coll = testDB[collName];
- coll.drop();
-
- callbackFn(coll);
- MongoRunner.stopMongod(conn);
- }
-
- {
- const st = new ShardingTest({shards: 2, mongosOptions: options});
- const testDB = st.getDB("test");
- var coll = testDB[collName];
- coll.drop();
- st.shardColl(coll, {_id: 1}, {_id: 1});
-
- callbackFn(coll);
- st.stop();
- }
-}
-/**
- * We run the command on an new database with an empty collection that has an index {v:1}. We then
- * obtain the queryStats key entry that is created and check the following things.
- * 1. The command associated with the key matches the commandName.
- * 2. The fields nested inside of queryShape field of the key exactly matches those given by
- * shapeFields.
- * 3. The list of fields of the key exactly matches those given by keyFields.
- * /**
- * @param {string} commandName - string name of type of command, ex. "find" or "aggregate"
- * @param {object} commandObj - The command that will be run
- * @param {object} shapeFields - List of fields that are part of the queryShape and should be nested
- * inside of Query Shape
- * @param {object} keyFields - List of outer fields not nested inside queryShape but should be part
- * of the key
- */
-function runCommandAndValidateQueryStats({coll, commandName, commandObj, shapeFields, keyFields}) {
- const testDB = coll.getDB();
- assert.commandWorked(testDB.runCommand(commandObj));
- const entry = getLatestQueryStatsEntry(testDB.getMongo(), {collName: coll.getName()});
-
- assert.eq(entry.key.queryShape.command, commandName);
- const kApplicationName = "MongoDB Shell";
- assert.eq(entry.key.client.application.name, kApplicationName);
-
- {
- assert(hasValueAtPath(entry, "key.client.driver"), entry);
- assert(hasValueAtPath(entry, "key.client.driver.name"), entry);
- assert(hasValueAtPath(entry, "key.client.driver.version"), entry);
- }
-
- {
- assert(hasValueAtPath(entry, "key.client.os"), entry);
- assert(hasValueAtPath(entry, "key.client.os.type"), entry);
- assert(hasValueAtPath(entry, "key.client.os.name"), entry);
- assert(hasValueAtPath(entry, "key.client.os.architecture"), entry);
- assert(hasValueAtPath(entry, "key.client.os.version"), entry);
- }
-
- // SERVER-83926 Make sure the mongos section doesn't show up.
- assert(!hasValueAtPath(entry, "key.client.mongos"), entry);
-
- // Every path in shapeFields is in the queryShape.
- let shapeFieldsPrefixes = [];
- for (const field of shapeFields) {
- assert(hasValueAtPath(entry.key.queryShape, field),
- `QueryShape: ${tojson(entry.key.queryShape)} is missing field ${field}`);
- shapeFieldsPrefixes.push(field.split(".")[0]);
- }
-
- // Every field in queryShape is in shapeFields or is the base of a path in shapeFields.
- for (const field in entry.key.queryShape) {
- assert(shapeFieldsPrefixes.includes(field),
- `Unexpected field ${field} in shape for ${commandName}`);
- }
-
- // Every path in keyFields is in the key.
- let keyFieldsPrefixes = [];
- for (const field of keyFields) {
- if (field === "collectionType" && FixtureHelpers.isMongos(testDB)) {
- // TODO SERVER-76263 collectionType is not yet available on mongos.
- continue;
- }
- assert(hasValueAtPath(entry.key, field),
- `Key: ${tojson(entry.key)} is missing field ${field}`);
- keyFieldsPrefixes.push(field.split(".")[0]);
- }
-
- // Every field in the key is in keyFields or is the base of a path in keyFields.
- for (const field in entry.key) {
- assert(keyFieldsPrefixes.includes(field),
- `Unexpected field ${field} in key for ${commandName}`);
- }
-
- // $hint can only be string(index name) or object (index spec).
- assert.throwsWithCode(() => {
- coll.find({v: {$eq: 2}}).hint({'v': 60, $hint: -128}).itcount();
- }, ErrorCodes.BadValue);
-}
-
-/**
- * Helper function to verify that each of the query stats entries has a unique hash and returns a
- * list of the hashes.
- * @param {list} entries - List of entries returned from $queryStats.
- * @returns {list} list of unique hashes corresponding to the entries.
- */
-function getQueryStatsKeyHashes(entries) {
- const keyHashes = {};
- for (const entry of entries) {
- assert(entry.keyHash && entry.keyHash !== "",
- `Entry does not have a 'keyHash' field: ${tojson(entry)}`);
- keyHashes[entry.keyHash] = entry;
- }
- // We expect all keys and hashes to be unique, so assert that we have as many unique hashes as
- // entries.
- const keyHashArray = Object.keys(keyHashes);
- assert.eq(keyHashArray.length, entries.length, tojson(entries));
- return keyHashArray;
-}
-
-/**
- * Given a query stats entry, and stats that the entry should have, this function checks that the
- * entry is the result of a change stream request and that the metrics are what are expected.
- */
-function checkChangeStreamEntry({queryStatsEntry, db, collectionName, numExecs, numDocsReturned}) {
- assert.eq(collectionName, queryStatsEntry.key.queryShape.cmdNs.coll);
-
- // Confirm entry is a change stream request.
- let stringifiedPipeline = JSON.stringify(queryStatsEntry.key.queryShape.pipeline, null, 0);
- assert(stringifiedPipeline.includes("_internalChangeStream"));
-
- // TODO SERVER-76263 Support reporting 'collectionType' on a sharded cluster.
- if (!FixtureHelpers.isMongos(db)) {
- assert.eq("changeStream", queryStatsEntry.key.collectionType);
- }
-
- // Checking that metrics match expected metrics.
- assert.eq(queryStatsEntry.metrics.execCount, numExecs);
- assert.eq(queryStatsEntry.metrics.docsReturned.sum, numDocsReturned);
-
- // FirstResponseExecMicros and TotalExecMicros match since each getMore is recorded as a new
- // first response.
- assert.eq(queryStatsEntry.metrics.totalExecMicros.sum,
- queryStatsEntry.metrics.firstResponseExecMicros.sum);
- assert.eq(queryStatsEntry.metrics.totalExecMicros.max,
- queryStatsEntry.metrics.firstResponseExecMicros.max);
- assert.eq(queryStatsEntry.metrics.totalExecMicros.min,
- queryStatsEntry.metrics.firstResponseExecMicros.min);
-}
-
-/**
- * Given a change stream cursor, this function will return the number of getMores executed until the
- * change stream is updated. This only applies when the cursor is waiting for one new document (or
- * set the batchSize of the input cursor to 1) so each hasNext() call will correspond to an internal
- * getMore.
- */
-function getNumberOfGetMoresUntilNextDocForChangeStream(cursor) {
- let numGetMores = 0;
- assert.soon(() => {
- numGetMores++;
- return cursor.hasNext();
- });
-
- // Get the document that is on the cursor to reset the cursor to a state where calling hasNext()
- // corresponds to a getMore.
- cursor.next();
- return numGetMores;
-}