[CELEBORN-1754][CIP-14] Add exceptions and checking utils to cppClient

### What changes were proposed in this pull request?
This PR adds exceptions and checking utils code to CppClient.
Besides, the ctest framework is added to CppClient for UTs.

### Why are the changes needed?
To provide exception utils and UT frmework to CppClient.

### Does this PR introduce _any_ user-facing change?
No.

### How was this patch tested?
Compilation and UTs.

Closes #2966 from HolyLow/issue/celeborn-1754-add-exceptions-utils-to-cppClient.

Authored-by: HolyLow <jiaming.xie7@gmail.com>
Signed-off-by: mingji <fengmingxiao.fmx@alibaba-inc.com>
This commit is contained in:
HolyLow 2024-12-04 14:05:38 +08:00 committed by mingji
parent cc04d1315e
commit b2b9a0ab4b
9 changed files with 1455 additions and 7 deletions

View File

@ -266,8 +266,10 @@ Meta Velox
./cpp/celeborn/utils/StackTrace.cpp
./cpp/celeborn/utils/CelebornException.h
./cpp/celeborn/utils/CelebornException.cpp
./cpp/celeborn/utils/Exceptions.h
./cpp/celeborn/utils/Exceptions.cpp
./cpp/celeborn/utils/flags.cpp
./cpp/celeborn/utils/tests/ExceptionTest.cpp
------------------------------------------------------------------------------------
This product bundles various third-party components under the CC0 license.

View File

@ -19,6 +19,7 @@ if (NOT DEFINED PACKAGE_VERSION)
endif ()
project("celeborn" VERSION ${PACKAGE_VERSION} LANGUAGES CXX C)
enable_testing()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
@ -140,4 +141,14 @@ include_directories(SYSTEM ${OPENSSL_INCLUDE_DIR})
include_directories(SYSTEM celeborn)
include_directories(.)
option(CELEBORN_BUILD_TESTS "CELEBORN_BUILD_TESTS" ON)
if(CELEBORN_BUILD_TESTS)
### TODO: we use prebuilt gtest prebuilt package here. A better way is
### to use source package, but the setting would be more complicated.
### Maybe we could change the method later.
find_package(GTest CONFIG REQUIRED)
# Include after project() but before add_subdirectory().
include(CTest)
endif()
add_subdirectory(celeborn)

View File

@ -14,7 +14,7 @@ docker run \
-w /celeborn \
-it --rm \
--name celeborn-cpp-dev-container \
holylow/celeborn-cpp-dev:0.1 \
holylow/celeborn-cpp-dev:0.2 \
/bin/bash
```
@ -38,13 +38,14 @@ bash setup-ubuntu.sh
```
Other platforms are not supported yet, and you could use the container above as your dev environment.
## Compile
## Compile and test
Currently, the modules are under development.
You could compile the code within the dev container by
You could compile the code and run the tests within the dev container by
```
cd celeborn/cpp
mkdir -p build && cd build
cmake ..
make
ctest
```

View File

