mirror of https://github.com/mongodb/mongo
SERVER-111511 Ensure lifetime of catalog-owned pointers stored in QueryPlannerParams (#41881)
GitOrigin-RevId: 96dce3da49b8d2e9e0d328048cb56930eb1bdb2b
This commit is contained in:
parent
d152f51d37
commit
39539342a1
|
|
@ -0,0 +1,170 @@
|
||||||
|
/**
|
||||||
|
* Test that the query engine is capable of running queries when the catalog's in memory cache of
|
||||||
|
* objects is destroyed during a yield. This testcase serves as a regression test for SERVER-105873
|
||||||
|
* which was a problem with the query optimizer stashing a raw pointer to a storage-owned object
|
||||||
|
* which is destroyed during a yield and concurrent collMod operation. The comments inline represent
|
||||||
|
* the state of the code before SERVER-105873 was fixed.
|
||||||
|
*/
|
||||||
|
(function() {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
load("jstests/libs/fail_point_util.js"); // For configureFailPoint
|
||||||
|
|
||||||
|
function insertDocs(coll) {
|
||||||
|
let docs = [{a: "abc", b: "def"}];
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
docs.push({
|
||||||
|
a: 1,
|
||||||
|
b: 1,
|
||||||
|
c: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
assert.commandWorked(coll.insertMany(docs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test stale pointer for partial filter expression of an index.
|
||||||
|
function testPartialIndex(conn) {
|
||||||
|
let db = conn.getDB("test");
|
||||||
|
const collName = jsTestName();
|
||||||
|
const coll = db[collName];
|
||||||
|
coll.drop();
|
||||||
|
|
||||||
|
insertDocs(coll);
|
||||||
|
|
||||||
|
// Ensure first branch of $or has multiple indexed plans and must run the multiplanner. This
|
||||||
|
// ensures we yield the collection acquisition.
|
||||||
|
assert.commandWorked(
|
||||||
|
coll.createIndex({a: 1}, {partialFilterExpression: {b: 1}, name: "a_partial"}));
|
||||||
|
assert.commandWorked(coll.createIndex({b: 1}));
|
||||||
|
|
||||||
|
// Ensure we yield the collection acquisition.
|
||||||
|
assert.commandWorked(db.adminCommand({setParameter: 1, internalQueryExecYieldIterations: 1}));
|
||||||
|
|
||||||
|
// Configure the fail point to pause after the query planner has taken pointers to the catalog
|
||||||
|
// owned objects.
|
||||||
|
let pauseAfterFillingOutIndexEntries =
|
||||||
|
configureFailPoint(conn, "pauseAfterFillingOutIndexEntries");
|
||||||
|
|
||||||
|
const awaitQuery = startParallelShell(function() {
|
||||||
|
const coll = db[jsTestName()];
|
||||||
|
const res = coll.find({
|
||||||
|
$or: [
|
||||||
|
{a: 1, b: 1},
|
||||||
|
// Ensure one branch does not have an indexed plan. This forces the
|
||||||
|
// subplanner to fallback to `choosePlanWholeQuery()`.
|
||||||
|
{c: null},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
assert.eq(1001, res.length);
|
||||||
|
}, db.getMongo().port);
|
||||||
|
|
||||||
|
// At this point, the parallel shell's query is paused after filling out index entries in
|
||||||
|
// 'QueryPlannerParams' and no subplanning or multiplanning has began.
|
||||||
|
pauseAfterFillingOutIndexEntries.wait();
|
||||||
|
|
||||||
|
// Perform a collMod which forces the catalog to destroy its in-memory cache of the 'a_partial'
|
||||||
|
// index.
|
||||||
|
assert.commandWorked(
|
||||||
|
db.runCommand({collMod: collName, index: {name: "a_partial", hidden: true}}));
|
||||||
|
assert.commandWorked(
|
||||||
|
db.runCommand({collMod: collName, index: {name: "a_partial", hidden: false}}));
|
||||||
|
|
||||||
|
// Begin subplanning and multiplanning of the first branch. This results in a yield which
|
||||||
|
// releases the one and only reference to the initial copy of the 'a_partial'
|
||||||
|
// IndexCatalogEntryImpl which owns the partial filter MatchExpression. On restore, the catalog
|
||||||
|
// constructs a new copy of the 'a_partial' IndexCatalogEntryImpl. However, before
|
||||||
|
// SERVER-105873, the query engine was saving a raw pointer to the partial filter expression
|
||||||
|
// owned by the first copy of the IndexCatalogEntryImpl, which at this point is now destroyed.
|
||||||
|
// When the subplanner tries to plans the second branch, it is only able to create a collscan
|
||||||
|
// plan, which results in falling back to `choosePlanWholeQuery()`, which tries to dereference
|
||||||
|
// the destroyed partial index pointer when determining index eligibility.
|
||||||
|
pauseAfterFillingOutIndexEntries.off();
|
||||||
|
awaitQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test stale pointer to wildcard projection.
|
||||||
|
// The repro is nearly identical to that of 'testPartialIndex()', so we've omitted the inline
|
||||||
|
// comments.
|
||||||
|
function testWildcardIndex(conn) {
|
||||||
|
let db = conn.getDB("test");
|
||||||
|
const collName = jsTestName();
|
||||||
|
const coll = db[collName];
|
||||||
|
coll.drop();
|
||||||
|
insertDocs(coll);
|
||||||
|
|
||||||
|
assert.commandWorked(coll.createIndex({"a.$**": 1}, {name: "a_wildcard"}));
|
||||||
|
assert.commandWorked(coll.createIndex({b: 1}));
|
||||||
|
assert.commandWorked(db.adminCommand({setParameter: 1, internalQueryExecYieldIterations: 1}));
|
||||||
|
let pauseAfterFillingOutIndexEntries =
|
||||||
|
configureFailPoint(conn, "pauseAfterFillingOutIndexEntries");
|
||||||
|
const awaitQuery = startParallelShell(function() {
|
||||||
|
const coll = db[jsTestName()];
|
||||||
|
const res = coll.find({
|
||||||
|
$or: [{"a.foo": 1, b: 1}, {c: null}],
|
||||||
|
})
|
||||||
|
.toArray();
|
||||||
|
assert.eq(1, res.length);
|
||||||
|
}, db.getMongo().port);
|
||||||
|
|
||||||
|
pauseAfterFillingOutIndexEntries.wait();
|
||||||
|
assert.commandWorked(
|
||||||
|
db.runCommand({collMod: collName, index: {name: "a_wildcard", hidden: true}}));
|
||||||
|
assert.commandWorked(
|
||||||
|
db.runCommand({collMod: collName, index: {name: "a_wildcard", hidden: false}}));
|
||||||
|
pauseAfterFillingOutIndexEntries.off();
|
||||||
|
awaitQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test stale pointer to collator.
|
||||||
|
// The repro is nearly identical to that of 'testPartialIndex()', so we've omitted the inline
|
||||||
|
// comments.
|
||||||
|
function testCollation(conn) {
|
||||||
|
let db = conn.getDB("test");
|
||||||
|
const collName = jsTestName();
|
||||||
|
const coll = db[collName];
|
||||||
|
coll.drop();
|
||||||
|
|
||||||
|
// Create collection with collation
|
||||||
|
const collation = {locale: "fr_CA", strength: 2};
|
||||||
|
assert.commandWorked(db.createCollection(collName, {collation: collation}));
|
||||||
|
insertDocs(coll);
|
||||||
|
|
||||||
|
assert.commandWorked(coll.createIndex({a: 1}, {name: "a_collation", collation: collation}));
|
||||||
|
assert.commandWorked(coll.createIndex({b: 1}, {collation: collation}));
|
||||||
|
assert.commandWorked(db.adminCommand({setParameter: 1, internalQueryExecYieldIterations: 1}));
|
||||||
|
let pauseAfterFillingOutIndexEntries =
|
||||||
|
configureFailPoint(conn, "pauseAfterFillingOutIndexEntries");
|
||||||
|
const awaitQuery = startParallelShell(function() {
|
||||||
|
const coll = db[jsTestName()];
|
||||||
|
const res = coll.find({
|
||||||
|
// Ensure predicate has string operands so collator is consulted.
|
||||||
|
$or: [{a: "abc", b: "def"}, {c: null}],
|
||||||
|
})
|
||||||
|
.collation({locale: "fr_CA", strength: 2})
|
||||||
|
.toArray();
|
||||||
|
assert.eq(1, res.length);
|
||||||
|
}, db.getMongo().port);
|
||||||
|
|
||||||
|
pauseAfterFillingOutIndexEntries.wait();
|
||||||
|
assert.commandWorked(
|
||||||
|
db.runCommand({collMod: collName, index: {name: "a_collation", hidden: true}}));
|
||||||
|
assert.commandWorked(
|
||||||
|
db.runCommand({collMod: collName, index: {name: "a_collation", hidden: false}}));
|
||||||
|
pauseAfterFillingOutIndexEntries.off();
|
||||||
|
awaitQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTest(testFn) {
|
||||||
|
let conn = MongoRunner.runMongod();
|
||||||
|
try {
|
||||||
|
testFn(conn);
|
||||||
|
} finally {
|
||||||
|
MongoRunner.stopMongod(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest(testPartialIndex);
|
||||||
|
runTest(testWildcardIndex);
|
||||||
|
runTest(testCollation);
|
||||||
|
})();
|
||||||
|
|
@ -77,7 +77,8 @@ CoreIndexInfo indexInfoFromIndexCatalogEntry(const IndexCatalogEntry& ice) {
|
||||||
IndexEntry::Identifier{desc->indexName()},
|
IndexEntry::Identifier{desc->indexName()},
|
||||||
ice.getFilterExpression(),
|
ice.getFilterExpression(),
|
||||||
ice.getCollator(),
|
ice.getCollator(),
|
||||||
projExec};
|
projExec,
|
||||||
|
ice.shared_from_this()};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,8 @@
|
||||||
|
|
||||||
namespace mongo {
|
namespace mongo {
|
||||||
|
|
||||||
|
MONGO_FAIL_POINT_DEFINE(pauseAfterFillingOutIndexEntries);
|
||||||
|
|
||||||
boost::intrusive_ptr<ExpressionContext> makeExpressionContextForGetExecutor(
|
boost::intrusive_ptr<ExpressionContext> makeExpressionContextForGetExecutor(
|
||||||
OperationContext* opCtx, const BSONObj& requestCollation, const NamespaceString& nss) {
|
OperationContext* opCtx, const BSONObj& requestCollation, const NamespaceString& nss) {
|
||||||
invariant(opCtx);
|
invariant(opCtx);
|
||||||
|
|
@ -272,7 +274,8 @@ IndexEntry indexEntryFromIndexCatalogEntry(OperationContext* opCtx,
|
||||||
ice.getFilterExpression(),
|
ice.getFilterExpression(),
|
||||||
desc->infoObj(),
|
desc->infoObj(),
|
||||||
ice.getCollator(),
|
ice.getCollator(),
|
||||||
wildcardProjection};
|
wildcardProjection,
|
||||||
|
ice.shared_from_this()};
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnIndexEntry columnIndexEntryFromIndexCatalogEntry(OperationContext* opCtx,
|
ColumnIndexEntry columnIndexEntryFromIndexCatalogEntry(OperationContext* opCtx,
|
||||||
|
|
@ -462,6 +465,7 @@ void fillOutPlannerParams(OperationContext* opCtx,
|
||||||
plannerParams->availableMemoryBytes =
|
plannerParams->availableMemoryBytes =
|
||||||
static_cast<long long>(ProcessInfo::getMemSizeMB()) * kMB;
|
static_cast<long long>(ProcessInfo::getMemSizeMB()) * kMB;
|
||||||
}
|
}
|
||||||
|
pauseAfterFillingOutIndexEntries.pauseWhileSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::map<NamespaceString, SecondaryCollectionInfo> fillOutSecondaryCollectionsInformation(
|
std::map<NamespaceString, SecondaryCollectionInfo> fillOutSecondaryCollectionsInformation(
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ public:
|
||||||
{},
|
{},
|
||||||
nullptr,
|
nullptr,
|
||||||
nullptr,
|
nullptr,
|
||||||
|
nullptr,
|
||||||
wildcardPos};
|
wildcardPos};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,14 +61,16 @@ struct CoreIndexInfo {
|
||||||
Identifier ident,
|
Identifier ident,
|
||||||
const MatchExpression* fe = nullptr,
|
const MatchExpression* fe = nullptr,
|
||||||
const CollatorInterface* ci = nullptr,
|
const CollatorInterface* ci = nullptr,
|
||||||
const IndexPathProjection* indexPathProj = nullptr)
|
const IndexPathProjection* indexPathProj = nullptr,
|
||||||
|
std::shared_ptr<const IndexCatalogEntry> iceStorage = nullptr)
|
||||||
: identifier(std::move(ident)),
|
: identifier(std::move(ident)),
|
||||||
keyPattern(kp),
|
keyPattern(kp),
|
||||||
filterExpr(fe),
|
filterExpr(fe),
|
||||||
type(type),
|
type(type),
|
||||||
sparse(sp),
|
sparse(sp),
|
||||||
collator(ci),
|
collator(ci),
|
||||||
indexPathProjection(indexPathProj) {
|
indexPathProjection(indexPathProj),
|
||||||
|
indexCatalogEntryStorage(std::move(iceStorage)) {
|
||||||
// If a projection executor exists, we always expect a $** index
|
// If a projection executor exists, we always expect a $** index
|
||||||
if (indexPathProjection != nullptr)
|
if (indexPathProjection != nullptr)
|
||||||
invariant(type == IndexType::INDEX_WILDCARD || type == IndexType::INDEX_COLUMN);
|
invariant(type == IndexType::INDEX_WILDCARD || type == IndexType::INDEX_COLUMN);
|
||||||
|
|
@ -129,6 +131,10 @@ struct CoreIndexInfo {
|
||||||
// hashing.
|
// hashing.
|
||||||
BSONObj keyPattern;
|
BSONObj keyPattern;
|
||||||
|
|
||||||
|
// If this index is a partial index, 'filterExpr' is the MatchExpression representing the
|
||||||
|
// filter predicate. Otherwise, 'filterExpr' is null. It is the caller's responsibility to
|
||||||
|
// ensure that the pointer remains valid for the lifetime of this CoreIndexInfo.
|
||||||
|
// See 'indexCatalogEntryStorage' for more details.
|
||||||
const MatchExpression* filterExpr;
|
const MatchExpression* filterExpr;
|
||||||
|
|
||||||
// What type of index is this? (What access method can we use on the index described by the
|
// What type of index is this? (What access method can we use on the index described by the
|
||||||
|
|
@ -139,12 +145,22 @@ struct CoreIndexInfo {
|
||||||
|
|
||||||
// Null if this index orders strings according to the simple binary compare. If non-null,
|
// Null if this index orders strings according to the simple binary compare. If non-null,
|
||||||
// represents the collator used to generate index keys for indexed strings.
|
// represents the collator used to generate index keys for indexed strings.
|
||||||
|
// It is the caller's responsibility to ensure that the pointer remains valid for the lifetime
|
||||||
|
// of this CoreIndexInfo.
|
||||||
|
// See 'indexCatalogEntryStorage' for more details.
|
||||||
const CollatorInterface* collator = nullptr;
|
const CollatorInterface* collator = nullptr;
|
||||||
|
|
||||||
// For $** indexes, a pointer to the projection executor owned by the index access method. Null
|
// For $** indexes, a pointer to the projection executor owned by the index access method. Null
|
||||||
// unless this IndexEntry represents a wildcard or column storeindex, in which case this is
|
// unless this IndexEntry represents a wildcard or column store index, in which case this is
|
||||||
// always non-null.
|
// always non-null. It is the caller's responsibility to ensure that the pointer remains valid
|
||||||
|
// for the lifetime of this CoreIndexInfo. See 'indexCatalogEntryStorage' for more details.
|
||||||
const IndexPathProjection* indexPathProjection = nullptr;
|
const IndexPathProjection* indexPathProjection = nullptr;
|
||||||
|
|
||||||
|
// Optional shared_ptr to the IndexCatalogEntry (storage's in-memory representation of an index
|
||||||
|
// in the catalog). This is used to keep the IndexCatalogEntry alive as long as this
|
||||||
|
// CoreIndexInfo. This is useful because the 'filterExpr', 'collator' and 'indexPathProjection'
|
||||||
|
// members are often pointers to data owned by IndexCatalogEntry.
|
||||||
|
std::shared_ptr<const IndexCatalogEntry> indexCatalogEntryStorage;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -168,8 +184,10 @@ struct IndexEntry : CoreIndexInfo {
|
||||||
const BSONObj& io,
|
const BSONObj& io,
|
||||||
const CollatorInterface* ci,
|
const CollatorInterface* ci,
|
||||||
const WildcardProjection* wildcardProjection,
|
const WildcardProjection* wildcardProjection,
|
||||||
|
std::shared_ptr<const IndexCatalogEntry> iceStorage = nullptr,
|
||||||
size_t wildcardPos = 0)
|
size_t wildcardPos = 0)
|
||||||
: CoreIndexInfo(kp, type, sp, std::move(ident), fe, ci, wildcardProjection),
|
: CoreIndexInfo(
|
||||||
|
kp, type, sp, std::move(ident), fe, ci, wildcardProjection, std::move(iceStorage)),
|
||||||
version(version),
|
version(version),
|
||||||
multikey(mk),
|
multikey(mk),
|
||||||
unique(unq),
|
unique(unq),
|
||||||
|
|
|
||||||
|
|
@ -461,6 +461,7 @@ boost::optional<IndexEntry> createExpandedIndexEntry(const IndexEntry& wildcardI
|
||||||
wildcardIndex.infoObj,
|
wildcardIndex.infoObj,
|
||||||
wildcardIndex.collator,
|
wildcardIndex.collator,
|
||||||
wildcardIndex.indexPathProjection,
|
wildcardIndex.indexPathProjection,
|
||||||
|
wildcardIndex.indexCatalogEntryStorage,
|
||||||
wildcardFieldPos);
|
wildcardFieldPos);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue