Skip to content

Commit 1918252

Browse files
committed
Preserve current endpoint across failed reconnects
Keep the current endpoint set to the last successfully connected endpoint. ResetConnection retries only that endpoint, while ResetConnectionEndpoint tries it first before failing over to the remaining endpoints. Restore the previous endpoint if all reconnection attempts fail. Validate non-empty endpoint lists in RoundRobinEndpointsIterator and add coverage for reconnect and failover ordering.
1 parent d19e5e9 commit 1918252

6 files changed

Lines changed: 157 additions & 65 deletions

File tree

clickhouse/base/endpoints_iterator.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,21 @@
33

44
namespace clickhouse {
55

6+
namespace {
7+
8+
const std::vector<Endpoint> & ValidateEndpoints(const std::vector<Endpoint>& endpoints)
9+
{
10+
if (endpoints.empty()) {
11+
throw ValidationError("The list of endpoints is empty");
12+
}
13+
return endpoints;
14+
}
15+
16+
} // anonymous namespace
17+
618
RoundRobinEndpointsIterator::RoundRobinEndpointsIterator(const std::vector<Endpoint>& _endpoints)
7-
: endpoints (_endpoints)
19+
: endpoints (ValidateEndpoints(_endpoints))
20+
// set `current_index` to the values such that `Next` returns an element at index 0
821
, current_index (endpoints.size() - 1ull)
922
{
1023
}

clickhouse/client.cpp

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -179,15 +179,6 @@ std::unique_ptr<SocketFactory> GetSocketFactory(const ClientOptions& opts) {
179179
return std::make_unique<NonSecureSocketFactory>();
180180
}
181181

182-
std::unique_ptr<EndpointsIteratorBase> GetEndpointsIterator(const ClientOptions& opts) {
183-
if (opts.endpoints.empty())
184-
{
185-
throw ValidationError("The list of endpoints is empty");
186-
}
187-
188-
return std::make_unique<RoundRobinEndpointsIterator>(opts.endpoints);
189-
}
190-
191182
} // anonymous namespace
192183

