diff options
-rw-r--r-- | jstests/core/apply_ops1.js | 41 | ||||
-rw-r--r-- | jstests/core/apply_ops_atomicity.js | 10 | ||||
-rw-r--r-- | src/mongo/db/repl/apply_ops.cpp | 117 |
3 files changed, 115 insertions, 53 deletions
diff --git a/jstests/core/apply_ops1.js b/jstests/core/apply_ops1.js index cb440addf5e..6dc551e766f 100644 --- a/jstests/core/apply_ops1.js +++ b/jstests/core/apply_ops1.js @@ -286,9 +286,44 @@ 'applyOps should fail on unknown operation type "x" with valid "ns" value'); assert.eq(0, t.find().count(), "Non-zero amount of documents in collection to start"); - assert.commandFailed( - db.adminCommand({applyOps: [{"op": "i", "ns": t.getFullName(), "o": {_id: 5, x: 17}}]}), - "Applying an insert operation on a non-existent collection should fail"); + + /** + * Test function for running CRUD operations on non-existent namespaces using various + * combinations of invalid namespaces (collection/database), allowAtomic and alwaysUpsert. + * + * Leave 'expectedErrorCode' undefined if this command is expected to run successfully. + */ + function testCrudOperationOnNonExistentNamespace(optype, o, o2, expectedErrorCode) { + expectedErrorCode = expectedErrorCode || ErrorCodes.OK; + const t2 = db.getSiblingDB('apply_ops1_no_such_db').getCollection('t'); + [t, t2].forEach(coll => { + const op = {op: optype, ns: coll.getFullName(), o: o, o2: o2}; + [false, true].forEach(allowAtomic => { + [false, true].forEach(alwaysUpsert => { + const cmd = { + applyOps: [op], + allowAtomic: allowAtomic, + alwaysUpsert: alwaysUpsert + }; + jsTestLog('Testing applyOps on non-existent namespace: ' + tojson(cmd)); + if (expectedErrorCode === ErrorCodes.OK) { + assert.commandWorked(db.adminCommand(cmd)); + } else { + assert.commandFailedWithCode(db.adminCommand(cmd), expectedErrorCode); + } + }); + }); + }); + } + + // Insert and update operations on non-existent collections/databases should return + // NamespaceNotFound. + testCrudOperationOnNonExistentNamespace('i', {_id: 0}, {}, ErrorCodes.NamespaceNotFound); + testCrudOperationOnNonExistentNamespace('u', {x: 0}, {_id: 0}, ErrorCodes.NamespaceNotFound); + + // Delete operations on non-existent collections/databases should return OK for idempotency + // reasons. + testCrudOperationOnNonExistentNamespace('d', {_id: 0}, {}); assert.commandWorked(db.createCollection(t.getName())); var a = assert.commandWorked( diff --git a/jstests/core/apply_ops_atomicity.js b/jstests/core/apply_ops_atomicity.js index 911ce32a311..e704815a889 100644 --- a/jstests/core/apply_ops_atomicity.js +++ b/jstests/core/apply_ops_atomicity.js @@ -29,10 +29,12 @@ var newDBName = "apply_ops_atomicity"; var newDB = db.getSiblingDB(newDBName); assert.commandWorked(newDB.dropDatabase()); - // Do an update on a non-existent database, since only 'u' ops can implicitly create - // collections. - assert.commandWorked(newDB.runCommand( - {applyOps: [{op: "u", ns: newDBName + ".foo", o: {_id: 5, x: 17}, o2: {_id: 5, x: 16}}]})); + // Updates on a non-existent database no longer implicitly create collections and will fail with + // a NamespaceNotFound error. + assert.commandFailedWithCode(newDB.runCommand({ + applyOps: [{op: "u", ns: newDBName + ".foo", o: {_id: 5, x: 17}, o2: {_id: 5, x: 16}}] + }), + ErrorCodes.NamespaceNotFound); var sawTooManyLocksError = false; diff --git a/src/mongo/db/repl/apply_ops.cpp b/src/mongo/db/repl/apply_ops.cpp index 30eaee93482..aeb493e610c 100644 --- a/src/mongo/db/repl/apply_ops.cpp +++ b/src/mongo/db/repl/apply_ops.cpp @@ -123,8 +123,7 @@ Status _applyOps(OperationContext* opCtx, if (*opType == 'n') continue; - const std::string ns = opObj["ns"].String(); - const NamespaceString nss{ns}; + const NamespaceString nss(opObj["ns"].String()); // Need to check this here, or OldClientContext may fail an invariant. if (*opType != 'c' && !nss.isValid()) @@ -136,63 +135,89 @@ Status _applyOps(OperationContext* opCtx, invariant(opCtx->lockState()->isW()); invariant(*opType != 'c'); - if (!dbHolder().get(opCtx, ns)) { + auto db = dbHolder().get(opCtx, nss.ns()); + if (!db) { throw DBException( ErrorCodes::NamespaceNotFound, "cannot create a database in atomic applyOps mode; will retry without " "atomicity"); } - OldClientContext ctx(opCtx, ns); + // 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. + // Additionally for inserts, we fail early on non-existent collections. + auto collection = db->getCollection(opCtx, nss); + if (!collection && !nss.isSystemDotIndexes() && (*opType == 'i' || *opType == 'u')) { + throw DBException( + ErrorCodes::NamespaceNotFound, + str::stream() + << "cannot apply insert or update operation on a non-existent namespace " + << nss.ns() + << " in atomic applyOps mode: " + << redact(opObj)); + } + + OldClientContext ctx(opCtx, nss.ns()); status = repl::applyOperation_inlock(opCtx, ctx.db(), opObj, alwaysUpsert); if (!status.isOK()) return status; } else { try { - writeConflictRetry(opCtx, "applyOps", ns, [&] { - if (*opType == 'c') { - invariant(opCtx->lockState()->isW()); - status = repl::applyCommand_inlock(opCtx, opObj, true); - } else { - Lock::DBLock dbWriteLock(opCtx, nss.db(), MODE_IX); - Lock::CollectionLock lock(opCtx->lockState(), nss.ns(), MODE_IX); - OldClientContext ctx(opCtx, ns); - 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); - BSONObjBuilder command; - command.append("createIndexes", indexNss.coll()); - { - BSONArrayBuilder indexes(command.subarrayStart("indexes")); - indexes.append(indexSpec); - indexes.doneFast(); + status = writeConflictRetry( + opCtx, "applyOps", nss.ns(), [opCtx, nss, opObj, opType, alwaysUpsert] { + if (*opType == 'c') { + invariant(opCtx->lockState()->isW()); + return repl::applyCommand_inlock(opCtx, opObj, true); + } + + AutoGetCollection autoColl(opCtx, nss, MODE_IX); + if (!autoColl.getCollection() && !nss.isSystemDotIndexes()) { + // For idempotency reasons, return success on delete operations. + if (*opType == 'd') { + return Status::OK(); } - const BSONObj commandObj = command.done(); - - DBDirectClient client(opCtx); - BSONObj infoObj; - client.runCommand(nsToDatabase(ns), commandObj, infoObj); - status = getStatusFromCommandResult(infoObj); - - // Only uassert to stop applyOps only when building indexes, - // but not for CRUD ops. - uassertStatusOK(status); - } else { - status = - repl::applyOperation_inlock(opCtx, ctx.db(), opObj, alwaysUpsert); + throw DBException(ErrorCodes::NamespaceNotFound, + str::stream() + << "cannot apply insert or update operation on a " + "non-existent namespace " + << nss.ns() + << ": " + << mongo::redact(opObj)); + } + + OldClientContext ctx(opCtx, nss.ns()); + + if (!nss.isSystemDotIndexes()) { + return repl::applyOperation_inlock( + opCtx, ctx.db(), opObj, alwaysUpsert); + } + + auto fieldO = opObj["o"]; + BSONObj indexSpec; + NamespaceString indexNss; + std::tie(indexSpec, indexNss) = + repl::prepForApplyOpsIndexInsert(fieldO, opObj, nss); + 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(nss.db().toString(), commandObj, infoObj); + + // Uassert to stop applyOps only when building indexes, but not for CRUD + // ops. + uassertStatusOK(getStatusFromCommandResult(infoObj)); + + return Status::OK(); + }); } catch (const DBException& ex) { ab.append(false); result->append("applied", ++(*numApplied)); |