Skip to content

Commit bad7c53

Browse files
zhihuiFanRuoxinXu
authored andcommitted
SERVER-9306 Ability to temporarily forbid query optimizer from using index ("Hidden Index") SERVER-47275 Take over and complete Hidden Indexes PR
Co-authored-by: Ruoxin Xu <[email protected]>
1 parent 17edea3 commit bad7c53

29 files changed

+445
-87
lines changed

jstests/core/hidden_index.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Test expected behavior for hidden indexes. A hidden index is invisible to the query planner so
3+
* it will not be used in planning. It is handled in the same way as other indexes by the index
4+
* catalog and for TTL purposes.
5+
* @tags: [
6+
* multiversion_incompatible,
7+
* requires_fcv_44,
8+
* requires_non_retryable_commands, // CollMod is not retryable.
9+
* ]
10+
*/
11+
12+
(function() {
13+
'use strict';
14+
load("jstests/libs/analyze_plan.js"); // For getPlanStages.
15+
load("jstests/libs/collection_drop_recreate.js"); // For assert[Drop|Create]Collection.
16+
load("jstests/libs/fixture_helpers.js"); // For FixtureHelpers.
17+
load("jstests/libs/get_index_helpers.js"); // For GetIndexHelpers.findByName.
18+
19+
const collName = "hidden_index";
20+
let coll = assertDropAndRecreateCollection(db, collName);
21+
22+
function numOfUsedIXSCAN(query) {
23+
const explain = assert.commandWorked(coll.find(query).explain());
24+
const ixScans = getPlanStages(explain.queryPlanner.winningPlan, "IXSCAN");
25+
return ixScans.length;
26+
}
27+
28+
function validateHiddenIndexBehaviour(query, index_type, wildcard) {
29+
let index_name;
30+
if (wildcard)
31+
index_name = 'a.$**_' + index_type;
32+
else
33+
index_name = 'a_' + index_type;
34+
35+
if (wildcard)
36+
assert.commandWorked(coll.createIndex({"a.$**": index_type}));
37+
else
38+
assert.commandWorked(coll.createIndex({"a": index_type}));
39+
40+
let idxSpec = GetIndexHelpers.findByName(coll.getIndexes(), index_name);
41+
assert.eq(idxSpec.hidden, undefined);
42+
assert.gt(numOfUsedIXSCAN(query), 0);
43+
44+
assert.commandWorked(coll.hideIndex(index_name));
45+
idxSpec = GetIndexHelpers.findByName(coll.getIndexes(), index_name);
46+
assert(idxSpec.hidden);
47+
if (index_type === "text") {
48+
assert.commandFailedWithCode(coll.runCommand("find", {filter: query}, {hint: {a: 1}}), 291);
49+
assert.commandWorked(coll.dropIndexes());
50+
return;
51+
}
52+
assert.eq(numOfUsedIXSCAN(query), 0);
53+
54+
assert.commandWorked(coll.unhideIndex(index_name));
55+
idxSpec = GetIndexHelpers.findByName(coll.getIndexes(), index_name);
56+
assert.eq(idxSpec.hidden, undefined);
57+
assert.gt(numOfUsedIXSCAN(query), 0);
58+
59+
assert.commandWorked(coll.dropIndex(index_name));
60+
61+
if (wildcard)
62+
assert.commandWorked(coll.createIndex({"a.$**": index_type}, {hidden: true}));
63+
else
64+
assert.commandWorked(coll.createIndex({"a": index_type}, {hidden: true}));
65+
66+
idxSpec = GetIndexHelpers.findByName(coll.getIndexes(), index_name);
67+
assert(idxSpec.hidden);
68+
assert.eq(numOfUsedIXSCAN(query), 0);
69+
assert.commandWorked(coll.dropIndexes());
70+
}
71+
72+
// Normal index testing.
73+
validateHiddenIndexBehaviour({a: 1}, 1);
74+
75+
// GEO index testing.
76+
validateHiddenIndexBehaviour({
77+
a: {$geoWithin: {$geometry: {type: "Polygon", coordinates: [[[0, 0], [3, 6], [6, 1], [0, 0]]]}}}
78+
},
79+
"2dsphere");
80+
81+
// Fts index.
82+
validateHiddenIndexBehaviour({$text: {$search: "java"}}, "text");
83+
84+
// Wildcard index.
85+
validateHiddenIndexBehaviour({"a.f": 1}, 1, true);
86+
87+
// Hidden index on capped collection.
88+
if (!FixtureHelpers.isMongos(db)) {
89+
coll = assertDropAndRecreateCollection(db, collName, {capped: true, size: 100});
90+
validateHiddenIndexBehaviour({a: 1}, 1);
91+
coll = assertDropAndRecreateCollection(db, collName);
92+
}
93+
// Test that index 'hidden' status can be found in listIndexes command.
94+
assert.commandWorked(coll.createIndex({lsIdx: 1}, {hidden: true}));
95+
let res = assert.commandWorked(db.runCommand({"listIndexes": collName}));
96+
let idxSpec = GetIndexHelpers.findByName(res.cursor.firstBatch, "lsIdx_1");
97+
assert.eq(idxSpec.hidden, true);
98+
99+
// Can't hide any index in a system collection.
100+
const systemColl = db.getSiblingDB('admin').system.version;
101+
assert.commandWorked(systemColl.createIndex({a: 1}));
102+
assert.commandFailedWithCode(systemColl.hideIndex("a_1"), 2);
103+
assert.commandFailedWithCode(systemColl.createIndex({a: 1}, {hidden: true}), 2);
104+
105+
// Can't hide the '_id' index.
106+
assert.commandFailed(coll.hideIndex("_id_"));
107+
108+
// Can't 'hint' a hidden index.
109+
assert.commandWorked(coll.createIndex({"a": 1}, {"hidden": true}));
110+
assert.commandFailedWithCode(coll.runCommand("find", {hint: {a: 1}}), 2);
111+
112+
// We can change ttl index and hide info at the same time.
113+
assert.commandWorked(coll.dropIndexes());
114+
assert.commandWorked(coll.createIndex({"tm": 1}, {expireAfterSeconds: 10}));
115+
idxSpec = GetIndexHelpers.findByName(coll.getIndexes(), "tm_1");
116+
assert.eq(idxSpec.hidden, undefined);
117+
assert.eq(idxSpec.expireAfterSeconds, 10);
118+
119+
db.runCommand({
120+
"collMod": coll.getName(),
121+
"index": {"name": "tm_1", "expireAfterSeconds": 1, "hidden": true}
122+
});
123+
idxSpec = GetIndexHelpers.findByName(coll.getIndexes(), "tm_1");
124+
assert(idxSpec.hidden);
125+
assert.eq(idxSpec.expireAfterSeconds, 1);
126+
})();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Make sure the TTL index still work after we hide it
2+
(function() {
3+
"use strict";
4+
let runner = MongoRunner.runMongod({setParameter: "ttlMonitorSleepSecs=1"});
5+
let coll = runner.getDB("test").ttl_hiddenl_index;
6+
coll.drop();
7+
8+
// Create TTL index.
9+
assert.commandWorked(coll.ensureIndex({x: 1}, {expireAfterSeconds: 0}));
10+
let now = new Date();
11+
12+
assert.commandWorked(coll.hideIndex("x_1"));
13+
14+
// Insert docs after having set hidden index in order to prevent inserted docs being expired out
15+
// before the hidden index is set.
16+
assert.commandWorked(coll.insert({x: now}));
17+
assert.commandWorked(coll.insert({x: now}));
18+
19+
// Wait for the TTL monitor to run at least twice (in case we weren't finished setting up our
20+
// collection when it ran the first time).
21+
var ttlPass = coll.getDB().serverStatus().metrics.ttl.passes;
22+
assert.soon(function() {
23+
return coll.getDB().serverStatus().metrics.ttl.passes >= ttlPass + 2;
24+
}, "TTL monitor didn't run before timing out.");
25+
26+
assert.eq(coll.count(), 0, "We should get 0 documents after TTL monitor run");
27+
28+
MongoRunner.stopMongod(runner);
29+
})();

