diff options
author | Daniel Gottlieb <daniel.gottlieb@mongodb.com> | 2021-06-17 12:20:58 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-06-22 16:52:38 +0000 |
commit | 807f5130e28fc2295e6ed6bf382a8aff4b9f6fec (patch) | |
tree | ab47d9c57bf005ededc7284eece641fd7bc25a85 /src/mongo/db | |
parent | da173c4995cd000da3d585c254f681f13edd0b2c (diff) | |
download | mongo-807f5130e28fc2295e6ed6bf382a8aff4b9f6fec.tar.gz |
SERVER-56630: Unittest OpObserverImpl::onUpdate for non-transaction calls.
Diffstat (limited to 'src/mongo/db')
-rw-r--r-- | src/mongo/db/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/dbhelpers.cpp | 15 | ||||
-rw-r--r-- | src/mongo/db/dbhelpers.h | 9 | ||||
-rw-r--r-- | src/mongo/db/op_observer_impl_test.cpp | 280 |
4 files changed, 298 insertions, 7 deletions
diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript index 799e6670b72..35d82f2606c 100644 --- a/src/mongo/db/SConscript +++ b/src/mongo/db/SConscript @@ -2429,6 +2429,7 @@ if wiredtiger: 'range_arithmetic', 'read_write_concern_defaults_mock', 'record_id_helpers', + 'repl/image_collection_entry', 'repl/mock_repl_coord_server_fixture', 'repl/oplog_interface_local', 'repl/repl_coordinator_interface', diff --git a/src/mongo/db/dbhelpers.cpp b/src/mongo/db/dbhelpers.cpp index 0a1a5e44fd2..7090e277550 100644 --- a/src/mongo/db/dbhelpers.cpp +++ b/src/mongo/db/dbhelpers.cpp @@ -71,6 +71,21 @@ bool Helpers::findOne(OperationContext* opCtx, return true; } +BSONObj Helpers::findOneForTesting(OperationContext* opCtx, + const CollectionPtr& collection, + const BSONObj& query, + const bool invariantOnError) { + BSONObj ret; + const bool requiresIndex = true; + bool found = findOne(opCtx, collection, query, ret, requiresIndex); + if (invariantOnError) { + invariant(found); + } + + return ret.getOwned(); +} + + /* fetch a single object from collection ns that matches query set your db SavedContext first */ diff --git a/src/mongo/db/dbhelpers.h b/src/mongo/db/dbhelpers.h index 0d624e5769a..f1caed50dfe 100644 --- a/src/mongo/db/dbhelpers.h +++ b/src/mongo/db/dbhelpers.h @@ -66,6 +66,15 @@ struct Helpers { bool requireIndex = false); /** + * If `invariantOnError` is true, an error (e.g: no document found) will crash the + * process. Otherwise the empty BSONObj will be returned. + */ + static BSONObj findOneForTesting(OperationContext* opCtx, + const CollectionPtr& collection, + const BSONObj& query, + const bool invariantOnError = true); + + /** * Similar to the 'findOne()' overload above, except returns the RecordId of the first matching * document, or a null RecordId if no such document exists. */ diff --git a/src/mongo/db/op_observer_impl_test.cpp b/src/mongo/db/op_observer_impl_test.cpp index a26bdebe285..3a2355251ba 100644 --- a/src/mongo/db/op_observer_impl_test.cpp +++ b/src/mongo/db/op_observer_impl_test.cpp @@ -27,23 +27,30 @@ * it in the license file. */ +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest + #include "mongo/platform/basic.h" #include "mongo/db/catalog/import_collection_oplog_entry_gen.h" #include "mongo/db/client.h" #include "mongo/db/concurrency/locker_noop.h" +#include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/db_raii.h" #include "mongo/db/dbdirectclient.h" +#include "mongo/db/dbhelpers.h" #include "mongo/db/keys_collection_client_sharded.h" #include "mongo/db/keys_collection_manager.h" #include "mongo/db/logical_time_validator.h" #include "mongo/db/op_observer_impl.h" +#include "mongo/db/op_observer_registry.h" #include "mongo/db/read_write_concern_defaults.h" #include "mongo/db/read_write_concern_defaults_cache_lookup_mock.h" +#include "mongo/db/repl/image_collection_entry_gen.h" #include "mongo/db/repl/oplog.h" #include "mongo/db/repl/oplog_entry.h" #include "mongo/db/repl/oplog_interface_local.h" #include "mongo/db/repl/repl_client_info.h" +#include "mongo/db/repl/repl_server_parameters_gen.h" #include "mongo/db/repl/replication_coordinator_mock.h" #include "mongo/db/repl/storage_interface_impl.h" #include "mongo/db/service_context_d_test_fixture.h" @@ -51,6 +58,7 @@ #include "mongo/db/transaction_participant.h" #include "mongo/db/transaction_participant_gen.h" #include "mongo/idl/server_parameter_test_util.h" +#include "mongo/logv2/log.h" #include "mongo/rpc/get_status_from_command_result.h" #include "mongo/unittest/death_test.h" #include "mongo/util/clock_source_mock.h" @@ -85,18 +93,74 @@ public: ReadWriteConcernDefaults::create(getServiceContext(), _lookupMock.getFetchDefaultsFn()); } + void resetOplogAndTransactions(OperationContext* opCtx) const { + ::mongo::writeConflictRetry( + opCtx, "deleteAll", NamespaceString::kRsOplogNamespace.ns(), [&] { + opCtx->recoveryUnit()->setTimestampReadSource( + RecoveryUnit::ReadSource::kNoTimestamp); + opCtx->recoveryUnit()->abandonSnapshot(); + + WriteUnitOfWork wunit(opCtx); + { + AutoGetCollection collRaii( + opCtx, NamespaceString::kRsOplogNamespace, LockMode::MODE_X); + invariant(collRaii.getWritableCollection()->truncate(opCtx).isOK()); + } + { + AutoGetCollection collRaii(opCtx, + NamespaceString::kSessionTransactionsTableNamespace, + LockMode::MODE_X); + if (collRaii) { + invariant(collRaii.getWritableCollection()->truncate(opCtx).isOK()); + } else { + auto db = collRaii.ensureDbExists(); + invariant(db->createCollection( + opCtx, NamespaceString::kSessionTransactionsTableNamespace)); + } + } + { + AutoGetCollection collRaii( + opCtx, NamespaceString::kConfigImagesNamespace, LockMode::MODE_X); + if (collRaii) { + invariant(collRaii.getWritableCollection()->truncate(opCtx).isOK()); + } else { + auto db = collRaii.ensureDbExists(); + invariant( + db->createCollection(opCtx, NamespaceString::kConfigImagesNamespace)); + } + } + wunit.commit(); + }); + } + protected: // Assert that the oplog has the expected number of entries, and return them - std::vector<BSONObj> getNOplogEntries(OperationContext* opCtx, int n) { - std::vector<BSONObj> result(n); + std::vector<BSONObj> getNOplogEntries(OperationContext* opCtx, int numExpected) { + std::vector<BSONObj> allOplogEntries; repl::OplogInterfaceLocal oplogInterface(opCtx); auto oplogIter = oplogInterface.makeIterator(); - for (int i = n - 1; i >= 0; i--) { + while (true) { + StatusWith<std::pair<BSONObj, RecordId>> swEntry = oplogIter->next(); + if (swEntry.getStatus() == ErrorCodes::CollectionIsEmpty) { + break; + } + allOplogEntries.push_back(swEntry.getValue().first); + } + if (allOplogEntries.size() != static_cast<std::size_t>(numExpected)) { + LOGV2(5739903, + "Incorrect number of oplog entries made", + "numExpected"_attr = numExpected, + "numFound"_attr = allOplogEntries.size(), + "entries"_attr = allOplogEntries); + } + ASSERT_EQUALS(allOplogEntries.size(), numExpected); + + std::vector<BSONObj> ret(numExpected); + for (int idx = numExpected - 1; idx >= 0; idx--) { // The oplogIterator returns the entries in reverse order. - auto opEntry = unittest::assertGet(oplogIter->next()); - result[i] = opEntry.first; + ret[idx] = allOplogEntries[numExpected - idx - 1]; } - ASSERT_EQUALS(ErrorCodes::CollectionIsEmpty, oplogIter->next().getStatus()); + // Some unittests reuse the same OperationContext to read the oplog and end up acquiring the // RSTL lock after using the OplogInterfaceLocal. This is a hack to make sure we do not hold // RSTL lock for prepared transactions. @@ -104,7 +168,7 @@ protected: TransactionParticipant::get(opCtx).transactionIsPrepared()) { opCtx->lockState()->unlockRSTLforPrepare(); } - return result; + return ret; } // Assert that oplog only has a single entry and return that oplog entry. @@ -1485,6 +1549,208 @@ TEST_F(OpObserverRetryableFindAndModifyTest, RetryableFindAndModifyDeleteHasNeed "preImage"_sd); } + +enum class RetryableOptions { NotRetryable, WithOplog, WithSideCollection }; +using StoreDocOption = CollectionUpdateArgs::StoreDocOption; +struct TestCase { + StoreDocOption imageType; + bool alwaysRecordPreImages; + RetryableOptions retryableOptions; + + int numOutputOplogs; + + std::string getImageTypeStr() const { + switch (imageType) { + case StoreDocOption::None: + return "None"; + case StoreDocOption::PreImage: + return "PreImage"; + case StoreDocOption::PostImage: + return "PostImage"; + } + MONGO_UNREACHABLE; + } + + std::string getRetryableOptionsStr() const { + switch (retryableOptions) { + case RetryableOptions::NotRetryable: + return "Not retryable"; + case RetryableOptions::WithOplog: + return "Images in oplog"; + case RetryableOptions::WithSideCollection: + return "Images in side collection"; + } + MONGO_UNREACHABLE; + } +}; + +OplogEntry findByTimestamp(const std::vector<BSONObj>& oplogs, Timestamp ts) { + for (auto& oplog : oplogs) { + const auto& entry = assertGet(OplogEntry::parse(oplog)); + if (entry.getTimestamp() == ts) { + return entry; + } + } + + FAIL("Not found."); + // C++/clang isn't smart enough to know FAIL is guaranteed to throw. + MONGO_UNREACHABLE +} + +TEST_F(OpObserverTest, TestFundamentalOnUpdateOutputs) { + // Create a registry that only registers the Impl. It can be challenging to call methods on the + // Impl directly. It falls into cases where `ReservedTimes` is expected to be instantiated. Due + // to strong encapsulation, we use the registry that managers the `ReservedTimes` on our behalf. + OpObserverRegistry opObserver; + opObserver.addObserver(std::make_unique<OpObserverImpl>()); + + auto opCtxRaii = cc().makeOperationContext(); + OperationContext* opCtx = opCtxRaii.get(); + NamespaceString nss("test", "coll"); + CollectionUUID uuid = CollectionUUID::gen(); + + RAIIServerParameterControllerForTest ffRaii("featureFlagRetryableFindAndModify", true); + + const bool kRecordPreImages = true; + const bool kDoNotRecordPreImages = false; + + std::vector<TestCase> cases{ + // Regular updates. + {StoreDocOption::None, kDoNotRecordPreImages, RetryableOptions::NotRetryable, 1}, + {StoreDocOption::None, kRecordPreImages, RetryableOptions::NotRetryable, 2}, + {StoreDocOption::None, kRecordPreImages, RetryableOptions::WithOplog, 2}, + {StoreDocOption::None, kRecordPreImages, RetryableOptions::WithSideCollection, 2}, + // FindAndModify asking for a preImage. + {StoreDocOption::PreImage, kDoNotRecordPreImages, RetryableOptions::NotRetryable, 1}, + {StoreDocOption::PreImage, kDoNotRecordPreImages, RetryableOptions::WithOplog, 2}, + {StoreDocOption::PreImage, kDoNotRecordPreImages, RetryableOptions::WithSideCollection, 1}, + {StoreDocOption::PreImage, kRecordPreImages, RetryableOptions::NotRetryable, 2}, + {StoreDocOption::PreImage, kRecordPreImages, RetryableOptions::WithOplog, 2}, + {StoreDocOption::PreImage, kRecordPreImages, RetryableOptions::WithSideCollection, 2}, + // FindAndModify asking for a postImage. + {StoreDocOption::PostImage, kDoNotRecordPreImages, RetryableOptions::NotRetryable, 1}, + {StoreDocOption::PostImage, kDoNotRecordPreImages, RetryableOptions::WithOplog, 2}, + {StoreDocOption::PostImage, kDoNotRecordPreImages, RetryableOptions::WithSideCollection, 1}, + {StoreDocOption::PostImage, kRecordPreImages, RetryableOptions::NotRetryable, 2}, + {StoreDocOption::PostImage, kRecordPreImages, RetryableOptions::WithOplog, 3}, + {StoreDocOption::PostImage, kRecordPreImages, RetryableOptions::WithSideCollection, 2}}; + + for (std::size_t idx = 0; idx < cases.size(); ++idx) { + const auto& testCase = cases[idx]; + LOGV2(5739902, + "TestCase", + "ImageType"_attr = testCase.getImageTypeStr(), + "AlwaysRecordPreImages"_attr = testCase.alwaysRecordPreImages, + "RetryableOptions"_attr = testCase.getRetryableOptionsStr(), + "ExpectedOplogEntries"_attr = testCase.numOutputOplogs); + + // Phase 1: Clearing any state and setting up fixtures/the update call. + resetOplogAndTransactions(opCtx); + + boost::optional<MongoDOperationContextSession> contextSession; + boost::optional<TransactionParticipant::Participant> txnParticipant; + CollectionUpdateArgs updateArgs; + switch (testCase.retryableOptions) { + case RetryableOptions::NotRetryable: + updateArgs.stmtIds = {kUninitializedStmtId}; + break; + case RetryableOptions::WithOplog: + repl::gStoreFindAndModifyImagesInSideCollection.store(false); + updateArgs.stmtIds = {1}; + break; + case RetryableOptions::WithSideCollection: + repl::gStoreFindAndModifyImagesInSideCollection.store(true); + updateArgs.stmtIds = {1}; + break; + } + if (testCase.retryableOptions != RetryableOptions::NotRetryable) { + opCtx->setLogicalSessionId(makeLogicalSessionIdForTest()); + opCtx->setTxnNumber(TxnNumber(idx)); + contextSession.emplace(opCtx); + txnParticipant.emplace(TransactionParticipant::get(opCtx)); + txnParticipant->beginOrContinue(opCtx, TxnNumber(idx), boost::none, boost::none); + } + + if (testCase.imageType == StoreDocOption::None && !testCase.alwaysRecordPreImages) { + updateArgs.preImageDoc = boost::none; + } else { + updateArgs.preImageDoc = BSON("_id" << 0 << "preImage" << true); + } + + updateArgs.updatedDoc = BSON("_id" << 0 << "postImage" << true); + updateArgs.update = + BSON("$set" << BSON("postImage" << true) << "$unset" << BSON("preImage" << 1)); + updateArgs.criteria = BSON("_id" << 0); + updateArgs.storeDocOption = testCase.imageType; + updateArgs.preImageRecordingEnabledForCollection = testCase.alwaysRecordPreImages; + OplogUpdateEntryArgs update(std::move(updateArgs), nss, uuid); + + // Phase 2: Call the code we're testing. + WriteUnitOfWork wuow(opCtx); + AutoGetCollection locks(opCtx, nss, LockMode::MODE_IX); + opObserver.onUpdate(opCtx, update); + wuow.commit(); + + // Phase 3: Analyze the results: + + // This `getNOplogEntries` also asserts that all oplogs are retrieved. + std::vector<BSONObj> oplogs = getNOplogEntries(opCtx, testCase.numOutputOplogs); + // Entries are returned in ascending timestamp order. + const OplogEntry& actualOp = assertGet(OplogEntry::parse(oplogs.back())); + + const bool checkPreImageInOplog = testCase.alwaysRecordPreImages || + (testCase.imageType == StoreDocOption::PreImage && + testCase.retryableOptions == RetryableOptions::WithOplog); + if (checkPreImageInOplog) { + ASSERT(actualOp.getPreImageOpTime()); + const Timestamp preImageOpTime = actualOp.getPreImageOpTime()->getTimestamp(); + ASSERT_FALSE(preImageOpTime.isNull()); + OplogEntry preImage = findByTimestamp(oplogs, preImageOpTime); + ASSERT_EQUALS(0, update.updateArgs.preImageDoc->woCompare(preImage.getObject())); + } + + const bool checkPostImageInOplog = testCase.imageType == StoreDocOption::PostImage && + testCase.retryableOptions == RetryableOptions::WithOplog; + if (checkPostImageInOplog) { + ASSERT(actualOp.getPostImageOpTime()); + const Timestamp postImageOpTime = actualOp.getPostImageOpTime()->getTimestamp(); + ASSERT_FALSE(postImageOpTime.isNull()); + OplogEntry postImage = findByTimestamp(oplogs, postImageOpTime); + ASSERT_EQUALS(0, update.updateArgs.updatedDoc.woCompare(postImage.getObject())); + } + + bool checkSideCollection = testCase.imageType != StoreDocOption::None && + testCase.retryableOptions == RetryableOptions::WithSideCollection; + if (checkSideCollection && testCase.alwaysRecordPreImages && + testCase.imageType == StoreDocOption::PreImage) { + // When `alwaysRecordPreImages` is enabled for a collection, we always store an image in + // the oplog. To avoid unnecessary writes, we won't also store an image in the side + // collection. + checkSideCollection = false; + } + + if (checkSideCollection) { + AutoGetCollection sideCollection( + opCtx, NamespaceString::kConfigImagesNamespace, LockMode::MODE_IS); + auto imageEntry = repl::ImageEntry::parse( + IDLParserErrorContext("image entry"), + Helpers::findOneForTesting(opCtx, + sideCollection.getCollection(), + BSON("_id" << actualOp.getSessionId()->toBSON()))); + const BSONObj& expectedImage = testCase.imageType == StoreDocOption::PreImage + ? update.updateArgs.preImageDoc.get() + : update.updateArgs.updatedDoc; + ASSERT_EQUALS(0, expectedImage.woCompare(imageEntry.getImage())); + if (testCase.imageType == StoreDocOption::PreImage) { + ASSERT(imageEntry.getImageKind() == repl::RetryImageEnum::kPreImage); + } else { + ASSERT(imageEntry.getImageKind() == repl::RetryImageEnum::kPostImage); + } + } + } +} + + TEST_F(OpObserverMultiEntryTransactionTest, TransactionalInsertTest) { const NamespaceString nss1("testDB", "testColl"); const NamespaceString nss2("testDB2", "testColl2"); |