/** * Copyright (C) 2015 MongoDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the GNU Affero General Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */ #include "mongo/platform/basic.h" #include "mongo/db/catalog/coll_mod.h" #include #include #include "mongo/bson/simple_bsonelement_comparator.h" #include "mongo/db/background.h" #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/collection_catalog_entry.h" #include "mongo/db/catalog/database.h" #include "mongo/db/client.h" #include "mongo/db/db_raii.h" #include "mongo/db/index/index_descriptor.h" #include "mongo/db/repl/replication_coordinator_global.h" #include "mongo/db/service_context.h" #include "mongo/db/storage/recovery_unit.h" #include "mongo/db/views/view_catalog.h" namespace mongo { namespace { struct CollModRequest { const IndexDescriptor* idx = nullptr; BSONElement indexExpireAfterSeconds = {}; BSONElement viewPipeLine = {}; std::string viewOn = {}; BSONElement collValidator = {}; std::string collValidationAction = {}; std::string collValidationLevel = {}; BSONElement usePowerOf2Sizes = {}; BSONElement noPadding = {}; }; StatusWith parseCollModRequest(OperationContext* opCtx, const NamespaceString& nss, Collection* coll, const BSONObj& cmdObj, BSONObjBuilder* oplogEntryBuilder) { bool isView = !coll; CollModRequest cmr; BSONForEach(e, cmdObj) { const auto fieldName = e.fieldNameStringData(); if (Command::isGenericArgument(fieldName)) { continue; // Don't add to oplog builder. } else if (fieldName == "collMod") { // no-op } else if (fieldName == "index" && !isView) { BSONObj indexObj = e.Obj(); StringData indexName; BSONObj keyPattern; BSONElement nameElem = indexObj["name"]; BSONElement keyPatternElem = indexObj["keyPattern"]; if (nameElem && keyPatternElem) { return Status(ErrorCodes::InvalidOptions, "Cannot specify both key pattern and name."); } if (!nameElem && !keyPatternElem) { return Status(ErrorCodes::InvalidOptions, "Must specify either index name or key pattern."); } if (nameElem) { if (nameElem.type() != BSONType::String) { return Status(ErrorCodes::InvalidOptions, "Index name must be a string."); } indexName = nameElem.valueStringData(); } if (keyPatternElem) { if (keyPatternElem.type() != BSONType::Object) { return Status(ErrorCodes::InvalidOptions, "Key pattern must be an object."); } keyPattern = keyPatternElem.embeddedObject(); } cmr.indexExpireAfterSeconds = indexObj["expireAfterSeconds"]; if (cmr.indexExpireAfterSeconds.eoo()) { return Status(ErrorCodes::InvalidOptions, "no expireAfterSeconds field"); } if (!cmr.indexExpireAfterSeconds.isNumber()) { return Status(ErrorCodes::InvalidOptions, "expireAfterSeconds field must be a number"); } if (!indexName.empty()) { cmr.idx = coll->getIndexCatalog()->findIndexByName(opCtx, indexName); if (!cmr.idx) { return Status(ErrorCodes::IndexNotFound, str::stream() << "cannot find index " << indexName << " for ns " << nss.ns()); } } else { std::vector indexes; coll->getIndexCatalog()->findIndexesByKeyPattern( opCtx, keyPattern, false, &indexes); if (indexes.size() > 1) { return Status(ErrorCodes::AmbiguousIndexKeyPattern, str::stream() << "index keyPattern " << keyPattern << " matches " << indexes.size() << " indexes," << " must use index name. " << "Conflicting indexes:" << indexes[0]->infoObj() << ", " << indexes[1]->infoObj()); } else if (indexes.empty()) { return Status(ErrorCodes::IndexNotFound, str::stream() << "cannot find index " << keyPattern << " for ns " << nss.ns()); } cmr.idx = indexes[0]; } BSONElement oldExpireSecs = cmr.idx->infoObj().getField("expireAfterSeconds"); if (oldExpireSecs.eoo()) { return Status(ErrorCodes::InvalidOptions, "no expireAfterSeconds field to update"); } if (!oldExpireSecs.isNumber()) { return Status(ErrorCodes::InvalidOptions, "existing expireAfterSeconds field is not a number"); } } else if (fieldName == "validator" && !isView) { auto statusW = coll->parseValidator(e.Obj()); if (!statusW.isOK()) return statusW.getStatus(); cmr.collValidator = e; } else if (fieldName == "validationLevel" && !isView) { auto statusW = coll->parseValidationLevel(e.String()); if (!statusW.isOK()) return statusW.getStatus(); cmr.collValidationLevel = e.String(); } else if (fieldName == "validationAction" && !isView) { auto statusW = coll->parseValidationAction(e.String()); if (!statusW.isOK()) return statusW.getStatus(); cmr.collValidationAction = e.String(); } else if (fieldName == "pipeline") { if (!isView) { return Status(ErrorCodes::InvalidOptions, "'pipeline' option only supported on a view"); } if (e.type() != mongo::Array) { return Status(ErrorCodes::InvalidOptions, "not a valid aggregation pipeline"); } cmr.viewPipeLine = e; } else if (fieldName == "viewOn") { if (!isView) { return Status(ErrorCodes::InvalidOptions, "'viewOn' option only supported on a view"); } if (e.type() != mongo::String) { return Status(ErrorCodes::InvalidOptions, "'viewOn' option must be a string"); } cmr.viewOn = e.str(); } else { if (isView) { return Status(ErrorCodes::InvalidOptions, str::stream() << "option not supported on a view: " << fieldName); } // As of SERVER-17312 we only support these two options. When SERVER-17320 is // resolved this will need to be enhanced to handle other options. typedef CollectionOptions CO; if (fieldName == "usePowerOf2Sizes") cmr.usePowerOf2Sizes = e; else if (fieldName == "noPadding") cmr.noPadding = e; else return Status(ErrorCodes::InvalidOptions, str::stream() << "unknown option to collMod: " << fieldName); } oplogEntryBuilder->append(e); } return {std::move(cmr)}; } /** * Set a collection option flag for 'UsePowerOf2Sizes' or 'NoPadding'. Appends both the new and * old flag setting to the given 'result' builder. */ void setCollectionOptionFlag(OperationContext* opCtx, Collection* coll, BSONElement& collOptionElement, BSONObjBuilder* result) { const StringData flagName = collOptionElement.fieldNameStringData(); int flag; if (flagName == "usePowerOf2Sizes") { flag = CollectionOptions::Flag_UsePowerOf2Sizes; } else if (flagName == "noPadding") { flag = CollectionOptions::Flag_NoPadding; } else { flag = 0; } CollectionCatalogEntry* cce = coll->getCatalogEntry(); const int oldFlags = cce->getCollectionOptions(opCtx).flags; const bool oldSetting = oldFlags & flag; const bool newSetting = collOptionElement.trueValue(); result->appendBool(flagName.toString() + "_old", oldSetting); result->appendBool(flagName.toString() + "_new", newSetting); const int newFlags = newSetting ? (oldFlags | flag) // set flag : (oldFlags & ~flag); // clear flag // NOTE we do this unconditionally to ensure that we note that the user has // explicitly set flags, even if they are just setting the default. cce->updateFlags(opCtx, newFlags); const CollectionOptions newOptions = cce->getCollectionOptions(opCtx); invariant(newOptions.flags == newFlags); invariant(newOptions.flagsSet); } } // namespace } // namespace mongo mongo::Status mongo::collMod(OperationContext* opCtx, const NamespaceString& nss, const BSONObj& cmdObj, BSONObjBuilder* result) { StringData dbName = nss.db(); AutoGetDb autoDb(opCtx, dbName, MODE_X); Database* const db = autoDb.getDb(); Collection* coll = db ? db->getCollection(opCtx, nss) : nullptr; // May also modify a view instead of a collection. boost::optional view; if (db && !coll) { const auto sharedView = db->getViewCatalog()->lookup(opCtx, nss.ns()); if (sharedView) { // We copy the ViewDefinition as it is modified below to represent the requested state. view = {*sharedView}; } } // This can kill all cursors so don't allow running it while a background operation is in // progress. BackgroundOperation::assertNoBgOpInProgForNs(nss); // If db/collection/view does not exist, short circuit and return. if (!db || (!coll && !view)) { return Status(ErrorCodes::NamespaceNotFound, "ns does not exist"); } OldClientContext ctx(opCtx, nss.ns()); bool userInitiatedWritesAndNotPrimary = opCtx->writesAreReplicated() && !repl::getGlobalReplicationCoordinator()->canAcceptWritesFor(opCtx, nss); if (userInitiatedWritesAndNotPrimary) { return Status(ErrorCodes::NotMaster, str::stream() << "Not primary while setting collection options on " << nss.ns()); } BSONObjBuilder oplogEntryBuilder; auto statusW = parseCollModRequest(opCtx, nss, coll, cmdObj, &oplogEntryBuilder); if (!statusW.isOK()) { return statusW.getStatus(); } CollModRequest cmr = statusW.getValue(); WriteUnitOfWork wunit(opCtx); // Handle collMod on a view and return early. The View Catalog handles the creation of oplog // entries for modifications on a view. if (view) { if (!cmr.viewPipeLine.eoo()) view->setPipeline(cmr.viewPipeLine); if (!cmr.viewOn.empty()) view->setViewOn(NamespaceString(dbName, cmr.viewOn)); ViewCatalog* catalog = db->getViewCatalog(); BSONArrayBuilder pipeline; for (auto& item : view->pipeline()) { pipeline.append(item); } auto errorStatus = catalog->modifyView(opCtx, nss, view->viewOn(), BSONArray(pipeline.obj())); if (!errorStatus.isOK()) { return errorStatus; } wunit.commit(); return Status::OK(); } // In order to facilitate the replication rollback process, which makes a best effort attempt to // "undo" a set of oplog operations, we store a snapshot of the old collection options to // provide to the OpObserver. TTL index updates aren't a part of collection options so we // save the relevant TTL index data in a separate object. CollectionOptions oldCollOptions = coll->getCatalogEntry()->getCollectionOptions(opCtx); boost::optional ttlInfo; // Handle collMod operation type appropriately. // TTLIndex if (!cmr.indexExpireAfterSeconds.eoo()) { BSONElement& newExpireSecs = cmr.indexExpireAfterSeconds; BSONElement oldExpireSecs = cmr.idx->infoObj().getField("expireAfterSeconds"); if (SimpleBSONElementComparator::kInstance.evaluate(oldExpireSecs != newExpireSecs)) { result->appendAs(oldExpireSecs, "expireAfterSeconds_old"); // Change the value of "expireAfterSeconds" on disk. coll->getCatalogEntry()->updateTTLSetting( opCtx, cmr.idx->indexName(), newExpireSecs.safeNumberLong()); // Notify the index catalog that the definition of this index changed. cmr.idx = coll->getIndexCatalog()->refreshEntry(opCtx, cmr.idx); result->appendAs(newExpireSecs, "expireAfterSeconds_new"); } // Save previous TTL index expiration. ttlInfo = TTLCollModInfo{Seconds(newExpireSecs.safeNumberLong()), Seconds(oldExpireSecs.safeNumberLong()), cmr.idx->indexName()}; } // Validator if (!cmr.collValidator.eoo()) coll->setValidator(opCtx, cmr.collValidator.Obj()).transitional_ignore(); // ValidationAction if (!cmr.collValidationAction.empty()) coll->setValidationAction(opCtx, cmr.collValidationAction).transitional_ignore(); // ValidationLevel if (!cmr.collValidationLevel.empty()) coll->setValidationLevel(opCtx, cmr.collValidationLevel).transitional_ignore(); // UsePowerof2Sizes if (!cmr.usePowerOf2Sizes.eoo()) setCollectionOptionFlag(opCtx, coll, cmr.usePowerOf2Sizes, result); // NoPadding if (!cmr.noPadding.eoo()) setCollectionOptionFlag(opCtx, coll, cmr.noPadding, result); // Only observe non-view collMods, as view operations are observed as operations on the // system.views collection. getGlobalServiceContext()->getOpObserver()->onCollMod( opCtx, nss, coll->uuid(), oplogEntryBuilder.obj(), oldCollOptions, ttlInfo); wunit.commit(); return Status::OK(); }