src/mongo/db/auth/auth_op_observer.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,11 @@ void AuthOpObserver::onCollMod(OperationContext* opCtx,
116116
OptionalCollectionUUID uuid,
117117
const BSONObj& collModCmd,
118118
const CollectionOptions& oldCollOptions,
119-
boost::optional<TTLCollModInfo> ttlInfo) {
119+
boost::optional<IndexCollModInfo> indexInfo) {
120120
const auto cmdNss = nss.getCommandNS();
121121

122122
// Create the 'o' field object.
123-
const auto cmdObj = makeCollModCmdObj(collModCmd, oldCollOptions, ttlInfo);
123+
const auto cmdObj = makeCollModCmdObj(collModCmd, oldCollOptions, indexInfo);
124124

125125
AuthorizationManager::get(opCtx->getServiceContext())
126126
->logOp(opCtx, "c", cmdNss, cmdObj, nullptr);

src/mongo/db/auth/auth_op_observer.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class AuthOpObserver final : public OpObserver {
113113
OptionalCollectionUUID uuid,
114114
const BSONObj& collModCmd,
115115
const CollectionOptions& oldCollOptions,
116-
boost::optional<TTLCollModInfo> ttlInfo) final;
116+
boost::optional<IndexCollModInfo> indexInfo) final;
117117

