This is an automated email from the ASF dual-hosted git repository.
ethanfeng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git
The following commit(s) were added to refs/heads/main by this push:
new b2b9a0ab4 [CELEBORN-1754][CIP-14] Add exceptions and checking utils to
cppClient
b2b9a0ab4 is described below
commit b2b9a0ab4b8a8313b0d189bc5e03c22c10c4d93f
Author: HolyLow <[email protected]>
AuthorDate: Wed Dec 4 14:05:38 2024 +0800
[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 <[email protected]>
Signed-off-by: mingji <[email protected]>
---
LICENSE | 4 +-
cpp/CMakeLists.txt | 11 +
cpp/README.md | 7 +-
cpp/celeborn/utils/CMakeLists.txt | 8 +-
cpp/celeborn/utils/Exceptions.cpp | 26 +
cpp/celeborn/utils/Exceptions.h | 421 ++++++++++++
cpp/celeborn/utils/{ => tests}/CMakeLists.txt | 11 +-
cpp/celeborn/utils/tests/ExceptionTest.cpp | 932 ++++++++++++++++++++++++++
cpp/scripts/setup-ubuntu.sh | 21 +-
9 files changed, 1432 insertions(+), 9 deletions(-)
diff --git a/LICENSE b/LICENSE
index 0147fe37f..798e05593 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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.
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index 1ad45f867..e3d869fbb 100644
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -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)
diff --git a/cpp/README.md b/cpp/README.md
index aefb67c25..159ce5ab9 100644
--- a/cpp/README.md
+++ b/cpp/README.md
@@ -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
```
diff --git a/cpp/celeborn/utils/CMakeLists.txt
b/cpp/celeborn/utils/CMakeLists.txt
index ed5340180..f040d73b7 100644
--- a/cpp/celeborn/utils/CMakeLists.txt
+++ b/cpp/celeborn/utils/CMakeLists.txt
@@ -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()
diff --git a/cpp/celeborn/utils/Exceptions.cpp
b/cpp/celeborn/utils/Exceptions.cpp
new file mode 100644
index 000000000..202d50d14
--- /dev/null
+++ b/cpp/celeborn/utils/Exceptions.cpp
@@ -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
diff --git a/cpp/celeborn/utils/Exceptions.h b/cpp/celeborn/utils/Exceptions.h
new file mode 100644
index 000000000..2a1e2fc98
--- /dev/null
+++ b/cpp/celeborn/utils/Exceptions.h
@@ -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
diff --git a/cpp/celeborn/utils/CMakeLists.txt
b/cpp/celeborn/utils/tests/CMakeLists.txt
similarity index 80%
copy from cpp/celeborn/utils/CMakeLists.txt
copy to cpp/celeborn/utils/tests/CMakeLists.txt
index ed5340180..a820b4ac1 100644
--- a/cpp/celeborn/utils/CMakeLists.txt
+++ b/cpp/celeborn/utils/tests/CMakeLists.txt
@@ -12,9 +12,14 @@
# 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_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}
@@ -22,4 +27,6 @@ target_link_libraries(
${FOLLY_WITH_DEPENDENCIES}
${GLOG}
${GFLAGS_LIBRARIES}
-)
+ GTest::gtest
+ GTest::gmock
+ GTest::gtest_main)
diff --git a/cpp/celeborn/utils/tests/ExceptionTest.cpp
b/cpp/celeborn/utils/tests/ExceptionTest.cpp
new file mode 100644
index 000000000..b1dc302ef
--- /dev/null
+++ b/cpp/celeborn/utils/tests/ExceptionTest.cpp
@@ -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"));
+ }
+}
diff --git a/cpp/scripts/setup-ubuntu.sh b/cpp/scripts/setup-ubuntu.sh
index 1ebd4cb81..a0b6d88ca 100755
--- a/cpp/scripts/setup-ubuntu.sh
+++ b/cpp/scripts/setup-ubuntu.sh
@@ -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
)
}