@ -12,7 +12,9 @@
# 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.
add_library(utils ProcessBase.cpp StackTrace.cpp CelebornException.cpp flags.cpp)
add_library(
utils
ProcessBase.cpp StackTrace.cpp CelebornException.cpp Exceptions.cpp flags.cpp)
target_link_libraries(
utils
@ -23,3 +25,7 @@ target_link_libraries(
${GLOG}
${GFLAGS_LIBRARIES}
)
if(CELEBORN_BUILD_TESTS)
add_subdirectory(tests)
endif()

View File

@ -0,0 +1,26 @@
/*
* Based on Exceptions.cpp from Facebook Velox
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* 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 "celeborn/utils/Exceptions.h"
namespace celeborn::detail {
CELEBORN_DEFINE_CHECK_FAIL_TEMPLATES(::celeborn::CelebornRuntimeError);
CELEBORN_DEFINE_CHECK_FAIL_TEMPLATES(::celeborn::CelebornUserError);
} // namespace celeborn::detail

View File

@ -0,0 +1,421 @@
/*
* Based on Exceptions.h from Facebook Velox
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* 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 <memory>
#include <sstream>
#include <fmt/format.h>
#include <fmt/ostream.h>
#include <glog/logging.h>
#include <folly/Conv.h>
#include <folly/Exception.h>
#include <folly/Preprocessor.h>
#include "celeborn/utils/CelebornException.h"
// TODO: maybe remove the file permanently...
// #include "celeborn/utils/FmtStdFormatters.h"
namespace celeborn {
namespace detail {
struct CelebornCheckFailArgs {
const char* file;
size_t line;
const char* function;
const char* expression;
const char* errorSource;
const char* errorCode;
bool isRetriable;
};
struct CompileTimeEmptyString {
CompileTimeEmptyString() = default;
constexpr operator const char*() const {
return "";
}
constexpr operator std::string_view() const {
return {};
}
operator std::string() const {
return {};
}
};
// celebornCheckFail is defined as a separate helper function rather than
// a macro or inline `throw` expression to allow the compiler *not* to
// inline it when it is large. Having an out-of-line error path helps
// otherwise-small functions that call error-checking macros stay
// small and thus stay eligible for inlining.
template <typename Exception, typename StringType>
[[noreturn]] void celebornCheckFail(
const CelebornCheckFailArgs& args,
StringType s) {
static_assert(
!std::is_same_v<StringType, std::string>,
"BUG: we should not pass std::string by value to celebornCheckFail");
if constexpr (!std::is_same_v<Exception, CelebornUserError>) {
LOG(ERROR) << "Line: " << args.file << ":" << args.line
<< ", Function:" << args.function
<< ", Expression: " << args.expression << " " << s
<< ", Source: " << args.errorSource
<< ", ErrorCode: " << args.errorCode;
}
++threadNumCelebornThrow();
throw Exception(
args.file,
args.line,
args.function,
args.expression,
s,
args.errorSource,
args.errorCode,
args.isRetriable);
}
// CelebornCheckFailStringType helps us pass by reference to
// celebornCheckFail exactly when the string type is std::string.
template <typename T>
struct CelebornCheckFailStringType;
template <>
struct CelebornCheckFailStringType<CompileTimeEmptyString> {
using type = CompileTimeEmptyString;
};
template <>
struct CelebornCheckFailStringType<const char*> {
using type = const char*;
};
template <>
struct CelebornCheckFailStringType<std::string> {
using type = const std::string&;
};
// Declare explicit instantiations of celebornCheckFail for the given
// exceptionType. Just like normal function declarations (prototypes),
// this allows the compiler to assume that they are defined elsewhere
// and simply insert a function call for the linker to fix up, rather
// than emitting a definition of these templates into every
// translation unit they are used in.
#define CELEBORN_DECLARE_CHECK_FAIL_TEMPLATES(exception_type) \
namespace detail { \
extern template void \
celebornCheckFail<exception_type, CompileTimeEmptyString>( \
const CelebornCheckFailArgs& args, \
CompileTimeEmptyString); \
extern template void celebornCheckFail<exception_type, const char*>( \
const CelebornCheckFailArgs& args, \
const char*); \
extern template void celebornCheckFail<exception_type, const std::string&>( \
const CelebornCheckFailArgs& args, \
const std::string&); \
} // namespace detail
// Definitions corresponding to CELEBORN_DECLARE_CHECK_FAIL_TEMPLATES. Should
// only be used in Exceptions.cpp.
#define CELEBORN_DEFINE_CHECK_FAIL_TEMPLATES(exception_type) \
template void celebornCheckFail<exception_type, CompileTimeEmptyString>( \
const CelebornCheckFailArgs& args, CompileTimeEmptyString); \
template void celebornCheckFail<exception_type, const char*>( \
const CelebornCheckFailArgs& args, const char*); \
template void celebornCheckFail<exception_type, const std::string&>( \
const CelebornCheckFailArgs& args, const std::string&);
// When there is no message passed, we can statically detect this case
// and avoid passing even a single unnecessary argument pointer,
// minimizing size and thus maximizing eligibility for inlining.
inline CompileTimeEmptyString errorMessage() {
return {};
}
inline const char* errorMessage(const char* s) {
return s;
}
inline std::string errorMessage(const std::string& str) {
return str;
}
template <typename... Args>
std::string errorMessage(fmt::string_view fmt, const Args&... args) {
return fmt::vformat(fmt, fmt::make_format_args(args...));
}
} // namespace detail
#define _CELEBORN_THROW_IMPL( \
exception, exprStr, errorSource, errorCode, isRetriable, ...) \
{ \
/* GCC 9.2.1 doesn't accept this code with constexpr. */ \
static const ::celeborn::detail::CelebornCheckFailArgs \
celebornCheckFailArgs = { \
__FILE__, \
__LINE__, \
__FUNCTION__, \
exprStr, \
errorSource, \
errorCode, \
isRetriable}; \
auto message = ::celeborn::detail::errorMessage(__VA_ARGS__); \
::celeborn::detail::celebornCheckFail< \
exception, \
typename ::celeborn::detail::CelebornCheckFailStringType< \
decltype(message)>::type>(celebornCheckFailArgs, message); \
}
#define _CELEBORN_CHECK_AND_THROW_IMPL( \
expr, exprStr, exception, errorSource, errorCode, isRetriable, ...) \
if (UNLIKELY(!(expr))) { \
_CELEBORN_THROW_IMPL( \
exception, exprStr, errorSource, errorCode, isRetriable, __VA_ARGS__); \
}
#define _CELEBORN_THROW(exception, ...) \
_CELEBORN_THROW_IMPL(exception, "", ##__VA_ARGS__)
CELEBORN_DECLARE_CHECK_FAIL_TEMPLATES(::celeborn::CelebornRuntimeError);
#define _CELEBORN_CHECK_IMPL(expr, exprStr, ...) \
_CELEBORN_CHECK_AND_THROW_IMPL( \
expr, \
exprStr, \
::celeborn::CelebornRuntimeError, \
::celeborn::error_source::kErrorSourceRuntime.c_str(), \
::celeborn::error_code::kInvalidState.c_str(), \
/* isRetriable */ false, \
##__VA_ARGS__)
// If the caller passes a custom message (4 *or more* arguments), we
// have to construct a format string from ours ("({} vs. {})") plus
// theirs by adding a space and shuffling arguments. If they don't (exactly 3
// arguments), we can just pass our own format string and arguments straight
// through.
#define _CELEBORN_CHECK_OP_WITH_USER_FMT_HELPER( \
implmacro, expr1, expr2, op, user_fmt, ...) \
implmacro( \
(expr1)op(expr2), \
#expr1 " " #op " " #expr2, \
"({} vs. {}) " user_fmt, \
expr1, \
expr2, \
##__VA_ARGS__)
#define _CELEBORN_CHECK_OP_HELPER(implmacro, expr1, expr2, op, ...) \
if constexpr (FOLLY_PP_DETAIL_NARGS(__VA_ARGS__) > 0) { \
_CELEBORN_CHECK_OP_WITH_USER_FMT_HELPER( \
implmacro, expr1, expr2, op, __VA_ARGS__); \
} else { \
implmacro( \
(expr1)op(expr2), \
#expr1 " " #op " " #expr2, \
"({} vs. {})", \
expr1, \
expr2); \
}
#define _CELEBORN_CHECK_OP(expr1, expr2, op, ...) \
_CELEBORN_CHECK_OP_HELPER( \
_CELEBORN_CHECK_IMPL, expr1, expr2, op, ##__VA_ARGS__)
#define _CELEBORN_USER_CHECK_IMPL(expr, exprStr, ...) \
_CELEBORN_CHECK_AND_THROW_IMPL( \
expr, \
exprStr, \
::celeborn::CelebornUserError, \
::celeborn::error_source::kErrorSourceUser.c_str(), \
::celeborn::error_code::kInvalidArgument.c_str(), \
/* isRetriable */ false, \
##__VA_ARGS__)
#define _CELEBORN_USER_CHECK_OP(expr1, expr2, op, ...) \
_CELEBORN_CHECK_OP_HELPER( \
_CELEBORN_USER_CHECK_IMPL, expr1, expr2, op, ##__VA_ARGS__)
// For all below macros, an additional message can be passed using a
// format string and arguments, as with `fmt::format`.
#define CELEBORN_CHECK(expr, ...) \
_CELEBORN_CHECK_IMPL(expr, #expr, ##__VA_ARGS__)
#define CELEBORN_CHECK_GT(e1, e2, ...) \
_CELEBORN_CHECK_OP(e1, e2, >, ##__VA_ARGS__)
#define CELEBORN_CHECK_GE(e1, e2, ...) \
_CELEBORN_CHECK_OP(e1, e2, >=, ##__VA_ARGS__)
#define CELEBORN_CHECK_LT(e1, e2, ...) \
_CELEBORN_CHECK_OP(e1, e2, <, ##__VA_ARGS__)
#define CELEBORN_CHECK_LE(e1, e2, ...) \
_CELEBORN_CHECK_OP(e1, e2, <=, ##__VA_ARGS__)
#define CELEBORN_CHECK_EQ(e1, e2, ...) \
_CELEBORN_CHECK_OP(e1, e2, ==, ##__VA_ARGS__)
#define CELEBORN_CHECK_NE(e1, e2, ...) \
_CELEBORN_CHECK_OP(e1, e2, !=, ##__VA_ARGS__)
#define CELEBORN_CHECK_NULL(e, ...) CELEBORN_CHECK(e == nullptr, ##__VA_ARGS__)
#define CELEBORN_CHECK_NOT_NULL(e, ...) \
CELEBORN_CHECK(e != nullptr, ##__VA_ARGS__)
#define CELEBORN_CHECK_OK(expr) \
do { \
::celeborn::Status _s = (expr); \
_CELEBORN_CHECK_IMPL(_s.ok(), #expr, _s.toString()); \
} while (false)
#define CELEBORN_UNSUPPORTED(...) \
_CELEBORN_THROW( \
::celeborn::CelebornUserError, \
::celeborn::error_source::kErrorSourceUser.c_str(), \
::celeborn::error_code::kUnsupported.c_str(), \
/* isRetriable */ false, \
##__VA_ARGS__)
#define CELEBORN_ARITHMETIC_ERROR(...) \
_CELEBORN_THROW( \
::celeborn::CelebornUserError, \
::celeborn::error_source::kErrorSourceUser.c_str(), \
::celeborn::error_code::kArithmeticError.c_str(), \
/* isRetriable */ false, \
##__VA_ARGS__)
#define CELEBORN_SCHEMA_MISMATCH_ERROR(...) \
_CELEBORN_THROW( \
::celeborn::CelebornUserError, \
::celeborn::error_source::kErrorSourceUser.c_str(), \
::celeborn::error_code::kSchemaMismatch.c_str(), \
/* isRetriable */ false, \
##__VA_ARGS__)
#define CELEBORN_FILE_NOT_FOUND_ERROR(...) \
_CELEBORN_THROW( \
::celeborn::CelebornRuntimeError, \
::celeborn::error_source::kErrorSourceRuntime.c_str(), \
::celeborn::error_code::kFileNotFound.c_str(), \
/* isRetriable */ false, \
##__VA_ARGS__)
#define CELEBORN_UNREACHABLE(...) \
_CELEBORN_THROW( \
::celeborn::CelebornRuntimeError, \
::celeborn::error_source::kErrorSourceRuntime.c_str(), \
::celeborn::error_code::kUnreachableCode.c_str(), \
/* isRetriable */ false, \
##__VA_ARGS__)
#ifndef NDEBUG
#define CELEBORN_DCHECK(expr, ...) CELEBORN_CHECK(expr, ##__VA_ARGS__)
#define CELEBORN_DCHECK_GT(e1, e2, ...) CELEBORN_CHECK_GT(e1, e2, ##__VA_ARGS__)
#define CELEBORN_DCHECK_GE(e1, e2, ...) CELEBORN_CHECK_GE(e1, e2, ##__VA_ARGS__)
#define CELEBORN_DCHECK_LT(e1, e2, ...) CELEBORN_CHECK_LT(e1, e2, ##__VA_ARGS__)
#define CELEBORN_DCHECK_LE(e1, e2, ...) CELEBORN_CHECK_LE(e1, e2, ##__VA_ARGS__)
#define CELEBORN_DCHECK_EQ(e1, e2, ...) CELEBORN_CHECK_EQ(e1, e2, ##__VA_ARGS__)
#define CELEBORN_DCHECK_NE(e1, e2, ...) CELEBORN_CHECK_NE(e1, e2, ##__VA_ARGS__)
#define CELEBORN_DCHECK_NULL(e, ...) CELEBORN_CHECK_NULL(e, ##__VA_ARGS__)
#define CELEBORN_DCHECK_NOT_NULL(e, ...) \
CELEBORN_CHECK_NOT_NULL(e, ##__VA_ARGS__)
#else
#define CELEBORN_DCHECK(expr, ...) CELEBORN_CHECK(true)
#define CELEBORN_DCHECK_GT(e1, e2, ...) CELEBORN_CHECK(true)
#define CELEBORN_DCHECK_GE(e1, e2, ...) CELEBORN_CHECK(true)
#define CELEBORN_DCHECK_LT(e1, e2, ...) CELEBORN_CHECK(true)
#define CELEBORN_DCHECK_LE(e1, e2, ...) CELEBORN_CHECK(true)
#define CELEBORN_DCHECK_EQ(e1, e2, ...) CELEBORN_CHECK(true)
#define CELEBORN_DCHECK_NE(e1, e2, ...) CELEBORN_CHECK(true)
#define CELEBORN_DCHECK_NULL(e, ...) CELEBORN_CHECK(true)
#define CELEBORN_DCHECK_NOT_NULL(e, ...) CELEBORN_CHECK(true)
#endif
#define CELEBORN_FAIL(...) \
_CELEBORN_THROW( \
::celeborn::CelebornRuntimeError, \
::celeborn::error_source::kErrorSourceRuntime.c_str(), \
::celeborn::error_code::kInvalidState.c_str(), \
/* isRetriable */ false, \
##__VA_ARGS__)
CELEBORN_DECLARE_CHECK_FAIL_TEMPLATES(::celeborn::CelebornUserError);
// For all below macros, an additional message can be passed using a
// format string and arguments, as with `fmt::format`.
#define CELEBORN_USER_CHECK(expr, ...) \
_CELEBORN_USER_CHECK_IMPL(expr, #expr, ##__VA_ARGS__)
#define CELEBORN_USER_CHECK_GT(e1, e2, ...) \
_CELEBORN_USER_CHECK_OP(e1, e2, >, ##__VA_ARGS__)
#define CELEBORN_USER_CHECK_GE(e1, e2, ...) \
_CELEBORN_USER_CHECK_OP(e1, e2, >=, ##__VA_ARGS__)
#define CELEBORN_USER_CHECK_LT(e1, e2, ...) \
_CELEBORN_USER_CHECK_OP(e1, e2, <, ##__VA_ARGS__)
#define CELEBORN_USER_CHECK_LE(e1, e2, ...) \
_CELEBORN_USER_CHECK_OP(e1, e2, <=, ##__VA_ARGS__)
#define CELEBORN_USER_CHECK_EQ(e1, e2, ...) \
_CELEBORN_USER_CHECK_OP(e1, e2, ==, ##__VA_ARGS__)
#define CELEBORN_USER_CHECK_NE(e1, e2, ...) \
_CELEBORN_USER_CHECK_OP(e1, e2, !=, ##__VA_ARGS__)
#define CELEBORN_USER_CHECK_NULL(e, ...) \
CELEBORN_USER_CHECK(e == nullptr, ##__VA_ARGS__)
#define CELEBORN_USER_CHECK_NOT_NULL(e, ...) \
CELEBORN_USER_CHECK(e != nullptr, ##__VA_ARGS__)
#ifndef NDEBUG
#define CELEBORN_USER_DCHECK(expr, ...) CELEBORN_USER_CHECK(expr, ##__VA_ARGS__)
#define CELEBORN_USER_DCHECK_GT(e1, e2, ...) \
CELEBORN_USER_CHECK_GT(e1, e2, ##__VA_ARGS__)
#define CELEBORN_USER_DCHECK_GE(e1, e2, ...) \
CELEBORN_USER_CHECK_GE(e1, e2, ##__VA_ARGS__)
#define CELEBORN_USER_DCHECK_LT(e1, e2, ...) \
CELEBORN_USER_CHECK_LT(e1, e2, ##__VA_ARGS__)
#define CELEBORN_USER_DCHECK_LE(e1, e2, ...) \
CELEBORN_USER_CHECK_LE(e1, e2, ##__VA_ARGS__)
#define CELEBORN_USER_DCHECK_EQ(e1, e2, ...) \
CELEBORN_USER_CHECK_EQ(e1, e2, ##__VA_ARGS__)
#define CELEBORN_USER_DCHECK_NE(e1, e2, ...) \
CELEBORN_USER_CHECK_NE(e1, e2, ##__VA_ARGS__)
#define CELEBORN_USER_DCHECK_NOT_NULL(e, ...) \
CELEBORN_USER_CHECK_NOT_NULL(e, ##__VA_ARGS__)
#define CELEBORN_USER_DCHECK_NULL(e, ...) \
CELEBORN_USER_CHECK_NULL(e, ##__VA_ARGS__)
#else
#define CELEBORN_USER_DCHECK(expr, ...) CELEBORN_USER_CHECK(true)
#define CELEBORN_USER_DCHECK_GT(e1, e2, ...) CELEBORN_USER_CHECK(true)
#define CELEBORN_USER_DCHECK_GE(e1, e2, ...) CELEBORN_USER_CHECK(true)
#define CELEBORN_USER_DCHECK_LT(e1, e2, ...) CELEBORN_USER_CHECK(true)
#define CELEBORN_USER_DCHECK_LE(e1, e2, ...) CELEBORN_USER_CHECK(true)
#define CELEBORN_USER_DCHECK_EQ(e1, e2, ...) CELEBORN_USER_CHECK(true)
#define CELEBORN_USER_DCHECK_NE(e1, e2, ...) CELEBORN_USER_CHECK(true)
#define CELEBORN_USER_DCHECK_NULL(e, ...) CELEBORN_USER_CHECK(true)
#define CELEBORN_USER_DCHECK_NOT_NULL(e, ...) CELEBORN_USER_CHECK(true)
#endif
#define CELEBORN_USER_FAIL(...) \
_CELEBORN_THROW( \
::celeborn::CelebornUserError, \
::celeborn::error_source::kErrorSourceUser.c_str(), \
::celeborn::error_code::kInvalidArgument.c_str(), \
/* isRetriable */ false, \
##__VA_ARGS__)
#define CELEBORN_NYI(...) \
_CELEBORN_THROW( \
::celeborn::CelebornRuntimeError, \
::celeborn::error_source::kErrorSourceRuntime.c_str(), \
::celeborn::error_code::kNotImplemented.c_str(), \
/* isRetriable */ false, \
##__VA_ARGS__)
} // namespace celeborn

View File

@ -0,0 +1,32 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
add_executable(celeborn_utils_test ExceptionTest.cpp)
add_test(NAME celeborn_utils_test COMMAND celeborn_utils_test)
target_link_libraries(
celeborn_utils_test
PRIVATE
utils
${WANGLE}
${FIZZ}
${LIBSODIUM_LIBRARY}
${FOLLY_WITH_DEPENDENCIES}
${GLOG}
${GFLAGS_LIBRARIES}
GTest::gtest
GTest::gmock
GTest::gtest_main)

View File

@ -0,0 +1,932 @@
/*
* Based on ExceptionTest.cpp from Facebook Velox
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* 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 <fmt/format.h>
#include <folly/Random.h>
#include <gtest/gtest.h>
#include "celeborn/utils/Exceptions.h"
using namespace celeborn;
struct Counter {
mutable int counter = 0;
};
std::ostream& operator<<(std::ostream& os, const Counter& c) {
os << c.counter;
++c.counter;
return os;
}
template <>
struct fmt::formatter<Counter> {
constexpr auto parse(format_parse_context& ctx) {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Counter& c, FormatContext& ctx) const {
auto x = c.counter++;
return format_to(ctx.out(), "{}", x);
}
};
template <typename T>
void verifyException(
std::function<void()> f,
std::function<void(const T&)> exceptionVerifier) {
try {
f();
FAIL() << "Expected exception of type " << typeid(T).name()
<< ", but no exception was thrown.";
} catch (const T& e) {
exceptionVerifier(e);
} catch (...) {
FAIL() << "Expected exception of type " << typeid(T).name()
<< ", but instead got an exception of a different type.";
}
}
void verifyCelebornException(
std::function<void()> f,
const std::string& messagePrefix) {
verifyException<CelebornException>(f, [&messagePrefix](const auto& e) {
EXPECT_TRUE(folly::StringPiece{e.what()}.startsWith(messagePrefix))
<< "\nException message prefix mismatch.\n\nExpected prefix: "
<< messagePrefix << "\n\nActual message: " << e.what();
});
}
void testExceptionTraceCollectionControl(bool userException, bool enabled) {
// Disable rate control in the test.
FLAGS_celeborn_exception_user_stacktrace_rate_limit_ms = 0;
FLAGS_celeborn_exception_system_stacktrace_rate_limit_ms = 0;
if (userException) {
FLAGS_celeborn_exception_user_stacktrace_enabled = enabled ? true : false;
FLAGS_celeborn_exception_system_stacktrace_enabled =
folly::Random::oneIn(2);
} else {
FLAGS_celeborn_exception_system_stacktrace_enabled = enabled ? true : false;
FLAGS_celeborn_exception_user_stacktrace_enabled = folly::Random::oneIn(2);
}
try {
if (userException) {
throw CelebornUserError(
"file_name",
1,
"function_name()",
"operator()",
"test message",
"",
error_code::kArithmeticError,
false);
} else {
throw CelebornRuntimeError(
"file_name",
1,
"function_name()",
"operator()",
"test message",
"",
error_code::kArithmeticError,
false);
}
} catch (CelebornException& e) {
SCOPED_TRACE(fmt::format(
"enabled: {}, user flag: {}, sys flag: {}",
enabled,
FLAGS_celeborn_exception_user_stacktrace_enabled,
FLAGS_celeborn_exception_system_stacktrace_enabled));
ASSERT_EQ(
userException, e.exceptionType() == CelebornException::Type::kUser);
ASSERT_EQ(enabled, e.stackTrace() != nullptr);
}
}
void testExceptionTraceCollectionRateControl(
bool userException,
bool hasRateLimit) {
// Disable rate control in the test.
// Enable trace rate control in the test.
FLAGS_celeborn_exception_user_stacktrace_enabled = true;
FLAGS_celeborn_exception_system_stacktrace_enabled = true;
// Set rate control interval to a large value to avoid time related test
// flakiness.
const int kRateLimitIntervalMs = 4000;
if (hasRateLimit) {
// Wait a bit to ensure that the last stack trace collection time has
// passed sufficient long.
/* sleep override */
std::this_thread::sleep_for(
std::chrono::milliseconds(kRateLimitIntervalMs)); // NOLINT
}
if (userException) {
FLAGS_celeborn_exception_user_stacktrace_rate_limit_ms =
hasRateLimit ? kRateLimitIntervalMs : 0;
FLAGS_celeborn_exception_system_stacktrace_rate_limit_ms =
folly::Random::rand32();
} else {
// Set rate control to a large interval to avoid time related test
// flakiness.
FLAGS_celeborn_exception_system_stacktrace_rate_limit_ms =
hasRateLimit ? kRateLimitIntervalMs : 0;
FLAGS_celeborn_exception_user_stacktrace_rate_limit_ms =
folly::Random::rand32();
}
for (int iter = 0; iter < 3; ++iter) {
try {
if (userException) {
throw CelebornUserError(
"file_name",
1,
"function_name()",
"operator()",
"test message",
"",
error_code::kArithmeticError,
false);
} else {
throw CelebornRuntimeError(
"file_name",
1,
"function_name()",
"operator()",
"test message",
"",
error_code::kArithmeticError,
false);
}
} catch (CelebornException& e) {
SCOPED_TRACE(fmt::format(
"userException: {}, hasRateLimit: {}, user limit: {}ms, sys limit: {}ms",
userException,
hasRateLimit,
FLAGS_celeborn_exception_user_stacktrace_rate_limit_ms,
FLAGS_celeborn_exception_system_stacktrace_rate_limit_ms));
ASSERT_EQ(
userException, e.exceptionType() == CelebornException::Type::kUser);
ASSERT_EQ(!hasRateLimit || ((iter % 2) == 0), e.stackTrace() != nullptr);
// NOTE: with rate limit control, we want to verify if we can collect
// stack trace after waiting for a while.
if (hasRateLimit && (iter % 2 != 0)) {
/* sleep override */
std::this_thread::sleep_for(
std::chrono::milliseconds(kRateLimitIntervalMs)); // NOLINT
}
}
}
}
// Ensures that expressions on the stream are not evaluated unless the condition
// is met.
TEST(ExceptionTest, lazyStreamEvaluation) {
Counter c;
EXPECT_EQ(0, c.counter);
CELEBORN_CHECK(true, "{}", c);
EXPECT_EQ(0, c.counter);
EXPECT_THROW(
([&]() { CELEBORN_CHECK(false, "{}", c); })(), CelebornRuntimeError);
EXPECT_EQ(1, c.counter);
CELEBORN_CHECK(true, "{}", c);
EXPECT_EQ(1, c.counter);
EXPECT_THROW(
([&]() { CELEBORN_USER_CHECK(false, "{}", c); })(), CelebornUserError);
EXPECT_EQ(2, c.counter);
EXPECT_THROW(
([&]() { CELEBORN_CHECK(false, "{}", c); })(), CelebornRuntimeError);
EXPECT_EQ(3, c.counter);
// Simple types.
size_t i = 0;
CELEBORN_CHECK(true, "{}", i++);
EXPECT_EQ(0, i);
CELEBORN_CHECK(true, "{}", ++i);
EXPECT_EQ(0, i);
EXPECT_THROW(
([&]() { CELEBORN_CHECK(false, "{}", i++); })(), CelebornRuntimeError);
EXPECT_EQ(1, i);
EXPECT_THROW(
([&]() { CELEBORN_CHECK(false, "{}", ++i); })(), CelebornRuntimeError);
EXPECT_EQ(2, i);
}
TEST(ExceptionTest, messageCheck) {
verifyCelebornException(
[]() { CELEBORN_CHECK(4 > 5, "Test message 1"); },
"Exception: CelebornRuntimeError\nError Source: RUNTIME\n"
"Error Code: INVALID_STATE\nReason: Test message 1\n"
"Retriable: False\nExpression: 4 > 5\nFunction: operator()\nFile: ");
}
TEST(ExceptionTest, messageUnreachable) {
verifyCelebornException(
[]() { CELEBORN_UNREACHABLE("Test message 3"); },
"Exception: CelebornRuntimeError\nError Source: RUNTIME\n"
"Error Code: UNREACHABLE_CODE\nReason: Test message 3\n"
"Retriable: False\nFunction: operator()\nFile: ");
}
#define RUN_TEST(test) \
TEST_##test( \
CELEBORN_CHECK_##test, \
"RUNTIME", \
"INVALID_STATE", \
"CelebornRuntimeError"); \
TEST_##test( \
CELEBORN_USER_CHECK_##test, \
"USER", \
"INVALID_ARGUMENT", \
"CelebornUserError");
#define TEST_GT(macro, system, code, prefix) \
verifyCelebornException( \
[]() { macro(4, 5); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (4 vs. 5)" \
"\nRetriable: False" \
"\nExpression: 4 > 5" \
"\nFunction: operator()" \
"\nFile: "); \
\
verifyCelebornException( \
[]() { macro(3, 3); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (3 vs. 3)" \
"\nRetriable: False" \
"\nExpression: 3 > 3" \
"\nFunction: operator()" \
"\nFile: "); \
\
verifyCelebornException( \
[]() { macro(-1, 1, "Message 1"); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (-1 vs. 1) Message 1" \
"\nRetriable: False" \
"\nExpression: -1 > 1" \
"\nFunction: operator()" \
"\nFile: "); \
\
macro(3, 2); \
macro(1, -1, "Message 2");
TEST(ExceptionTest, greaterThan) {
RUN_TEST(GT);
}
#define TEST_GE(macro, system, code, prefix) \
verifyCelebornException( \
[]() { macro(4, 5); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (4 vs. 5)" \
"\nRetriable: False" \
"\nExpression: 4 >= 5" \
"\nFunction: operator()" \
"\nFile: "); \
\
verifyCelebornException( \
[]() { macro(-1, 1, "Message 1"); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (-1 vs. 1) Message 1" \
"\nRetriable: False" \
"\nExpression: -1 >= 1" \
"\nFunction: operator()" \
"\nFile: "); \
\
macro(3, 2); \
macro(3, 3); \
macro(1, -1, "Message 2");
TEST(ExceptionTest, greaterEqual) {
RUN_TEST(GE);
}
#define TEST_LT(macro, system, code, prefix) \
verifyCelebornException( \
[]() { macro(5, 4); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (5 vs. 4)" \
"\nRetriable: False" \
"\nExpression: 5 < 4" \
"\nFunction: operator()" \
"\nFile: "); \
\
verifyCelebornException( \
[]() { macro(2, 2); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (2 vs. 2)" \
"\nRetriable: False" \
"\nExpression: 2 < 2" \
"\nFunction: operator()" \
"\nFile: "); \
\
verifyCelebornException( \
[]() { macro(1, -1, "Message 1"); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (1 vs. -1) Message 1" \
"\nRetriable: False" \
"\nExpression: 1 < -1" \
"\nFunction: operator()" \
"\nFile: "); \
\
macro(2, 3); \
macro(-1, 1, "Message 2");
TEST(ExceptionTest, lessThan) {
RUN_TEST(LT);
}
#define TEST_LE(macro, system, code, prefix) \
verifyCelebornException( \
[]() { macro(6, 2); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (6 vs. 2)" \
"\nRetriable: False" \
"\nExpression: 6 <= 2" \
"\nFunction: operator()" \
"\nFile: "); \
\
verifyCelebornException( \
[]() { macro(3, -3, "Message 1"); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (3 vs. -3) Message 1" \
"\nRetriable: False" \
"\nExpression: 3 <= -3" \
"\nFunction: operator()" \
"\nFile: "); \
\
macro(5, 54); \
macro(1, 1); \
macro(-3, 3, "Message 2");
TEST(ExceptionTest, lessEqual) {
RUN_TEST(LE);
}
#define TEST_EQ(macro, system, code, prefix) \
{ \
verifyCelebornException( \
[]() { macro(1, 2); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (1 vs. 2)" \
"\nRetriable: False" \
"\nExpression: 1 == 2" \
"\nFunction: operator()" \
"\nFile: "); \
\
verifyCelebornException( \
[]() { macro(2, 1, "Message 1"); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (2 vs. 1) Message 1" \
"\nRetriable: False" \
"\nExpression: 2 == 1" \
"\nFunction: operator()" \
"\nFile: "); \
\
auto t = true; \
auto f = false; \
macro(521, 521); \
macro(1.1, 1.1); \
macro(true, t, "Message 2"); \
macro(f, false, "Message 3"); \
}
TEST(ExceptionTest, equal) {
RUN_TEST(EQ);
}
#define TEST_NE(macro, system, code, prefix) \
{ \
verifyCelebornException( \
[]() { macro(1, 1); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (1 vs. 1)" \
"\nRetriable: False" \
"\nExpression: 1 != 1" \
"\nFunction: operator()" \
"\nFile: "); \
\
verifyCelebornException( \
[]() { macro(2.2, 2.2, "Message 1"); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: (2.2 vs. 2.2) Message 1" \
"\nRetriable: False" \
"\nExpression: 2.2 != 2.2" \
"\nFunction: operator()" \
"\nFile: "); \
\
auto t = true; \
auto f = false; \
macro(521, 522); \
macro(1.2, 1.1); \
macro(true, f, "Message 2"); \
macro(t, false, "Message 3"); \
}
TEST(ExceptionTest, notEqual) {
RUN_TEST(NE);
}
#define TEST_NOT_NULL(macro, system, code, prefix) \
{ \
verifyCelebornException( \
[]() { macro(nullptr); }, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nRetriable: False" \
"\nExpression: nullptr != nullptr" \
"\nFunction: operator()" \
"\nFile: "); \
verifyCelebornException( \
[]() { \
std::shared_ptr<int> a; \
macro(a, "Message 1"); \
}, \
"Exception: " prefix "\nError Source: " system "\nError Code: " code \
"\nReason: Message 1" \
"\nRetriable: False" \
"\nExpression: a != nullptr" \
"\nFunction: operator()" \
"\nFile: "); \
auto b = std::make_shared<int>(5); \
macro(b); \
}
TEST(ExceptionTest, notNull) {
RUN_TEST(NOT_NULL);
}
TEST(ExceptionTest, expressionString) {
size_t i = 1;
size_t j = 100;
constexpr auto msgTemplate =
"Exception: CelebornRuntimeError"
"\nError Source: RUNTIME"
"\nError Code: INVALID_STATE"
"\nReason: ({1})"
"\nRetriable: False"
"\nExpression: {0}"
"\nFunction: operator()"
"\nFile: ";
verifyCelebornException(
[&]() { CELEBORN_CHECK_EQ(i, j); },
fmt::format(msgTemplate, "i == j", "1 vs. 100"));
verifyCelebornException(
[&]() { CELEBORN_CHECK_NE(i, 1); },
fmt::format(msgTemplate, "i != 1", "1 vs. 1"));
verifyCelebornException(
[&]() { CELEBORN_CHECK_LT(i + j, j); },
fmt::format(msgTemplate, "i + j < j", "101 vs. 100"));
verifyCelebornException(
[&]() { CELEBORN_CHECK_GE(i + j * 2, 1000); },
fmt::format(msgTemplate, "i + j * 2 >= 1000", "201 vs. 1000"));
}
TEST(ExceptionTest, notImplemented) {
verifyCelebornException(
[]() { CELEBORN_NYI(); },
"Exception: CelebornRuntimeError\nError Source: RUNTIME\n"
"Error Code: NOT_IMPLEMENTED\n"
"Retriable: False\nFunction: operator()\nFile: ");
verifyCelebornException(
[]() { CELEBORN_NYI("Message 1"); },
"Exception: CelebornRuntimeError\nError Source: RUNTIME\n"
"Error Code: NOT_IMPLEMENTED\nReason: Message 1\nRetriable: False\n"
"Function: operator()\nFile: ");
}
TEST(ExceptionTest, errorCode) {
std::string msgTemplate =
"Exception: {}"
"\nError Source: {}"
"\nError Code: {}"
"\nRetriable: {}"
"\nExpression: {}"
"\nFunction: {}"
"\nFile: ";
verifyCelebornException(
[&]() { CELEBORN_FAIL(); },
fmt::format(
"Exception: {}"
"\nError Source: {}"
"\nError Code: {}"
"\nRetriable: {}"
"\nFunction: {}"
"\nFile: ",
"CelebornRuntimeError",
"RUNTIME",
"INVALID_STATE",
"False",
"operator()"));
verifyCelebornException(
[&]() { CELEBORN_USER_FAIL(); },
fmt::format(
"Exception: {}"
"\nError Source: {}"
"\nError Code: {}"
"\nRetriable: {}"
"\nFunction: {}"
"\nFile: ",
"CelebornUserError",
"USER",
"INVALID_ARGUMENT",
"False",
"operator()"));
}
TEST(ExceptionTest, context) {
// No context.
verifyCelebornException(
[&]() { CELEBORN_CHECK_EQ(1, 3); },
"Exception: CelebornRuntimeError"
"\nError Source: RUNTIME"
"\nError Code: INVALID_STATE"
"\nReason: (1 vs. 3)"
"\nRetriable: False"
"\nExpression: 1 == 3"
"\nFunction: operator()"
"\nFile: ");
// With context.
int callCount = 0;
struct MessageFunctionArg {
std::string message;
int* callCount;
};
auto messageFunction = [](celeborn::CelebornException::Type exceptionType,
void* untypedArg) {
auto arg = static_cast<MessageFunctionArg*>(untypedArg);
++(*arg->callCount);
switch (exceptionType) {
case celeborn::CelebornException::Type::kUser:
return fmt::format("User error: {}", arg->message);
case celeborn::CelebornException::Type::kSystem:
return fmt::format("System error: {}", arg->message);
default:
return fmt::format("Unexpected error type: {}", arg->message);
}
};
{
// Create multi-layer contexts with top level marked as essential.
MessageFunctionArg topLevelTroubleshootingAid{
"Top-level troubleshooting aid.", &callCount};
celeborn::ExceptionContextSetter additionalContext(
{.messageFunc = messageFunction, .arg = &topLevelTroubleshootingAid});
MessageFunctionArg midLevelTroubleshootingAid{
"Mid-level troubleshooting aid.", &callCount};
celeborn::ExceptionContextSetter midLevelContext(
{messageFunction, &midLevelTroubleshootingAid});
MessageFunctionArg innerLevelTroubleshootingAid{
"Inner-level troubleshooting aid.", &callCount};
celeborn::ExceptionContextSetter innerLevelContext(
{messageFunction, &innerLevelTroubleshootingAid});
verifyCelebornException(
[&]() { CELEBORN_CHECK_EQ(1, 3); },
"Exception: CelebornRuntimeError"
"\nError Source: RUNTIME"
"\nError Code: INVALID_STATE"
"\nReason: (1 vs. 3)"
"\nRetriable: False"
"\nExpression: 1 == 3"
"\nContext: System error: Inner-level troubleshooting aid."
"\nTop-Level Context: System error: Top-level troubleshooting aid."
"\nFunction: operator()"
"\nFile: ");
EXPECT_EQ(2, callCount);
verifyCelebornException(
[&]() { CELEBORN_USER_CHECK_EQ(1, 3); },
"Exception: CelebornUserError"
"\nError Source: USER"
"\nError Code: INVALID_ARGUMENT"
"\nReason: (1 vs. 3)"
"\nRetriable: False"
"\nExpression: 1 == 3"
"\nContext: User error: Inner-level troubleshooting aid."
"\nTop-Level Context: User error: Top-level troubleshooting aid."
"\nFunction: operator()"
"\nFile: ");
EXPECT_EQ(4, callCount);
}
{
callCount = 0;
// Create multi-layer contexts with none marked as essential.
MessageFunctionArg topLevelTroubleshootingAid{
"Top-level troubleshooting aid.", &callCount};
celeborn::ExceptionContextSetter additionalContext(
{.messageFunc = messageFunction, .arg = &topLevelTroubleshootingAid});
MessageFunctionArg midLevelTroubleshootingAid{
"Mid-level troubleshooting aid.", &callCount};
celeborn::ExceptionContextSetter midLevelContext(
{.messageFunc = messageFunction, .arg = &midLevelTroubleshootingAid});
MessageFunctionArg innerLevelTroubleshootingAid{
"Inner-level troubleshooting aid.", &callCount};
celeborn::ExceptionContextSetter innerLevelContext(
{messageFunction, &innerLevelTroubleshootingAid});
verifyCelebornException(
[&]() { CELEBORN_CHECK_EQ(1, 3); },
"Exception: CelebornRuntimeError"
"\nError Source: RUNTIME"
"\nError Code: INVALID_STATE"
"\nReason: (1 vs. 3)"
"\nRetriable: False"
"\nExpression: 1 == 3"
"\nContext: System error: Inner-level troubleshooting aid."
"\nTop-Level Context: System error: Top-level troubleshooting aid."
"\nFunction: operator()"
"\nFile: ");
EXPECT_EQ(2, callCount);
verifyCelebornException(
[&]() { CELEBORN_USER_CHECK_EQ(1, 3); },
"Exception: CelebornUserError"
"\nError Source: USER"
"\nError Code: INVALID_ARGUMENT"
"\nReason: (1 vs. 3)"
"\nRetriable: False"
"\nExpression: 1 == 3"
"\nContext: User error: Inner-level troubleshooting aid."
"\nTop-Level Context: User error: Top-level troubleshooting aid."
"\nFunction: operator()"
"\nFile: ");
EXPECT_EQ(4, callCount);
}
// Different context.
{
callCount = 0;
// Create a single layer of context. Context and top-level context are
// expected to be the same.
MessageFunctionArg debuggingInfo{"Debugging info.", &callCount};
celeborn::ExceptionContextSetter context({messageFunction, &debuggingInfo});
verifyCelebornException(
[&]() { CELEBORN_CHECK_EQ(1, 3); },
"Exception: CelebornRuntimeError"
"\nError Source: RUNTIME"
"\nError Code: INVALID_STATE"
"\nReason: (1 vs. 3)"
"\nRetriable: False"
"\nExpression: 1 == 3"
"\nContext: System error: Debugging info."
"\nTop-Level Context: Same as context."
"\nFunction: operator()"
"\nFile: ");
EXPECT_EQ(1, callCount);
verifyCelebornException(
[&]() { CELEBORN_USER_CHECK_EQ(1, 3); },
"Exception: CelebornUserError"
"\nError Source: USER"
"\nError Code: INVALID_ARGUMENT"
"\nReason: (1 vs. 3)"
"\nRetriable: False"
"\nExpression: 1 == 3"
"\nContext: User error: Debugging info."
"\nTop-Level Context: Same as context."
"\nFunction: operator()"
"\nFile: ");
EXPECT_EQ(2, callCount);
}
callCount = 0;
// No context.
verifyCelebornException(
[&]() { CELEBORN_CHECK_EQ(1, 3); },
"Exception: CelebornRuntimeError"
"\nError Source: RUNTIME"
"\nError Code: INVALID_STATE"
"\nReason: (1 vs. 3)"
"\nRetriable: False"
"\nExpression: 1 == 3"
"\nFunction: operator()"
"\nFile: ");
EXPECT_EQ(0, callCount);
// With message function throwing an exception.
auto throwingMessageFunction =
[](celeborn::CelebornException::Type /*exceptionType*/,
void* untypedArg) -> std::string {
auto arg = static_cast<MessageFunctionArg*>(untypedArg);
++(*arg->callCount);
CELEBORN_FAIL("Test failure.");
};
{
MessageFunctionArg debuggingInfo{"Debugging info.", &callCount};
celeborn::ExceptionContextSetter context(
{throwingMessageFunction, &debuggingInfo});
verifyCelebornException(
[&]() { CELEBORN_CHECK_EQ(1, 3); },
"Exception: CelebornRuntimeError"
"\nError Source: RUNTIME"
"\nError Code: INVALID_STATE"
"\nReason: (1 vs. 3)"
"\nRetriable: False"
"\nExpression: 1 == 3"
"\nContext: Failed to produce additional context."
"\nTop-Level Context: Same as context."
"\nFunction: operator()"
"\nFile: ");
EXPECT_EQ(1, callCount);
}
}
TEST(ExceptionTest, traceCollectionEnabling) {
// Switch on/off tests.
for (const bool enabled : {false, true}) {
for (const bool userException : {false, true}) {
testExceptionTraceCollectionControl(userException, enabled);
}
}
}
TEST(ExceptionTest, traceCollectionRateControl) {
// Rate limit tests.
for (const bool withLimit : {false, true}) {
for (const bool userException : {false, true}) {
testExceptionTraceCollectionRateControl(userException, withLimit);
}
}
}
TEST(ExceptionTest, wrappedException) {
try {
throw std::invalid_argument("This is a test.");
} catch (const std::exception& e) {
CelebornUserError ve(std::current_exception(), e.what(), false);
ASSERT_EQ(ve.message(), "This is a test.");
ASSERT_TRUE(ve.isUserError());
ASSERT_EQ(ve.context(), "");
ASSERT_EQ(ve.topLevelContext(), "");
ASSERT_THROW(
std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
}
try {
throw std::invalid_argument("This is a test.");
} catch (const std::exception& e) {
CelebornRuntimeError ve(std::current_exception(), e.what(), false);
ASSERT_EQ(ve.message(), "This is a test.");
ASSERT_FALSE(ve.isUserError());
ASSERT_EQ(ve.context(), "");
ASSERT_EQ(ve.topLevelContext(), "");
ASSERT_THROW(
std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
}
try {
CELEBORN_FAIL("This is a test.");
} catch (const CelebornException& e) {
ASSERT_EQ(e.message(), "This is a test.");
ASSERT_TRUE(e.wrappedException() == nullptr);
}
}
TEST(ExceptionTest, wrappedExceptionWithContext) {
auto messageFunction = [](celeborn::CelebornException::Type exceptionType,
void* untypedArg) {
auto data = static_cast<char*>(untypedArg);
switch (exceptionType) {
case celeborn::CelebornException::Type::kUser:
return fmt::format("User error: {}", data);
case celeborn::CelebornException::Type::kSystem:
return fmt::format("System error: {}", data);
default:
return fmt::format("Unexpected error type: {}", data);
}
};
std::string data = "lakes";
celeborn::ExceptionContextSetter context({messageFunction, data.data()});
try {
throw std::invalid_argument("This is a test.");
} catch (const std::exception& e) {
CelebornUserError ve(std::current_exception(), e.what(), false);
ASSERT_EQ(ve.message(), "This is a test.");
ASSERT_TRUE(ve.isUserError());
ASSERT_EQ(ve.context(), "User error: lakes");
ASSERT_EQ(ve.topLevelContext(), "Same as context.");
ASSERT_THROW(
std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
}
try {
throw std::invalid_argument("This is a test.");
} catch (const std::exception& e) {
CelebornRuntimeError ve(std::current_exception(), e.what(), false);
ASSERT_EQ(ve.message(), "This is a test.");
ASSERT_FALSE(ve.isUserError());
ASSERT_EQ(ve.context(), "System error: lakes");
ASSERT_EQ(ve.topLevelContext(), "Same as context.");
ASSERT_THROW(
std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
}
std::string innerData = "mountains";
celeborn::ExceptionContextSetter innerContext(
{messageFunction, innerData.data()});
try {
throw std::invalid_argument("This is a test.");
} catch (const std::exception& e) {
CelebornUserError ve(std::current_exception(), e.what(), false);
ASSERT_EQ(ve.message(), "This is a test.");
ASSERT_TRUE(ve.isUserError());
ASSERT_EQ(ve.context(), "User error: mountains");
ASSERT_EQ(ve.topLevelContext(), "User error: lakes");
ASSERT_THROW(
std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
}
try {
throw std::invalid_argument("This is a test.");
} catch (const std::exception& e) {
CelebornRuntimeError ve(std::current_exception(), e.what(), false);
ASSERT_EQ(ve.message(), "This is a test.");
ASSERT_FALSE(ve.isUserError());
ASSERT_EQ(ve.context(), "System error: mountains");
ASSERT_EQ(ve.topLevelContext(), "System error: lakes");
ASSERT_THROW(
std::rethrow_exception(ve.wrappedException()), std::invalid_argument);
}
}
TEST(ExceptionTest, exceptionMacroInlining) {
// Verify that the right formatting method is inlined when using
// _CELEBORN_THROW macro. This test can be removed if fmt::vformat changes
// behavior and starts ignoring extra brackets.
// The following string should throw an error when passed to fmt::vformat.
std::string errorStr = "This {} {is a test.";
// Inlined with the method that directly returns the std::string input.
try {
CELEBORN_USER_FAIL(errorStr);
} catch (const CelebornUserError& ve) {
ASSERT_EQ(ve.message(), errorStr);
}
// Inlined with the method that directly returns the char* input.
try {
CELEBORN_USER_FAIL(errorStr.c_str());
} catch (const CelebornUserError& ve) {
ASSERT_EQ(ve.message(), errorStr);
}
// Inlined with the method that passes the errorStr and the next argument via
// fmt::vformat. Should throw format_error.
try {
CELEBORN_USER_FAIL(errorStr, "definitely");
} catch (const std::exception& e) {
ASSERT_TRUE(folly::StringPiece{e.what()}.startsWith("argument not found"));
}
}

View File

@ -65,6 +65,21 @@ BOOST_VERSION="boost-1.84.0"
ARROW_VERSION="15.0.0"
STEMMER_VERSION="2.2.0"
# Install gtest library package for tests.
function install_gtest {
${SUDO} apt-get install -y libgtest-dev cmake
mkdir -p $HOME/build
pushd $HOME/build
${SUDO} cmake /usr/src/googletest/googletest
${SUDO} make
${SUDO} cp lib/libgtest* /usr/lib/
popd
${SUDO} rm -rf $HOME/build
${SUDO} mkdir /usr/local/lib/googletest
${SUDO} ln -s /usr/lib/libgtest.a /usr/local/lib/googletest/libgtest.a
${SUDO} ln -s /usr/lib/libgtest_main.a /usr/local/lib/googletest/libgtest_main.a
}
# Install packages required for build.
function install_build_prerequisites {
${SUDO} apt update
@ -82,6 +97,8 @@ function install_build_prerequisites {
pkg-config \
gdb \
wget
install_gtest
# Install to /usr/local to make it available to all users.
${SUDO} pip3 install cmake==3.28.3
@ -140,8 +157,8 @@ function install_protobuf {
cd ${DEPENDENCY_DIR}/protobuf
./configure CXXFLAGS="-fPIC" --prefix=${INSTALL_PREFIX}
make "-j${NPROC}"
sudo make install
sudo ldconfig
${SUDO} make install
${SUDO} ldconfig
)
}