193184
class Client::Impl {
@@ -269,8 +260,6 @@ class Client::Impl {
269260
/// call fuc several times.
270261
void RetryGuard(std::function<void()> func);
271262

272-
void RetryConnectToTheEndpoint(std::function<void()>& func);
273-
274263
private:
275264
enum class State : uint8_t {
276265
Idle = 0,
@@ -312,6 +301,8 @@ class Client::Impl {
312301
std::unique_ptr<SocketBase> socket_;
313302
std::unique_ptr<EndpointsIteratorBase> endpoints_iterator;
314303

304+
// current_endpoint_ points to the last successfully connected endpoint, and always
305+
// holds a value. The variable remain wrapped as optional for backwards compatibility.
315306
std::optional<Endpoint> current_endpoint_;
316307

317308
ServerInfo server_info_;
@@ -337,7 +328,8 @@ Client::Impl::Impl(const ClientOptions& opts,
337328
: options_(modifyClientOptions(opts))
338329
, events_(nullptr)
339330
, socket_factory_(std::move(socket_factory))
340-
, endpoints_iterator(GetEndpointsIterator(options_))
331+
, endpoints_iterator(std::make_unique<RoundRobinEndpointsIterator>(options_.endpoints))
332+
, current_endpoint_(endpoints_iterator->Next())
341333
{
342334
CreateConnection();
343335

@@ -614,36 +606,39 @@ void Client::Impl::ResetConnection() {
614606
}
615607

616608
void Client::Impl::ResetConnectionEndpoint() {
617-
current_endpoint_.reset();
618-
for (size_t i = 0; i < options_.endpoints.size();)
609+
std::optional<Endpoint> last_endpoint = current_endpoint_;
610+
for (size_t i = 1; ; ++i)
619611
{
620612
try
621613
{
622-
current_endpoint_ = endpoints_iterator->Next();
623614
ResetConnection();
624615
return;
625616
} catch (const std::system_error&) {
626-
if (++i == options_.endpoints.size())
617+
current_endpoint_ = endpoints_iterator->Next();
618+
if (i >= options_.endpoints.size())
627619
{
628-
current_endpoint_.reset();
620+
current_endpoint_ = last_endpoint;
629621
throw;
630622
}
623+
} catch (...) {
624+
current_endpoint_ = last_endpoint;
625+
throw;
631626
}
632627
}
633628
}
634629

635630
void Client::Impl::CreateConnection() {
636631
// make sure to try to connect to each endpoint at least once even if `options_.send_retries` is 0
637632
const size_t max_attempts = (options_.send_retries ? options_.send_retries : 1);
638-
for (size_t i = 0; i < max_attempts;)
633+
for (size_t i = 1; ; ++i)
639634
{
640635
try
641636
{
642637
// Try to connect to each endpoint before throwing exception.
643638
ResetConnectionEndpoint();
644639
return;
645640
} catch (const std::system_error&) {
646-
if (++i >= max_attempts)
641+
if (i >= max_attempts)
647642
{
648643
throw;
649644
}
@@ -1227,33 +1222,36 @@ bool Client::Impl::ReceiveHello() {
12271222

12281223
void Client::Impl::RetryGuard(std::function<void()> func) {
12291224

1230-
if (current_endpoint_)
1231-
{
1232-
for (unsigned int i = 0; ; ++i) {
1233-
try {
1234-
func();
1235-
return;
1236-
} catch (const std::system_error&) {
1237-
bool ok = true;
1225+
for (unsigned int i = 1; ; ++i) {
1226+
try {
1227+
func();
1228+
return;
1229+
} catch (const std::system_error&) {
1230+
// if send_retries == 0 do not try anymore, throw right away
1231+
if (options_.send_retries == 0) {
1232+
throw;
1233+
}
12381234

1239-
try {
1240-
socket_factory_->sleepFor(options_.retry_timeout);
1241-
ResetConnection();
1242-
} catch (...) {
1243-
ok = false;
1244-
}
1235+
// If `send_retries` attempts failed, try other endpoints
1236+
if (i >= options_.send_retries) {
1237+
break;
1238+
}
12451239

1246-
if (!ok && i == options_.send_retries) {
1247-
break;
1248-
}
1240+
// otherwise sleep and try again
1241+
try {
1242+
socket_factory_->sleepFor(options_.retry_timeout);
1243+
ResetConnection();
1244+
} catch (const std::system_error&) {
12491245
}
1246+
12501247
}
12511248
}
1249+
12521250
// Connections with current_endpoint_ are broken.
1253-
// Trying to establish with the another one from the list.
1251+
// Trying to establish with another one from the list.
12541252
size_t connection_attempts_count = options_.endpoints.size() * options_.send_retries;
1255-
1256-
for (size_t i = 0; i < connection_attempts_count;)
1253+
std::optional<Endpoint> last_endpoint = current_endpoint_;
1254+
for (size_t i = 1; ; ++i)
12571255
{
12581256
try
12591257
{
@@ -1263,11 +1261,14 @@ void Client::Impl::RetryGuard(std::function<void()> func) {
12631261
func();
12641262
return;
12651263
} catch (const std::system_error&) {
1266-
if (++i == connection_attempts_count)
1264+
if (i >= connection_attempts_count)
12671265
{
1268-
current_endpoint_.reset();
1266+
current_endpoint_ = last_endpoint;
12691267
throw;
12701268
}
1269+
} catch (...) {
1270+
current_endpoint_ = last_endpoint;
1271+
throw;
12711272
}
12721273
}
12731274
}

clickhouse/client.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,11 +338,12 @@ class Client {
338338

339339
const ServerInfo& GetServerInfo() const;
340340

341-
/// Get current connected endpoint.
342-
/// In case when client is not connected to any endpoint, nullopt will returned.
341+
/// Get current endpoint, i.e. the last successfully connected endpoint.
342+
/// It remains optional for backward compatibility, but now always contains a value.
343343
const std::optional<Endpoint>& GetCurrentEndpoint() const;
344344

345-
// Try to connect to different endpoints one by one only one time. If it doesn't work, throw an exception.
345+
/// Try to reconnect to different endpoints one by one only one time. If it doesn't work, throw
346+
/// an exception. The function starts with the last successfully connected endpoint.
346347
void ResetConnectionEndpoint();
347348

348349
struct Version

ut/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ cc_test(
110110
"readonly_client_test.cpp",
111111
"readonly_client_test.h",
112112
"roundtrip_tests.cpp",
113+
"test_socket_factory_adapters.h",
113114
# Test entry point and shared support code.
114115
"main.cpp",
115116
"roundtrip_column.cpp",

ut/client_ut.cpp

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include "readonly_client_test.h"
1010
#include "connection_failed_client_test.h"
11+
#include "test_socket_factory_adapters.h"
1112
#include "ut/utils_comparison.h"
1213
#include "utils.h"
1314
#include "ut/roundtrip_column.h"
@@ -1849,31 +1850,44 @@ INSTANTIATE_TEST_SUITE_P(MultipleEndpointsFailed, ConnectionFailedClientTest,
18491850

18501851
class ResetConnectionTestCase : public testing::TestWithParam<ClientOptions> {};
18511852

1852-
TEST_P(ResetConnectionTestCase, ResetConnectionEndpointTest) {
1853-
const auto & client_options = GetParam();
1854-
std::unique_ptr<Client> client;
1853+
TEST(ResetConnectionEndpointTest, ReconnectsCurrentBeforeFailover) {
1854+
const Endpoint primary{"primary", 9000};
1855+
const Endpoint secondary{"secondary", 9000};
1856+
const Endpoint actual_endpoint{LocalHostEndpoint.host, LocalHostEndpoint.port};
18551857

1856-
try {
1857-
client = std::make_unique<Client>(client_options);
1858-
auto endpoint = client->GetCurrentEndpoint().value();
1859-
ASSERT_EQ("localhost", endpoint.host);
1860-
ASSERT_EQ(9000u, endpoint.port);
1858+
ClientOptions options(LocalHostEndpoint);
1859+
options.SetHost("");
1860+
options.SetEndpoints({primary, secondary});
18611861

1862-
client->ResetConnectionEndpoint();
1863-
endpoint = client->GetCurrentEndpoint().value();
1864-
ASSERT_EQ("127.0.0.1", endpoint.host);
1865-
ASSERT_EQ(9000u, endpoint.port);
1862+
// Redirect both logical endpoints to the same reachable test server.
1863+
auto base_socket_factory = std::make_unique<NonSecureSocketFactory>();
1864+
auto socket_factory = std::make_unique<FailOnceSocketFactoryAdapter>(*base_socket_factory, actual_endpoint);
1865+
auto * const adapter = socket_factory.get();
18661866

1867-
client->ResetConnectionEndpoint();
1867+
// The initial connection selects the first endpoint.
1868+
Client client(options, std::move(socket_factory));
1869+
ASSERT_EQ(primary, client.GetCurrentEndpoint().value());
18681870

1869-
endpoint = client->GetCurrentEndpoint().value();
1870-
ASSERT_EQ("localhost", endpoint.host);
1871-
ASSERT_EQ(9000u, endpoint.port);
1871+
// A healthy current endpoint is retried without advancing.
1872+
adapter->SetFailEndpoint(std::nullopt);
1873+
adapter->ClearConnectRequests();
1874+
client.ResetConnectionEndpoint();
1875+
EXPECT_EQ(primary, client.GetCurrentEndpoint().value());
1876+
EXPECT_EQ(std::vector<Endpoint>{primary}, adapter->ConnectRequests());
18721877

1873-
SUCCEED();
1874-
} catch (const std::exception & e) {
1875-
FAIL() << "Got an unexpected exception : " << e.what();
1876-
}
1878+
// Failure of the current endpoint advances to the next endpoint.
1879+
adapter->SetFailEndpoint(primary);
1880+
adapter->ClearConnectRequests();
1881+
client.ResetConnectionEndpoint();
1882+
EXPECT_EQ(secondary, client.GetCurrentEndpoint().value());
1883+
EXPECT_EQ((std::vector<Endpoint>{primary, secondary}), adapter->ConnectRequests());
1884+
1885+
// Failure of the last endpoint wraps around to the first endpoint.
1886+
adapter->SetFailEndpoint(secondary);
1887+
adapter->ClearConnectRequests();
1888+
client.ResetConnectionEndpoint();
1889+
EXPECT_EQ(primary, client.GetCurrentEndpoint().value());
1890+
EXPECT_EQ((std::vector<Endpoint>{secondary, primary}), adapter->ConnectRequests());
18771891
}
18781892

18791893
TEST_P(ResetConnectionTestCase, ResetConnectionTest) {

ut/test_socket_factory_adapters.h

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#pragma once
2+
3+
#include "clickhouse/base/socket.h"
4+
5+
#include <chrono>
6+
#include <memory>
7+
#include <optional>
8+
#include <system_error>
9+
#include <utility>
10+
#include <vector>
11+
12+
namespace clickhouse {
13+
14+
/** Records requested endpoints and optionally fails one matching connection attempt.
15+
*
16+
* Successful attempts are redirected to actual_endpoint, allowing tests to exercise
17+
* failover between distinct logical endpoints using a single reachable server. Setting
18+
* fail_endpoint makes the next matching attempt throw connection_refused and then
19+
* clears the value. The wrapped factory must outlive the adapter.
20+
*/
21+
struct FailOnceSocketFactoryAdapter : public SocketFactory {
22+
SocketFactory & socket_factory;
23+
Endpoint actual_endpoint;
24+
std::vector<Endpoint> connect_requests{};
25+
std::optional<Endpoint> fail_endpoint{};
26+
27+
FailOnceSocketFactoryAdapter(SocketFactory & socket_factory,
28+
Endpoint actual_endpoint)
29+
: socket_factory(socket_factory)
30+
, actual_endpoint(std::move(actual_endpoint))
31+
{}
32+
33+
std::unique_ptr<SocketBase> connect(const ClientOptions& opts,
34+
const Endpoint& endpoint) override {
35+
connect_requests.push_back(endpoint);
36+
37+
if (fail_endpoint && fail_endpoint.value() == endpoint) {
38+
fail_endpoint.reset();
39+
throw std::system_error(std::make_error_code(std::errc::connection_refused));
40+
}
41+
42+
return socket_factory.connect(opts, actual_endpoint);
43+
}
44+
45+
void SetFailEndpoint(std::optional<Endpoint> endpoint) {
46+
fail_endpoint = std::move(endpoint);
47+
}
48+
49+
const std::vector<Endpoint> & ConnectRequests() const {
50+
return connect_requests;
51+
}
52+
53+
void ClearConnectRequests() {
54+
connect_requests.clear();
55+
}
56+
57+
void sleepFor(const std::chrono::milliseconds& duration) override {
58+
socket_factory.sleepFor(duration);
59+
}
60+
};
61+
62+
} // namespace clickhouse

0 commit comments

Comments
 (0)