118118
void onDropDatabase(OperationContext* opCtx, const std::string& dbName) final;
119119

src/mongo/db/catalog/coll_mod.cpp

Lines changed: 104 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ MONGO_FAIL_POINT_DEFINE(assertAfterIndexUpdate);
6969
struct CollModRequest {
7070
const IndexDescriptor* idx = nullptr;
7171
BSONElement indexExpireAfterSeconds = {};
72+
BSONElement indexHidden = {};
7273
BSONElement viewPipeLine = {};
7374
std::string viewOn = {};
7475
BSONElement collValidator = {};
@@ -125,14 +126,18 @@ StatusWith<CollModRequest> parseCollModRequest(OperationContext* opCtx,
125126
}
126127

127128
cmr.indexExpireAfterSeconds = indexObj["expireAfterSeconds"];
128-
if (cmr.indexExpireAfterSeconds.eoo()) {
129-
return Status(ErrorCodes::InvalidOptions, "no expireAfterSeconds field");
129+
cmr.indexHidden = indexObj["hidden"];
130+
131+
if (cmr.indexExpireAfterSeconds.eoo() && cmr.indexHidden.eoo()) {
132+
return Status(ErrorCodes::InvalidOptions, "no expireAfterSeconds or hidden field");
130133
}
131-
if (!cmr.indexExpireAfterSeconds.isNumber()) {
134+
if (!cmr.indexExpireAfterSeconds.eoo() && !cmr.indexExpireAfterSeconds.isNumber()) {
132135
return Status(ErrorCodes::InvalidOptions,
133136
"expireAfterSeconds field must be a number");
134137
}
135-
138+
if (!cmr.indexHidden.eoo() && !cmr.indexHidden.isBoolean()) {
139+
return Status(ErrorCodes::InvalidOptions, "hidden field must be a boolean");
140+
}
136141
if (!indexName.empty()) {
137142
cmr.idx = coll->getIndexCatalog()->findIndexByName(opCtx, indexName);
138143
if (!cmr.idx) {
@@ -161,15 +166,17 @@ StatusWith<CollModRequest> parseCollModRequest(OperationContext* opCtx,
161166
cmr.idx = indexes[0];
162167
}
163168

164-
BSONElement oldExpireSecs = cmr.idx->infoObj().getField("expireAfterSeconds");
165-
if (oldExpireSecs.eoo()) {
166-
return Status(ErrorCodes::InvalidOptions, "no expireAfterSeconds field to update");
167-
}
168-
if (!oldExpireSecs.isNumber()) {
169-
return Status(ErrorCodes::InvalidOptions,
170-
"existing expireAfterSeconds field is not a number");
169+
if (!cmr.indexExpireAfterSeconds.eoo()) {
170+
BSONElement oldExpireSecs = cmr.idx->infoObj().getField("expireAfterSeconds");
171+
if (oldExpireSecs.eoo()) {
172+
return Status(ErrorCodes::InvalidOptions,
173+
"no expireAfterSeconds field to update");
174+
}
175+
if (!oldExpireSecs.isNumber()) {
176+
return Status(ErrorCodes::InvalidOptions,
177+
"existing expireAfterSeconds field is not a number");
178+
}
171179
}
172-
173180
} else if (fieldName == "validator" && !isView) {
174181
// Save this to a variable to avoid reading the atomic variable multiple times.
175182
const auto currentFCV = serverGlobalParams.featureCompatibility.getVersion();
@@ -250,20 +257,35 @@ class CollModResultChange : public RecoveryUnit::Change {
250257
public:
251258
CollModResultChange(const BSONElement& oldExpireSecs,
252259
const BSONElement& newExpireSecs,
260+
const BSONElement& oldHidden,
261+
const BSONElement& newHidden,
253262
BSONObjBuilder* result)
254-
: _oldExpireSecs(oldExpireSecs), _newExpireSecs(newExpireSecs), _result(result) {}
263+
: _oldExpireSecs(oldExpireSecs),
264+
_newExpireSecs(newExpireSecs),
265+
_oldHidden(oldHidden),
266+
_newHidden(newHidden),
267+
_result(result) {}
255268

256269
void commit(boost::optional<Timestamp>) override {
257270
// add the fields to BSONObjBuilder result
258-
_result->appendAs(_oldExpireSecs, "expireAfterSeconds_old");
259-
_result->appendAs(_newExpireSecs, "expireAfterSeconds_new");
271+
if (!_oldExpireSecs.eoo()) {
272+
_result->appendAs(_oldExpireSecs, "expireAfterSeconds_old");
273+
_result->appendAs(_newExpireSecs, "expireAfterSeconds_new");
274+
}
275+
if (!_newHidden.eoo()) {
276+
bool oldValue = _oldHidden.eoo() ? false : _oldHidden.booleanSafe();
277+
_result->append("hidden_old", oldValue);
278+
_result->appendAs(_newHidden, "hidden_new");
279+
}
260280
}
261281

262282
void rollback() override {}
263283

264284
private:
265285
const BSONElement _oldExpireSecs;
266286
const BSONElement _newExpireSecs;
287+
const BSONElement _oldHidden;
288+
const BSONElement _newHidden;
267289
BSONObjBuilder* _result;
268290
};
269291

@@ -326,6 +348,19 @@ Status _collModInternal(OperationContext* opCtx,
326348
const CollModRequest cmrOld = statusW.getValue();
327349
CollModRequest cmrNew = statusW.getValue();
328350

351+
if (!cmrOld.indexHidden.eoo()) {
352+
353+
if (serverGlobalParams.featureCompatibility.getVersion() <
354+
ServerGlobalParams::FeatureCompatibility::Version::kFullyUpgradedTo46 &&
355+
cmrOld.indexHidden.booleanSafe()) {
356+
return Status(ErrorCodes::BadValue, "Hidden indexes can only be created with FCV 4.6");
357+
}
358+
if (coll->ns().isSystem())
359+
return Status(ErrorCodes::BadValue, "Can't hide index on system collection");
360+
if (cmrOld.idx->isIdIndex())
361+
return Status(ErrorCodes::BadValue, "can't hide _id index");
362+
}
363+
329364
return writeConflictRetry(opCtx, "collMod", nss.ns(), [&] {
330365
WriteUnitOfWork wunit(opCtx);
331366

@@ -362,40 +397,64 @@ Status _collModInternal(OperationContext* opCtx,
362397
CollectionOptions oldCollOptions =
363398
DurableCatalog::get(opCtx)->getCollectionOptions(opCtx, coll->getCatalogId());
364399

365-
boost::optional<TTLCollModInfo> ttlInfo;
400+
boost::optional<IndexCollModInfo> indexCollModInfo;
366401

367402
// Handle collMod operation type appropriately.
368403

369-
// TTLIndex
370-
if (!cmrOld.indexExpireAfterSeconds.eoo()) {
371-
BSONElement newExpireSecs = cmrOld.indexExpireAfterSeconds;
372-
BSONElement oldExpireSecs = cmrOld.idx->infoObj().getField("expireAfterSeconds");
373-
374-
if (SimpleBSONElementComparator::kInstance.evaluate(oldExpireSecs != newExpireSecs)) {
375-
// Change the value of "expireAfterSeconds" on disk.
376-
DurableCatalog::get(opCtx)->updateTTLSetting(opCtx,
377-
coll->getCatalogId(),
378-
cmrOld.idx->indexName(),
379-
newExpireSecs.safeNumberLong());
380-
381-
// Notify the index catalog that the definition of this index changed. This will
382-
// invalidate the idx pointer in cmrOld. On rollback of this WUOW, the idx pointer
383-
// in cmrNew will be invalidated and the idx pointer in cmrOld will be valid again.
384-
cmrNew.idx = coll->getIndexCatalog()->refreshEntry(opCtx, cmrOld.idx);
385-
opCtx->recoveryUnit()->registerChange(
386-
std::make_unique<CollModResultChange>(oldExpireSecs, newExpireSecs, result));
387-
388-
if (MONGO_unlikely(assertAfterIndexUpdate.shouldFail())) {
389-
LOGV2(20307, "collMod - assertAfterIndexUpdate fail point enabled.");
390-
uasserted(50970, "trigger rollback after the index update");
404+
if (!cmrOld.indexExpireAfterSeconds.eoo() || !cmrOld.indexHidden.eoo()) {
405+
BSONElement newExpireSecs = {};
406+
BSONElement oldExpireSecs = {};
407+
BSONElement newHidden = {};
408+
BSONElement oldHidden = {};
409+
// TTL Index
410+
if (!cmrOld.indexExpireAfterSeconds.eoo()) {
411+
newExpireSecs = cmrOld.indexExpireAfterSeconds;
412+
oldExpireSecs = cmrOld.idx->infoObj().getField("expireAfterSeconds");
413+
if (SimpleBSONElementComparator::kInstance.evaluate(oldExpireSecs !=
414+
newExpireSecs)) {
415+
// Change the value of "expireAfterSeconds" on disk.
416+
DurableCatalog::get(opCtx)->updateTTLSetting(opCtx,
417+
coll->getCatalogId(),
418+
cmrOld.idx->indexName(),
419+
newExpireSecs.safeNumberLong());
420+
}
421+
}
422+
423+
// User wants to hide or unhide index.
424+
if (!cmrOld.indexHidden.eoo()) {
425+
newHidden = cmrOld.indexHidden;
426+
oldHidden = cmrOld.idx->infoObj().getField("hidden");
427+
// Make sure when we set 'hidden' to false, we can remove the hidden field from
428+
// catalog.
429+
if (SimpleBSONElementComparator::kInstance.evaluate(oldHidden != newHidden)) {
430+
DurableCatalog::get(opCtx)->updateHiddenSetting(opCtx,
431+
coll->getCatalogId(),
432+
cmrOld.idx->indexName(),
433+
newHidden.booleanSafe());
391434
}
392435
}
393436

394437

395-
// Save previous TTL index expiration.
396-
ttlInfo = TTLCollModInfo{Seconds(newExpireSecs.safeNumberLong()),
397-
Seconds(oldExpireSecs.safeNumberLong()),
398-
cmrNew.idx->indexName()};
438+
indexCollModInfo = IndexCollModInfo{
439+
cmrOld.indexExpireAfterSeconds.eoo() ? boost::optional<Seconds>()
440+
: Seconds(newExpireSecs.safeNumberLong()),
441+
cmrOld.indexExpireAfterSeconds.eoo() ? boost::optional<Seconds>()
442+
: Seconds(oldExpireSecs.safeNumberLong()),
443+
cmrOld.indexHidden.eoo() ? boost::optional<bool>() : newHidden.booleanSafe(),
444+
cmrOld.indexHidden.eoo() ? boost::optional<bool>() : oldHidden.booleanSafe(),
445+
cmrNew.idx->indexName()};
446+
447+
// Notify the index catalog that the definition of this index changed. This will
448+
// invalidate the idx pointer in cmrOld. On rollback of this WUOW, the idx pointer
449+
// in cmrNew will be invalidated and the idx pointer in cmrOld will be valid again.
450+
cmrNew.idx = coll->getIndexCatalog()->refreshEntry(opCtx, cmrOld.idx);
451+
opCtx->recoveryUnit()->registerChange(std::make_unique<CollModResultChange>(
452+
oldExpireSecs, newExpireSecs, oldHidden, newHidden, result));
453+
454+
if (MONGO_unlikely(assertAfterIndexUpdate.shouldFail())) {
455+
LOGV2(20307, "collMod - assertAfterIndexUpdate fail point enabled.");
456+
uasserted(50970, "trigger rollback after the index update");
457+
}
399458
}
400459

401460
// The Validator, ValidationAction and ValidationLevel are already parsed and must be OK.
@@ -412,8 +471,10 @@ Status _collModInternal(OperationContext* opCtx,
412471

413472
// Only observe non-view collMods, as view operations are observed as operations on the
414473
// system.views collection.
474+
415475
auto* const opObserver = opCtx->getServiceContext()->getOpObserver();
416-
opObserver->onCollMod(opCtx, nss, coll->uuid(), oplogEntryObj, oldCollOptions, ttlInfo);
476+
opObserver->onCollMod(
477+
opCtx, nss, coll->uuid(), oplogEntryObj, oldCollOptions, indexCollModInfo);
417478

418479
wunit.commit();
419480
return Status::OK();

0 commit comments

Comments
 (0)