summaryrefslogtreecommitdiff
path: root/src/mongo/db/concurrency/locker_impl_test.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/concurrency/locker_impl_test.cpp')
-rw-r--r--src/mongo/db/concurrency/locker_impl_test.cpp1392
1 files changed, 1392 insertions, 0 deletions
diff --git a/src/mongo/db/concurrency/locker_impl_test.cpp b/src/mongo/db/concurrency/locker_impl_test.cpp
new file mode 100644
index 00000000000..6e67f7c31ea
--- /dev/null
+++ b/src/mongo/db/concurrency/locker_impl_test.cpp
@@ -0,0 +1,1392 @@
+/**
+ * Copyright (C) 2018-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * 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
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * 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 Server Side 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 <algorithm>
+#include <string>
+#include <vector>
+
+#include "mongo/config.h"
+#include "mongo/db/concurrency/lock_manager_test_help.h"
+#include "mongo/db/concurrency/locker.h"
+#include "mongo/db/curop.h"
+#include "mongo/db/service_context_test_fixture.h"
+#include "mongo/transport/session.h"
+#include "mongo/transport/transport_layer_mock.h"
+#include "mongo/unittest/death_test.h"
+#include "mongo/unittest/unittest.h"
+#include "mongo/util/fail_point.h"
+#include "mongo/util/timer.h"
+
+#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest
+
+
+namespace mongo {
+
+class LockerImplTest : public ServiceContextTest {};
+
+TEST_F(LockerImplTest, LockNoConflict) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ LockerImpl locker(opCtx->getServiceContext());
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+
+ locker.lock(resId, MODE_X);
+
+ ASSERT(locker.isLockHeldForMode(resId, MODE_X));
+ ASSERT(locker.isLockHeldForMode(resId, MODE_S));
+
+ ASSERT(locker.unlock(resId));
+
+ ASSERT(locker.isLockHeldForMode(resId, MODE_NONE));
+
+ locker.unlockGlobal();
+}
+
+TEST_F(LockerImplTest, ReLockNoConflict) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ LockerImpl locker(opCtx->getServiceContext());
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+
+ locker.lock(resId, MODE_S);
+ locker.lock(resId, MODE_X);
+
+ ASSERT(!locker.unlock(resId));
+ ASSERT(locker.isLockHeldForMode(resId, MODE_X));
+
+ ASSERT(locker.unlock(resId));
+ ASSERT(locker.isLockHeldForMode(resId, MODE_NONE));
+
+ ASSERT(locker.unlockGlobal());
+}
+
+TEST_F(LockerImplTest, ConflictWithTimeout) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ LockerImpl locker1(opCtx->getServiceContext());
+ locker1.lockGlobal(opCtx.get(), MODE_IX);
+ locker1.lock(resId, MODE_X);
+
+ LockerImpl locker2(opCtx->getServiceContext());
+ locker2.lockGlobal(opCtx.get(), MODE_IX);
+
+ ASSERT_THROWS_CODE(locker2.lock(opCtx.get(), resId, MODE_S, Date_t::now()),
+ AssertionException,
+ ErrorCodes::LockTimeout);
+
+ ASSERT(locker2.getLockMode(resId) == MODE_NONE);
+
+ ASSERT(locker1.unlock(resId));
+
+ ASSERT(locker1.unlockGlobal());
+ ASSERT(locker2.unlockGlobal());
+}
+
+TEST_F(LockerImplTest, ConflictUpgradeWithTimeout) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ LockerImpl locker1(opCtx->getServiceContext());
+ locker1.lockGlobal(opCtx.get(), MODE_IS);
+ locker1.lock(resId, MODE_S);
+
+ LockerImpl locker2(opCtx->getServiceContext());
+ locker2.lockGlobal(opCtx.get(), MODE_IS);
+ locker2.lock(resId, MODE_S);
+
+ // Try upgrading locker 1, which should block and timeout
+ ASSERT_THROWS_CODE(locker1.lock(opCtx.get(), resId, MODE_X, Date_t::now() + Milliseconds(1)),
+ AssertionException,
+ ErrorCodes::LockTimeout);
+
+ locker1.unlockGlobal();
+ locker2.unlockGlobal();
+}
+
+TEST_F(LockerImplTest, FailPointInLockFailsGlobalNonIntentLocksIfTheyCannotBeImmediatelyGranted) {
+ transport::TransportLayerMock transportLayer;
+ std::shared_ptr<transport::Session> session = transportLayer.createSession();
+
+ auto newClient = getServiceContext()->makeClient("userClient", session);
+ AlternativeClientRegion acr(newClient);
+ auto newOpCtx = cc().makeOperationContext();
+
+ LockerImpl locker1(newOpCtx->getServiceContext());
+ locker1.lockGlobal(newOpCtx.get(), MODE_IX);
+
+ {
+ FailPointEnableBlock failWaitingNonPartitionedLocks("failNonIntentLocksIfWaitNeeded");
+
+ // MODE_S attempt.
+ LockerImpl locker2(newOpCtx->getServiceContext());
+ ASSERT_THROWS_CODE(
+ locker2.lockGlobal(newOpCtx.get(), MODE_S), DBException, ErrorCodes::LockTimeout);
+
+ // MODE_X attempt.
+ LockerImpl locker3(newOpCtx->getServiceContext());
+ ASSERT_THROWS_CODE(
+ locker3.lockGlobal(newOpCtx.get(), MODE_X), DBException, ErrorCodes::LockTimeout);
+ }
+
+ locker1.unlockGlobal();
+}
+
+TEST_F(LockerImplTest, FailPointInLockFailsNonIntentLocksIfTheyCannotBeImmediatelyGranted) {
+ transport::TransportLayerMock transportLayer;
+ std::shared_ptr<transport::Session> session = transportLayer.createSession();
+
+ auto newClient = getServiceContext()->makeClient("userClient", session);
+ AlternativeClientRegion acr(newClient);
+ auto newOpCtx = cc().makeOperationContext();
+
+ // Granted MODE_X lock, fail incoming MODE_S and MODE_X.
+ const ResourceId resId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ LockerImpl locker1(newOpCtx->getServiceContext());
+ locker1.lockGlobal(newOpCtx.get(), MODE_IX);
+ locker1.lock(newOpCtx.get(), resId, MODE_X);
+
+ {
+ FailPointEnableBlock failWaitingNonPartitionedLocks("failNonIntentLocksIfWaitNeeded");
+
+ // MODE_S attempt.
+ LockerImpl locker2(newOpCtx->getServiceContext());
+ locker2.lockGlobal(newOpCtx.get(), MODE_IS);
+ ASSERT_THROWS_CODE(locker2.lock(newOpCtx.get(), resId, MODE_S, Date_t::max()),
+ DBException,
+ ErrorCodes::LockTimeout);
+
+ // The timed out MODE_S attempt shouldn't be present in the map of lock requests because it
+ // won't ever be granted.
+ ASSERT(locker2.getRequestsForTest().find(resId).finished());
+ locker2.unlockGlobal();
+
+ // MODE_X attempt.
+ LockerImpl locker3(newOpCtx->getServiceContext());
+ locker3.lockGlobal(newOpCtx.get(), MODE_IX);
+ ASSERT_THROWS_CODE(locker3.lock(newOpCtx.get(), resId, MODE_X, Date_t::max()),
+ DBException,
+ ErrorCodes::LockTimeout);
+
+ // The timed out MODE_X attempt shouldn't be present in the map of lock requests because it
+ // won't ever be granted.
+ ASSERT(locker3.getRequestsForTest().find(resId).finished());
+ locker3.unlockGlobal();
+ }
+
+ locker1.unlockGlobal();
+}
+
+TEST_F(LockerImplTest, ReadTransaction) {
+ auto opCtx = makeOperationContext();
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ locker.lockGlobal(opCtx.get(), MODE_IS);
+ locker.unlockGlobal();
+
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.unlockGlobal();
+
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lockGlobal(opCtx.get(), MODE_IS);
+ locker.unlockGlobal();
+ locker.unlockGlobal();
+}
+
+/**
+ * Test that saveLockerImpl works by examining the output.
+ */
+TEST_F(LockerImplTest, saveAndRestoreGlobal) {
+ auto opCtx = makeOperationContext();
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ // No lock requests made, no locks held.
+ ASSERT_FALSE(locker.canSaveLockState());
+
+ // Lock the global lock, but just once.
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+
+ // We've locked the global lock. This should be reflected in the lockInfo.
+ Locker::LockSnapshot lockInfo;
+ locker.saveLockStateAndUnlock(&lockInfo);
+ ASSERT(!locker.isLocked());
+ ASSERT_EQUALS(MODE_IX, lockInfo.globalMode);
+
+ // Restore the lock(s) we had.
+ locker.restoreLockState(opCtx.get(), lockInfo);
+
+ ASSERT(locker.isLocked());
+ ASSERT(locker.unlockGlobal());
+}
+
+/**
+ * Test that saveLockerImpl can save and restore the RSTL.
+ */
+TEST_F(LockerImplTest, saveAndRestoreRSTL) {
+ auto opCtx = makeOperationContext();
+
+ Locker::LockSnapshot lockInfo;
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ const ResourceId resIdDatabase(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB"));
+
+ // Acquire locks.
+ locker.lock(resourceIdReplicationStateTransitionLock, MODE_IX);
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lock(resIdDatabase, MODE_IX);
+
+ // Save the lock state.
+ locker.saveLockStateAndUnlock(&lockInfo);
+ ASSERT(!locker.isLocked());
+ ASSERT_EQUALS(MODE_IX, lockInfo.globalMode);
+
+ // Check locks are unlocked.
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resourceIdReplicationStateTransitionLock));
+ ASSERT(!locker.isLocked());
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdDatabase));
+
+ // Restore the lock(s) we had.
+ locker.restoreLockState(opCtx.get(), lockInfo);
+
+ // Check locks are re-locked.
+ ASSERT(locker.isLocked());
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resourceIdReplicationStateTransitionLock));
+
+ ASSERT(locker.unlockGlobal());
+ ASSERT(locker.unlock(resourceIdReplicationStateTransitionLock));
+}
+
+/**
+ * Test that we don't unlock when we have the global lock more than once.
+ */
+TEST_F(LockerImplTest, saveAndRestoreGlobalAcquiredTwice) {
+ auto opCtx = makeOperationContext();
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ // No lock requests made, no locks held.
+ ASSERT_FALSE(locker.canSaveLockState());
+
+ // Lock the global lock.
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+
+ // This shouldn't actually unlock as we're in a nested scope.
+ ASSERT_FALSE(locker.canSaveLockState());
+
+ ASSERT(locker.isLocked());
+
+ // We must unlockGlobal twice.
+ ASSERT(!locker.unlockGlobal());
+ ASSERT(locker.unlockGlobal());
+}
+
+/**
+ * Tests that restoreLockerImpl works by locking a db and collection and saving + restoring.
+ */
+TEST_F(LockerImplTest, saveAndRestoreDBAndCollection) {
+ auto opCtx = makeOperationContext();
+
+ Locker::LockSnapshot lockInfo;
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ const ResourceId resIdDatabase(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB"));
+ const ResourceId resIdCollection(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ // Lock some stuff.
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lock(resIdDatabase, MODE_IX);
+ locker.lock(resIdCollection, MODE_IX);
+ locker.saveLockStateAndUnlock(&lockInfo);
+
+ // Things shouldn't be locked anymore.
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection));
+
+ // Restore lock state.
+ locker.restoreLockState(opCtx.get(), lockInfo);
+
+ // Make sure things were re-locked.
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdCollection));
+
+ ASSERT(locker.unlockGlobal());
+}
+
+TEST_F(LockerImplTest, releaseWriteUnitOfWork) {
+ auto opCtx = makeOperationContext();
+
+ Locker::LockSnapshot lockInfo;
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ const ResourceId resIdDatabase(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB"));
+ const ResourceId resIdCollection(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ locker.beginWriteUnitOfWork();
+ // Lock some stuff.
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lock(resIdDatabase, MODE_IX);
+ locker.lock(resIdCollection, MODE_IX);
+ // Unlock them so that they will be pending to unlock.
+ ASSERT_FALSE(locker.unlock(resIdCollection));
+ ASSERT_FALSE(locker.unlock(resIdDatabase));
+ ASSERT_FALSE(locker.unlockGlobal());
+
+ locker.releaseWriteUnitOfWorkAndUnlock(&lockInfo);
+
+ // Things shouldn't be locked anymore.
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection));
+ ASSERT_FALSE(locker.isLocked());
+
+ // Destructor should succeed since the locker's state should be empty.
+}
+
+TEST_F(LockerImplTest, restoreWriteUnitOfWork) {
+ auto opCtx = makeOperationContext();
+
+ Locker::LockSnapshot lockInfo;
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ const ResourceId resIdDatabase(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB"));
+ const ResourceId resIdCollection(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ locker.beginWriteUnitOfWork();
+ // Lock some stuff.
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lock(resIdDatabase, MODE_IX);
+ locker.lock(resIdCollection, MODE_IX);
+ // Unlock them so that they will be pending to unlock.
+ ASSERT_FALSE(locker.unlock(resIdCollection));
+ ASSERT_FALSE(locker.unlock(resIdDatabase));
+ ASSERT_FALSE(locker.unlockGlobal());
+
+ locker.releaseWriteUnitOfWorkAndUnlock(&lockInfo);
+
+ // Things shouldn't be locked anymore.
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection));
+ ASSERT_FALSE(locker.isLocked());
+
+ // Restore lock state.
+ locker.restoreWriteUnitOfWorkAndLock(opCtx.get(), lockInfo);
+
+ // Make sure things were re-locked.
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdCollection));
+ ASSERT(locker.isLocked());
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection));
+ ASSERT_FALSE(locker.isLocked());
+}
+
+TEST_F(LockerImplTest, releaseAndRestoreWriteUnitOfWorkWithoutUnlock) {
+ auto opCtx = makeOperationContext();
+
+ Locker::WUOWLockSnapshot lockInfo;
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ const ResourceId resIdDatabase(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB"));
+ const ResourceId resIdCollection(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+ const ResourceId resIdCollection2(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection2"));
+
+ locker.beginWriteUnitOfWork();
+ // Lock some stuff.
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lock(resIdDatabase, MODE_IX);
+ locker.lock(resIdCollection, MODE_X);
+
+ // Recursive global lock.
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->recursiveCount, 2U);
+
+ ASSERT_FALSE(locker.unlockGlobal());
+
+ // Unlock them so that they will be pending to unlock.
+ ASSERT_FALSE(locker.unlock(resIdCollection));
+ ASSERT_FALSE(locker.unlock(resIdDatabase));
+ ASSERT_FALSE(locker.unlockGlobal());
+ ASSERT_EQ(locker.numResourcesToUnlockAtEndUnitOfWorkForTest(), 3UL);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->unlockPending, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->recursiveCount, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->unlockPending, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->recursiveCount, 1U);
+
+ locker.releaseWriteUnitOfWork(&lockInfo);
+ ASSERT_EQ(lockInfo.unlockPendingLocks.size(), 3UL);
+
+ // Things should still be locked.
+ ASSERT_EQUALS(MODE_X, locker.getLockMode(resIdCollection));
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdDatabase));
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->unlockPending, 0U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->recursiveCount, 1U);
+ ASSERT(locker.isLocked());
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->unlockPending, 0U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->recursiveCount, 1U);
+
+ // The locker is no longer participating the two-phase locking.
+ ASSERT_FALSE(locker.inAWriteUnitOfWork());
+ ASSERT_EQ(locker.numResourcesToUnlockAtEndUnitOfWorkForTest(), 0UL);
+
+ // Start a new WUOW with the same locker. Any new locks acquired in the new WUOW
+ // should participate two-phase locking.
+ {
+ locker.beginWriteUnitOfWork();
+
+ // Grab new locks inside the new WUOW.
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lock(resIdDatabase, MODE_IX);
+ locker.lock(resIdCollection2, MODE_IX);
+
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdCollection2));
+ ASSERT(locker.isLocked());
+
+ locker.unlock(resIdCollection2);
+ ASSERT_EQ(locker.numResourcesToUnlockAtEndUnitOfWorkForTest(), 1UL);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->unlockPending, 0U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->recursiveCount, 2U);
+ locker.unlock(resIdDatabase);
+ ASSERT_EQ(locker.numResourcesToUnlockAtEndUnitOfWorkForTest(), 1UL);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->unlockPending, 0U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->recursiveCount, 1U);
+ locker.unlockGlobal();
+ ASSERT_EQ(locker.numResourcesToUnlockAtEndUnitOfWorkForTest(), 1UL);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->unlockPending, 0U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->recursiveCount, 1U);
+ locker.endWriteUnitOfWork();
+ }
+ ASSERT_FALSE(locker.inAWriteUnitOfWork());
+ ASSERT_EQ(locker.numResourcesToUnlockAtEndUnitOfWorkForTest(), 0UL);
+
+ ASSERT_EQUALS(MODE_X, locker.getLockMode(resIdCollection));
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdDatabase));
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->unlockPending, 0U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->recursiveCount, 1U);
+ ASSERT(locker.isLocked());
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->unlockPending, 0U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->recursiveCount, 1U);
+ // The new locks has been released.
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection2));
+
+ // Restore lock state.
+ locker.restoreWriteUnitOfWork(lockInfo);
+
+ ASSERT_TRUE(locker.inAWriteUnitOfWork());
+
+ // Make sure things are still locked.
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_X, locker.getLockMode(resIdCollection));
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->unlockPending, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->recursiveCount, 1U);
+ ASSERT(locker.isLocked());
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->unlockPending, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->recursiveCount, 1U);
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_FALSE(locker.inAWriteUnitOfWork());
+
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection));
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection2));
+ ASSERT_FALSE(locker.isLocked());
+ ASSERT_EQ(locker.numResourcesToUnlockAtEndUnitOfWorkForTest(), 0U);
+ ASSERT(locker.getRequestsForTest().find(resourceIdGlobal).finished());
+}
+
+TEST_F(LockerImplTest, releaseAndRestoreReadOnlyWriteUnitOfWork) {
+ auto opCtx = makeOperationContext();
+
+ Locker::LockSnapshot lockInfo;
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ const ResourceId resIdDatabase(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB"));
+ const ResourceId resIdCollection(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ // Snapshot transactions delay shared locks as well.
+ locker.setSharedLocksShouldTwoPhaseLock(true);
+
+ locker.beginWriteUnitOfWork();
+ // Lock some stuff in IS mode.
+ locker.lockGlobal(opCtx.get(), MODE_IS);
+ locker.lock(resIdDatabase, MODE_IS);
+ locker.lock(resIdCollection, MODE_IS);
+ // Unlock them.
+ ASSERT_FALSE(locker.unlock(resIdCollection));
+ ASSERT_FALSE(locker.unlock(resIdDatabase));
+ ASSERT_FALSE(locker.unlockGlobal());
+ ASSERT_EQ(3u, locker.numResourcesToUnlockAtEndUnitOfWorkForTest());
+
+ // Things shouldn't be locked anymore.
+ locker.releaseWriteUnitOfWorkAndUnlock(&lockInfo);
+
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection));
+ ASSERT_FALSE(locker.isLocked());
+
+ // Restore lock state.
+ locker.restoreWriteUnitOfWorkAndLock(opCtx.get(), lockInfo);
+
+ ASSERT_EQUALS(MODE_IS, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_IS, locker.getLockMode(resIdCollection));
+ ASSERT_TRUE(locker.isLocked());
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection));
+ ASSERT_FALSE(locker.isLocked());
+}
+
+TEST_F(LockerImplTest, releaseAndRestoreEmptyWriteUnitOfWork) {
+ Locker::LockSnapshot lockInfo;
+ auto opCtx = makeOperationContext();
+ LockerImpl locker(opCtx->getServiceContext());
+
+ // Snapshot transactions delay shared locks as well.
+ locker.setSharedLocksShouldTwoPhaseLock(true);
+
+ locker.beginWriteUnitOfWork();
+
+ // Nothing to yield.
+ ASSERT_FALSE(locker.canSaveLockState());
+ ASSERT_FALSE(locker.isLocked());
+
+ locker.endWriteUnitOfWork();
+}
+
+TEST_F(LockerImplTest, releaseAndRestoreWriteUnitOfWorkWithRecursiveLocks) {
+ auto opCtx = makeOperationContext();
+
+ Locker::LockSnapshot lockInfo;
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ const ResourceId resIdDatabase(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB"));
+ const ResourceId resIdCollection(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ locker.beginWriteUnitOfWork();
+ // Lock some stuff.
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lock(resIdDatabase, MODE_IX);
+ locker.lock(resIdCollection, MODE_IX);
+ // Recursively lock them again with a weaker mode.
+ locker.lockGlobal(opCtx.get(), MODE_IS);
+ locker.lock(resIdDatabase, MODE_IS);
+ locker.lock(resIdCollection, MODE_IS);
+
+ // Make sure locks are converted.
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdCollection));
+ ASSERT_TRUE(locker.isWriteLocked());
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->recursiveCount, 2U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->recursiveCount, 2U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdCollection).objAddr()->recursiveCount, 2U);
+
+ // Unlock them so that they will be pending to unlock.
+ ASSERT_FALSE(locker.unlock(resIdCollection));
+ ASSERT_FALSE(locker.unlock(resIdDatabase));
+ ASSERT_FALSE(locker.unlockGlobal());
+ // Make sure locks are still acquired in the correct mode.
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdCollection));
+ ASSERT_TRUE(locker.isWriteLocked());
+ // Make sure unlocking converted locks decrements the locks' recursiveCount instead of
+ // incrementing unlockPending.
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->recursiveCount, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->unlockPending, 0U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->recursiveCount, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->unlockPending, 0U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdCollection).objAddr()->recursiveCount, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdCollection).objAddr()->unlockPending, 0U);
+
+ // Unlock again so unlockPending == recursiveCount.
+ ASSERT_FALSE(locker.unlock(resIdCollection));
+ ASSERT_FALSE(locker.unlock(resIdDatabase));
+ ASSERT_FALSE(locker.unlockGlobal());
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->recursiveCount, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->unlockPending, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->recursiveCount, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->unlockPending, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdCollection).objAddr()->recursiveCount, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdCollection).objAddr()->unlockPending, 1U);
+
+ locker.releaseWriteUnitOfWorkAndUnlock(&lockInfo);
+
+ // Things shouldn't be locked anymore.
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection));
+ ASSERT_FALSE(locker.isLocked());
+
+ // Restore lock state.
+ locker.restoreWriteUnitOfWorkAndLock(opCtx.get(), lockInfo);
+
+ // Make sure things were re-locked in the correct mode.
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_IX, locker.getLockMode(resIdCollection));
+ ASSERT_TRUE(locker.isWriteLocked());
+ // Make sure locks were coalesced after restore and are pending to unlock as before.
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->recursiveCount, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resourceIdGlobal).objAddr()->unlockPending, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->recursiveCount, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdDatabase).objAddr()->unlockPending, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdCollection).objAddr()->recursiveCount, 1U);
+ ASSERT_EQ(locker.getRequestsForTest().find(resIdCollection).objAddr()->unlockPending, 1U);
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdDatabase));
+ ASSERT_EQUALS(MODE_NONE, locker.getLockMode(resIdCollection));
+ ASSERT_FALSE(locker.isLocked());
+}
+
+TEST_F(LockerImplTest, DefaultLocker) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB"));
+
+ LockerImpl locker(opCtx->getServiceContext());
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lock(resId, MODE_X);
+
+ // Make sure only Global and TestDB resources are locked.
+ Locker::LockerInfo info;
+ locker.getLockerInfo(&info, boost::none);
+ ASSERT(!info.waitingResource.isValid());
+ ASSERT_EQUALS(2U, info.locks.size());
+ ASSERT_EQUALS(RESOURCE_GLOBAL, info.locks[0].resourceId.getType());
+ ASSERT_EQUALS(resId, info.locks[1].resourceId);
+
+ ASSERT(locker.unlockGlobal());
+}
+
+TEST_F(LockerImplTest, SharedLocksShouldTwoPhaseLockIsTrue) {
+ // Test that when setSharedLocksShouldTwoPhaseLock is true and we are in a WUOW, unlock on IS
+ // and S locks are postponed until endWriteUnitOfWork() is called. Mode IX and X locks always
+ // participate in two-phased locking, regardless of the setting.
+
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId1(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB1"));
+ const ResourceId resId2(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB2"));
+ const ResourceId resId3(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection3"));
+ const ResourceId resId4(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection4"));
+
+ LockerImpl locker(opCtx->getServiceContext());
+ locker.setSharedLocksShouldTwoPhaseLock(true);
+
+ locker.lockGlobal(opCtx.get(), MODE_IS);
+ ASSERT_EQ(locker.getLockMode(resourceIdGlobal), MODE_IS);
+
+ locker.lock(resourceIdReplicationStateTransitionLock, MODE_IS);
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_IS);
+
+ locker.lock(resId1, MODE_IS);
+ locker.lock(resId2, MODE_IX);
+ locker.lock(resId3, MODE_S);
+ locker.lock(resId4, MODE_X);
+ ASSERT_EQ(locker.getLockMode(resId1), MODE_IS);
+ ASSERT_EQ(locker.getLockMode(resId2), MODE_IX);
+ ASSERT_EQ(locker.getLockMode(resId3), MODE_S);
+ ASSERT_EQ(locker.getLockMode(resId4), MODE_X);
+
+ locker.beginWriteUnitOfWork();
+
+ ASSERT_FALSE(locker.unlock(resId1));
+ ASSERT_FALSE(locker.unlock(resId2));
+ ASSERT_FALSE(locker.unlock(resId3));
+ ASSERT_FALSE(locker.unlock(resId4));
+ ASSERT_EQ(locker.getLockMode(resId1), MODE_IS);
+ ASSERT_EQ(locker.getLockMode(resId2), MODE_IX);
+ ASSERT_EQ(locker.getLockMode(resId3), MODE_S);
+ ASSERT_EQ(locker.getLockMode(resId4), MODE_X);
+
+ ASSERT_FALSE(locker.unlock(resourceIdReplicationStateTransitionLock));
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_IS);
+
+ ASSERT_FALSE(locker.unlockGlobal());
+ ASSERT_EQ(locker.getLockMode(resourceIdGlobal), MODE_IS);
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_EQ(locker.getLockMode(resId1), MODE_NONE);
+ ASSERT_EQ(locker.getLockMode(resId2), MODE_NONE);
+ ASSERT_EQ(locker.getLockMode(resId3), MODE_NONE);
+ ASSERT_EQ(locker.getLockMode(resId4), MODE_NONE);
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_NONE);
+ ASSERT_EQ(locker.getLockMode(resourceIdGlobal), MODE_NONE);
+}
+
+TEST_F(LockerImplTest, ModeIXAndXLockParticipatesInTwoPhaseLocking) {
+ // Unlock on mode IX and X locks during a WUOW should always be postponed until
+ // endWriteUnitOfWork() is called. Mode IS and S locks should unlock immediately.
+
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId1(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB1"));
+ const ResourceId resId2(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB2"));
+ const ResourceId resId3(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection3"));
+ const ResourceId resId4(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection4"));
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ ASSERT_EQ(locker.getLockMode(resourceIdGlobal), MODE_IX);
+
+ locker.lock(resourceIdReplicationStateTransitionLock, MODE_IX);
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_IX);
+
+ locker.lock(resId1, MODE_IS);
+ locker.lock(resId2, MODE_IX);
+ locker.lock(resId3, MODE_S);
+ locker.lock(resId4, MODE_X);
+ ASSERT_EQ(locker.getLockMode(resId1), MODE_IS);
+ ASSERT_EQ(locker.getLockMode(resId2), MODE_IX);
+ ASSERT_EQ(locker.getLockMode(resId3), MODE_S);
+ ASSERT_EQ(locker.getLockMode(resId4), MODE_X);
+
+ locker.beginWriteUnitOfWork();
+
+ ASSERT_TRUE(locker.unlock(resId1));
+ ASSERT_FALSE(locker.unlock(resId2));
+ ASSERT_TRUE(locker.unlock(resId3));
+ ASSERT_FALSE(locker.unlock(resId4));
+ ASSERT_EQ(locker.getLockMode(resId1), MODE_NONE);
+ ASSERT_EQ(locker.getLockMode(resId2), MODE_IX);
+ ASSERT_EQ(locker.getLockMode(resId3), MODE_NONE);
+ ASSERT_EQ(locker.getLockMode(resId4), MODE_X);
+
+ ASSERT_FALSE(locker.unlock(resourceIdReplicationStateTransitionLock));
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_IX);
+
+ ASSERT_FALSE(locker.unlockGlobal());
+ ASSERT_EQ(locker.getLockMode(resourceIdGlobal), MODE_IX);
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_EQ(locker.getLockMode(resId2), MODE_NONE);
+ ASSERT_EQ(locker.getLockMode(resId4), MODE_NONE);
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_NONE);
+ ASSERT_EQ(locker.getLockMode(resourceIdGlobal), MODE_NONE);
+}
+
+TEST_F(LockerImplTest, RSTLUnlocksWithNestedLock) {
+ auto opCtx = makeOperationContext();
+ LockerImpl locker(opCtx->getServiceContext());
+
+ locker.lock(resourceIdReplicationStateTransitionLock, MODE_IX);
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_IX);
+
+ locker.beginWriteUnitOfWork();
+
+ // Do a nested lock acquisition.
+ locker.lock(resourceIdReplicationStateTransitionLock, MODE_IX);
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_IX);
+
+ ASSERT(locker.unlockRSTLforPrepare());
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_NONE);
+
+ ASSERT_FALSE(locker.unlockRSTLforPrepare());
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_NONE);
+
+ ASSERT_FALSE(locker.unlockRSTLforPrepare());
+ ASSERT_FALSE(locker.unlock(resourceIdReplicationStateTransitionLock));
+}
+
+TEST_F(LockerImplTest, RSTLModeIXWithTwoPhaseLockingCanBeUnlockedWhenPrepared) {
+ auto opCtx = makeOperationContext();
+ LockerImpl locker(opCtx->getServiceContext());
+
+ locker.lock(resourceIdReplicationStateTransitionLock, MODE_IX);
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_IX);
+
+ locker.beginWriteUnitOfWork();
+
+ ASSERT_FALSE(locker.unlock(resourceIdReplicationStateTransitionLock));
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_IX);
+
+ ASSERT(locker.unlockRSTLforPrepare());
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_NONE);
+
+ ASSERT_FALSE(locker.unlockRSTLforPrepare());
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_NONE);
+
+ ASSERT_FALSE(locker.unlockRSTLforPrepare());
+ ASSERT_FALSE(locker.unlock(resourceIdReplicationStateTransitionLock));
+}
+
+TEST_F(LockerImplTest, RSTLModeISWithTwoPhaseLockingCanBeUnlockedWhenPrepared) {
+ auto opCtx = makeOperationContext();
+ LockerImpl locker(opCtx->getServiceContext());
+
+ locker.lock(resourceIdReplicationStateTransitionLock, MODE_IS);
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_IS);
+
+ locker.beginWriteUnitOfWork();
+
+ ASSERT(locker.unlockRSTLforPrepare());
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_NONE);
+
+ ASSERT_FALSE(locker.unlockRSTLforPrepare());
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_NONE);
+
+ ASSERT_FALSE(locker.unlockRSTLforPrepare());
+ ASSERT_FALSE(locker.unlock(resourceIdReplicationStateTransitionLock));
+}
+
+TEST_F(LockerImplTest, RSTLTwoPhaseLockingBehaviorModeIS) {
+ auto opCtx = makeOperationContext();
+ LockerImpl locker(opCtx->getServiceContext());
+
+ locker.lock(resourceIdReplicationStateTransitionLock, MODE_IS);
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_IS);
+
+ locker.beginWriteUnitOfWork();
+
+ ASSERT_TRUE(locker.unlock(resourceIdReplicationStateTransitionLock));
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_NONE);
+
+ ASSERT_FALSE(locker.unlockRSTLforPrepare());
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_EQ(locker.getLockMode(resourceIdReplicationStateTransitionLock), MODE_NONE);
+
+ ASSERT_FALSE(locker.unlockRSTLforPrepare());
+ ASSERT_FALSE(locker.unlock(resourceIdReplicationStateTransitionLock));
+}
+
+TEST_F(LockerImplTest, OverrideLockRequestTimeout) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resIdFirstDB(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "FirstDB"));
+ const ResourceId resIdSecondDB(
+ RESOURCE_DATABASE, DatabaseName::createDatabaseName_forTest(boost::none, "SecondDB"));
+
+ LockerImpl locker1(opCtx->getServiceContext());
+ LockerImpl locker2(opCtx->getServiceContext());
+
+ // Set up locker2 to override lock requests' provided timeout if greater than 1000 milliseconds.
+ locker2.setMaxLockTimeout(Milliseconds(1000));
+
+ locker1.lockGlobal(opCtx.get(), MODE_IX);
+ locker2.lockGlobal(opCtx.get(), MODE_IX);
+
+ // locker1 acquires FirstDB under an exclusive lock.
+ locker1.lock(resIdFirstDB, MODE_X);
+ ASSERT_TRUE(locker1.isLockHeldForMode(resIdFirstDB, MODE_X));
+
+ // locker2's attempt to acquire FirstDB with unlimited wait time should timeout after 1000
+ // milliseconds and throw because _maxLockRequestTimeout is set to 1000 milliseconds.
+ ASSERT_THROWS_CODE(locker2.lock(opCtx.get(), resIdFirstDB, MODE_X, Date_t::max()),
+ AssertionException,
+ ErrorCodes::LockTimeout);
+
+ // locker2's attempt to acquire an uncontested lock should still succeed normally.
+ locker2.lock(resIdSecondDB, MODE_X);
+
+ ASSERT_TRUE(locker1.unlock(resIdFirstDB));
+ ASSERT_TRUE(locker1.isLockHeldForMode(resIdFirstDB, MODE_NONE));
+ ASSERT_TRUE(locker2.unlock(resIdSecondDB));
+ ASSERT_TRUE(locker2.isLockHeldForMode(resIdSecondDB, MODE_NONE));
+
+ ASSERT(locker1.unlockGlobal());
+ ASSERT(locker2.unlockGlobal());
+}
+
+TEST_F(LockerImplTest, DoNotWaitForLockAcquisition) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resIdFirstDB(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "FirstDB"));
+ const ResourceId resIdSecondDB(
+ RESOURCE_DATABASE, DatabaseName::createDatabaseName_forTest(boost::none, "SecondDB"));
+
+ LockerImpl locker1(opCtx->getServiceContext());
+ LockerImpl locker2(opCtx->getServiceContext());
+
+ // Set up locker2 to immediately return if a lock is unavailable, regardless of supplied
+ // deadlines in the lock request.
+ locker2.setMaxLockTimeout(Milliseconds(0));
+
+ locker1.lockGlobal(opCtx.get(), MODE_IX);
+ locker2.lockGlobal(opCtx.get(), MODE_IX);
+
+ // locker1 acquires FirstDB under an exclusive lock.
+ locker1.lock(resIdFirstDB, MODE_X);
+ ASSERT_TRUE(locker1.isLockHeldForMode(resIdFirstDB, MODE_X));
+
+ // locker2's attempt to acquire FirstDB with unlimited wait time should fail immediately and
+ // throw because _maxLockRequestTimeout was set to 0.
+ ASSERT_THROWS_CODE(locker2.lock(opCtx.get(), resIdFirstDB, MODE_X, Date_t::max()),
+ AssertionException,
+ ErrorCodes::LockTimeout);
+
+ // locker2's attempt to acquire an uncontested lock should still succeed normally.
+ locker2.lock(resIdSecondDB, MODE_X);
+
+ ASSERT_TRUE(locker1.unlock(resIdFirstDB));
+ ASSERT_TRUE(locker1.isLockHeldForMode(resIdFirstDB, MODE_NONE));
+ ASSERT_TRUE(locker2.unlock(resIdSecondDB));
+ ASSERT_TRUE(locker2.isLockHeldForMode(resIdSecondDB, MODE_NONE));
+
+ ASSERT(locker1.unlockGlobal());
+ ASSERT(locker2.unlockGlobal());
+}
+
+namespace {
+/**
+ * Helper function to determine if 'lockerInfo' contains a lock with ResourceId 'resourceId' and
+ * lock mode 'mode' within 'lockerInfo.locks'.
+ */
+bool lockerInfoContainsLock(const Locker::LockerInfo& lockerInfo,
+ const ResourceId& resourceId,
+ const LockMode& mode) {
+ return (1U ==
+ std::count_if(lockerInfo.locks.begin(),
+ lockerInfo.locks.end(),
+ [&resourceId, &mode](const Locker::OneLock& lock) {
+ return lock.resourceId == resourceId && lock.mode == mode;
+ }));
+}
+} // namespace
+
+TEST_F(LockerImplTest, GetLockerInfoShouldReportHeldLocks) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId dbId(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB"));
+ const ResourceId collectionId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ // Take an exclusive lock on the collection.
+ LockerImpl locker(opCtx->getServiceContext());
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lock(dbId, MODE_IX);
+ locker.lock(collectionId, MODE_X);
+
+ // Assert it shows up in the output of getLockerInfo().
+ Locker::LockerInfo lockerInfo;
+ locker.getLockerInfo(&lockerInfo, boost::none);
+
+ ASSERT(lockerInfoContainsLock(lockerInfo, resourceIdGlobal, MODE_IX));
+ ASSERT(lockerInfoContainsLock(lockerInfo, dbId, MODE_IX));
+ ASSERT(lockerInfoContainsLock(lockerInfo, collectionId, MODE_X));
+ ASSERT_EQ(3U, lockerInfo.locks.size());
+
+ ASSERT(locker.unlock(collectionId));
+ ASSERT(locker.unlock(dbId));
+ ASSERT(locker.unlockGlobal());
+}
+
+TEST_F(LockerImplTest, GetLockerInfoShouldReportPendingLocks) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId dbId(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "TestDB"));
+ const ResourceId collectionId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ // Take an exclusive lock on the collection.
+ LockerImpl successfulLocker(opCtx->getServiceContext());
+ successfulLocker.lockGlobal(opCtx.get(), MODE_IX);
+ successfulLocker.lock(dbId, MODE_IX);
+ successfulLocker.lock(collectionId, MODE_X);
+
+ // Now attempt to get conflicting locks.
+ LockerImpl conflictingLocker(opCtx->getServiceContext());
+ conflictingLocker.lockGlobal(opCtx.get(), MODE_IS);
+ conflictingLocker.lock(dbId, MODE_IS);
+ ASSERT_EQ(LOCK_WAITING,
+ conflictingLocker.lockBeginForTest(nullptr /* opCtx */, collectionId, MODE_IS));
+
+ // Assert the held locks show up in the output of getLockerInfo().
+ Locker::LockerInfo lockerInfo;
+ conflictingLocker.getLockerInfo(&lockerInfo, boost::none);
+ ASSERT(lockerInfoContainsLock(lockerInfo, resourceIdGlobal, MODE_IS));
+ ASSERT(lockerInfoContainsLock(lockerInfo, dbId, MODE_IS));
+ ASSERT(lockerInfoContainsLock(lockerInfo, collectionId, MODE_IS));
+ ASSERT_EQ(3U, lockerInfo.locks.size());
+
+ // Assert it reports that it is waiting for the collection lock.
+ ASSERT_EQ(collectionId, lockerInfo.waitingResource);
+
+ // Make sure it no longer reports waiting once unlocked.
+ ASSERT(successfulLocker.unlock(collectionId));
+ ASSERT(successfulLocker.unlock(dbId));
+ ASSERT(successfulLocker.unlockGlobal());
+
+ conflictingLocker.lockCompleteForTest(
+ nullptr /* opCtx */, collectionId, MODE_IS, Date_t::now());
+
+ conflictingLocker.getLockerInfo(&lockerInfo, boost::none);
+ ASSERT_FALSE(lockerInfo.waitingResource.isValid());
+
+ ASSERT(conflictingLocker.unlock(collectionId));
+ ASSERT(conflictingLocker.unlock(dbId));
+ ASSERT(conflictingLocker.unlockGlobal());
+}
+
+TEST_F(LockerImplTest, GetLockerInfoShouldSubtractBase) {
+ auto opCtx = makeOperationContext();
+ auto locker = opCtx->lockState();
+ const ResourceId dbId(RESOURCE_DATABASE,
+ DatabaseName::createDatabaseName_forTest(boost::none, "SubtractTestDB"));
+
+ auto numAcquisitions = [&](boost::optional<SingleThreadedLockStats> baseStats) {
+ Locker::LockerInfo info;
+ locker->getLockerInfo(&info, baseStats);
+ return info.stats.get(dbId, MODE_IX).numAcquisitions;
+ };
+ auto getBaseStats = [&] {
+ return CurOp::get(opCtx.get())->getLockStatsBase();
+ };
+
+ locker->lockGlobal(opCtx.get(), MODE_IX);
+
+ // Obtain a lock before any other ops have been pushed to the stack.
+ locker->lock(dbId, MODE_IX);
+ locker->unlock(dbId);
+
+ ASSERT_EQUALS(numAcquisitions(getBaseStats()), 1) << "The acquisition should be reported";
+
+ // Push another op to the stack and obtain a lock.
+ CurOp superOp;
+ superOp.push(opCtx.get());
+ locker->lock(dbId, MODE_IX);
+ locker->unlock(dbId);
+
+ ASSERT_EQUALS(numAcquisitions(getBaseStats()), 1)
+ << "Only superOp's acquisition should be reported";
+
+ // Then push another op to the stack and obtain another lock.
+ CurOp subOp;
+ subOp.push(opCtx.get());
+ locker->lock(dbId, MODE_IX);
+ locker->unlock(dbId);
+
+ ASSERT_EQUALS(numAcquisitions(getBaseStats()), 1)
+ << "Only the latest acquisition should be reported";
+
+ ASSERT_EQUALS(numAcquisitions({}), 3)
+ << "All acquisitions should be reported when no base is subtracted out.";
+
+ ASSERT(locker->unlockGlobal());
+}
+
+TEST_F(LockerImplTest, ReaquireLockPendingUnlock) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ LockerImpl locker(opCtx->getServiceContext());
+ locker.lockGlobal(opCtx.get(), MODE_IS);
+
+ locker.lock(resId, MODE_X);
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_X));
+
+ locker.beginWriteUnitOfWork();
+
+ ASSERT_FALSE(locker.unlock(resId));
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_X));
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->unlockPending == 1);
+
+ // Reacquire lock pending unlock.
+ locker.lock(resId, MODE_X);
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 0);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->unlockPending == 0);
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_X));
+
+ locker.unlockGlobal();
+}
+
+TEST_F(LockerImplTest, AcquireLockPendingUnlockWithCoveredMode) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ LockerImpl locker(opCtx->getServiceContext());
+ locker.lockGlobal(opCtx.get(), MODE_IS);
+
+ locker.lock(resId, MODE_X);
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_X));
+
+ locker.beginWriteUnitOfWork();
+
+ ASSERT_FALSE(locker.unlock(resId));
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_X));
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->unlockPending == 1);
+
+ // Attempt to lock the resource with a mode that is covered by the existing mode.
+ locker.lock(resId, MODE_IX);
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 0);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->unlockPending == 0);
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_IX));
+
+ locker.unlockGlobal();
+}
+
+TEST_F(LockerImplTest, ConvertLockPendingUnlock) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ LockerImpl locker(opCtx->getServiceContext());
+ locker.lockGlobal(opCtx.get(), MODE_IS);
+
+ locker.lock(resId, MODE_IX);
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_IX));
+
+ locker.beginWriteUnitOfWork();
+
+ ASSERT_FALSE(locker.unlock(resId));
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_IX));
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->unlockPending == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->recursiveCount == 1);
+
+ // Convert lock pending unlock.
+ locker.lock(resId, MODE_X);
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->unlockPending == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->recursiveCount == 2);
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 0);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->unlockPending == 0);
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_X));
+
+ locker.unlockGlobal();
+}
+
+TEST_F(LockerImplTest, ConvertLockPendingUnlockAndUnlock) {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ LockerImpl locker(opCtx->getServiceContext());
+ locker.lockGlobal(opCtx.get(), MODE_IS);
+
+ locker.lock(resId, MODE_IX);
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_IX));
+
+ locker.beginWriteUnitOfWork();
+
+ ASSERT_FALSE(locker.unlock(resId));
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_IX));
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->unlockPending == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->recursiveCount == 1);
+
+ // Convert lock pending unlock.
+ locker.lock(resId, MODE_X);
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->unlockPending == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->recursiveCount == 2);
+
+ // Unlock the lock conversion.
+ ASSERT_FALSE(locker.unlock(resId));
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 1);
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->unlockPending == 1);
+ // Make sure we still hold X lock and unlock the weaker mode to decrement recursiveCount instead
+ // of incrementing unlockPending.
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_X));
+ ASSERT(locker.getRequestsForTest().find(resId).objAddr()->recursiveCount == 1);
+
+ locker.endWriteUnitOfWork();
+
+ ASSERT(locker.numResourcesToUnlockAtEndUnitOfWorkForTest() == 0);
+ ASSERT(locker.getRequestsForTest().find(resId).finished());
+ ASSERT_TRUE(locker.isLockHeldForMode(resId, MODE_NONE));
+
+ locker.unlockGlobal();
+}
+
+TEST_F(LockerImplTest, SetTicketAcquisitionForLockRAIIType) {
+ auto opCtx = makeOperationContext();
+
+ // By default, ticket acquisition is required.
+ ASSERT_TRUE(opCtx->lockState()->shouldWaitForTicket());
+
+ {
+ ScopedAdmissionPriorityForLock setTicketAquisition(opCtx->lockState(),
+ AdmissionContext::Priority::kImmediate);
+ ASSERT_FALSE(opCtx->lockState()->shouldWaitForTicket());
+ }
+
+ ASSERT_TRUE(opCtx->lockState()->shouldWaitForTicket());
+
+ opCtx->lockState()->setAdmissionPriority(AdmissionContext::Priority::kImmediate);
+ ASSERT_FALSE(opCtx->lockState()->shouldWaitForTicket());
+
+ {
+ ScopedAdmissionPriorityForLock setTicketAquisition(opCtx->lockState(),
+ AdmissionContext::Priority::kImmediate);
+ ASSERT_FALSE(opCtx->lockState()->shouldWaitForTicket());
+ }
+
+ ASSERT_FALSE(opCtx->lockState()->shouldWaitForTicket());
+}
+
+// This test exercises the lock dumping code in ~LockerImpl in case locks are held on destruction.
+DEATH_TEST_F(LockerImplTest,
+ LocksHeldOnDestructionCausesALocksDump,
+ "Operation ending while holding locks.") {
+ auto opCtx = makeOperationContext();
+
+ const ResourceId resId(
+ RESOURCE_COLLECTION,
+ NamespaceString::createNamespaceString_forTest(boost::none, "TestDB.collection"));
+
+ LockerImpl locker(opCtx->getServiceContext());
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lock(resId, MODE_X);
+
+ ASSERT(locker.isLockHeldForMode(resId, MODE_X));
+ ASSERT(locker.isLockHeldForMode(resId, MODE_S));
+
+ // 'locker' destructor should invariant because locks are still held.
+}
+
+DEATH_TEST_F(LockerImplTest, SaveAndRestoreGlobalRecursivelyIsFatal, "7033800") {
+ auto opCtx = makeOperationContext();
+
+ Locker::LockSnapshot lockInfo;
+
+ LockerImpl locker(opCtx->getServiceContext());
+
+ // No lock requests made, no locks held.
+ locker.saveLockStateAndUnlock(&lockInfo);
+ ASSERT_EQUALS(0U, lockInfo.locks.size());
+
+ // Lock the global lock.
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+ locker.lockGlobal(opCtx.get(), MODE_IX);
+
+ // Should invariant
+ locker.saveLockStateAndUnlock(&lockInfo);
+}
+
+} // namespace mongo