diff options
Diffstat (limited to 'src/mongo/db/catalog/apply_ops.cpp')
| -rw-r--r-- | src/mongo/db/catalog/apply_ops.cpp | 439 |
1 files changed, 259 insertions, 180 deletions
diff --git a/src/mongo/db/catalog/apply_ops.cpp b/src/mongo/db/catalog/apply_ops.cpp index 672a37a10c7..ec5c7681f0e 100644 --- a/src/mongo/db/catalog/apply_ops.cpp +++ b/src/mongo/db/catalog/apply_ops.cpp @@ -32,6 +32,7 @@ #include "mongo/db/catalog/apply_ops.h" +#include "mongo/bson/util/bson_extract.h" #include "mongo/db/catalog/collection.h" #include "mongo/db/catalog/database.h" #include "mongo/db/catalog/database_holder.h" @@ -42,21 +43,31 @@ #include "mongo/db/curop.h" #include "mongo/db/db_raii.h" #include "mongo/db/dbdirectclient.h" +#include "mongo/db/index/index_descriptor.h" #include "mongo/db/matcher/extensions_callback_disallow_extensions.h" #include "mongo/db/matcher/matcher.h" #include "mongo/db/op_observer.h" #include "mongo/db/operation_context.h" +#include "mongo/db/query/collation/collation_spec.h" #include "mongo/db/repl/oplog.h" #include "mongo/db/repl/replication_coordinator_global.h" #include "mongo/db/service_context.h" +#include "mongo/rpc/get_status_from_command_result.h" +#include "mongo/util/fail_point_service.h" #include "mongo/util/log.h" namespace mongo { namespace { + +const auto kPreconditionFieldName = "preCondition"_sd; + +// If enabled, causes loop in _applyOps() to hang after applying current operation. +MONGO_FP_DECLARE(applyOpsPauseBetweenOperations); + /** * Return true iff the applyOpsCmd can be executed in a single WriteUnitOfWork. */ -bool canBeAtomic(const BSONObj& applyOpCmd) { +bool _areOpsCrudOnly(const BSONObj& applyOpCmd) { for (const auto& elem : applyOpCmd.firstElement().Obj()) { const char* names[] = {"ns", "op"}; BSONElement fields[2]; @@ -89,14 +100,11 @@ bool canBeAtomic(const BSONObj& applyOpCmd) { return true; } -Status _applyOps(OperationContext* txn, +Status _applyOps(OperationContext* opCtx, const std::string& dbName, const BSONObj& applyOpCmd, BSONObjBuilder* result, int* numApplied) { - dassert(txn->lockState()->isLockHeldForMode( - ResourceId(RESOURCE_GLOBAL, ResourceId::SINGLETON_GLOBAL), MODE_X)); - BSONObj ops = applyOpCmd.firstElement().Obj(); // apply @@ -107,148 +115,169 @@ Status _applyOps(OperationContext* txn, BSONArrayBuilder ab; const bool alwaysUpsert = applyOpCmd.hasField("alwaysUpsert") ? applyOpCmd["alwaysUpsert"].trueValue() : true; - const bool haveWrappingWUOW = txn->lockState()->inAWriteUnitOfWork(); - - { - repl::UnreplicatedWritesBlock uwb(txn); + const bool haveWrappingWUOW = opCtx->lockState()->inAWriteUnitOfWork(); - while (i.more()) { - BSONElement e = i.next(); - const BSONObj& opObj = e.Obj(); + while (i.more()) { + BSONElement e = i.next(); + const BSONObj& opObj = e.Obj(); - // Ignore 'n' operations. - const char* opType = opObj["op"].valuestrsafe(); - if (*opType == 'n') - continue; + // Ignore 'n' operations. + const char* opType = opObj["op"].valuestrsafe(); + if (*opType == 'n') + continue; - const std::string ns = opObj["ns"].String(); + const NamespaceString nss(opObj["ns"].String()); - // Need to check this here, or OldClientContext may fail an invariant. - if (*opType != 'c' && !NamespaceString(ns).isValid()) - return {ErrorCodes::InvalidNamespace, "invalid ns: " + ns}; + // Need to check this here, or OldClientContext may fail an invariant. + if (*opType != 'c' && !nss.isValid()) + return {ErrorCodes::InvalidNamespace, "invalid ns: " + nss.ns()}; - Status status(ErrorCodes::InternalError, ""); + Status status(ErrorCodes::InternalError, ""); - if (haveWrappingWUOW) { - invariant(*opType != 'c'); + if (haveWrappingWUOW) { + invariant(opCtx->lockState()->isW()); + invariant(*opType != 'c'); + auto db = dbHolder().get(opCtx, nss.ns()); + if (!db) { + throw DBException( + "cannot create a database in atomic applyOps mode; will retry without " + "atomicity", + ErrorCodes::NamespaceNotFound); + } - if (!dbHolder().get(txn, ns)) { - throw DBException( - "cannot create a database in atomic applyOps mode; will retry without " - "atomicity", - ErrorCodes::NamespaceNotFound); - } + // When processing an update on a non-existent collection, applyOperation_inlock() + // returns UpdateOperationFailed on updates and allows the collection to be + // implicitly created on upserts. We detect both cases here and fail early with + // NamespaceNotFound. + auto collection = db->getCollection(nss); + if (!collection && !nss.isSystemDotIndexes() && (*opType == 'i' || *opType == 'u')) { + throw DBException(str::stream() << "cannot apply insert or update operation on " + "a non-existent namespace " + << nss.ns() + << ": " + << redact(opObj), + ErrorCodes::NamespaceNotFound); + } - OldClientContext ctx(txn, ns); - status = repl::applyOperation_inlock(txn, ctx.db(), opObj, alwaysUpsert); - if (!status.isOK()) - return status; - logOpForDbHash(txn, ns.c_str()); - } else { - try { - // Run operations under a nested lock as a hack to prevent yielding. - // - // The list of operations is supposed to be applied atomically; yielding - // would break atomicity by allowing an interruption or a shutdown to occur - // after only some operations are applied. We are already locked globally - // at this point, so taking a DBLock on the namespace creates a nested lock, - // and yields are disallowed for operations that hold a nested lock. - // - // We do not have a wrapping WriteUnitOfWork so it is possible for a journal - // commit to happen with a subset of ops applied. - Lock::GlobalWrite globalWriteLockDisallowTempRelease(txn->lockState()); - - // Ensures that yielding will not happen (see the comment above). - DEV { - Locker::LockSnapshot lockSnapshot; - invariant(!txn->lockState()->saveLockStateAndUnlock(&lockSnapshot)); - }; - - MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN { - if (*opType == 'c') { - status = repl::applyCommand_inlock(txn, opObj, true); + OldClientContext ctx(opCtx, nss.ns()); + status = repl::applyOperation_inlock(opCtx, ctx.db(), opObj, alwaysUpsert); + if (!status.isOK()) + return status; + logOpForDbHash(opCtx, nss.ns().c_str()); + } else { + try { + MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN { + if (*opType == 'c') { + invariant(opCtx->lockState()->isW()); + uassertStatusOK(status = repl::applyCommand_inlock(opCtx, opObj, true)); + } else { + const char* names[] = {"o", "ns"}; + BSONElement fields[2]; + opObj.getFields(2, names, fields); + BSONElement& fieldO = fields[0]; + BSONElement& fieldNs = fields[1]; + const StringData ns = fieldNs.valueStringData(); + NamespaceString requestNss{ns}; + + if (nss.isSystemDotIndexes()) { + BSONObj indexSpec; + NamespaceString indexNss; + std::tie(indexSpec, indexNss) = + repl::prepForApplyOpsIndexInsert(fieldO, opObj, requestNss); + if (!indexSpec["collation"]) { + // If the index spec does not include a collation, explicitly + // specify the simple collation, so the index does not inherit the + // collection default collation. + auto indexVersion = indexSpec["v"]; + // The index version is populated by prepForApplyOpsIndexInsert(). + invariant(indexVersion); + if (indexVersion.isNumber() && + (indexVersion.numberInt() >= + static_cast<int>(IndexDescriptor::IndexVersion::kV2))) { + BSONObjBuilder bob; + bob.append("collation", CollationSpec::kSimpleSpec); + bob.appendElements(indexSpec); + indexSpec = bob.obj(); + } + } + BSONObjBuilder command; + command.append("createIndexes", indexNss.coll()); + { + BSONArrayBuilder indexes(command.subarrayStart("indexes")); + indexes.append(indexSpec); + indexes.doneFast(); + } + const BSONObj commandObj = command.done(); + + DBDirectClient client(opCtx); + BSONObj infoObj; + client.runCommand(nsToDatabase(ns), commandObj, infoObj); + status = getStatusFromCommandResult(infoObj); } else { - OldClientContext ctx(txn, ns); - - status = - repl::applyOperation_inlock(txn, ctx.db(), opObj, alwaysUpsert); + AutoGetCollection autoColl(opCtx, nss, MODE_IX); + if (!autoColl.getCollection() && !nss.isSystemDotIndexes()) { + // For idempotency reasons, return success on delete operations. + if (*opType == 'd') { + status = Status::OK(); + } else { + throw DBException( + str::stream() + << "cannot apply insert or update operation on" + " a non-existent namespace " + << nss.ns() + << ": " + << mongo::redact(opObj), + ErrorCodes::NamespaceNotFound); + } + } else { + OldClientContext ctx(opCtx, nss.ns()); + status = repl::applyOperation_inlock( + opCtx, ctx.db(), opObj, alwaysUpsert); + } } } - MONGO_WRITE_CONFLICT_RETRY_LOOP_END(txn, "applyOps", ns); - } catch (const DBException& ex) { - ab.append(false); - result->append("applied", ++(*numApplied)); - result->append("code", ex.getCode()); - result->append("codeName", - ErrorCodes::errorString(ErrorCodes::fromInt(ex.getCode()))); - result->append("errmsg", ex.what()); - result->append("results", ab.arr()); - return Status(ErrorCodes::UnknownError, ex.what()); } - WriteUnitOfWork wuow(txn); - logOpForDbHash(txn, ns.c_str()); - wuow.commit(); + MONGO_WRITE_CONFLICT_RETRY_LOOP_END(opCtx, "applyOps", nss.ns()); + } catch (const DBException& ex) { + ab.append(false); + result->append("applied", ++(*numApplied)); + result->append("code", ex.getCode()); + result->append("codeName", + ErrorCodes::errorString(ErrorCodes::fromInt(ex.getCode()))); + result->append("errmsg", ex.what()); + result->append("results", ab.arr()); + return ex.toStatus(); } - - ab.append(status.isOK()); - if (!status.isOK()) { - log() << "applyOps error applying: " << status; - errors++; - } - - (*numApplied)++; + WriteUnitOfWork wuow(opCtx); + logOpForDbHash(opCtx, nss.ns().c_str()); + wuow.commit(); } - result->append("applied", *numApplied); - result->append("results", ab.arr()); - } // set replicatedWrites back to original value - - if (txn->writesAreReplicated()) { - // We want this applied atomically on slaves - // so we re-wrap without the pre-condition for speed - - std::string tempNS = str::stream() << dbName << ".$cmd"; - - // TODO: possibly use mutable BSON to remove preCondition field - // once it is available - BSONObjBuilder cmdBuilder; - - for (auto elem : applyOpCmd) { - auto name = elem.fieldNameStringData(); - if (name == "preCondition") - continue; - if (name == "bypassDocumentValidation") - continue; - cmdBuilder.append(elem); + ab.append(status.isOK()); + if (!status.isOK()) { + log() << "applyOps error applying: " << status; + errors++; } - const BSONObj cmdRewritten = cmdBuilder.done(); - - auto opObserver = getGlobalServiceContext()->getOpObserver(); - invariant(opObserver); - if (haveWrappingWUOW) { - opObserver->onApplyOps(txn, tempNS, cmdRewritten); - } else { - // When executing applyOps outside of a wrapping WriteUnitOfWOrk, always logOp the - // command regardless of whether the individial ops succeeded and rely on any - // failures to also on secondaries. This isn't perfect, but it's what the command - // has always done and is part of its "correct" behavior. - while (true) { - try { - WriteUnitOfWork wunit(txn); - opObserver->onApplyOps(txn, tempNS, cmdRewritten); - - wunit.commit(); - break; - } catch (const WriteConflictException& wce) { - LOG(2) << "WriteConflictException while logging applyOps command, retrying."; - txn->recoveryUnit()->abandonSnapshot(); - continue; - } + (*numApplied)++; + + if (MONGO_FAIL_POINT(applyOpsPauseBetweenOperations)) { + // While holding a database lock under MMAPv1, we would be implicitly holding the + // flush lock here. This would prevent other threads from acquiring the global + // lock or any database locks. We release all locks temporarily while the fail + // point is enabled to allow other threads to make progress. + boost::optional<Lock::TempRelease> release; + auto storageEngine = opCtx->getServiceContext()->getGlobalStorageEngine(); + if (storageEngine->isMmapV1() && !opCtx->lockState()->isW()) { + release.emplace(opCtx->lockState()); } + MONGO_FAIL_POINT_PAUSE_WHILE_SET(applyOpsPauseBetweenOperations); } } + result->append("applied", *numApplied); + result->append("results", ab.arr()); + if (errors != 0) { return Status(ErrorCodes::UnknownError, "applyOps had one or more errors applying ops"); } @@ -256,94 +285,144 @@ Status _applyOps(OperationContext* txn, return Status::OK(); } -Status preconditionOK(OperationContext* txn, const BSONObj& applyOpCmd, BSONObjBuilder* result) { - dassert(txn->lockState()->isLockHeldForMode( - ResourceId(RESOURCE_GLOBAL, ResourceId::SINGLETON_GLOBAL), MODE_X)); - - if (applyOpCmd["preCondition"].type() == Array) { - BSONObjIterator i(applyOpCmd["preCondition"].Obj()); - while (i.more()) { - BSONObj preCondition = i.next().Obj(); - if (preCondition["ns"].type() != BSONType::String) { - return {ErrorCodes::InvalidNamespace, - str::stream() << "ns in preCondition must be a string, but found type: " - << typeName(preCondition["ns"].type())}; - } - const NamespaceString nss(preCondition["ns"].valueStringData()); - if (!nss.isValid()) { - return {ErrorCodes::InvalidNamespace, "invalid ns: " + nss.ns()}; - } +bool _hasPrecondition(const BSONObj& applyOpCmd) { + return applyOpCmd[kPreconditionFieldName].type() == Array; +} - DBDirectClient db(txn); - BSONObj realres = db.findOne(nss.ns(), preCondition["q"].Obj()); +Status _checkPrecondition(OperationContext* opCtx, + const BSONObj& applyOpCmd, + BSONObjBuilder* result) { + invariant(opCtx->lockState()->isW()); + invariant(_hasPrecondition(applyOpCmd)); + + for (auto elem : applyOpCmd[kPreconditionFieldName].Obj()) { + auto preCondition = elem.Obj(); + if (preCondition["ns"].type() != BSONType::String) { + return {ErrorCodes::InvalidNamespace, + str::stream() << "ns in preCondition must be a string, but found type: " + << typeName(preCondition["ns"].type())}; + } + const NamespaceString nss(preCondition["ns"].valueStringData()); + if (!nss.isValid()) { + return {ErrorCodes::InvalidNamespace, "invalid ns: " + nss.ns()}; + } - // Get collection default collation. - Database* database = dbHolder().get(txn, nss.db()); - if (!database) { - return {ErrorCodes::NamespaceNotFound, - "database in ns does not exist: " + nss.ns()}; - } - Collection* collection = database->getCollection(nss.ns()); - if (!collection) { - return {ErrorCodes::NamespaceNotFound, - "collection in ns does not exist: " + nss.ns()}; - } - const CollatorInterface* collator = collection->getDefaultCollator(); - - // Apply-ops would never have a $where/$text matcher. Using the "DisallowExtensions" - // callback ensures that parsing will throw an error if $where or $text are found. - Matcher matcher( - preCondition["res"].Obj(), ExtensionsCallbackDisallowExtensions(), collator); - if (!matcher.matches(realres)) { - result->append("got", realres); - result->append("whatFailed", preCondition); - return {ErrorCodes::BadValue, "preCondition failed"}; - } + DBDirectClient db(opCtx); + BSONObj realres = db.findOne(nss.ns(), preCondition["q"].Obj()); + + // Get collection default collation. + Database* database = dbHolder().get(opCtx, nss.db()); + if (!database) { + return {ErrorCodes::NamespaceNotFound, "database in ns does not exist: " + nss.ns()}; + } + Collection* collection = database->getCollection(nss.ns()); + if (!collection) { + return {ErrorCodes::NamespaceNotFound, "collection in ns does not exist: " + nss.ns()}; + } + const CollatorInterface* collator = collection->getDefaultCollator(); + + // Apply-ops would never have a $where/$text matcher. Using the "DisallowExtensions" + // callback ensures that parsing will throw an error if $where or $text are found. + Matcher matcher( + preCondition["res"].Obj(), ExtensionsCallbackDisallowExtensions(), collator); + if (!matcher.matches(realres)) { + result->append("got", realres); + result->append("whatFailed", preCondition); + return {ErrorCodes::BadValue, "preCondition failed"}; } } + return Status::OK(); } } // namespace -Status applyOps(OperationContext* txn, +Status applyOps(OperationContext* opCtx, const std::string& dbName, const BSONObj& applyOpCmd, BSONObjBuilder* result) { - ScopedTransaction scopedXact(txn, MODE_X); - Lock::GlobalWrite globalWriteLock(txn->lockState()); + bool allowAtomic = false; + uassertStatusOK( + bsonExtractBooleanFieldWithDefault(applyOpCmd, "allowAtomic", true, &allowAtomic)); + auto areOpsCrudOnly = _areOpsCrudOnly(applyOpCmd); + auto isAtomic = allowAtomic && areOpsCrudOnly; + auto hasPrecondition = _hasPrecondition(applyOpCmd); + + ScopedTransaction scopedXact(opCtx, MODE_X); + boost::optional<Lock::GlobalWrite> globalWriteLock; + boost::optional<Lock::DBLock> dbWriteLock; + + // There's only one case where we are allowed to take the database lock instead of the global + // lock - no preconditions; only CRUD ops; and non-atomic mode. + if (!hasPrecondition && areOpsCrudOnly && !allowAtomic) { + dbWriteLock.emplace(opCtx->lockState(), dbName, MODE_IX); + } else { + globalWriteLock.emplace(opCtx->lockState()); + } - bool userInitiatedWritesAndNotPrimary = txn->writesAreReplicated() && + bool userInitiatedWritesAndNotPrimary = opCtx->writesAreReplicated() && !repl::getGlobalReplicationCoordinator()->canAcceptWritesForDatabase(dbName); if (userInitiatedWritesAndNotPrimary) return Status(ErrorCodes::NotMaster, str::stream() << "Not primary while applying ops to database " << dbName); - Status preconditionStatus = preconditionOK(txn, applyOpCmd, result); - if (!preconditionStatus.isOK()) { - return preconditionStatus; + if (hasPrecondition) { + auto status = _checkPrecondition(opCtx, applyOpCmd, result); + if (!status.isOK()) { + return status; + } } int numApplied = 0; - if (!canBeAtomic(applyOpCmd)) - return _applyOps(txn, dbName, applyOpCmd, result, &numApplied); + if (!isAtomic) + return _applyOps(opCtx, dbName, applyOpCmd, result, &numApplied); // Perform write ops atomically + invariant(globalWriteLock); try { MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN { BSONObjBuilder intermediateResult; - WriteUnitOfWork wunit(txn); + WriteUnitOfWork wunit(opCtx); numApplied = 0; - uassertStatusOK(_applyOps(txn, dbName, applyOpCmd, &intermediateResult, &numApplied)); + { + // Suppress replication for atomic operations until end of applyOps. + repl::UnreplicatedWritesBlock uwb(opCtx); + uassertStatusOK( + _applyOps(opCtx, dbName, applyOpCmd, &intermediateResult, &numApplied)); + } + // Generate oplog entry for all atomic ops collectively. + if (opCtx->writesAreReplicated()) { + // We want this applied atomically on slaves so we rewrite the oplog entry without + // the pre-condition for speed. + + std::string tempNS = str::stream() << dbName << ".$cmd"; + + BSONObjBuilder cmdBuilder; + + for (auto elem : applyOpCmd) { + auto name = elem.fieldNameStringData(); + if (name == kPreconditionFieldName) + continue; + if (name == "bypassDocumentValidation") + continue; + cmdBuilder.append(elem); + } + + const BSONObj cmdRewritten = cmdBuilder.done(); + + auto opObserver = getGlobalServiceContext()->getOpObserver(); + invariant(opObserver); + opObserver->onApplyOps(opCtx, tempNS, cmdRewritten); + } wunit.commit(); result->appendElements(intermediateResult.obj()); } - MONGO_WRITE_CONFLICT_RETRY_LOOP_END(txn, "applyOps", dbName); + MONGO_WRITE_CONFLICT_RETRY_LOOP_END(opCtx, "applyOps", dbName); } catch (const DBException& ex) { if (ex.getCode() == ErrorCodes::NamespaceNotFound) { // Retry in non-atomic mode, since MMAP cannot implicitly create a new database // within an active WriteUnitOfWork. - return _applyOps(txn, dbName, applyOpCmd, result, &numApplied); + return _applyOps(opCtx, dbName, applyOpCmd, result, &numApplied); } BSONArrayBuilder ab; ++numApplied; |
