Skip to content

CXX-3065 guard against use of REQUIRE within APM callbacks #1171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmake/BsoncxxUtil.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ function(bsoncxx_add_library TARGET OUTPUT_NAME LINK_TYPE)
if(BSONCXX_POLY_USE_MNMLSTC AND NOT BSONCXX_POLY_USE_SYSTEM_MNMLSTC)
target_include_directories(
${TARGET}
SYSTEM
PUBLIC
$<BUILD_INTERFACE:${CORE_INCLUDE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/bsoncxx/v_noabi/bsoncxx/third_party/mnmlstc>
Expand Down
3 changes: 3 additions & 0 deletions src/bsoncxx/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ add_executable(test_bson
bson_util_itoa.cpp
bson_validate.cpp
bson_value.cpp
exception_guard.cpp
json.cpp
oid.cpp
optional.test.cpp
Expand Down Expand Up @@ -141,6 +142,8 @@ set_dist_list(src_bsoncxx_test_DIST
bson_validate.cpp
bson_value.cpp
catch.hh
exception_guard.cpp
exception_guard.hh
json.cpp
oid.cpp
optional.test.cpp
Expand Down
225 changes: 225 additions & 0 deletions src/bsoncxx/test/exception_guard.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// Copyright 2009-present MongoDB, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <bsoncxx/test/exception_guard.hh>

//

#include <atomic>
#include <chrono>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>

#include <bsoncxx/test/catch.hh>

namespace {

TEST_CASE("bsoncxx::test::exception_guard", "[test]") {
using EGuard = bsoncxx::test::exception_guard_state;

EGuard eguard;

SECTION("init") {
CHECK(eguard.ptr == nullptr);
CHECK(eguard.file == bsoncxx::stdx::string_view(""));
CHECK(eguard.line == 0u);
CHECK(eguard.func == bsoncxx::stdx::string_view(""));
}

SECTION("reset") {
// clang-format off
BSONCXX_TEST_EXCEPTION_GUARD_RESET(eguard); const auto line = __LINE__;
// clang-format on

CHECK(eguard.ptr == nullptr);
CHECK(eguard.file == bsoncxx::stdx::string_view(__FILE__));
CHECK(eguard.line == line);
CHECK(eguard.func == bsoncxx::stdx::string_view(__func__));
}

SECTION("simple") {
SECTION("no throw") {
BSONCXX_TEST_EXCEPTION_GUARD_BEGIN(eguard);
BSONCXX_TEST_EXCEPTION_GUARD_END(eguard);

CHECK(eguard.ptr == nullptr);
CHECK(eguard.file == bsoncxx::stdx::string_view(""));
CHECK(eguard.line == 0u);
CHECK(eguard.func == bsoncxx::stdx::string_view(""));

BSONCXX_TEST_EXCEPTION_GUARD_CHECK(eguard);
SUCCEED("no exception was thrown by the check");
}

SECTION("throw") {
struct EGuardException {};

EGuard expected;

// clang-format off
BSONCXX_TEST_EXCEPTION_GUARD_BEGIN(eguard);
throw EGuardException();
BSONCXX_TEST_EXCEPTION_GUARD_END(eguard); BSONCXX_TEST_EXCEPTION_GUARD_RESET(expected);
// clang-format on

CHECK(eguard.ptr != nullptr);
CHECK(eguard.file == expected.file);
CHECK(eguard.line == expected.line);
CHECK(eguard.func == expected.func);
CHECK(eguard.ignored.empty());

const auto check_expr = [&] { BSONCXX_TEST_EXCEPTION_GUARD_CHECK(eguard); };
REQUIRE_THROWS_AS(check_expr(), EGuardException);
REQUIRE_THROWS_AS(check_expr(), EGuardException); // Rethrow is OK.

CHECK(eguard.ptr != nullptr);
CHECK(eguard.file == expected.file);
CHECK(eguard.line == expected.line);
CHECK(eguard.func == expected.func);
CHECK(eguard.ignored.empty());
}

SECTION("ignored") {
struct EGuardException : std::runtime_error {
using std::runtime_error::runtime_error;
};

// clang-format off
BSONCXX_TEST_EXCEPTION_GUARD_BEGIN(eguard);
throw EGuardException("one");
BSONCXX_TEST_EXCEPTION_GUARD_END(eguard);
// clang-format on

REQUIRE(eguard.ptr != nullptr);
REQUIRE(eguard.ignored.size() == 0u);

EGuard expected;

// clang-format off
BSONCXX_TEST_EXCEPTION_GUARD_BEGIN(eguard);
throw EGuardException("two");
BSONCXX_TEST_EXCEPTION_GUARD_END(eguard); BSONCXX_TEST_EXCEPTION_GUARD_RESET(expected);
// clang-format on

const auto npos = std::string::npos;

REQUIRE(eguard.ignored.size() == 1u);
{
const auto& log = eguard.ignored[0];
const auto log_view = bsoncxx::stdx::string_view(log);

CAPTURE(log);
CAPTURE(expected.file);
CAPTURE(expected.line);
CAPTURE(expected.func);

CHECK_THAT(log, Catch::Contains("two"));
CHECK(log_view.find(expected.file) != npos);
CHECK(log_view.find(std::to_string(expected.line)) != npos);
CHECK(log_view.find(expected.func) == npos); // Func is not logged.
}

// clang-format off
BSONCXX_TEST_EXCEPTION_GUARD_BEGIN(eguard);
throw Catch::TestFailureException(); // As-if by `REQUIRE(false)`.
BSONCXX_TEST_EXCEPTION_GUARD_END(eguard); BSONCXX_TEST_EXCEPTION_GUARD_RESET(expected);
// clang-format on

REQUIRE(eguard.ignored.size() == 2u);
{
const auto& log = eguard.ignored[1];
const auto log_view = bsoncxx::stdx::string_view(log);

CAPTURE(log);
CAPTURE(expected.file);
CAPTURE(expected.line);
CAPTURE(expected.func);

CHECK_THAT(log, Catch::Contains("Catch::TestFailureException"));
CHECK(log_view.find(expected.file) != npos);
CHECK(log_view.find(std::to_string(expected.line)) != npos);
CHECK(log_view.find(expected.func) == npos); // Func is not logged.
}

// The original exception.
const auto check_expr = [&] { BSONCXX_TEST_EXCEPTION_GUARD_CHECK(eguard); };
REQUIRE_THROWS_WITH(check_expr(), Catch::Contains("one"));
}
}

SECTION("concurrent") {
EGuard expected;

struct EGuardException : std::runtime_error {
int id;
EGuardException(int i) : std::runtime_error(std::to_string(i)), id(i) {}
};

{
std::atomic_int counter;
std::atomic_bool latch;

std::atomic_init(&counter, 0);
std::atomic_init(&latch, false);

auto fn = [&] {
// A simple latch to maximize parallelism.
while (!latch.load()) {
}

BSONCXX_TEST_EXCEPTION_GUARD_BEGIN(eguard);
throw EGuardException(++counter);
// clang-format off
BSONCXX_TEST_EXCEPTION_GUARD_END(eguard); BSONCXX_TEST_EXCEPTION_GUARD_RESET(expected);
// clang-format on
};

std::vector<std::thread> threads;

threads.emplace_back(fn);
threads.emplace_back(fn);
threads.emplace_back(fn);

std::this_thread::sleep_for(std::chrono::milliseconds(100));
latch.store(true);

for (auto& thread : threads) {
thread.join();
}

REQUIRE(counter.load() == 3);
}

REQUIRE(eguard.ptr != nullptr);
CHECK(eguard.file == expected.file);
CHECK(eguard.line == expected.line);
CHECK(eguard.func == expected.func);

auto test = [&] {
try {
BSONCXX_TEST_EXCEPTION_GUARD_CHECK(eguard);
FAIL("should have thrown an EGuardException");
} catch (const EGuardException& e) {
CAPTURE(e.id);
CHECK(e.id > 0);
}
};

REQUIRE_NOTHROW(test());
}
}

} // namespace
104 changes: 104 additions & 0 deletions src/bsoncxx/test/exception_guard.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2009-present MongoDB, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <cstddef>
#include <exception>
#include <mutex>
#include <sstream>
#include <string>
#include <vector>

#include <bsoncxx/stdx/string_view.hpp>
#include <bsoncxx/test/catch.hh>

#include <bsoncxx/config/private/prelude.hh>

namespace bsoncxx {
namespace test {

struct exception_guard_state {
std::mutex m = {};
std::exception_ptr ptr = {};
stdx::string_view file = {};
std::size_t line = {};
stdx::string_view func = {};
std::vector<std::string> ignored; // Cannot use INFO() in guarded regions.
};

#define BSONCXX_TEST_EXCEPTION_GUARD_RESET(e) \
if (1) { \
((void)e); \
std::lock_guard<std::mutex> _eguard_lock{e.m}; \
e.ptr = {}; \
e.file = __FILE__; \
e.line = __LINE__; \
e.func = __func__; \
} else \
((void)0)

// Marks the beginning of a guarded region wherein any exceptions thrown are stored by exception
// guard state. Only the FIRST exception is stored; any others are caught and ignored.
#define BSONCXX_TEST_EXCEPTION_GUARD_BEGIN(e) \
try { \
(void)e; \
((void)0)

// Marks the end of a guarded region.
#define BSONCXX_TEST_EXCEPTION_GUARD_END(e) \
(void)e; \
} \
catch (...) { \
std::lock_guard<std::mutex> _eguard_lock{e.m}; \
if (!e.ptr) { \
e.ptr = std::current_exception(); \
e.file = __FILE__; \
e.line = __LINE__; \
e.func = __func__; \
} else { \
std::ostringstream oss; \
oss << __FILE__ << ":" << __LINE__ << ": exception guard ignored: "; \
try { \
throw; \
} catch (const std::exception& exc) { \
oss << exc.what(); \
} catch (const Catch::TestFailureException&) { \
oss << "Catch::TestFailureException"; \
} catch (...) { \
oss << "unknown exception"; \
} \
e.ignored.push_back(oss.str()); \
} \
} \
((void)0)

// Rethrow the stored exception if present.
#define BSONCXX_TEST_EXCEPTION_GUARD_CHECK(e) \
if (1) { \
(void)e; \
std::lock_guard<std::mutex> _eguard_lock{e.m}; \
for (auto const& log : e.ignored) { \
UNSCOPED_INFO(log); \
} \
if (e.ptr) { \
std::rethrow_exception(e.ptr); \
} \
} else \
((void)0)

} // namespace test
} // namespace bsoncxx

#include <bsoncxx/config/private/postlude.hh>
2 changes: 1 addition & 1 deletion src/bsoncxx/test/string_view.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
#include <bsoncxx/stdx/operators.hpp>
#include <bsoncxx/stdx/string_view.hpp>
#include <bsoncxx/stdx/type_traits.hpp>
#include <third_party/catch/include/catch.hpp>
#include <bsoncxx/test/catch.hh>

#include <bsoncxx/config/prelude.hpp>

Expand Down
2 changes: 1 addition & 1 deletion src/bsoncxx/test/type_traits.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#include <type_traits>

#include <bsoncxx/stdx/type_traits.hpp>
#include <third_party/catch/include/catch.hpp>
#include <bsoncxx/test/catch.hh>

#include <bsoncxx/config/prelude.hpp>

Expand Down
2 changes: 1 addition & 1 deletion src/mongocxx/test/client_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <bsoncxx/stdx/optional.hpp>
#include <bsoncxx/stdx/string_view.hpp>
#include <bsoncxx/string/to_string.hpp>
#include <bsoncxx/test/catch.hh>
#include <bsoncxx/types.hpp>
#include <bsoncxx/types/bson_value/view_or_value.hpp>
#include <mongocxx/client.hpp>
Expand All @@ -36,7 +37,6 @@
#include <mongocxx/exception/operation_exception.hpp>
#include <mongocxx/private/libmongoc.hh>
#include <mongocxx/test/client_helpers.hh>
#include <third_party/catch/include/catch.hpp>

#include <mongocxx/config/private/prelude.hh>

Expand Down
Loading