Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ci/jobs/scripts/check_style/check_cpp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
74 changes: 74 additions & 0 deletions docs/en/antalya/error_codes.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions src/Common/AntalyaErrorCodes.h
Original file line number Diff line number Diff line change
@@ -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;
}
147 changes: 110 additions & 37 deletions src/Common/ErrorCodes.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#include <Common/CurrentThread.h>
#include <Common/AntalyaErrorCodes.h>
#include <Common/ErrorCodes.h>
#include <Common/Exception.h>
#include <chrono>
#include <algorithm>
#include <array>
#include <limits>

/** 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
Expand Down Expand Up @@ -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) \
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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<Entry>({
#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<UInt16>::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<ErrorPairHolder, antalya_entries.size()> 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<size_t>(end()))
return static_cast<ErrorCode>(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<size_t>(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)
Expand Down
14 changes: 11 additions & 3 deletions src/Common/ErrorCodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<void *>;
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading