Files
mongo/src/mongo/util/fail_point_test.cpp
Gregory Noma ee8af39967 SERVER-106696 Accept fail point mode for FailPointEnableBlock (#37740)
GitOrigin-RevId: 06fde6c38870d61f52cdabae4937be44ff685bd3
2025-06-26 15:39:38 +00:00

543 lines
18 KiB
C++

/**
* 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/util/fail_point.h"
#include "mongo/bson/bsonelement.h"
#include "mongo/bson/bsonmisc.h"
#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/db/client.h"
#include "mongo/db/exec/mutable_bson/mutable_bson_test_utils.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/service_context.h"
#include "mongo/platform/atomic_word.h"
#include "mongo/stdx/thread.h"
#include "mongo/stdx/type_traits.h"
#include "mongo/unittest/thread_assertion_monitor.h"
#include "mongo/unittest/unittest.h"
#include "mongo/util/clock_source.h"
#include "mongo/util/clock_source_mock.h"
#include "mongo/util/tick_source.h"
#include "mongo/util/tick_source_mock.h"
#include "mongo/util/time_support.h"
#include <cstddef>
#include <functional>
#include <limits>
#include <memory>
#include <ostream>
#include <stdexcept>
#include <string>
#include <vector>
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest
using mongo::BSONObj;
using mongo::FailPoint;
using mongo::FailPointEnableBlock;
namespace stdx = mongo::stdx;
namespace mongo_test {
namespace {
#if 0 // Uncomment this block to manually test the _valid flag operation
extern FailPoint notYetFailPointTest;
[[maybe_unused]] bool expectAnInvariantViolation = notYetFailPointTest.shouldFail();
MONGO_FAIL_POINT_DEFINE(notYetFailPointTest);
#endif
// Used by tests in this file that need access to a failpoint that is a registered in the
// FailPointRegistry.
MONGO_FAIL_POINT_DEFINE(dummy2);
} // namespace
TEST(FailPoint, InitialState) {
FailPoint failPoint("testFP");
ASSERT_FALSE(failPoint.shouldFail());
}
TEST(FailPoint, AlwaysOn) {
FailPoint failPoint("testFP");
failPoint.setMode(FailPoint::alwaysOn);
ASSERT(failPoint.shouldFail());
if (auto scopedFp = failPoint.scoped(); MONGO_unlikely(scopedFp.isActive())) {
ASSERT(scopedFp.getData().isEmpty());
}
for (size_t x = 0; x < 50; x++) {
ASSERT(failPoint.shouldFail());
}
}
TEST(FailPoint, NTimes) {
FailPoint failPoint("testFP");
failPoint.setMode(FailPoint::nTimes, 4);
ASSERT(failPoint.shouldFail());
ASSERT(failPoint.shouldFail());
ASSERT(failPoint.shouldFail());
ASSERT(failPoint.shouldFail());
for (size_t x = 0; x < 50; x++) {
ASSERT_FALSE(failPoint.shouldFail());
}
}
TEST(FailPoint, BlockOff) {
FailPoint failPoint("testFP");
bool called = false;
failPoint.execute([&](const BSONObj&) { called = true; });
ASSERT_FALSE(called);
}
TEST(FailPoint, BlockAlwaysOn) {
FailPoint failPoint("testFP");
failPoint.setMode(FailPoint::alwaysOn);
bool called = false;
failPoint.execute([&](const BSONObj&) { called = true; });
ASSERT(called);
}
TEST(FailPoint, BlockNTimes) {
FailPoint failPoint("testFP");
failPoint.setMode(FailPoint::nTimes, 1);
size_t counter = 0;
for (size_t x = 0; x < 10; x++) {
failPoint.execute([&](auto&&...) { counter++; });
}
ASSERT_EQUALS(1U, counter);
}
TEST(FailPoint, BlockWithException) {
FailPoint failPoint("testFP");
failPoint.setMode(FailPoint::alwaysOn);
bool threw = false;
try {
failPoint.execute(
[&](const BSONObj&) { throw std::logic_error("BlockWithException threw"); });
} catch (const std::logic_error&) {
threw = true;
}
ASSERT(threw);
// This will get into an infinite loop if reference counter was not
// properly decremented
failPoint.setMode(FailPoint::off);
}
TEST(FailPoint, SetGetParam) {
FailPoint failPoint("testFP");
failPoint.setMode(FailPoint::alwaysOn, 0, BSON("x" << 20));
failPoint.execute([&](const BSONObj& data) { ASSERT_EQUALS(20, data["x"].numberInt()); });
}
TEST(FailPoint, DisableAllFailpoints) {
auto& registry = mongo::globalFailPointRegistry();
FailPoint& fp1 = *registry.find("dummy");
FailPoint& fp2 = *registry.find("dummy2");
int counter1 = 0;
int counter2 = 0;
fp1.execute([&](const BSONObj&) { counter1++; });
fp2.execute([&](const BSONObj&) { counter2++; });
ASSERT_EQ(0, counter1);
ASSERT_EQ(0, counter2);
fp1.setMode(FailPoint::alwaysOn);
fp2.setMode(FailPoint::alwaysOn);
fp1.execute([&](const BSONObj&) { counter1++; });
fp2.execute([&](const BSONObj&) { counter2++; });
ASSERT_EQ(1, counter1);
ASSERT_EQ(1, counter2);
registry.disableAllFailpoints();
fp1.execute([&](const BSONObj&) { counter1++; });
fp2.execute([&](const BSONObj&) { counter2++; });
ASSERT_EQ(1, counter1);
ASSERT_EQ(1, counter2);
// Check that you can still enable and continue using FailPoints after a call to
// disableAllFailpoints()
fp1.setMode(FailPoint::alwaysOn);
fp2.setMode(FailPoint::alwaysOn);
fp1.execute([&](const BSONObj&) { counter1++; });
fp2.execute([&](const BSONObj&) { counter2++; });
ASSERT_EQ(2, counter1);
ASSERT_EQ(2, counter2);
// Reset the state for future tests.
registry.disableAllFailpoints();
}
TEST(FailPoint, Stress) {
mongo::unittest::ThreadAssertionMonitor monitor;
monitor
.spawnController([&] {
mongo::AtomicWord<bool> done{false};
FailPoint fp("testFP");
fp.setMode(FailPoint::alwaysOn, 0, BSON("a" << 44));
auto fpGuard =
mongo::ScopeGuard([&] { fp.setMode(FailPoint::off, 0, BSON("a" << 66)); });
std::vector<stdx::thread> tasks;
mongo::ScopeGuard joinGuard = [&] {
for (auto&& t : tasks)
if (t.joinable())
t.join();
};
auto launchLoop = [&](auto&& f) {
tasks.push_back(monitor.spawn([&, f] {
while (!done.load())
f();
}));
};
launchLoop([&] {
fp.execute([](const BSONObj& data) {
ASSERT_EQ(data["a"].numberInt(), 44) << "blockTask" << data.toString();
});
});
launchLoop([&] {
try {
fp.execute([](const BSONObj& data) {
ASSERT_EQ(data["a"].numberInt(), 44)
<< "blockWithExceptionTask" << data.toString();
throw std::logic_error("blockWithExceptionTask threw");
});
} catch (const std::logic_error&) {
}
});
launchLoop([&] { fp.shouldFail(); });
launchLoop([&] {
if (fp.shouldFail()) {
fp.setMode(FailPoint::off, 0);
} else {
fp.setMode(FailPoint::alwaysOn, 0, BSON("a" << 44));
}
});
mongo::sleepsecs(5);
done.store(true);
})
.join();
}
static void parallelFailPointTestThread(FailPoint* fp,
const int64_t numIterations,
const int32_t seed,
int64_t* outNumActivations) {
fp->setThreadPRNGSeed(seed);
int64_t numActivations = 0;
for (int64_t i = 0; i < numIterations; ++i) {
if (fp->shouldFail()) {
++numActivations;
}
}
*outNumActivations = numActivations;
}
/**
* Encounters a failpoint with the given fpMode and fpVal numEncountersPerThread
* times in each of numThreads parallel threads, and returns the number of total
* times that the failpoint was activiated.
*/
static int64_t runParallelFailPointTest(FailPoint::Mode fpMode,
FailPoint::ValType fpVal,
const int32_t numThreads,
const int32_t numEncountersPerThread) {
ASSERT_GT(numThreads, 0);
ASSERT_GT(numEncountersPerThread, 0);
FailPoint failPoint("testFP");
failPoint.setMode(fpMode, fpVal);
std::vector<stdx::thread*> tasks;
std::vector<int64_t> counts(numThreads, 0);
ASSERT_EQUALS(static_cast<uint32_t>(numThreads), counts.size());
for (int32_t i = 0; i < numThreads; ++i) {
tasks.push_back(new stdx::thread(parallelFailPointTestThread,
&failPoint,
numEncountersPerThread,
i, // hardcoded seed, different for each thread.
&counts[i]));
}
int64_t totalActivations = 0;
for (int32_t i = 0; i < numThreads; ++i) {
tasks[i]->join();
delete tasks[i];
totalActivations += counts[i];
}
return totalActivations;
}
TEST(FailPoint, RandomActivationP0) {
ASSERT_EQUALS(0, runParallelFailPointTest(FailPoint::random, 0, 1, 1000000));
}
TEST(FailPoint, RandomActivationP5) {
ASSERT_APPROX_EQUAL(500000,
runParallelFailPointTest(
FailPoint::random, std::numeric_limits<int32_t>::max() / 2, 10, 100000),
1000);
}
TEST(FailPoint, RandomActivationP01) {
ASSERT_APPROX_EQUAL(
10000,
runParallelFailPointTest(
FailPoint::random, std::numeric_limits<int32_t>::max() / 100, 10, 100000),
500);
}
TEST(FailPoint, RandomActivationP001) {
ASSERT_APPROX_EQUAL(
1000,
runParallelFailPointTest(
FailPoint::random, std::numeric_limits<int32_t>::max() / 1000, 10, 100000),
500);
}
TEST(FailPoint, parseBSONEmptyFails) {
auto swTuple = FailPoint::parseBSON(BSONObj());
ASSERT_FALSE(swTuple.isOK());
}
TEST(FailPoint, parseBSONInvalidModeFails) {
auto swTuple = FailPoint::parseBSON(BSON("missingModeField" << 1));
ASSERT_FALSE(swTuple.isOK());
swTuple = FailPoint::parseBSON(BSON("mode" << 1));
ASSERT_FALSE(swTuple.isOK());
swTuple = FailPoint::parseBSON(BSON("mode" << true));
ASSERT_FALSE(swTuple.isOK());
swTuple = FailPoint::parseBSON(BSON("mode" << "notAMode"));
ASSERT_FALSE(swTuple.isOK());
swTuple = FailPoint::parseBSON(BSON("mode" << BSON("invalidSubField" << 1)));
ASSERT_FALSE(swTuple.isOK());
swTuple = FailPoint::parseBSON(BSON("mode" << BSON("times" << "notAnInt")));
ASSERT_FALSE(swTuple.isOK());
swTuple = FailPoint::parseBSON(BSON("mode" << BSON("times" << -5)));
ASSERT_FALSE(swTuple.isOK());
swTuple = FailPoint::parseBSON(BSON("mode" << BSON("activationProbability" << "notADouble")));
ASSERT_FALSE(swTuple.isOK());
double greaterThan1 = 1.3;
swTuple = FailPoint::parseBSON(BSON("mode" << BSON("activationProbability" << greaterThan1)));
ASSERT_FALSE(swTuple.isOK());
double lessThan1 = -0.3;
swTuple = FailPoint::parseBSON(BSON("mode" << BSON("activationProbability" << lessThan1)));
ASSERT_FALSE(swTuple.isOK());
}
TEST(FailPoint, parseBSONValidModeSucceeds) {
auto swTuple = FailPoint::parseBSON(BSON("mode" << "off"));
ASSERT_TRUE(swTuple.isOK());
swTuple = FailPoint::parseBSON(BSON("mode" << "alwaysOn"));
ASSERT_TRUE(swTuple.isOK());
swTuple = FailPoint::parseBSON(BSON("mode" << BSON("times" << 1)));
ASSERT_TRUE(swTuple.isOK());
swTuple = FailPoint::parseBSON(BSON("mode" << BSON("activationProbability" << 0.2)));
ASSERT_TRUE(swTuple.isOK());
}
TEST(FailPoint, parseBSONInvalidDataFails) {
auto swTuple = FailPoint::parseBSON(BSON("mode" << "alwaysOn"
<< "data"
<< "notABSON"));
ASSERT_FALSE(swTuple.isOK());
}
TEST(FailPoint, parseBSONValidDataSucceeds) {
auto swTuple = FailPoint::parseBSON(BSON("mode" << "alwaysOn"
<< "data" << BSON("a" << 1)));
ASSERT_TRUE(swTuple.isOK());
}
TEST(FailPoint, FailPointEnableBlockBasicTest) {
auto failPoint = mongo::globalFailPointRegistry().find("dummy");
ASSERT_FALSE(failPoint->shouldFail());
{
FailPointEnableBlock dummyFp("dummy");
ASSERT_TRUE(failPoint->shouldFail());
}
ASSERT_FALSE(failPoint->shouldFail());
}
TEST(FailPoint, FailPointEnableBlockByPointer) {
auto failPoint = mongo::globalFailPointRegistry().find("dummy");
ASSERT_FALSE(failPoint->shouldFail());
{
FailPointEnableBlock dummyFp(failPoint);
ASSERT_TRUE(failPoint->shouldFail());
}
ASSERT_FALSE(failPoint->shouldFail());
}
TEST(FailPoint, FailPointEnableBlockWithMode) {
auto failPoint = mongo::globalFailPointRegistry().find("dummy");
ASSERT_FALSE(failPoint->shouldFail());
{
FailPointEnableBlock dummyFp(
"dummy", FailPoint::ModeOptions{.mode = FailPoint::Mode::nTimes, .val = 1});
ASSERT_TRUE(failPoint->shouldFail());
ASSERT_FALSE(failPoint->shouldFail());
}
ASSERT_FALSE(failPoint->shouldFail());
}
TEST(FailPoint, FailPointEnableBlockByPointerWithMode) {
auto failPoint = mongo::globalFailPointRegistry().find("dummy");
ASSERT_FALSE(failPoint->shouldFail());
{
FailPointEnableBlock dummyFp(
failPoint, FailPoint::ModeOptions{.mode = FailPoint::Mode::nTimes, .val = 1});
ASSERT_TRUE(failPoint->shouldFail());
ASSERT_FALSE(failPoint->shouldFail());
}
ASSERT_FALSE(failPoint->shouldFail());
}
TEST(FailPoint, ExecuteIfBasicTest) {
FailPoint failPoint("testFP");
failPoint.setMode(FailPoint::nTimes, 1, BSON("skip" << true));
{
bool hit = false;
failPoint.executeIf([](const BSONObj&) { ASSERT(!"shouldn't get here"); },
[&hit](const BSONObj& obj) {
hit = obj["skip"].trueValue();
return false;
});
ASSERT(hit);
}
{
bool hit = false;
failPoint.executeIf(
[&hit](const BSONObj& data) {
hit = true;
ASSERT(!data.isEmpty());
},
[](const BSONObj&) { return true; });
ASSERT(hit);
}
failPoint.executeIf([](auto&&) { ASSERT(!"shouldn't get here"); }, [](auto&&) { return true; });
}
} // namespace mongo_test
namespace mongo {
/**
* Runs the given function with an operation context that has a deadline and asserts that
* the function is interruptible.
*/
void assertFunctionInterruptible(std::function<void(Interruptible* interruptible)> f) {
const std::shared_ptr<ClockSourceMock> mockClock = std::make_shared<ClockSourceMock>();
const auto service = ServiceContext::make(std::make_unique<SharedClockSourceAdapter>(mockClock),
std::make_unique<SharedClockSourceAdapter>(mockClock),
std::make_unique<TickSourceMock<>>());
const auto client = service->getService()->makeClient("FailPointTest");
auto opCtx = client->makeOperationContext();
opCtx->setDeadlineAfterNowBy(Milliseconds{999}, ErrorCodes::ExceededTimeLimit);
stdx::thread th([&] {
ASSERT_THROWS_CODE(f(opCtx.get()), AssertionException, ErrorCodes::ExceededTimeLimit);
});
mockClock->advance(Milliseconds{1000});
th.join();
}
TEST(FailPoint, PauseWhileSetInterruptibility) {
FailPoint failPoint("testFP");
failPoint.setMode(FailPoint::alwaysOn);
assertFunctionInterruptible(
[&failPoint](Interruptible* interruptible) { failPoint.pauseWhileSet(interruptible); });
failPoint.setMode(FailPoint::off);
}
TEST(FailPoint, PauseWhileSetCancelability) {
FailPoint failPoint("testFP");
failPoint.setMode(FailPoint::alwaysOn);
CancellationSource cs;
CancellationToken ct = cs.token();
cs.cancel();
ASSERT_THROWS_CODE(failPoint.pauseWhileSetAndNotCanceled(Interruptible::notInterruptible(), ct),
DBException,
ErrorCodes::Interrupted);
failPoint.setMode(FailPoint::off);
}
TEST(FailPoint, WaitForFailPointTimeout) {
FailPoint failPoint("testFP");
failPoint.setMode(FailPoint::alwaysOn);
assertFunctionInterruptible([&failPoint](Interruptible* interruptible) {
failPoint.waitForTimesEntered(interruptible, 1);
});
failPoint.setMode(FailPoint::off);
}
} // namespace mongo