mirror of https://github.com/mongodb/mongo
160 lines
7.6 KiB
JavaScript
160 lines
7.6 KiB
JavaScript
/**
|
|
* Overrides Mongo.prototype.runCommand for query settings supported commands in order to run
|
|
* explain on them multiple times in a row and ensure that the reported 'queryShapeHash' value is
|
|
* same.
|
|
**/
|
|
import {getCommandName, getExplainCommand, getInnerCommand} from "jstests/libs/cmd_object_utils.js";
|
|
import {DiscoverTopology} from "jstests/libs/discover_topology.js";
|
|
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
|
|
import {getCollectionNameFromFullNamespace} from "jstests/libs/namespace_utils.js";
|
|
import {OverrideHelpers} from "jstests/libs/override_methods/override_helpers.js";
|
|
import {getQueryPlanners} from "jstests/libs/query/analyze_plan.js";
|
|
import {QuerySettingsUtils} from "jstests/libs/query/query_settings_utils.js";
|
|
|
|
/**
|
|
* We need to set secondary ok in order to propagate commands on secondary nodes.
|
|
*/
|
|
const connectFn = (host) => {
|
|
const conn = new Mongo(host, undefined, {gRPC: false});
|
|
conn.setSecondaryOk();
|
|
return conn;
|
|
};
|
|
|
|
// Flag which tracks if we run this test using the two-cluster fixture.
|
|
const isMultiShardedClusterFixture = TestData.isMultiShardedClusterFixture || false;
|
|
|
|
// Because the topology doesn't change throughout the run of a test, we can cache all the connection
|
|
// and re-use them to not overload the server with new connections.
|
|
const topologyCache = {};
|
|
|
|
function getTopologyConnections(conn) {
|
|
if (!topologyCache.allConnections) {
|
|
jsTest.log.debug(`Discovering topology...`);
|
|
topologyCache.allConnections = getAllMongosConnections(conn)
|
|
.flatMap((connection) => DiscoverTopology.findNonConfigNodes(connection))
|
|
.map(connectFn);
|
|
// Assert that all hosts are different.
|
|
const setOfHosts = new Set(topologyCache.allConnections.map((el) => el.toString()));
|
|
assert.eq(setOfHosts.size, topologyCache.allConnections.length);
|
|
jsTest.log.debug("List vs set topology...", {setOfHosts, list: topologyCache.allConnections});
|
|
}
|
|
return topologyCache.allConnections;
|
|
}
|
|
|
|
function getAllMongosConnections(conn) {
|
|
if (!topologyCache.mongosConnectionsArr) {
|
|
jsTest.log.debug(`Settings the mongos connections array...`);
|
|
if (isMultiShardedClusterFixture) {
|
|
const connections = conn.getDB("config").multiShardedClusterFixture.find().sort({_id: 1}).toArray();
|
|
assert.eq(connections.length, 2);
|
|
// Set the connections array to include both when using a multi-cluster fixture.
|
|
topologyCache.mongosConnectionsArr = connections.map((doc) => connectFn(doc.connectionString));
|
|
} else {
|
|
topologyCache.mongosConnectionsArr = [conn];
|
|
}
|
|
}
|
|
return topologyCache.mongosConnectionsArr;
|
|
}
|
|
|
|
/**
|
|
* Given a connection, discover all the cluster connected nodes (both mongod and mongos), and
|
|
* assert that all the explain results for 'explainCmd' have identical query shape hashes.
|
|
*/
|
|
export function assertQueryShapeHashStability(conn, dbName, explainCmd) {
|
|
let explainResults;
|
|
try {
|
|
// We run explain on all connections in the topology and assert that the query shape hash is
|
|
// the same on all nodes.
|
|
explainResults = getTopologyConnections(conn)
|
|
.map((conn) => conn.getDB(dbName))
|
|
.map((db) => {
|
|
jsTest.log.info("About to run the explain", {host: db.getMongo().host});
|
|
const explainResult = retryOnRetryableError(() => assert.commandWorked(db.runCommand(explainCmd)), 50);
|
|
return explainResult;
|
|
});
|
|
} catch (ex) {
|
|
// Fuzzer may generate invalid commands, which will fail on assert.commandWorked().
|
|
// If explain command failed, ignore the exception.
|
|
if (TestData.isRunningQueryShapeHashFuzzer) {
|
|
return;
|
|
}
|
|
|
|
const expectedErrorCodes = [ErrorCodes.CommandOnShardedViewNotSupportedOnMongod, ErrorCodes.NamespaceNotFound];
|
|
if (expectedErrorCodes.includes(ex.code)) {
|
|
return;
|
|
}
|
|
|
|
throw ex;
|
|
}
|
|
|
|
const isRawOperationOnLegacyTimeseries = (() => {
|
|
if (explainCmd.explain.rawData !== true) {
|
|
// This is not a 'rawData' operation.
|
|
return false;
|
|
}
|
|
const isSystemBucketsNamespace = (nss) => {
|
|
return getCollectionNameFromFullNamespace(nss).startsWith("system.buckets.");
|
|
};
|
|
return explainResults.some((explainRes) =>
|
|
getQueryPlanners(explainRes).some((queryPlanner) => isSystemBucketsNamespace(queryPlanner.namespace)),
|
|
);
|
|
})();
|
|
|
|
// TODO SERVER-103551 remove this once query shape hash calculation for legacy timeseries
|
|
// collection is fixed
|
|
if (isRawOperationOnLegacyTimeseries) {
|
|
// Operations that specify `rawData` targeting legacy timeseries collection will not produce
|
|
// a query shape hash on the shards of a sharded cluster (SERVER-103069)
|
|
return;
|
|
}
|
|
|
|
// Check that all the explain commands executed on all nodes returned the same 'queryShapeHash'.
|
|
assert.gt(explainResults.length, 0, `Found explain results array to be empty`);
|
|
const firstQueryShapeHash = explainResults[0].queryShapeHash;
|
|
assert(
|
|
explainResults.every((explainRes) => explainRes.queryShapeHash === firstQueryShapeHash),
|
|
`Not all nodes returned same QueryShapeHash in explain command results. Explain command: ${tojson(
|
|
explainCmd,
|
|
)}. Explain results from all nodes: ${tojson(explainResults)}`,
|
|
);
|
|
}
|
|
|
|
function runCommandOverride(conn, dbName, cmdName, cmdObj, clientFunction, makeFuncArgs) {
|
|
// Do not run explain on queries that have 'batchSize' set to zero, as in majority of the tests
|
|
// we are expecting an error when calling getMore() on that cursor.
|
|
const hasBatchSizeZero = cmdObj.cursor && cmdObj.cursor.batchSize === 0;
|
|
const res = clientFunction.apply(conn, makeFuncArgs(cmdObj));
|
|
if (res.ok && !hasBatchSizeZero) {
|
|
// Only run the test if the original command works. Some tests assert on commands failing,
|
|
// so we should simply bubble these commands through without any additional checks.
|
|
OverrideHelpers.withPreOverrideRunCommand(() => {
|
|
if (isMultiShardedClusterFixture) {
|
|
const mongosConnArr = getAllMongosConnections(conn);
|
|
// In case we run the test using the two-cluster fixture, assert we have exactly two
|
|
// mongos connections.
|
|
assert.eq(mongosConnArr.length, 2);
|
|
// Mirror the command on the second cluster to ensure the collections exists.
|
|
// TODO SERVER-100658 Explain on non-existent collection returns empty results for
|
|
// sharded cluster aggregations - Assess if this is still needed.
|
|
const secondClusterMongos = mongosConnArr[1];
|
|
retryOnRetryableError(() => clientFunction.apply(secondClusterMongos, makeFuncArgs(cmdObj)), 50);
|
|
FixtureHelpers.awaitReplication(secondClusterMongos.getDB("admin"));
|
|
}
|
|
const innerCmd = getInnerCommand(cmdObj);
|
|
if (!QuerySettingsUtils.isSupportedCommand(getCommandName(innerCmd))) {
|
|
return;
|
|
}
|
|
// Wrap command into explain, if it's not explain yet.
|
|
const explainCmd = getExplainCommand(innerCmd);
|
|
assertQueryShapeHashStability(conn, dbName, explainCmd);
|
|
});
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// Override the default runCommand with our custom version.
|
|
OverrideHelpers.overrideRunCommand(runCommandOverride);
|
|
|
|
// Always apply the override if a test spawns a parallel shell.
|
|
OverrideHelpers.prependOverrideInParallelShell("jstests/libs/override_methods/query_shape_hash_stability.js");
|