From 566dd987ed2fd0318c242b3f93ba0ca57e951b37 Mon Sep 17 00:00:00 2001 From: Andrey Zvonov <32552679+zvonand@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:35:09 +0200 Subject: [PATCH 1/2] Cherry-pick of https://github.com/Altinity/ClickHouse/pull/2378 with unresolved conflict markers (resolution in next commit) --- Original cherry-pick message follows: Merge pull request #2378 from Altinity/feature/antalya-26.6/antalya-protocol-version Antalya 26.6: Add an Antalya-only protocol version advertised in the `ServerHello` # Conflicts: # src/Server/TCPHandler.cpp --- docs/en/antalya/protocol.md | 50 ++++++++++++++++ .../interfaces/specs/NativeProtocol.mdx | 2 +- src/Client/Connection.cpp | 10 +++- src/Client/Connection.h | 1 + src/Core/AntalyaProtocol.cpp | 60 +++++++++++++++++++ src/Core/AntalyaProtocol.h | 23 +++++++ src/Core/tests/gtest_antalya_protocol.cpp | 34 +++++++++++ src/Server/TCPHandler.cpp | 6 +- .../test_antalya_protocol/__init__.py | 0 .../integration/test_antalya_protocol/test.py | 48 +++++++++++++++ .../02116_interactive_hello.expect | 2 +- .../03272_client_highlighting_bug.expect | 2 +- 12 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 docs/en/antalya/protocol.md create mode 100644 src/Core/AntalyaProtocol.cpp create mode 100644 src/Core/AntalyaProtocol.h create mode 100644 src/Core/tests/gtest_antalya_protocol.cpp create mode 100644 tests/integration/test_antalya_protocol/__init__.py create mode 100644 tests/integration/test_antalya_protocol/test.py diff --git a/docs/en/antalya/protocol.md b/docs/en/antalya/protocol.md new file mode 100644 index 000000000000..f7ca781159e7 --- /dev/null +++ b/docs/en/antalya/protocol.md @@ -0,0 +1,50 @@ +--- +description: 'How the Antalya fork versions its own wire-protocol changes independently of upstream ClickHouse' +sidebar_label: 'Antalya Protocol Version' +sidebar_position: 40 +slug: /antalya/protocol +title: 'Antalya Protocol Version' +doc_type: 'reference' +--- + +# Antalya protocol version {#antalya-protocol-version} + +Antalya versions its own wire-protocol changes with `DBMS_ANTALYA_PROTOCOL_VERSION`, a counter that +upstream ClickHouse cannot reach, defined in `src/Core/AntalyaProtocol.h`. A server advertises it in +the `ServerHello` name string, on every connection: + +```text +server -> client "ClickHouse (antalya:1)" +``` + +The client parses the suffix, caps the value with `min(own, server)` and keeps the result. `0` means +the peer is not an Antalya build. Negotiation is per hop and not transitive: initiator to worker and +worker to worker negotiate independently. + +Version 1 is the advertisement itself. Nothing is gated on it yet. + +## Adding an Antalya-only wire change {#adding-a-wire-change} + +- Bump `DBMS_ANTALYA_PROTOCOL_VERSION` by one and gate the change on the negotiated value. +- Never bump `DBMS_TCP_PROTOCOL_VERSION`, and never take a slot in + `DBMS_CLUSTER_PROCESSING_PROTOCOL_VERSION` for a feature upstream does not have. +- Keep the counter cumulative. A backport takes the whole contiguous range up to the value it needs, + or does not bump at all - the `min(own, server)` cap is only sound for a cumulative feature set. +- Gate only what the *client* decides to do. The server never learns the client's version, because + only the server advertises. +- Update this page, and update `docs/en/interfaces/specs/NativeProtocol.md` when the change alters + a packet layout described there. + +## Why a counter of our own {#why-a-counter-of-our-own} + +An upstream rebase can reuse the next value of an upstream protocol counter for a different feature. +Keeping the Antalya counter separate prevents the same version from describing two wire layouts. + +## Why the marker rides in `ServerHello` {#why-the-marker-rides-in-serverhello} + +The client `Hello` cannot advertise the version because it is sent before the peer is known. Its +`client_name` is also stored and validated against the Query packet, so changing it can raise +`CLIENT_INFO_DOES_NOT_MATCH` on an upstream peer. + +The server advertises through `server_name`, which is display text. The marker stays inside that +existing string because adding a field would make older peers read it as the next packet. diff --git a/docs/reference/interfaces/specs/NativeProtocol.mdx b/docs/reference/interfaces/specs/NativeProtocol.mdx index 6fd620ad09c3..6cf5f0eb8d00 100644 --- a/docs/reference/interfaces/specs/NativeProtocol.mdx +++ b/docs/reference/interfaces/specs/NativeProtocol.mdx @@ -449,7 +449,7 @@ Server → Client. The reply to ClientHello on successful authentication. | # | Field | Type | Role | Condition | Description | |---|------------------|---------|-----------|------------------------|-------------| -| 1 | server_name | String | universal | always | Server identifier | +| 1 | server_name | String | universal | always | Server identifier. An Altinity Antalya build appends `" (antalya:N)"`, where `N` is its Antalya protocol version; a client may ignore or strip the suffix. See [Antalya protocol version](/antalya/protocol). | | 2 | version_major | VarUInt | universal | always | Server major version | | 3 | version_minor | VarUInt | universal | always | Server minor version | | 4 | protocol_version | VarUInt | universal | always | Server's protocol version | diff --git a/src/Client/Connection.cpp b/src/Client/Connection.cpp index 901899ebcbab..2313a79ce83b 100644 --- a/src/Client/Connection.cpp +++ b/src/Client/Connection.cpp @@ -29,7 +29,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -425,8 +427,11 @@ void Connection::connect(const ConnectionTimeouts & timeouts) if (proto_recv_chunked == "chunked") in->enableChunked(); - LOG_TRACE(log_wrapper.get(), "Connected to {} server version {}.{}.{}.", - server_name, server_version_major, server_version_minor, server_version_patch); + LOG_TRACE(log_wrapper.get(), "Connected to {} server version {}.{}.{}{}.", + server_name, server_version_major, server_version_minor, server_version_patch, + (server_antalya_protocol_version > 0 + ? ", Antalya protocol: " + std::to_string(server_antalya_protocol_version) + : "")); /// Now that the handshake is complete, use the regular timeouts socket->setReceiveTimeout(timeouts.receive_timeout); @@ -678,6 +683,7 @@ void Connection::receiveHello() { readStringBinary(server_name, *in, DBMS_MAX_HELLO_STRING_SIZE); sanitizeUntrustedServerString(server_name); + server_antalya_protocol_version = AntalyaProtocol::parseMarker(server_name); readVarUInt(server_version_major, *in); readVarUInt(server_version_minor, *in); readVarUInt(server_revision, *in); diff --git a/src/Client/Connection.h b/src/Client/Connection.h index 7049e65b9218..ac5d4f57e84c 100644 --- a/src/Client/Connection.h +++ b/src/Client/Connection.h @@ -273,6 +273,7 @@ class Connection : public IServerConnection UInt64 server_parallel_replicas_protocol_version = 0; UInt64 worker_cluster_function_protocol_version = 0; UInt64 server_query_plan_serialization_version = 0; + UInt64 server_antalya_protocol_version = 0; String server_timezone; String server_display_name; SettingsChanges settings_from_server; diff --git a/src/Core/AntalyaProtocol.cpp b/src/Core/AntalyaProtocol.cpp new file mode 100644 index 000000000000..b697a52e2c3a --- /dev/null +++ b/src/Core/AntalyaProtocol.cpp @@ -0,0 +1,60 @@ +#include + +#include + +#include + + +namespace DB +{ + +namespace AntalyaProtocol +{ + +constexpr std::string_view MARKER_PREFIX = " (antalya:"; +constexpr size_t MAX_MARKER_DIGITS = 9; +constexpr size_t MAX_MARKER_SIZE = MARKER_PREFIX.size() + MAX_MARKER_DIGITS + 1; +constexpr UInt64 MAX_MARKER_VERSION = 999999999; + +static_assert( + DBMS_ANTALYA_PROTOCOL_VERSION >= 1 && DBMS_ANTALYA_PROTOCOL_VERSION <= MAX_MARKER_VERSION, + "DBMS_ANTALYA_PROTOCOL_VERSION does not fit the marker grammar"); + +String appendMarker(std::string_view name) +{ + String result; + result.reserve(name.size() + MAX_MARKER_SIZE); + result.append(name); + result.append(MARKER_PREFIX); + result.append(std::to_string(DBMS_ANTALYA_PROTOCOL_VERSION)); + result.push_back(')'); + return result; +} + +UInt64 parseMarker(std::string_view name) +{ + if (name.empty() || name.back() != ')') + return 0; + + const size_t marker_pos = name.rfind(MARKER_PREFIX); + if (marker_pos == std::string_view::npos) + return 0; + + const size_t first_digit = marker_pos + MARKER_PREFIX.size(); + const size_t digits = name.size() - first_digit - 1; + if (digits == 0 || digits > MAX_MARKER_DIGITS || name[first_digit] == '0') + return 0; + + UInt64 version; + const char * begin = name.data() + first_digit; + const char * end = name.data() + name.size() - 1; + const auto result = std::from_chars(begin, end, version); + if (result.ec != std::errc{} || result.ptr != end) + return 0; + + return std::min(version, DBMS_ANTALYA_PROTOCOL_VERSION); +} + +} + +} diff --git a/src/Core/AntalyaProtocol.h b/src/Core/AntalyaProtocol.h new file mode 100644 index 000000000000..c1bb7ece6776 --- /dev/null +++ b/src/Core/AntalyaProtocol.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +namespace DB +{ + +/// Bump for every Antalya-only wire protocol change. See `docs/en/antalya/protocol.md`. +static constexpr auto DBMS_ANTALYA_PROTOCOL_VERSION = 1; + +namespace AntalyaProtocol +{ + +String appendMarker(std::string_view name); + +/// Returns the negotiated version, or `0` if there is no marker. +UInt64 parseMarker(std::string_view name); + +} + +} diff --git a/src/Core/tests/gtest_antalya_protocol.cpp b/src/Core/tests/gtest_antalya_protocol.cpp new file mode 100644 index 000000000000..57fdbe5ef035 --- /dev/null +++ b/src/Core/tests/gtest_antalya_protocol.cpp @@ -0,0 +1,34 @@ +#include + +#include + +using namespace DB; +using namespace DB::AntalyaProtocol; + +TEST(AntalyaProtocol, AppendMarkerSpellsTheWireForm) +{ + EXPECT_EQ( + appendMarker("ClickHouse server"), + "ClickHouse server (antalya:" + std::to_string(DBMS_ANTALYA_PROTOCOL_VERSION) + ")"); +} + +TEST(AntalyaProtocol, RejectsInvalidMarkers) +{ + const String rejected[] = { + "", + "ClickHouse server", + "ClickHouse server (antalya:1", + "ClickHouse server (antalya:0)", + "ClickHouse server (antalya:01)", + "ClickHouse server (antalya:1234567890)", + "ClickHouse server (antalya:1x)", + }; + + for (auto name : rejected) + EXPECT_EQ(parseMarker(name), 0u) << "should not have parsed: " << name; +} + +TEST(AntalyaProtocol, ParsesMarkerAndCapsVersion) +{ + EXPECT_EQ(parseMarker("ClickHouse server (antalya:999999999)"), static_cast(DBMS_ANTALYA_PROTOCOL_VERSION)); +} diff --git a/src/Server/TCPHandler.cpp b/src/Server/TCPHandler.cpp index eb0d48fe301f..e230c38a9b12 100644 --- a/src/Server/TCPHandler.cpp +++ b/src/Server/TCPHandler.cpp @@ -10,7 +10,11 @@ #include #include #include +<<<<<<< HEAD #include +======= +#include +>>>>>>> b73e7588a4f (Merge pull request #2378 from Altinity/feature/antalya-26.6/antalya-protocol-version) #include #include #include @@ -2353,7 +2357,7 @@ void TCPHandler::processUnexpectedHello() void TCPHandler::sendHello() { writeVarUInt(Protocol::Server::Hello, *out); - writeStringBinary(VERSION_NAME, *out); + writeStringBinary(AntalyaProtocol::appendMarker(VERSION_NAME), *out); writeVarUInt(VERSION_MAJOR, *out); writeVarUInt(VERSION_MINOR, *out); writeVarUInt(DBMS_TCP_PROTOCOL_VERSION, *out); diff --git a/tests/integration/test_antalya_protocol/__init__.py b/tests/integration/test_antalya_protocol/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/integration/test_antalya_protocol/test.py b/tests/integration/test_antalya_protocol/test.py new file mode 100644 index 000000000000..81404c264cae --- /dev/null +++ b/tests/integration/test_antalya_protocol/test.py @@ -0,0 +1,48 @@ +import pytest + +from helpers.cluster import CLICKHOUSE_CI_MIN_TESTED_VERSION, ClickHouseCluster + +cluster = ClickHouseCluster(__file__) + +node1 = cluster.add_instance("node1") +node2 = cluster.add_instance("node2") +# An unmarked build predating Antalya protocol negotiation. +node_old = cluster.add_instance( + "node_old", + image="altinity/clickhouse-server", + tag=CLICKHOUSE_CI_MIN_TESTED_VERSION, + with_installed_binary=True, +) + +NEGOTIATED = "Antalya protocol: " + + +@pytest.fixture(scope="module") +def started_cluster(): + try: + cluster.start() + yield cluster + finally: + cluster.shutdown() + + +def count_in_log(node, substring): + return int(node.count_in_log(substring)) + + +def test_remote_function_negotiates(started_cluster): + initiator_before = count_in_log(node1, NEGOTIATED) + + assert node1.query("SELECT count() FROM remote('node2', system.one)") == "1\n" + + assert count_in_log(node1, NEGOTIATED) > initiator_before + + +def test_new_initiator_against_an_unmarked_worker(started_cluster): + before = count_in_log(node1, NEGOTIATED) + assert node1.query("SELECT count() FROM remote('node_old', numbers(10))") == "10\n" + assert count_in_log(node1, NEGOTIATED) == before + + +def test_unmarked_initiator_against_a_marked_server(started_cluster): + assert node_old.query("SELECT count() FROM remote('node1', numbers(10))") == "10\n" diff --git a/tests/queries/0_stateless/02116_interactive_hello.expect b/tests/queries/0_stateless/02116_interactive_hello.expect index 2bad60c6e412..c5a114b60db6 100755 --- a/tests/queries/0_stateless/02116_interactive_hello.expect +++ b/tests/queries/0_stateless/02116_interactive_hello.expect @@ -26,7 +26,7 @@ spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_CLIENT_BINARY \ # (?n) - Do not match new lines expect -re "(?n)ClickHouse client version \[\\d\]{2}\.\[\\d\]{1,2}\.\[\\d\]{1,2}\..*\r" expect -re "(?n)Connecting to database .* at localhost:9000 as user default\.\r" -expect -re "(?n)Connected to ClickHouse server version \[\\d\]{2}\.\[\\d\]{1,2}\.\[\\d\]{1,2}\.\r" +expect -re "(?n)Connected to ClickHouse( \\(antalya:\[\\d\]+\\))? server version \[\\d\]{2}\.\[\\d\]{1,2}\.\[\\d\]{1,2}\.\r" expect ":) " send -- "" diff --git a/tests/queries/0_stateless/03272_client_highlighting_bug.expect b/tests/queries/0_stateless/03272_client_highlighting_bug.expect index bd834318b711..245d2657158a 100755 --- a/tests/queries/0_stateless/03272_client_highlighting_bug.expect +++ b/tests/queries/0_stateless/03272_client_highlighting_bug.expect @@ -27,7 +27,7 @@ spawn bash -c "source $basedir/../shell_config.sh ; \$CLICKHOUSE_CLIENT_BINARY \ # (?n) - Do not match new lines expect -re "(?n)ClickHouse client version \[\\d\]{2}\.\[\\d\]{1,2}\.\[\\d\]{1,2}\..*\r" expect -re "(?n)Connecting to database .* at localhost:9000 as user default\.\r" -expect -re "(?n)Connected to ClickHouse server version \[\\d\]{2}\.\[\\d\]{1,2}\.\[\\d\]{1,2}\.\r" +expect -re "(?n)Connected to ClickHouse( \\(antalya:\[\\d\]+\\))? server version \[\\d\]{2}\.\[\\d\]{1,2}\.\[\\d\]{1,2}\.\r" expect ":) " send -- "SELECT (+123) AS x" From b856925e9c67f5259790525aa921605b2181ed88 Mon Sep 17 00:00:00 2001 From: Andrey Zvonov <32552679+zvonand@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:39:00 +0200 Subject: [PATCH 2/2] Resolve conflicts in cherry-pick of #2378 Kept both `Core/Block.h` (present on antalya-26.8) and `Core/AntalyaProtocol.h` (added by the source PR) includes in `src/Server/TCPHandler.cpp`. --- src/Server/TCPHandler.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Server/TCPHandler.cpp b/src/Server/TCPHandler.cpp index e230c38a9b12..d6a71d8220e6 100644 --- a/src/Server/TCPHandler.cpp +++ b/src/Server/TCPHandler.cpp @@ -10,11 +10,8 @@ #include #include #include -<<<<<<< HEAD -#include -======= #include ->>>>>>> b73e7588a4f (Merge pull request #2378 from Altinity/feature/antalya-26.6/antalya-protocol-version) +#include #include #include #include