sql_utils/base/status_builder.h (232 lines of code) (raw):

/* * Copyright 2023 Google LLC * * 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. */ #ifndef THIRD_PARTY_PY_BIGQUERY_ML_UTILS_SQL_UTILS_BASE_STATUS_BUILDER_H_ #define THIRD_PARTY_PY_BIGQUERY_ML_UTILS_SQL_UTILS_BASE_STATUS_BUILDER_H_ #include <iostream> #include <limits> #include <memory> #include <sstream> #include <string> #include <type_traits> #include <utility> #include "absl/base/attributes.h" #include "absl/base/log_severity.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "sql_utils/base/logging.h" #include "sql_utils/base/source_location.h" #include "sql_utils/base/status_payload.h" namespace bigquery_ml_utils_base { // Creates a status based on an original_status, but enriched with additional // information. The builder implicitly converts to Status and StatusOr<T> // allowing for it to be returned directly. // // StatusBuilder builder(original, SQL_LOC); // builder.Attach(proto); // builder << "info about error"; // return builder; // // It provides method chaining to simplify typical usage: // // return StatusBuilder(original, SQL_LOC) // .Log(base_logging::WARNING) << "oh no!"; // // In more detail: // - When the original status is OK, all methods become no-ops and nothing will // be logged. // - Messages streamed into the status builder are collected into a single // additional message string. // - The original Status's message and the additional message are joined // together when the result status is built. // - By default, the messages will be joined with a convenience separator // between the original message and the additional one. This behavior can be // changed with the `SetAppend()` and `SetPrepend()` methods of the builder. // - By default, the result status is not logged (but see `Log` method). // - All side effects (like logging) happen when the builder is converted to a // status. class ABSL_MUST_USE_RESULT StatusBuilder { public: // Creates a `StatusBuilder` from an StatusCode. If logging is enabled, // it will use `location` as the location from which the log message occurs. StatusBuilder(absl::StatusCode code, bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); // Creates a `StatusBuilder` based on an original status. If logging is // enabled, it will use `location` as the location from which the log message // occurs. StatusBuilder( const absl::Status& original_status, bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder( absl::Status&& original_status, bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder(const StatusBuilder& sb); StatusBuilder& operator=(const StatusBuilder& sb); StatusBuilder(StatusBuilder&&) = default; StatusBuilder& operator=(StatusBuilder&&) = default; // Mutates the builder so that the final additional message is prepended to // the original error message in the status. A convenience separator is not // placed between the messages. // // NOTE: Multiple calls to `SetPrepend` and `SetAppend` just adjust the // behavior of the final join of the original status with the extra message. // // Returns `*this` to allow method chaining. StatusBuilder& SetPrepend(); // Mutates the builder so that the final additional message is appended to the // original error message in the status. A convenience separator is not // placed between the messages. // // NOTE: Multiple calls to `SetPrepend` and `SetAppend` just adjust the // behavior of the final join of the original status with the extra message. // // Returns `*this` to allow method chaining. StatusBuilder& SetAppend(); // Mutates the builder to disable any logging that was set using any of the // logging functions below. Returns `*this` to allow method chaining. StatusBuilder& SetNoLogging(); // Mutates the builder so that the result status will be logged (without a // stack trace) when this builder is converted to a Status. This overrides // the logging settings from earlier calls to any of the logging mutator // functions. Returns `*this` to allow method chaining. StatusBuilder& Log(absl::LogSeverity level); StatusBuilder& LogError() { return Log(absl::LogSeverity::kError); } StatusBuilder& LogWarning() { return Log(absl::LogSeverity::kWarning); } StatusBuilder& LogInfo() { return Log(absl::LogSeverity::kInfo); } // Mutates the builder so that a stack trace will be logged if the status is // logged. One of the logging setters above should be called as well. If // logging is not yet enabled this behaves as if LogInfo().EmitStackTrace() // was called. Returns `*this` to allow method chaining. StatusBuilder& EmitStackTrace(); // Appends to the extra message that will be added to the original status. By // default, the extra message is added to the original message with a // separator (';') between the original message and the enriched one. template <typename T> StatusBuilder& operator<<(const T& value); // Attaches a proto containing additional details about the error. // Returns `*this` to allow method chaining. template <typename T> StatusBuilder& Attach(const T& data); // Sets the error code for the status that will be returned by this // StatusBuilder. Returns `*this` to allow method chaining. StatusBuilder& SetErrorCode(absl::StatusCode code); ///////////////////////////////// Adaptors ///////////////////////////////// // // A StatusBuilder `adaptor` is a functor which can be included in a builder // method chain. There are two common variants: // // 1. `Pure policy` adaptors modify the StatusBuilder and return the modified // object, which can then be chained with further adaptors or mutations. // // 2. `Terminal` adaptors consume the builder's Status and return some // other type of object. Alternatively, the consumed Status may be used // for side effects, e.g. by passing it to a side channel. A terminal // adaptor cannot be chained. // // Careful: The conversion of StatusBuilder to Status has side effects! // Adaptors must ensure that this conversion happens at most once in the // builder chain. The easiest way to do this is to determine the adaptor type // and follow the corresponding guidelines: // // Pure policy adaptors should: // (a) Take a StatusBuilder as input parameter. // (b) NEVER convert the StatusBuilder to Status: // - Never assign the builder to a Status variable. // - Never pass the builder to a function whose parameter type is Status, // including by reference (e.g. const Status&). // - Never pass the builder to any function which might convert the // builder to Status (i.e. this restriction is viral). // (c) Return a StatusBuilder (usually the input parameter). // // Terminal adaptors should: // (a) Take a Status as input parameter (not a StatusBuilder!). // (b) Return a type matching the enclosing function. (This can be `void`.) // // Adaptors do not necessarily fit into one of these categories. However, any // which satisfies the conversion rule can always be decomposed into a pure // adaptor chained into a terminal adaptor. (This is recommended.) // // Examples // // Pure adaptors allow teams to configure team-specific error handling // policies. For example: // // StatusBuilder TeamPolicy(StatusBuilder builder) { // AttachPayload(&builder, ...); // return std::move(builder).Log(base_logging::WARNING); // } // // SQL_RETURN_IF_ERROR(foo()).With(TeamPolicy); // // Because pure policy adaptors return the modified StatusBuilder, they // can be chained with further adaptors, e.g.: // // SQL_RETURN_IF_ERROR(foo()).With(TeamPolicy).With(OtherTeamPolicy); // // Terminal adaptors are often used for type conversion. This allows // SQL_RETURN_IF_ERROR to be used in functions which do not return Status. For // example, a function might want to return some default value on error: // // int GetSysCounter() { // int value; // SQL_RETURN_IF_ERROR(ReadCounterFile(filename, &value)) // .LogInfo() // .With([](const absl::Status& unused) { return 0; }); // return value; // } // Calls `adaptor` on this status builder to apply policies, type conversions, // and/or side effects on the StatusBuilder. Returns the value returned by // `adaptor`, which may be any type including `void`. See comments above. // template <typename Adaptor> auto With(Adaptor&& adaptor) & -> decltype( std::forward<Adaptor>(adaptor)(*this)) { return std::forward<Adaptor>(adaptor)(*this); } template <typename Adaptor> auto With(Adaptor&& adaptor) && -> decltype( std::forward<Adaptor>(adaptor)(std::move(*this))) { return std::forward<Adaptor>(adaptor)(std::move(*this)); } // Returns true if the Status created by this builder will be ok(). bool ok() const; // Returns the code for the Status created by this builder. absl::StatusCode code() const; // Returns true iff the status created by this builder will have the given // `code`. // // `StatusBuilder(Status(code, "")).Is(code)` is always true. In particular, // if the `code` is zero, returns true if `status_builder.ok()`. // Sample usage: // // StatusBuilder TeamPolicy(StatusBuilder builder) { // if (builder.Is(StatusCode::kCancelled)) { // builder.Log(base_logging::WARNING); // } // return std::move(builder); // } // ABSL_DEPRECATED("Use code() == code instead") ABSL_MUST_USE_RESULT bool Is(absl::StatusCode code) const; // Implicit conversion to Status. // // Careful: this operator has side effects, so it should be called at // most once. // // This override allows us to implement SQL_RETURN_IF_ERROR with 2 move // operations in the common case. operator absl::Status() const&; // NOLINT operator absl::Status() &&; template <typename T> operator absl::StatusOr<T>() const&; // NOLINT template <typename T> operator absl::StatusOr<T>() &&; // NOLINT // Returns the source location used to create this builder. bigquery_ml_utils_base::SourceLocation source_location() const; private: // Specifies how to join the error message in the original status and any // additional message that has been streamed into the builder. enum class MessageJoinStyle { kAnnotate, kAppend, kPrepend, }; // Creates a new status based on an old one by joining the message from the // original to an additional message. static absl::Status JoinMessageToStatus(absl::Status s, absl::string_view msg, MessageJoinStyle style); // Creates a Status from this builder and logs it if the builder has been // configured to log itself. absl::Status CreateStatusAndConditionallyLog() &&; // Conditionally logs if the builder has been configured to log. This method // is split from the above to isolate the portability issues around logging // into a single place. void ConditionallyLog(const absl::Status& result) const; // Infrequently set builder options, instantiated lazily. This reduces // average construction/destruction time (e.g. the `stream` is fairly // expensive). Stacks can also be blown if StatusBuilder grows too large. // This is primarily an issue for debug builds, which do not necessarily // re-use stack space within a function across the sub-scopes used by // status macros. struct Rep { explicit Rep() = default; Rep(const Rep& r); enum class LoggingMode { kDisabled, kLog, }; LoggingMode logging_mode = LoggingMode::kDisabled; // Corresponds to the levels in `base_logging::LogSeverity`. Only used when // `logging_mode == LoggingMode::kLog`. absl::LogSeverity log_severity; // The level at which the Status should be VLOGged. // Only used when `logging_mode == LoggingMode::kVLog`. int verbose_level; // Gathers additional messages added with `<<` for use in the final status. std::ostringstream stream; // Whether to log stack trace. Only used when `logging_mode != // LoggingMode::kDisabled`. bool should_log_stack_trace = false; // Specifies how to join the message in `status_` and `stream`. MessageJoinStyle message_join_style = MessageJoinStyle::kAnnotate; }; // The status that the result will be based on. Can be modified by Attach(). absl::Status status_; bigquery_ml_utils_base::SourceLocation location_; // nullptr if the result status will be OK. Extra fields moved to the heap to // minimize stack space. std::unique_ptr<Rep> rep_; }; // Each of the functions below creates StatusBuilder with a canonical error. // The error code of the StatusBuilder matches the name of the function. StatusBuilder AbortedErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder AlreadyExistsErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder CancelledErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder DataLossErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder DeadlineExceededErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder FailedPreconditionErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder InternalErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder InvalidArgumentErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder NotFoundErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder OutOfRangeErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder PermissionDeniedErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder UnauthenticatedErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder ResourceExhaustedErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder UnavailableErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder UnimplementedErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); StatusBuilder UnknownErrorBuilder( bigquery_ml_utils_base::SourceLocation location = SourceLocation::current()); inline StatusBuilder::StatusBuilder(absl::StatusCode code, bigquery_ml_utils_base::SourceLocation location) : status_(code, ""), location_(location) {} inline StatusBuilder::StatusBuilder(const absl::Status& original_status, bigquery_ml_utils_base::SourceLocation location) : status_(original_status), location_(location) {} inline StatusBuilder::StatusBuilder(absl::Status&& original_status, bigquery_ml_utils_base::SourceLocation location) : status_(std::move(original_status)), location_(location) {} inline StatusBuilder::StatusBuilder(const StatusBuilder& sb) : status_(sb.status_), location_(sb.location_) { if (sb.rep_ != nullptr) { rep_.reset(new Rep(*sb.rep_)); } } inline StatusBuilder& StatusBuilder::operator=(const StatusBuilder& sb) { status_ = sb.status_; location_ = sb.location_; if (sb.rep_ != nullptr) { rep_.reset(new Rep(*sb.rep_)); } else { rep_ = nullptr; } return *this; } inline StatusBuilder& StatusBuilder::SetPrepend() { if (status_.ok()) return *this; if (rep_ == nullptr) rep_.reset(new Rep()); rep_->message_join_style = MessageJoinStyle::kPrepend; return *this; } inline StatusBuilder& StatusBuilder::SetAppend() { if (status_.ok()) return *this; if (rep_ == nullptr) rep_.reset(new Rep()); rep_->message_join_style = MessageJoinStyle::kAppend; return *this; } inline StatusBuilder& StatusBuilder::SetNoLogging() { if (rep_ != nullptr) { rep_->logging_mode = Rep::LoggingMode::kDisabled; } return *this; } inline StatusBuilder& StatusBuilder::Log(absl::LogSeverity level) { if (status_.ok()) return *this; if (rep_ == nullptr) rep_.reset(new Rep()); rep_->logging_mode = Rep::LoggingMode::kLog; rep_->log_severity = level; rep_->should_log_stack_trace = false; return *this; } inline StatusBuilder& StatusBuilder::EmitStackTrace() { if (status_.ok()) return *this; if (rep_ == nullptr) { rep_.reset(new Rep()); rep_->logging_mode = Rep::LoggingMode::kLog; rep_->log_severity = absl::LogSeverity::kInfo; } rep_->should_log_stack_trace = true; return *this; } // Implicitly converts `builder` to `Status` and write it to `os`. inline std::ostream& operator<<(std::ostream& os, const StatusBuilder& builder) { return os << static_cast<absl::Status>(builder); } template <typename T> StatusBuilder& StatusBuilder::operator<<(const T& value) { if (status_.ok()) return *this; if (rep_ == nullptr) rep_.reset(new Rep()); rep_->stream << value; return *this; } inline bool StatusBuilder::ok() const { return status_.ok(); } inline absl::StatusCode StatusBuilder::code() const { return status_.code(); } inline bool StatusBuilder::Is(absl::StatusCode status_code) const { return status_.code() == status_code; } inline StatusBuilder::operator absl::Status() const& { if (rep_ == nullptr) return status_; return StatusBuilder(*this).CreateStatusAndConditionallyLog(); } inline StatusBuilder::operator absl::Status() && { if (rep_ == nullptr) return std::move(status_); return std::move(*this).CreateStatusAndConditionallyLog(); }; template <typename T> inline StatusBuilder::operator absl::StatusOr<T>() const& { if (rep_ == nullptr) return absl::StatusOr<T>(status_); return absl::StatusOr<T>( StatusBuilder(*this).CreateStatusAndConditionallyLog()); } template <typename T> inline StatusBuilder::operator absl::StatusOr<T>() && { if (rep_ == nullptr) return std::move(status_); return std::move(*this).CreateStatusAndConditionallyLog(); } inline bigquery_ml_utils_base::SourceLocation StatusBuilder::source_location() const { return location_; } // Attaches a proto containing additional details about the error. // Returns `*this` to allow method chaining. template <typename T> StatusBuilder& StatusBuilder::Attach(const T& data) { AttachPayload<T>(&status_, data); return *this; } } // namespace bigquery_ml_utils_base #endif // THIRD_PARTY_PY_BIGQUERY_ML_UTILS_SQL_UTILS_BASE_STATUS_BUILDER_H_