diff --git a/ci/jobs/scripts/check_style/check_cpp.sh b/ci/jobs/scripts/check_style/check_cpp.sh index 121e5340afe5..b23391fe5a18 100755 --- a/ci/jobs/scripts/check_style/check_cpp.sh +++ b/ci/jobs/scripts/check_style/check_cpp.sh @@ -126,6 +126,10 @@ EXTERN_TYPES_EXCLUDES=( ErrorCodes::values ErrorCodes::values[i] ErrorCodes::getErrorCodeByName + ErrorCodes::size + ErrorCodes::getCode + ErrorCodes::getValue + ErrorCodes::ANTALYA_ERROR_CODE_BASE ErrorCodes::Value ) # Check unused/undefined/duplicate ErrorCodes, ProfileEvents, CurrentMetrics declarations. diff --git a/docs/en/antalya/error_codes.md b/docs/en/antalya/error_codes.md new file mode 100644 index 000000000000..92d32e66a811 --- /dev/null +++ b/docs/en/antalya/error_codes.md @@ -0,0 +1,74 @@ +--- +description: 'Stable numeric identities for Antalya-specific errors and migration from legacy codes.' +sidebar_label: 'Error codes' +sidebar_position: 20 +slug: /antalya/error-codes +title: 'Antalya error codes' +doc_type: 'reference' +--- + +# Antalya error codes {#antalya-error-codes} + +Antalya-specific errors use a separate registry. Their numeric code is +`10000 + local_id`, where local IDs are positive, append-only, and never reused. +Upstream errors retain their existing numbers. Symbolic names remain in +`DB::ErrorCodes`; applications should prefer symbolic names over numeric codes +when possible. + +The high range is an Antalya convention, not a reservation recognized by +upstream ClickHouse. Build-time checks reject duplicate Antalya IDs or names, +overlapping numeric ranges, and names shared with upstream errors. Wire codes +must fit in `UInt16` because +`system.part_log.error` and `system.background_schedule_pool_log.error` use that +type. Local IDs therefore cannot exceed `55535`; this limit is enforced at build +time. Adding a new error requires a new local ID in `src/Common/AntalyaErrorCodes.h`. + +## Migration from legacy numbers {#migration-from-legacy-numbers} + +| Name | Legacy code | New code | +|---|---:|---:| +| `CATALOG_NAMESPACE_DISABLED` | 779 | 10001 | +| `PENDING_MUTATIONS_NOT_ALLOWED` | 1009 | 10002 | +| `EXPORT_PARTITION_ALREADY_EXPORTED` | 1010 | 10003 | +| `PARTITION_EXPORT_FAILED` | 1011 | 10004 | +| `CAS_WRITE_UNATTRIBUTED` | 1037 | 10005 | +| `CAS_DELETE_MARKER` | 1038 | 10006 | + +This is a numeric compatibility break. Update clients, monitoring rules, and +scripts that compare the legacy numbers. There are no legacy numeric aliases: +some legacy values identify different errors in upstream ClickHouse. Error +messages and symbolic names are unchanged. + +## Client and mixed-version behavior {#client-and-mixed-version-behavior} + +Exception packets retain their existing layout and signed 32-bit code field. +No protocol revision or peer negotiation is required. Antalya clients built +with this registry display the symbolic name. Older or upstream clients retain +the numeric code and message but may display an empty symbolic name. + +Distributed-query forwarding preserves the numeric identity, including codes +unknown to the intermediate server. However, preserving the code does not make +older Antalya servers recognize it in code-specific handling, such as partition +export conflict handling. Mixed-version feature behavior is not guaranteed; +upgrade participating servers together when relying on Antalya-specific error +handling. New servers likewise do not reinterpret legacy numbers as new errors. + +A shell exit status is only eight bits. Do not use `$?` to recover the full +numeric error code; inspect the client's error output instead. Query failure +continues to produce a nonzero exit status. + +## Accounting {#accounting} + +`system.errors`, `system.error_log`, and Prometheus error metrics include +registered Antalya errors. Counter storage grows with the number of registered +errors, not with the largest numeric code. Local and remote counters remain +separate. Unknown codes retain their identity in exceptions. Codes outside the +upstream array, unless registered by Antalya, share an unnamed out-of-range +accounting slot. Unassigned slots inside the upstream array keep their existing +accounting behavior. Unknown codes do not acquire a registered name or increment +an Antalya error's counters. The aggregated error log skips only the shared +out-of-range accounting slot, rather than reporting its slot number as an +exception identity. Errors in unassigned slots inside the upstream array remain +logged under their original numeric codes, even without a symbolic name. +`system.errors` and Prometheus omit unnamed entries. +`system.query_log.exception_code` retains the original numeric code. diff --git a/src/Common/AntalyaErrorCodes.h b/src/Common/AntalyaErrorCodes.h new file mode 100644 index 000000000000..c879bf0fb7b6 --- /dev/null +++ b/src/Common/AntalyaErrorCodes.h @@ -0,0 +1,17 @@ +#pragma once + +/// Local IDs are append-only, never reused, and independent of upstream assignments. +/// Keep this registry ordered by local ID. Wire codes are `10000 + local_id`. +/// Codes must fit in `UInt16` to preserve their identity in part and background-pool logs. +#define APPLY_FOR_ANTALYA_ERROR_CODES(M) \ + M(1, CATALOG_NAMESPACE_DISABLED) \ + M(2, PENDING_MUTATIONS_NOT_ALLOWED) \ + M(3, EXPORT_PARTITION_ALREADY_EXPORTED) \ + M(4, PARTITION_EXPORT_FAILED) \ + M(5, CAS_WRITE_UNATTRIBUTED) \ + M(6, CAS_DELETE_MARKER) + +namespace DB::ErrorCodes +{ +inline constexpr int ANTALYA_ERROR_CODE_BASE = 10000; +} diff --git a/src/Common/ErrorCodes.cpp b/src/Common/ErrorCodes.cpp index cfdc94b31795..5269246461f8 100644 --- a/src/Common/ErrorCodes.cpp +++ b/src/Common/ErrorCodes.cpp @@ -1,7 +1,11 @@ #include +#include #include #include #include +#include +#include +#include /** Previously, these constants were located in one enum. * But in this case there is a problem: when you add a new constant, you need to recompile @@ -658,7 +662,6 @@ M(776, RESOURCE_LIMIT_EXCEEDED) \ M(777, MEMORY_RESERVATION_KILLED) \ M(778, MEMORY_RESERVATION_FAILED) \ - M(779, CATALOG_NAMESPACE_DISABLED) \ \ M(900, DISTRIBUTED_CACHE_ERROR) \ M(901, CANNOT_USE_DISTRIBUTED_CACHE) \ @@ -676,19 +679,6 @@ M(1006, INVALID_CURSOR_LOOKUP) \ M(1007, ILLEGAL_STREAM) \ M(1008, TEMPORARY_DATA_NOT_IN_CACHE) \ - M(1009, PENDING_MUTATIONS_NOT_ALLOWED) \ - /* 1010 and 1011 predate the fork's error-code range policy stated below, and are kept as-is \ - * rather than renumbered: they currently collide with upstream ClickHouse's own 1010 \ - * (UNIQUE_KEY_DENSE_INDEX_UNREADABLE) and 1011 (HANDLER_ALREADY_EXISTS). */ \ - M(1010, EXPORT_PARTITION_ALREADY_EXPORTED) \ - M(1011, PARTITION_EXPORT_FAILED) \ - /* 1012 and 1013 are intentionally skipped: they collide with upstream ClickHouse's \ - * HANDLER_DOESNT_EXIST and AMBIGUOUS_HANDLER. Fork-specific error codes live in the 1030-1099 \ - * range, chosen to sit well above upstream's maximum error code (1017 at the time this range \ - * was reserved) so upstream can keep adding codes below it without colliding with the fork's. \ - * A new fork error code goes in this range, not below 1030. CAS codes occupy 1037-1038. */ \ - M(1037, CAS_WRITE_UNATTRIBUTED) \ - M(1038, CAS_DELETE_MARKER) \ /* See END */ #ifdef APPLY_FOR_EXTERNAL_ERROR_CODES @@ -705,7 +695,21 @@ namespace ErrorCodes APPLY_FOR_ERROR_CODES(M) #undef M - constexpr ErrorCode END = 1038; +#define M(ID, NAME) extern const ErrorCode NAME = ANTALYA_ERROR_CODE_BASE + ID; + APPLY_FOR_ANTALYA_ERROR_CODES(M) +#undef M + + constexpr ErrorCode getUpstreamEnd() + { + ErrorCode maximum = 0; +#define M(VALUE, NAME) maximum = std::max(maximum, ErrorCode{VALUE}); + APPLY_FOR_ERROR_CODES(M) +#undef M + return maximum + 1; + } + + constexpr ErrorCode END = getUpstreamEnd(); + static_assert(END < ANTALYA_ERROR_CODE_BASE, "Upstream range overlaps the Antalya range"); ErrorPairHolder values[END + 1]{}; struct ErrorCodesNames @@ -719,52 +723,121 @@ namespace ErrorCodes } } static error_codes_names; + namespace + { + struct Entry + { + ErrorCode code; + std::string_view name; + }; + + constexpr auto antalya_entries = std::to_array({ +#define M(ID, NAME) Entry{ANTALYA_ERROR_CODE_BASE + ID, #NAME}, + APPLY_FOR_ANTALYA_ERROR_CODES(M) +#undef M + }); + + constexpr bool validateAntalyaIds() + { + Int64 previous_id = 0; +#define M(ID, NAME) \ + if (Int64{ID} <= previous_id || Int64{ID} > std::numeric_limits::max() - Int64{ANTALYA_ERROR_CODE_BASE}) \ + return false; \ + previous_id = ID; + APPLY_FOR_ANTALYA_ERROR_CODES(M) +#undef M + return true; + } + static_assert(validateAntalyaIds(), "Antalya IDs must be positive, ordered, unique, and fit UInt16 log columns"); + + constexpr bool validateAntalyaNames() + { + for (size_t index = 0; index < antalya_entries.size(); ++index) + { + const auto name = antalya_entries[index].name; + for (size_t previous = 0; previous < index; ++previous) + { + if (name == antalya_entries[previous].name) + return false; + } +#define M(VALUE, NAME) if (name == std::string_view(#NAME)) return false; + APPLY_FOR_ERROR_CODES(M) +#undef M + } + return true; + } + static_assert(validateAntalyaNames(), "Antalya error names must be unique and distinct from upstream names"); + + std::array antalya_values; + + size_t getIndex(ErrorCode code) + { + if (code >= 0 && code <= END) + return code; + for (size_t index = 0; index < antalya_entries.size(); ++index) + { + if (antalya_entries[index].code == code) + return END + 1 + index; + } + /// Preserve out-of-range accounting without attributing it to a registered error. + return END; + } + } + std::string_view getName(ErrorCode error_code) { - if (error_code < 0 || error_code > END) - return std::string_view(); - return error_codes_names.names[error_code]; + if (error_code >= 0 && error_code <= END) + return error_codes_names.names[error_code]; + for (const auto & entry : antalya_entries) + { + if (entry.code == error_code) + return entry.name; + } + return {}; } ErrorCode getErrorCodeByName(std::string_view error_name) { - for (int i = 0, end = ErrorCodes::end(); i < end; ++i) + for (size_t index = 0; index < size(); ++index) { - std::string_view name = ErrorCodes::getName(i); + const auto code = getCode(index); + std::string_view name = getName(code); if (name.empty()) continue; if (name == error_name) - return i; + return code; } throw Exception(NO_SUCH_ERROR_CODE, "No error code with name: '{}'", error_name); } ErrorCode end() { return END + 1; } - size_t increment(ErrorCode error_code, bool remote, const std::string & message, const std::string & format_string, const FramePointers & trace) + size_t size() { return end() + antalya_entries.size(); } + + ErrorCode getCode(size_t index) { - if (error_code < 0 || error_code >= end()) - { - /// For everything outside the range, use END. - /// (end() is the pointer pass the end, while END is the last value that has an element in values array). - error_code = end() - 1; - } + if (index < static_cast(end())) + return static_cast(index); + return antalya_entries.at(index - end()).code; + } - return values[error_code].increment(remote, message, format_string, trace); + ErrorPairHolder & getValue(size_t index) + { + if (index < static_cast(end())) + return values[index]; + return antalya_values.at(index - end()); } - void extendedMessage(ErrorCode error_code, bool remote, size_t error_index, const std::string & message) + size_t increment(ErrorCode error_code, bool remote, const std::string & message, const std::string & format_string, const FramePointers & trace) { - if (error_code < 0 || error_code >= end()) - { - /// For everything outside the range, use END. - /// (end() is the pointer pass the end, while END is the last value that has an element in values array). - error_code = end() - 1; - } + return getValue(getIndex(error_code)).increment(remote, message, format_string, trace); + } - values[error_code].extendedMessage(remote, error_index, message); + void extendedMessage(ErrorCode error_code, bool remote, size_t error_index, const std::string & message) + { + getValue(getIndex(error_code)).extendedMessage(remote, error_index, message); } size_t ErrorPairHolder::increment(bool remote, const std::string & message, const std::string & format_string, const FramePointers & trace) diff --git a/src/Common/ErrorCodes.h b/src/Common/ErrorCodes.h index fe75b8d72c66..3c1a3c7a7697 100644 --- a/src/Common/ErrorCodes.h +++ b/src/Common/ErrorCodes.h @@ -16,7 +16,7 @@ namespace DB namespace ErrorCodes { - /// ErrorCode identifier (index in array). + /// Numeric error identity, including the code transmitted to peers; not a storage index. using ErrorCode = int; using Value = size_t; using FramePointers = std::vector; @@ -65,12 +65,20 @@ namespace ErrorCodes std::mutex mutex; }; - /// ErrorCode identifier -> current value of error_code. + /// Upstream numeric code -> counters. Antalya codes must not index this array. extern ErrorPairHolder values[]; - /// Get index just after last error_code identifier. + /// Get the upstream array boundary, including the unnamed out-of-range accounting slot. ErrorCode end(); + /// Number of counter slots, including upstream holes and the unnamed accounting sentinel. + /// Antalya entries occupy compact slots after the upstream array. + size_t size(); + /// Translate a storage index into the actual error code. The index must be less than `size`. + ErrorCode getCode(size_t index); + /// Access counters by storage index, not by numeric error identity. + ErrorPairHolder & getValue(size_t index); + /// Increments the counter of errors for a specified error code, and remembers some information about the last error. /// The function returns the index of the passed error among other errors with the same code and the same `remote` flag /// since the program startup. diff --git a/src/Common/tests/gtest_error_codes.cpp b/src/Common/tests/gtest_error_codes.cpp new file mode 100644 index 000000000000..05b864dd0c4b --- /dev/null +++ b/src/Common/tests/gtest_error_codes.cpp @@ -0,0 +1,157 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace DB::ErrorCodes +{ +extern const int CATALOG_NAMESPACE_DISABLED; +extern const int CAS_DELETE_MARKER; +extern const int BAD_ARGUMENTS; +} + +namespace +{ +struct ExpectedError +{ + int code; + std::string_view name; +}; + +constexpr auto expected_antalya_errors = std::to_array({ +#define M(ID, NAME) ExpectedError{DB::ErrorCodes::ANTALYA_ERROR_CODE_BASE + ID, #NAME}, + APPLY_FOR_ANTALYA_ERROR_CODES(M) +#undef M +}); +} + +TEST(ErrorCodes, AntalyaIdentity) +{ + EXPECT_EQ(DB::ErrorCodes::CATALOG_NAMESPACE_DISABLED, 10001); + EXPECT_EQ(DB::ErrorCodes::CAS_DELETE_MARKER, 10006); + for (const auto & entry : expected_antalya_errors) + { + EXPECT_EQ(DB::ErrorCodes::getName(entry.code), entry.name); + EXPECT_EQ(DB::ErrorCodes::getErrorCodeByName(entry.name), entry.code); + } + EXPECT_EQ(DB::ErrorCodes::getErrorCodeByName("BAD_ARGUMENTS"), DB::ErrorCodes::BAD_ARGUMENTS); + for (int code : {-1, 10000, 10007, std::numeric_limits::max()}) + EXPECT_TRUE(DB::ErrorCodes::getName(code).empty()); + EXPECT_THROW(DB::ErrorCodes::getErrorCodeByName("NOT_A_REGISTERED_ERROR"), DB::Exception); +} + +TEST(ErrorCodes, ExceptionWireIdentity) +{ + for (int code : {10001, 10006, 10007, -1, std::numeric_limits::max()}) + { + auto original = DB::Exception::createDeprecated("wire identity", code); + DB::WriteBufferFromOwnString output; + DB::writeException(original, output, false); + DB::ReadBufferFromString input(output.str()); + auto decoded = DB::readException(input, "", true); + EXPECT_EQ(decoded.code(), code); + EXPECT_NE(decoded.message().find("wire identity"), std::string::npos); + } +} + +TEST(ErrorCodes, CompactEnumerationAndCounters) +{ + ASSERT_EQ(DB::ErrorCodes::size(), DB::ErrorCodes::end() + expected_antalya_errors.size()); + EXPECT_LT(DB::ErrorCodes::size(), DB::ErrorCodes::CATALOG_NAMESPACE_DISABLED); + EXPECT_TRUE(DB::ErrorCodes::getName(DB::ErrorCodes::end() - 1).empty()); + EXPECT_THROW(DB::ErrorCodes::getCode(DB::ErrorCodes::size()), std::out_of_range); + EXPECT_THROW(DB::ErrorCodes::getValue(DB::ErrorCodes::size()), std::out_of_range); + + for (size_t slot = 0; slot < DB::ErrorCodes::size(); ++slot) + { + const auto code = DB::ErrorCodes::getCode(slot); + const auto name = DB::ErrorCodes::getName(code); + if (!name.empty()) + EXPECT_EQ(DB::ErrorCodes::getErrorCodeByName(name), code); + if (slot < static_cast(DB::ErrorCodes::end())) + EXPECT_EQ(code, slot); + else + { + const auto & expected = expected_antalya_errors.at(slot - DB::ErrorCodes::end()); + EXPECT_EQ(code, expected.code); + EXPECT_EQ(name, expected.name); + } + + if (name.empty()) + continue; + for (bool remote : {false, true}) + { + const auto before = DB::ErrorCodes::getValue(slot).get(); + const auto index = DB::ErrorCodes::increment(code, remote, "slot test", "slot test", {}); + DB::ErrorCodes::extendedMessage(code, remote, index, "slot test extended"); + const auto after = DB::ErrorCodes::getValue(slot).get(); + EXPECT_EQ(after.local.count, before.local.count + !remote); + EXPECT_EQ(after.remote.count, before.remote.count + remote); + EXPECT_EQ((remote ? after.remote : after.local).message, "slot test extended"); + } + } +} + +TEST(ErrorCodes, UnknownAccountingDoesNotPolluteAntalya) +{ + const auto vendor_slot = DB::ErrorCodes::size() - 1; + const auto before = DB::ErrorCodes::getValue(vendor_slot).get(); + const auto sentinel = DB::ErrorCodes::end() - 1; + for (int code : {-1, DB::ErrorCodes::end(), 10000, 10007, std::numeric_limits::max()}) + { + const auto count = DB::ErrorCodes::getValue(sentinel).get().local.count; + const auto index = DB::ErrorCodes::increment(code, false, "unknown", "unknown", {}); + DB::ErrorCodes::extendedMessage(code, false, index, "unknown extended"); + const auto value = DB::ErrorCodes::getValue(sentinel).get().local; + EXPECT_EQ(value.count, count + 1); + EXPECT_EQ(value.message, "unknown extended"); + } + for (int code : {779, DB::ErrorCodes::end() - 2}) + { + ASSERT_GE(code, 0); + ASSERT_LT(code, DB::ErrorCodes::end()); + const auto ordinary_before = DB::ErrorCodes::getValue(code).get().local.count; + DB::ErrorCodes::increment(code, false, "ordinary", "ordinary", {}); + EXPECT_EQ(DB::ErrorCodes::getValue(code).get().local.count, ordinary_before + 1); + } + + const auto after = DB::ErrorCodes::getValue(vendor_slot).get(); + EXPECT_EQ(before.local.count, after.local.count); + EXPECT_EQ(before.remote.count, after.remote.count); +} + +TEST(ErrorCodes, AntalyaCodesSurviveNarrowLogColumns) +{ + auto check_log = [](auto element, int code) + { + element.error = static_cast(code); + DB::MutableColumns columns; + size_t error_column = 0; + for (const auto & column : element.getColumnsDescription().getAllPhysical()) + { + if (column.name == "error") + error_column = columns.size(); + columns.push_back(column.type->createColumn()); + } + element.appendToBlock(columns); + EXPECT_EQ(columns[error_column]->getUInt(0), code); + EXPECT_EQ(DB::ErrorCodes::getName(static_cast(columns[error_column]->getUInt(0))), DB::ErrorCodes::getName(code)); + }; + for (size_t slot = DB::ErrorCodes::end(); slot < DB::ErrorCodes::size(); ++slot) + { + const auto code = DB::ErrorCodes::getCode(slot); + check_log(DB::PartLogElement{}, code); + check_log(DB::BackgroundSchedulePoolLogElement{}, code); + } +} diff --git a/src/Interpreters/ErrorLog.cpp b/src/Interpreters/ErrorLog.cpp index b89ad69fcc71..82627190f43a 100644 --- a/src/Interpreters/ErrorLog.cpp +++ b/src/Interpreters/ErrorLog.cpp @@ -127,15 +127,19 @@ void ErrorLog::stepFunction(TimePoint current_time) auto event_time = std::chrono::system_clock::to_time_t(current_time); - for (ErrorCodes::ErrorCode code = 0, end = ErrorCodes::end(); code < end; ++code) + for (size_t index = 0, size = ErrorCodes::size(); index < size; ++index) { - const auto & error = ErrorCodes::values[code].get(); - if (error.local.count != previous_values.at(code).local) + const auto code = ErrorCodes::getCode(index); + if (index == static_cast(ErrorCodes::end() - 1)) + continue; + const auto error = ErrorCodes::getValue(index).get(); + auto & previous = previous_values.at(index); + if (error.local.count != previous.local) { ErrorLogElement local_elem { .event_time=event_time, .code=code, - .value=error.local.count - previous_values.at(code).local, + .value=error.local.count - previous.local, .remote=false, .last_error_time=(error.local.error_time_ms / 1000), .last_error_message=error.local.message, @@ -143,14 +147,14 @@ void ErrorLog::stepFunction(TimePoint current_time) .last_error_trace=error.local.trace }; this->add(std::move(local_elem)); - previous_values[code].local = error.local.count; + previous.local = error.local.count; } - if (error.remote.count != previous_values.at(code).remote) + if (error.remote.count != previous.remote) { ErrorLogElement remote_elem { .event_time=event_time, .code=code, - .value=error.remote.count - previous_values.at(code).remote, + .value=error.remote.count - previous.remote, .remote=true, .last_error_time=(error.remote.error_time_ms / 1000), .last_error_message=error.remote.message, @@ -158,7 +162,7 @@ void ErrorLog::stepFunction(TimePoint current_time) .last_error_trace=error.remote.trace }; add(std::move(remote_elem)); - previous_values[code].remote = error.remote.count; + previous.remote = error.remote.count; } } } diff --git a/src/Interpreters/ErrorLog.h b/src/Interpreters/ErrorLog.h index 4c317a376313..4fcacf668d9e 100644 --- a/src/Interpreters/ErrorLog.h +++ b/src/Interpreters/ErrorLog.h @@ -43,7 +43,7 @@ class ErrorLog : public PeriodicLog UInt64 remote = 0; }; /// stepFunction and flushBufferToLog may be executed concurrently, hence the mutex - std::vector previous_values TSA_GUARDED_BY(previous_values_mutex) = std::vector(ErrorCodes::end()); + std::vector previous_values TSA_GUARDED_BY(previous_values_mutex) = std::vector(ErrorCodes::size()); mutable std::mutex previous_values_mutex; }; diff --git a/src/Server/PrometheusMetricsWriter.cpp b/src/Server/PrometheusMetricsWriter.cpp index 8ce7036ba018..af30adf0001a 100644 --- a/src/Server/PrometheusMetricsWriter.cpp +++ b/src/Server/PrometheusMetricsWriter.cpp @@ -162,14 +162,15 @@ void PrometheusMetricsWriter::writeErrors(WriteBuffer & wb) const { size_t total_count = 0; - for (size_t i = 0, end = ErrorCodes::end(); i < end; ++i) + for (size_t i = 0, size = ErrorCodes::size(); i < size; ++i) { - const auto & error = ErrorCodes::values[i].get(); - std::string_view name = ErrorCodes::getName(static_cast(i)); + std::string_view name = ErrorCodes::getName(ErrorCodes::getCode(i)); if (name.empty()) continue; + const auto error = ErrorCodes::getValue(i).get(); + std::string key{error_metrics_prefix + toString(name)}; std::string help = fmt::format("The number of {} errors since last server restart", name); diff --git a/src/Storages/System/StorageSystemErrors.cpp b/src/Storages/System/StorageSystemErrors.cpp index e02340239ee2..90b2a9056c29 100644 --- a/src/Storages/System/StorageSystemErrors.cpp +++ b/src/Storages/System/StorageSystemErrors.cpp @@ -58,16 +58,17 @@ void StorageSystemErrors::fillData(MutableColumns & res_columns, ContextPtr cont } }; - for (size_t i = 0, end = ErrorCodes::end(); i < end; ++i) + for (size_t i = 0, size = ErrorCodes::size(); i < size; ++i) { - const auto & error = ErrorCodes::values[i].get(); - std::string_view name = ErrorCodes::getName(static_cast(i)); + const auto code = ErrorCodes::getCode(i); + std::string_view name = ErrorCodes::getName(code); if (name.empty()) continue; - add_row(name, i, error.local, /* remote= */ false); - add_row(name, i, error.remote, /* remote= */ true); + const auto error = ErrorCodes::getValue(i).get(); + add_row(name, code, error.local, /* remote= */ false); + add_row(name, code, error.remote, /* remote= */ true); } } diff --git a/tests/integration/test_antalya_error_codes/__init__.py b/tests/integration/test_antalya_error_codes/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/integration/test_antalya_error_codes/configs/errors.xml b/tests/integration/test_antalya_error_codes/configs/errors.xml new file mode 100644 index 000000000000..b1cffa725e14 --- /dev/null +++ b/tests/integration/test_antalya_error_codes/configs/errors.xml @@ -0,0 +1,16 @@ + + + system + error_log
+ 100 + 100 +
+ + /metrics + 8001 + false + false + false + true + +
diff --git a/tests/integration/test_antalya_error_codes/test.py b/tests/integration/test_antalya_error_codes/test.py new file mode 100644 index 000000000000..89012ce7f6f7 --- /dev/null +++ b/tests/integration/test_antalya_error_codes/test.py @@ -0,0 +1,161 @@ +import re + +import pytest +import requests + +from helpers.cluster import ClickHouseCluster + + +cluster = ClickHouseCluster(__file__) +origin = cluster.add_instance("origin", main_configs=["configs/errors.xml"]) +relay = cluster.add_instance("relay", main_configs=["configs/errors.xml"]) +edge = cluster.add_instance("edge", main_configs=["configs/errors.xml"]) + +ERRORS = [ + (10001, "CATALOG_NAMESPACE_DISABLED"), + (10002, "PENDING_MUTATIONS_NOT_ALLOWED"), + (10003, "EXPORT_PARTITION_ALREADY_EXPORTED"), + (10004, "PARTITION_EXPORT_FAILED"), + (10005, "CAS_WRITE_UNATTRIBUTED"), + (10006, "CAS_DELETE_MARKER"), +] +SETTINGS = {"allow_custom_error_code_in_throwif": 1} + + +@pytest.fixture(scope="module", autouse=True) +def started_cluster(): + try: + cluster.start() + yield + finally: + cluster.shutdown() + + +@pytest.mark.parametrize("code", [-1, 1009, 10000, 10007, 2147483647]) +def test_unknown_code_is_not_logged_as_sentinel(code): + message = f"unknown code {code} must not become sentinel" + error = origin.query_and_get_error( + f"SELECT throwIf(1, '{message}', toInt32({code}))", settings=SETTINGS + ) + assert f"Code: {code}." in error + origin.query("SYSTEM FLUSH LOGS") + assert ( + origin.query( + "SELECT count() FROM system.error_log " + f"WHERE position(last_error_message, '{message}') > 0" + ) + == "0\n" + ) + assert ( + origin.query( + "SELECT count() FROM system.query_log WHERE type IN " + "('ExceptionBeforeStart', 'ExceptionWhileProcessing') " + f"AND exception_code = {code} AND position(exception, '{message}') > 0" + ) + == "1\n" + ) + + +@pytest.mark.parametrize("code", [779, 780, 899]) +def test_upstream_gap_code_is_logged_with_original_identity(code): + message = f"upstream gap code {code} must retain its identity" + assert origin.query(f"SELECT errorCodeToName({code})") == "\n" + error = origin.query_and_get_error( + f"SELECT throwIf(1, '{message}', toInt32({code}))", settings=SETTINGS + ) + assert f"Code: {code}." in error + origin.query("SYSTEM FLUSH LOGS") + assert ( + origin.query( + "SELECT DISTINCT code FROM system.error_log " + f"WHERE position(last_error_message, '{message}') > 0" + ) + == f"{code}\n" + ) + + +@pytest.mark.parametrize("code,name", ERRORS) +def test_local_observability(code, name): + message = f"Antalya error {code}" + error = origin.query_and_get_error( + f"SELECT throwIf(1, '{message}', toInt32({code}))", settings=SETTINGS + ) + assert f"Code: {code}." in error + assert name in error + assert message in error + assert ( + origin.query( + f"SELECT count() FROM system.errors WHERE code = {code} " + f"AND name = '{name}' AND remote = 0 AND value > 0 " + f"AND position(last_error_message, '{message}') > 0" + ) + == "1\n" + ) + + origin.query("SYSTEM FLUSH LOGS") + assert ( + int( + origin.query( + f"SELECT count() FROM system.error_log WHERE code = {code} " + f"AND remote = 0 AND value > 0 AND position(last_error_message, '{message}') > 0" + ) + ) + > 0 + ) + + response = requests.get(f"http://{origin.ip_address}:8001/metrics", timeout=10) + response.raise_for_status() + metric = re.search(rf"^ClickHouseErrorMetric_{name} (\d+)$", response.text, re.MULTILINE) + assert metric is not None + assert int(metric.group(1)) > 0 + + +@pytest.mark.parametrize("code,name", [ERRORS[0], ERRORS[-1], (10007, "")]) +def test_distributed_rethrow(code, name): + table = f"vendor_error_{code}" + message = f"Antalya distributed error {code}" + try: + origin.query( + f"CREATE VIEW {table} AS SELECT throwIf(1, '{message}', toInt32({code})) AS value", + settings=SETTINGS, + ) + relay.query( + f"CREATE VIEW {table} AS SELECT * FROM remote('origin', default, {table})" + ) + error = edge.query_and_get_error( + f"SELECT * FROM remote('relay', default, {table})", settings=SETTINGS + ) + assert f"Code: {code}." in error + assert message in error + if name: + assert name in error + for node in (relay, edge): + assert ( + int( + node.query( + f"SELECT sum(value) FROM system.errors WHERE code = {code} " + f"AND name = '{name}' AND remote = 1" + ) + ) + > 0 + ) + node.query("SYSTEM FLUSH LOGS") + assert ( + int( + node.query( + f"SELECT count() FROM system.error_log WHERE code = {code} AND remote = 1" + ) + ) + > 0 + ) + else: + assert ( + edge.query( + f"SELECT count() FROM system.errors WHERE code = {code} " + "SETTINGS system_events_show_zero_values = 1" + ) + == "0\n" + ) + finally: + relay.query(f"DROP VIEW IF EXISTS {table}") + origin.query(f"DROP VIEW IF EXISTS {table}") diff --git a/tests/queries/0_stateless/05055_antalya_error_codes.reference b/tests/queries/0_stateless/05055_antalya_error_codes.reference new file mode 100644 index 000000000000..fe581e47b293 --- /dev/null +++ b/tests/queries/0_stateless/05055_antalya_error_codes.reference @@ -0,0 +1,13 @@ +CATALOG_NAMESPACE_DISABLED 10001 +PENDING_MUTATIONS_NOT_ALLOWED 10002 +EXPORT_PARTITION_ALREADY_EXPORTED 10003 +PARTITION_EXPORT_FAILED 10004 +CAS_WRITE_UNATTRIBUTED 10005 +CAS_DELETE_MARKER 10006 +CATALOG_NAMESPACE_DISABLED +PENDING_MUTATIONS_NOT_ALLOWED +EXPORT_PARTITION_ALREADY_EXPORTED +PARTITION_EXPORT_FAILED +CAS_WRITE_UNATTRIBUTED +CAS_DELETE_MARKER + diff --git a/tests/queries/0_stateless/05055_antalya_error_codes.sql b/tests/queries/0_stateless/05055_antalya_error_codes.sql new file mode 100644 index 000000000000..7fb6f06e1080 --- /dev/null +++ b/tests/queries/0_stateless/05055_antalya_error_codes.sql @@ -0,0 +1,13 @@ +SELECT name, code FROM system.errors +WHERE name IN ('CATALOG_NAMESPACE_DISABLED', 'PENDING_MUTATIONS_NOT_ALLOWED', + 'EXPORT_PARTITION_ALREADY_EXPORTED', 'PARTITION_EXPORT_FAILED', + 'CAS_WRITE_UNATTRIBUTED', 'CAS_DELETE_MARKER') AND remote = 0 +ORDER BY code SETTINGS system_events_show_zero_values = 1; + +SELECT errorCodeToName(number) FROM numbers(10001, 6); +SELECT errorCodeToName(toInt32(-1)), errorCodeToName(10000), errorCodeToName(10007), errorCodeToName(2147483647); + +SET allow_custom_error_code_in_throwif = 1; +SELECT throwIf(1, 'Antalya namespace regression', toInt32(10001)); -- { serverError CATALOG_NAMESPACE_DISABLED } +SELECT throwIf(1, 'Antalya namespace regression', toInt32(10006)); -- { serverError CAS_DELETE_MARKER } +SELECT throwIf(1, 'Unknown Antalya code', toInt32(10007)); -- { serverError 10007 }