/**
* Repro for the IFR-flag-pinning-vs-getMore bug: a $$SEARCH_META $search sub-pipeline inside
* $unionWith / $lookup against a SHARDED collection fails when the sub-pipeline is first pulled on a
* getMore rather than inside the original aggregate operation.
*
* Mechanism: with featureFlagSearchExtension on and featureFlagExtensionsInsideHybridSearch off, the
* extension $search stage inside a $unionWith/$lookup sub-pipeline throws an IFR kickback
* (document_source_extension_optimizable.h), so the router retries the aggregate with
* featureFlagSearchExtension pinned to false (cluster_aggregate.cpp). That pinning lives on the
* per-operation IFR context, so a getMore -- a new operation -- runs with the flag back at true. Work
* planned under flag=false then executes under flag=true and the query fails.
*
* Every case below is correct behavior and should pass. Cases that currently fail are marked. This
* is a repro script, not suite coverage: it is expected to fail until the bug is fixed, so do not
* commit it into a suite-selected directory as-is.
*
* Required ingredients: the search collection sharded across >= 2 shards (so the sub-pipeline is
* split and a metadata cursor exists) AND the sub-pipeline first pulled on a getMore.
*
* @tags: [
* requires_sharding,
* assumes_unsharded_collection,
* requires_getmore,
* ]
*/
import {getShardNames} from "jstests/libs/cluster_helpers/sharded_cluster_fixture_helpers.js";
import {after, before, describe, it} from "jstests/libs/mochalite.js";
import {createSearchIndex, dropSearchIndex} from "jstests/libs/query_integration_search/search.js";const kNumDocs = 200;
const kIndexName = "subpipeline_getmore_ifr_index";
const kNumLookups = 2;const kSearchQuery = {
index: kIndexName,
text: {query: "hello", path: "t"},
count: {type: "total"},
};const kGroup = {$group: {_id: "$k", padding: {$max: "$padding"}}};const kSearchSubPipeline = [{$search: kSearchQuery}, kGroup, {$addFields: {meta: "$$SEARCH_META"}}];describe("$$SEARCH_META in a sharded sub-pipeline first pulled on a getMore", function () {
let testDB;
let searchColl;
let outerColl; const unionPipeline = () => [
kGroup,
{$unionWith: {coll: searchColl.getName(), pipeline: kSearchSubPipeline}},
]; const lookupPipeline = () => [
kGroup,
{$limit: kNumLookups},
{$lookup: {from: searchColl.getName(), pipeline: kSearchSubPipeline, as: "searchMeta"}},
]; /**
* Runs 'pipeline' and logs the outcome before asserting, so that a failing run reports the error
* code of each case rather than only the first failure's stack.
*/
function run(label, pipeline, options) {
jsTest.log.info("running case", {label, options});
try {
const results = outerColl.aggregate(pipeline, options).toArray();
jsTest.log.info("case succeeded", {label, numResults: results.length});
return results;
} catch (e) {
jsTest.log.info("case FAILED", {label, code: e.code, errmsg: e.message});
throw e;
}
} function assertSearchMetaResolved(docs) {
assert.eq(kNumDocs, docs.length, "expected one group per distinct 'k'");
for (const doc of docs) {
assert.eq(Number(doc.meta.count.total), kNumDocs, "unexpected $$SEARCH_META", {doc});
}
} before(function () {
testDB = db.getSiblingDB(jsTestName());
searchColl = testDB.getCollection(jsTestName() + "_search");
outerColl = testDB.getCollection(jsTestName() + "_outer");
searchColl.drop();
outerColl.drop(); const shardNames = getShardNames(testDB.getMongo());
assert.gte(shardNames.length, 2, "Test requires at least 2 shards");
assert.commandWorked(
testDB.adminCommand({enableSharding: testDB.getName(), primaryShard: shardNames[0]}),
); const docs = [];
for (let i = 0; i < kNumDocs; i++) {
docs.push({_id: i, k: i, t: "hello", padding: "x".repeat(100)});
} function populateAndShard(coll) {
assert.commandWorked(coll.insert(docs));
assert.commandWorked(
testDB.adminCommand({shardCollection: coll.getFullName(), key: {_id: 1}}),
);
assert.commandWorked(
testDB.adminCommand({split: coll.getFullName(), middle: {_id: kNumDocs / 2}}),
);
assert.commandWorked(
testDB.adminCommand({
moveChunk: coll.getFullName(),
find: {_id: kNumDocs / 2},
to: shardNames[1],
_waitForDelete: true,
}),
);
}
populateAndShard(searchColl);
populateAndShard(outerColl); createSearchIndex(searchColl, {name: kIndexName, definition: {mappings: {dynamic: true}}});
}); after(function () {
dropSearchIndex(searchColl, {name: kIndexName});
searchColl.drop();
outerColl.drop();
}); function assertUnionResults(results) {
assert.eq(2 * kNumDocs, results.length, "unexpected result count", {
numResults: results.length,
});
assertSearchMetaResolved(results.filter((doc) => doc.hasOwnProperty("meta")));
} function assertLookupResults(results) {
assert.eq(kNumLookups, results.length, "unexpected result count", {
numResults: results.length,
});
for (const doc of results) {
assertSearchMetaResolved(doc.searchMeta);
}
} assertUnionResults(run("unionWith batchSize=1000", unionPipeline(), {batchSize: 1000}));
}); it("$lookup with the default batchSize", function () {
assertLookupResults(run("lookup default batchSize", lookupPipeline(), {}));
}); assertUnionResults(run("unionWith default batchSize", unionPipeline(), {}));
}); it("$unionWith with batchSize 0", function () {
assertUnionResults(run("unionWith batchSize=0", unionPipeline(), {batchSize: 0}));
}); it("$lookup with batchSize 0", function () {
assertLookupResults(run("lookup batchSize=0", lookupPipeline(), {batchSize: 0}));
});
});