diff --git a/src/iceberg/CMakeLists.txt b/src/iceberg/CMakeLists.txt index 4bebe4e4e..57ca73568 100644 --- a/src/iceberg/CMakeLists.txt +++ b/src/iceberg/CMakeLists.txt @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -set(ICEBERG_SOURCES demo_table.cc) +set(ICEBERG_SOURCES demo_table.cc status.cc) add_iceberg_lib(iceberg SOURCES diff --git a/src/iceberg/status.cc b/src/iceberg/status.cc new file mode 100644 index 000000000..f4257b46e --- /dev/null +++ b/src/iceberg/status.cc @@ -0,0 +1,97 @@ +/* + * 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. + */ + +#include "iceberg/status.h" + +#include +#include +#include +#include + +namespace iceberg { + +Status::Status(StatusCode code, const std::string& msg) + : Status::Status(code, msg, nullptr) {} + +Status::Status(StatusCode code, std::string msg, std::shared_ptr detail) { + state_ = new State{code, std::move(msg), std::move(detail)}; +} + +void Status::CopyFrom(const Status& s) { + if (ICEBERG_PREDICT_FALSE(state_ != nullptr)) { + DeleteState(); + } + if (s.state_ == nullptr) { + state_ = nullptr; + } else { + state_ = new State(*s.state_); + } +} + +std::string Status::CodeAsString() const { + if (state_ == nullptr) { + return "OK"; + } + return CodeAsString(code()); +} + +std::string Status::CodeAsString(StatusCode code) { + const char* type; + switch (code) { + case StatusCode::OK: + type = "OK"; + break; + case StatusCode::IOError: + type = "IOError"; + break; + case StatusCode::NotImplemented: + type = "NotImplemented"; + break; + case StatusCode::UnknownError: + type = "Unknown error"; + break; + default: + type = "Unknown"; + break; + } + return std::string(type); +} + +std::string Status::ToString() const { + std::ostringstream oss; + oss << CodeAsString(); + if (state_ == nullptr) { + return oss.str(); + } + oss << ": "; + oss << state_->msg; + if (state_->detail != nullptr) { + oss << ". Detail: " << state_->detail->ToString(); + } + + return oss.str(); +} + +void Status::Warn() const { std::cerr << *this; } + +void Status::Warn(const std::string& message) const { + std::cerr << message << ": " << *this; +} + +} // namespace iceberg diff --git a/src/iceberg/status.h b/src/iceberg/status.h new file mode 100644 index 000000000..74c8d8c00 --- /dev/null +++ b/src/iceberg/status.h @@ -0,0 +1,278 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "util/compare.h" +#include "util/macros.h" +#include "util/to_string_ostreamable.h" + +namespace iceberg { + +enum class StatusCode : char { + OK = 0, + IOError = 1, + NotImplemented = 2, + UnknownError = 127 +}; + +/// \brief An opaque class that allows subsystems to retain +/// additional information inside the Status. +class StatusDetail { + public: + virtual ~StatusDetail() = default; + /// \brief Return a unique id for the type of the StatusDetail + /// (effectively a poor man's substitute for RTTI). + virtual const char* type_id() const = 0; + /// \brief Produce a human-readable description of this status. + virtual std::string ToString() const = 0; + + bool operator==(const StatusDetail& other) const noexcept { + return std::string(type_id()) == other.type_id() && ToString() == other.ToString(); + } +}; + +/// \brief Status outcome object (success or error) +/// +/// The Status object is an object holding the outcome of an operation. +/// The outcome is represented as a StatusCode, either success +/// (StatusCode::OK) or an error (any other of the StatusCode enumeration values). +/// +/// Additionally, if an error occurred, a specific error message is generally +/// attached. +class [[nodiscard]] Status : public util::EqualityComparable, + public util::ToStringOstreamable { + public: + // Create a success status. + constexpr Status() noexcept : state_(nullptr) {} + ~Status() noexcept { + if (ICEBERG_PREDICT_FALSE(state_ != nullptr)) { + DeleteState(); + } + } + + Status(StatusCode code, const std::string& msg); + /// \brief Pluggable constructor for use by sub-systems. detail cannot be null. + Status(StatusCode code, std::string msg, std::shared_ptr detail); + + // Copy the specified status. + inline Status(const Status& s); + inline Status& operator=(const Status& s); + + // Move the specified status. + inline Status(Status&& s) noexcept; + inline Status& operator=(Status&& s) noexcept; + + inline bool Equals(const Status& other) const; + + // AND the statuses. + inline Status operator&(const Status& s) const noexcept; + inline Status operator&(Status&& s) const noexcept; + inline Status& operator&=(const Status& s) noexcept; + inline Status& operator&=(Status&& s) noexcept; + + /// Return a success status. + static Status OK() { return Status(); } + + template + static Status FromArgs(StatusCode code, std::string_view fmt, Args&&... args) { + return Status(code, std::vformat(fmt, std::make_format_args(args...))); + } + + template + static Status FromDetailAndArgs(StatusCode code, std::shared_ptr detail, + std::string_view fmt, Args&&... args) { + return Status(code, std::vformat(fmt, std::make_format_args(args...)), + std::move(detail)); + } + + /// Return an error status when some IO-related operation failed + template + static Status IOError(Args&&... args) { + return Status::FromArgs(StatusCode::IOError, std::forward(args)...); + } + + /// Return an error status when an operation or a combination of operation and + /// data types is unimplemented + template + static Status NotImplemented(Args&&... args) { + return Status::FromArgs(StatusCode::NotImplemented, std::forward(args)...); + } + + /// Return an error status for unknown errors + template + static Status UnknownError(Args&&... args) { + return Status::FromArgs(StatusCode::UnknownError, std::forward(args)...); + } + + /// Return true if the status indicates success. + constexpr bool ok() const { return (state_ == nullptr); } + + /// Return true if the status indicates an IO-related failure. + constexpr bool IsIOError() const { return code() == StatusCode::IOError; } + /// Return true if the status indicates an unimplemented operation. + constexpr bool IsNotImplemented() const { return code() == StatusCode::NotImplemented; } + /// Return true if the status indicates an unknown error. + constexpr bool IsUnknownError() const { return code() == StatusCode::UnknownError; } + + /// \brief Return a string representation of this status suitable for printing. + /// + /// The string "OK" is returned for success. + std::string ToString() const; + + /// \brief Return a string representation of the status code, without the message + /// text or POSIX code information. + std::string CodeAsString() const; + static std::string CodeAsString(StatusCode); + + /// \brief Return the StatusCode value attached to this status. + constexpr StatusCode code() const { return ok() ? StatusCode::OK : state_->code; } + + /// \brief Return the specific error message attached to this status. + const std::string& message() const { + static const std::string no_message = ""; + return ok() ? no_message : state_->msg; + } + + /// \brief Return the status detail attached to this message. + const std::shared_ptr& detail() const { + static std::shared_ptr no_detail = nullptr; + return state_ ? state_->detail : no_detail; + } + + /// \brief Return a new Status copying the existing status, but + /// updating with the existing detail. + Status WithDetail(std::shared_ptr new_detail) const { + return Status(code(), message(), std::move(new_detail)); + } + + /// \brief Return a new Status with changed message, copying the + /// existing status code and detail. + template + Status WithMessage(Args&&... args) const { + return FromArgs(code(), std::forward(args)...).WithDetail(detail()); + } + + void Warn() const; + void Warn(const std::string& message) const; + + private: + struct State { + StatusCode code; + std::string msg; + std::shared_ptr detail; + }; + // OK status has a `NULL` state_. Otherwise, `state_` points to + // a `State` structure containing the error code and message(s) + State* state_; + + void DeleteState() noexcept { + // On certain compilers, splitting off the slow path improves performance + // significantly. + delete state_; + state_ = nullptr; + } + + void CopyFrom(const Status& s); + inline void MoveFrom(Status& s); +}; + +void Status::MoveFrom(Status& s) { + if (ICEBERG_PREDICT_FALSE(state_ != nullptr)) { + DeleteState(); + } + state_ = s.state_; + s.state_ = nullptr; +} + +Status::Status(const Status& s) : state_{nullptr} { CopyFrom(s); } + +Status& Status::operator=(const Status& s) { + // The following condition catches both aliasing (when this == &s), + // and the common case where both s and *this are ok. + if (state_ != s.state_) { + CopyFrom(s); + } + return *this; +} + +Status::Status(Status&& s) noexcept : state_(s.state_) { s.state_ = nullptr; } + +Status& Status::operator=(Status&& s) noexcept { + MoveFrom(s); + return *this; +} + +bool Status::Equals(const Status& s) const { + if (state_ == s.state_) { + return true; + } + + if (ok() || s.ok()) { + return false; + } + + if (detail() != s.detail()) { + if ((detail() && !s.detail()) || (!detail() && s.detail())) { + return false; + } + return *detail() == *s.detail(); + } + + return code() == s.code() && message() == s.message(); +} + +Status Status::operator&(const Status& s) const noexcept { + if (ok()) { + return s; + } else { + return *this; + } +} + +Status Status::operator&(Status&& s) const noexcept { + if (ok()) { + return std::move(s); + } else { + return *this; + } +} + +Status& Status::operator&=(const Status& s) noexcept { + if (ok() && !s.ok()) { + CopyFrom(s); + } + return *this; +} + +Status& Status::operator&=(Status&& s) noexcept { + if (ok() && !s.ok()) { + MoveFrom(s); + } + return *this; +} + +} // namespace iceberg diff --git a/src/iceberg/util/compare.h b/src/iceberg/util/compare.h new file mode 100644 index 000000000..37cc99147 --- /dev/null +++ b/src/iceberg/util/compare.h @@ -0,0 +1,56 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +namespace iceberg { +namespace util { + +/// CRTP helper for declaring equality comparison. Defines operator== and operator!= +template +class EqualityComparable { + public: + ~EqualityComparable() { + static_assert( + std::is_same_v().Equals(std::declval())), + bool>, + "EqualityComparable depends on the method T::Equals(const T&) const"); + } + + template + bool Equals(const std::shared_ptr& other) const { + if (other == nullptr) { + return false; + } + return cast().Equals(*other); + } + + friend bool operator==(T const& a, T const& b) { return a.Equals(b); } + friend bool operator!=(T const& a, T const& b) { return !(a == b); } + + private: + const T& cast() const { return static_cast(*this); } +}; + +} // namespace util +} // namespace iceberg diff --git a/src/iceberg/util/macros.h b/src/iceberg/util/macros.h new file mode 100644 index 000000000..a3e652251 --- /dev/null +++ b/src/iceberg/util/macros.h @@ -0,0 +1,61 @@ +/* + * 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. + */ + +#pragma once + +#include + +#define ICEBERG_EXPAND(x) x +#define ICEBERG_STRINGIFY(x) #x +#define ICEBERG_CONCAT(x, y) x##y + +#ifndef ICEBERG_DISALLOW_COPY_AND_ASSIGN +# define ICEBERG_DISALLOW_COPY_AND_ASSIGN(TypeName) \ + TypeName(const TypeName&) = delete; \ + void operator=(const TypeName&) = delete +#endif + +#ifndef ICEBERG_DEFAULT_MOVE_AND_ASSIGN +# define ICEBERG_DEFAULT_MOVE_AND_ASSIGN(TypeName) \ + TypeName(TypeName&&) = default; \ + TypeName& operator=(TypeName&&) = default +#endif + +#if defined(__GNUC__) // GCC and compatible compilers (clang, Intel ICC) +# define ICEBERG_NORETURN __attribute__((noreturn)) +# define ICEBERG_NOINLINE __attribute__((noinline)) +# define ICEBERG_FORCE_INLINE __attribute__((always_inline)) +# define ICEBERG_PREDICT_FALSE(x) (__builtin_expect(!!(x), 0)) +# define ICEBERG_PREDICT_TRUE(x) (__builtin_expect(!!(x), 1)) +# define ICEBERG_RESTRICT __restrict +#elif defined(_MSC_VER) // MSVC +# define ICEBERG_NORETURN __declspec(noreturn) +# define ICEBERG_NOINLINE __declspec(noinline) +# define ICEBERG_FORCE_INLINE __forceinline +# define ICEBERG_PREDICT_FALSE(x) (x) +# define ICEBERG_PREDICT_TRUE(x) (x) +# define ICEBERG_RESTRICT __restrict +#else +# define ICEBERG_NORETURN +# define ICEBERG_NOINLINE +# define ICEBERG_FORCE_INLINE +# define ICEBERG_PREDICT_FALSE(x) (x) +# define ICEBERG_PREDICT_TRUE(x) (x) +# define ICEBERG_RESTRICT +#endif diff --git a/src/iceberg/util/to_string_ostreamable.h b/src/iceberg/util/to_string_ostreamable.h new file mode 100644 index 000000000..9682543ef --- /dev/null +++ b/src/iceberg/util/to_string_ostreamable.h @@ -0,0 +1,48 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +namespace iceberg { + +namespace util { + +/// CRTP helper for declaring string representaion. Defines operator<< +template +class ToStringOstreamable { + public: + ~ToStringOstreamable() { + static_assert( + std::is_same_v().ToString()), std::string>, + "ToStringOstreamable depends on the method T::ToString() const"); + } + + private: + const T& cast() const { return static_cast(*this); } + + friend inline std::ostream& operator<<(std::ostream& os, const ToStringOstreamable& t) { + return os << t.cast().ToString(); + } +}; + +} // namespace util +} // namespace iceberg diff --git a/test/core/CMakeLists.txt b/test/core/CMakeLists.txt index 551201779..b2e18ed94 100644 --- a/test/core/CMakeLists.txt +++ b/test/core/CMakeLists.txt @@ -16,7 +16,7 @@ # under the License. add_executable(core_unittest) -target_sources(core_unittest PRIVATE core_unittest.cc) +target_sources(core_unittest PRIVATE core_unittest.cc status_test.cc) target_link_libraries(core_unittest PRIVATE iceberg_static GTest::gtest_main) target_include_directories(core_unittest PRIVATE "${ICEBERG_INCLUDES}") add_test(NAME core_unittest COMMAND core_unittest) diff --git a/test/core/status_test.cc b/test/core/status_test.cc new file mode 100644 index 000000000..2d01e66e7 --- /dev/null +++ b/test/core/status_test.cc @@ -0,0 +1,126 @@ +/* + * 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. + */ + +#include "iceberg/status.h" + +#include + +#include + +namespace iceberg { + +namespace { +class TestStatusDetail : public StatusDetail { + public: + const char* type_id() const override { return "type_id"; } + std::string ToString() const override { return "a specific detail message"; } +}; +} // namespace + +TEST(StatusTest, TestCodeAndMessage) { + Status ok = Status::OK(); + ASSERT_EQ(StatusCode::OK, ok.code()); + Status file_error = Status::IOError("file error"); + ASSERT_EQ(StatusCode::IOError, file_error.code()); + ASSERT_EQ("file error", file_error.message()); +} + +TEST(StatusTest, TestToString) { + Status file_error = Status::IOError("file error"); + ASSERT_EQ("IOError: file error", file_error.ToString()); + std::stringstream ss; + ss << file_error; + ASSERT_EQ(file_error.ToString(), ss.str()); +} + +TEST(StatusTest, TestToStringWithDetail) { + Status status(StatusCode::IOError, "summary", std::make_shared()); + ASSERT_EQ("IOError: summary. Detail: a specific detail message", status.ToString()); + std::stringstream ss; + ss << status; + ASSERT_EQ(status.ToString(), ss.str()); +} + +TEST(StatusTest, TestWithDetail) { + Status status(StatusCode::IOError, "summary"); + auto detail = std::make_shared(); + Status new_status = status.WithDetail(detail); + ASSERT_EQ(new_status.code(), status.code()); + ASSERT_EQ(new_status.message(), status.message()); + ASSERT_EQ(new_status.detail(), detail); +} + +TEST(StatusTest, AndStatus) { + Status a = Status::OK(); + Status b = Status::OK(); + Status c = Status::IOError("file error"); + Status d = Status::NotImplemented("not implement value"); + + Status res; + res = a & b; + ASSERT_TRUE(res.ok()); + res = a & c; + ASSERT_TRUE(res.IsIOError()); + res = d & c; + ASSERT_TRUE(res.IsNotImplemented()); + + res = Status::OK(); + res &= c; + ASSERT_TRUE(res.IsIOError()); + res &= d; + ASSERT_TRUE(res.IsIOError()); + + // With rvalues + res = Status::OK() & Status::IOError("foo"); + ASSERT_TRUE(res.IsIOError()); + res = Status::IOError("foo") & Status::OK(); + ASSERT_TRUE(res.IsIOError()); + res = Status::IOError("foo") & Status::NotImplemented("bar"); + ASSERT_TRUE(res.IsIOError()); + + res = Status::OK(); + res &= Status::OK(); + ASSERT_TRUE(res.ok()); + res &= Status::IOError("foo"); + ASSERT_TRUE(res.IsIOError()); + res &= Status::UnknownError("bar"); + ASSERT_TRUE(res.IsIOError()); +} + +TEST(StatusTest, TestEquality) { + ASSERT_EQ(Status(), Status::OK()); + ASSERT_EQ(Status::IOError("error"), Status::IOError("error")); + ASSERT_NE(Status::IOError("error"), Status::OK()); + ASSERT_NE(Status::IOError("error"), Status::IOError("other error")); +} + +TEST(StatusTest, TestDetailEquality) { + const auto status_with_detail = + Status(StatusCode::IOError, "", std::make_shared()); + const auto status_with_detail2 = + Status(StatusCode::IOError, "", std::make_shared()); + const auto status_without_detail = Status::IOError(""); + + ASSERT_EQ(*status_with_detail.detail(), *status_with_detail2.detail()); + ASSERT_EQ(status_with_detail, status_with_detail2); + ASSERT_NE(status_with_detail, status_without_detail); + ASSERT_NE(status_without_detail, status_with_detail); +} + +} // namespace iceberg