Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
93e7bdc
Setting 'object_storage_cluster_fallback_if_empty'
ianton-ru Jul 7, 2026
0297ff2
Fix some issues
ianton-ru Jul 8, 2026
8bac602
Align cluster fallback planning with read path for object storage.
ianton-ru Jul 8, 2026
bc7aacf
Add tests for object_storage_cluster_fallback_if_empty.
ianton-ru Jul 9, 2026
1ad4ce9
Extend fallback tests for remote initiator with unknown cluster.
ianton-ru Jul 9, 2026
195f687
Do not apply object_storage_cluster_fallback_if_empty to explicit *Cl…
ianton-ru Jul 9, 2026
92a5142
Send pure s3() to remote initiator without converting to cluster first.
ianton-ru Jul 9, 2026
13eaae5
Allow object_storage_cluster_fallback_if_empty for tables with engine…
ianton-ru Jul 9, 2026
26cb88a
Fix after review
ianton-ru Jul 20, 2026
9d91e1f
Merge antalya-26.3
ianton-ru Jul 20, 2026
988e463
Simplify object_storage_cluster_fallback_to_local_if_empty control flow.
ianton-ru Jul 20, 2026
20fa735
Fix remote initiator with only object_storage_cluster for alternative…
ianton-ru Jul 20, 2026
86ff083
Restore pure remote path for ENGINE tables without object_storage_clu…
ianton-ru Jul 20, 2026
9e1b15f
Simplify cluster read routing after remote-initiator fixes.
ianton-ru Jul 20, 2026
4ba13df
Do not soft-fail a missing object_storage_remote_initiator_cluster.
ianton-ru Jul 21, 2026
4b41b1d
Require non-empty object_storage_cluster for local fallback.
ianton-ru Jul 21, 2026
afbd078
Apply object_storage_cluster fallback to ENGINE with remote initiator.
ianton-ru Jul 21, 2026
e0c3a5d
Simplify object_storage_cluster fallback routing helpers.
ianton-ru Jul 21, 2026
e8b4414
Document why remote-initiator deferral keeps plain s3() with query SE…
ianton-ru Jul 21, 2026
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
6 changes: 6 additions & 0 deletions src/Core/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7772,6 +7772,12 @@ Trigger processor to spill data into external storage adpatively. grace join is
)", EXPERIMENTAL) \
DECLARE(String, object_storage_cluster, "", R"(
Cluster to make distributed requests to object storages with alternative syntax.
)", EXPERIMENTAL) \
DECLARE(Bool, object_storage_cluster_fallback_to_local_if_empty, false, R"(
Execute the read locally if 'object_storage_cluster' is set but the cluster is empty or unknown.
Does not apply to explicit *Cluster table functions, to 'object_storage_remote_initiator_cluster', or to writes.
With remote initiator + remote_initiator_cluster, the initiator sends plain s3()/iceberg() and passes
object_storage_cluster as a query setting so the remote can resolve or fall back (OSC may be unknown locally).
)", EXPERIMENTAL) \
DECLARE(UInt64, object_storage_max_nodes, 0, R"(
Limit for hosts used for request in object storage cluster table functions - azureBlobStorageCluster, s3Cluster, hdfsCluster, etc.
Expand Down
1 change: 1 addition & 0 deletions src/Core/SettingsChangesHistory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const VersionToSettingsChangesMap & getSettingsChangesHistory()
{"object_storage_cluster_join_mode", "allow", "allow", "New setting"},
{"export_merge_tree_partition_task_timeout_seconds", "3600", "86400", "Increase default value to make it more realistic"},
{"export_merge_tree_part_allow_lossy_cast", false, false, "New setting to gate lossy casts in EXPORT PART/PARTITION behind explicit acknowledgment"},
{"object_storage_cluster_fallback_to_local_if_empty", false, false, "New setting"},
{"export_merge_tree_partition_retry_initial_backoff_seconds", 5, 5, "New setting for exponential back-off between failed part export retries in an export partition task"},
{"export_merge_tree_partition_retry_max_backoff_seconds", 300, 300, "New setting capping the exponential back-off between failed part export retries in an export partition task"},
{"export_merge_tree_partition_max_retries", 3, 3, "Obsolete and ignored: export partition tasks now retry retryable failures until the task timeout and fail immediately on non-retryable errors, instead of using a fixed retry budget"},
Expand Down
8 changes: 8 additions & 0 deletions src/Interpreters/Cluster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,14 @@ Cluster::Cluster(Cluster::SubclusterTag, const Cluster & from, const std::vector
initMisc();
}

size_t Cluster::getAllNodeCount() const
{
size_t count = 0;
for (const auto & shard : shards_info)
count += shard.getAllNodeCount();
return count;
}

std::vector<Strings> Cluster::getHostIDs() const
{
std::vector<Strings> host_ids;
Expand Down
3 changes: 3 additions & 0 deletions src/Interpreters/Cluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ class Cluster
/// The number of all shards.
size_t getShardCount() const { return shards_info.size(); }

/// The number of all nodes.
size_t getAllNodeCount() const;

/// Returns an array of arrays of strings in the format 'escaped_host_name:port' for all replicas of all shards in the cluster.
std::vector<Strings> getHostIDs() const;

Expand Down
220 changes: 189 additions & 31 deletions src/Storages/IStorageCluster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ namespace Setting
extern const SettingsUInt64 object_storage_max_nodes;
extern const SettingsBool object_storage_remote_initiator;
extern const SettingsString object_storage_remote_initiator_cluster;
extern const SettingsString object_storage_cluster;
extern const SettingsObjectStorageClusterJoinMode object_storage_cluster_join_mode;
extern const SettingsBool object_storage_cluster_fallback_to_local_if_empty;
}

namespace ErrorCodes
Expand Down Expand Up @@ -344,6 +346,124 @@ void IStorageCluster::updateQueryWithJoinToSendIfNeeded(
}
}

bool IStorageCluster::shouldFallbackToLocalOnEmptyCluster(ContextPtr context) const
{
return context->getSettingsRef()[Setting::object_storage_cluster_fallback_to_local_if_empty]
&& usesObjectStorageClusterSettingSyntax()
&& !getClusterName(context).empty();
}

bool IStorageCluster::shouldReadLocallyOnFallbackToPure(const ResolvedClusterRead & resolved, ContextPtr context) const
{
if (!resolved.fallback_to_pure)
return false;

if (!context->getSettingsRef()[Setting::object_storage_remote_initiator])
return true;

if (resolved.remote_initiator_cluster)
return false;

if (resolved.local_fallback)
return true;

throw Exception(ErrorCodes::BAD_ARGUMENTS,
"Setting 'object_storage_remote_initiator' can be used only with 'object_storage_remote_initiator_cluster', 'object_storage_cluster', or cluster name in arguments");
}

IStorageCluster::ResolvedClusterRead IStorageCluster::resolveClusterRead(ContextPtr context) const
{
/// Decision matrix for object_storage_cluster_fallback_to_local_if_empty / remote initiator:
/// - s3()/iceberg() (or ENGINE ... SETTINGS object_storage_cluster) + empty/unknown cluster + setting
/// -> read locally (fallback_to_pure).
/// - s3Cluster(...)/explicit *Cluster argument -> never fall back locally.
/// - object_storage_remote_initiator=1 with object_storage_remote_initiator_cluster set
/// -> defer object_storage_cluster resolution on the initiator. For setting-syntax / ENGINE,
/// always send plain s3()/iceberg() to the remote initiator with object_storage_cluster as a
/// query SETTINGS value (not *Cluster). OSC may be unknown locally and defined only on the
/// remote (or the reverse); *Cluster would bake the name into the function argument and
/// would also ignore object_storage_cluster_fallback_to_local_if_empty on the remote.
/// Empty local OSC is omitted so the remote can supply its own.
/// - object_storage_remote_initiator=1 with only object_storage_cluster (no remote_initiator_cluster)
/// -> clustered remote path: default initiator cluster to object_storage_cluster, send *Cluster.
/// - writes -> never apply local fallback (see write()).
ResolvedClusterRead result;

if (!isClusterSupported())
{
result.fallback_to_pure = true;
return result;
}

auto cluster_name_from_settings = getClusterName(context);
const auto & settings = context->getSettingsRef();
result.local_fallback = shouldFallbackToLocalOnEmptyCluster(context);

/// Defer OSC resolution when a separate remote-initiator cluster is set: the initiator must not
/// require a locally known object_storage_cluster (it may exist only on the remote node).
const bool defer_object_storage_cluster_resolution
= settings[Setting::object_storage_remote_initiator]
&& !settings[Setting::object_storage_remote_initiator_cluster].value.empty();
Comment on lines +404 to +406

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve remote-only initiator reads

When object_storage_remote_initiator=1 and only object_storage_remote_initiator_cluster is set on the initiator, this condition now forces fallback_to_pure=false even though there is no local object_storage_cluster to defer. read then takes the clustered path and rewrites the remote query with make_cluster_function=true, producing s3Cluster('', ...) instead of the previous s3(...); on remote users whose profile defines object_storage_cluster, that bypasses the fallback table function and the read runs only on the remote initiator, regressing the existing test_object_storage_remote_initiator_without_cluster_function case that expects replica subqueries. Only defer local cluster resolution when cluster_name_from_settings is non-empty.

Useful? React with 👍 / 👎.


/// Under deferral, setting-syntax / ENGINE always take the pure remote path (plain s3() + query SETTINGS).
result.fallback_to_pure = cluster_name_from_settings.empty()
|| (defer_object_storage_cluster_resolution && usesObjectStorageClusterSettingSyntax());
Comment on lines +409 to +410

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve *Cluster rewrite unless fallback is needed

With object_storage_remote_initiator=1, object_storage_remote_initiator_cluster set, and a valid non-empty object_storage_cluster, this condition forces the read into the pure remote path even when object_storage_cluster_fallback_to_local_if_empty is false. That bypasses the later make_cluster_function=true rewrite to s3Cluster/icebergCluster that this file still documents as needed for old ClickHouse versions, so during rolling upgrades the remote initiator can receive s3(... SETTINGS object_storage_cluster=...) instead of the compatible *Cluster call and fail to parse or distribute it. Please keep the *Cluster path unless the cluster is actually missing/empty and fallback is requested.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review. The observation is correct that with object_storage_remote_initiator=1 and a non-empty object_storage_remote_initiator_cluster, setting-syntax reads (s3() / ENGINE ... SETTINGS object_storage_cluster=...) always take the pure remote path and send plain s3()/iceberg() rather than rewriting to *Cluster.

That is intentional and should stay.

Why not restore *Cluster when fallback is off

Under remote-initiator deferral, the initiator must not assume it knows how to resolve object_storage_cluster:

  • OSC may be unknown / unset on the local node and defined only on the remote initiator (or in the remote user’s profile).
  • Or the reverse: OSC is set on the initiator query, but the remote swarm is empty/unknown and should apply object_storage_cluster_fallback_to_local_if_empty.

Sending s3Cluster('name', ...) (or equivalent) bakes the cluster name into the table-function argument. Explicit *Cluster never applies object_storage_cluster_fallback_to_local_if_empty, and it also forces the initiator to pick a concrete cluster-argument form even when the name is only meaningful on the remote side.

What we send instead

SELECT ... FROM remote('host', s3(...))
SETTINGS object_storage_cluster = '...', object_storage_cluster_fallback_to_local_if_empty = ...
  • Keep the plain s3() / iceberg() function.
  • Pass object_storage_cluster as a query setting (not a function argument).
  • If local OSC is empty, omit it so the remote can supply its own.

That lets the remote initiator distribute when it knows the cluster, or fall back locally when the setting allows — without the local node needing a live/known swarm definition.

The make_cluster_function=true / “old ClickHouse versions” rewrite remains for the non-deferred path (e.g. object_storage_remote_initiator=1 with only object_storage_cluster, no object_storage_remote_initiator_cluster), where the initiator cluster is the object-storage cluster itself and a *Cluster rewrite is appropriate.

So we will not change the deferral path back to *Cluster based on this comment.


const bool try_resolve_with_local_fallback
= !defer_object_storage_cluster_resolution
&& !result.fallback_to_pure
&& result.local_fallback;

if (try_resolve_with_local_fallback)
{
result.object_storage_cluster = getClusterImpl(
context,
cluster_name_from_settings,
isObjectStorage() ? settings[Setting::object_storage_max_nodes] : 0,
/*allow_null*/ true);
if (!result.object_storage_cluster)
result.fallback_to_pure = true;
}

if (settings[Setting::object_storage_remote_initiator])
{
auto remote_initiator_cluster_name = settings[Setting::object_storage_remote_initiator_cluster].value;
if (!remote_initiator_cluster_name.empty())
{
Comment on lines +430 to +432

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Default remote initiator to object_storage_cluster

When object_storage_remote_initiator=1 is used with alternative syntax and only object_storage_cluster is set, this new branch leaves remote_initiator_cluster unset because object_storage_remote_initiator_cluster is empty. That was a supported path: the old code defaulted the remote initiator cluster to cluster_name_from_settings, but now getQueryProcessingStage/read either throw BAD_ARGUMENTS or, with object_storage_cluster_fallback_if_empty=1, run the read locally instead of on a remote initiator. Please preserve the previous default to the object-storage cluster when the explicit remote-initiator cluster setting is absent.

Useful? React with 👍 / 👎.

/// Never soft-fail a missing/empty remote-initiator cluster via
/// object_storage_cluster_fallback_to_local_if_empty: that setting applies only to object_storage_cluster.
result.remote_initiator_cluster = getClusterImpl(
context,
remote_initiator_cluster_name,
/*max_hosts*/ 0);
}
}

return result;
}

void IStorageCluster::readFromRemoteInitiator(
QueryPlan & query_plan,
const Names & column_names,
const StorageSnapshotPtr & storage_snapshot,
SelectQueryInfo & query_info,
ContextPtr context,
QueryProcessingStage::Enum processed_stage,
size_t max_block_size,
size_t num_streams,
ASTPtr query_to_send,
ClusterPtr remote_initiator_cluster,
const String & remote_initiator_cluster_name)
{
auto storage_and_context = convertToRemote(remote_initiator_cluster, context, remote_initiator_cluster_name, query_to_send);
auto src_distributed = std::dynamic_pointer_cast<StorageDistributed>(storage_and_context.storage);
auto modified_query_info = query_info;
modified_query_info.cluster = src_distributed->getCluster();
auto new_storage_snapshot = storage_and_context.storage->getStorageSnapshot(storage_snapshot->metadata, storage_and_context.context);
storage_and_context.storage->read(
query_plan, column_names, new_storage_snapshot, modified_query_info, storage_and_context.context, processed_stage, max_block_size, num_streams);
}

/// The code executes on initiator
void IStorageCluster::read(
QueryPlan & query_plan,
Expand All @@ -365,32 +485,48 @@ void IStorageCluster::read(
const auto & settings = context->getSettingsRef();
ASTPtr query_to_send = query_info.query;

if (cluster_name_from_settings.empty())
auto resolved = resolveClusterRead(context);
ClusterPtr cluster = resolved.object_storage_cluster;

if (resolved.fallback_to_pure)
{
if (settings[Setting::object_storage_remote_initiator])
if (shouldReadLocallyOnFallbackToPure(resolved, context))
{
auto remote_initiator_cluster_name = settings[Setting::object_storage_remote_initiator_cluster].value;
if (remote_initiator_cluster_name.empty())
throw Exception(ErrorCodes::BAD_ARGUMENTS,
"Setting 'object_storage_remote_initiator' can be used only with 'object_storage_remote_initiator_cluster', 'object_storage_cluster', or cluster name in arguments");

/// rewrite query to execute `remote('remote_host', s3(...))`
/// remote_host can execute query itself or make on-cluster query depends on own `object_storage_cluster` setting
updateConfigurationIfNeeded(context);
updateQueryWithJoinToSendIfNeeded(query_to_send, query_info, context);
updateQueryToSendIfNeeded(query_to_send, storage_snapshot, context, /*make_cluster_function*/ false);

auto remote_initiator_cluster = getClusterImpl(context, remote_initiator_cluster_name);
auto storage_and_context = convertToRemote(remote_initiator_cluster, context, remote_initiator_cluster_name, query_to_send);
auto src_distributed = std::dynamic_pointer_cast<StorageDistributed>(storage_and_context.storage);
auto modified_query_info = query_info;
modified_query_info.cluster = src_distributed->getCluster();
auto new_storage_snapshot = storage_and_context.storage->getStorageSnapshot(storage_snapshot->metadata, storage_and_context.context);
storage_and_context.storage->read(query_plan, column_names, new_storage_snapshot, modified_query_info, storage_and_context.context, processed_stage, max_block_size, num_streams);
readFallBackToPure(query_plan, column_names, storage_snapshot, query_info, context, processed_stage, max_block_size, num_streams);
return;
}

readFallBackToPure(query_plan, column_names, storage_snapshot, query_info, context, processed_stage, max_block_size, num_streams);
auto remote_initiator_cluster_name = settings[Setting::object_storage_remote_initiator_cluster].value;

/// Send remote('host', s3(...)) with object_storage_cluster in query SETTINGS (not s3Cluster).
/// The remote may resolve OSC, fall back locally, or use its own OSC when local OSC is empty.
updateConfigurationIfNeeded(context);
updateQueryWithJoinToSendIfNeeded(query_to_send, query_info, context);
updateQueryToSendIfNeeded(query_to_send, storage_snapshot, context, /*make_cluster_function*/ false);

/// ENGINE tables keep object_storage_cluster on the storage, not in the initiator query context.
/// Propagate it into the context copied for remote() so the remote sees the same OSC as alt-syntax.
auto context_for_remote = context;
if (!cluster_name_from_settings.empty()
&& context->getSettingsRef()[Setting::object_storage_cluster].value.empty())
{
auto ctx = Context::createCopy(context);
ctx->setSetting("object_storage_cluster", cluster_name_from_settings);
context_for_remote = ctx;
}

readFromRemoteInitiator(
query_plan,
column_names,
storage_snapshot,
query_info,
context_for_remote,
processed_stage,
max_block_size,
num_streams,
query_to_send,
resolved.remote_initiator_cluster,
remote_initiator_cluster_name);
return;
}

Expand Down Expand Up @@ -430,17 +566,28 @@ void IStorageCluster::read(
auto remote_initiator_cluster_name = settings[Setting::object_storage_remote_initiator_cluster].value;
if (remote_initiator_cluster_name.empty())
remote_initiator_cluster_name = cluster_name_from_settings;
auto remote_initiator_cluster = getClusterImpl(context, remote_initiator_cluster_name);
auto storage_and_context = convertToRemote(remote_initiator_cluster, context, remote_initiator_cluster_name, query_to_send);
auto src_distributed = std::dynamic_pointer_cast<StorageDistributed>(storage_and_context.storage);
auto modified_query_info = query_info;
modified_query_info.cluster = src_distributed->getCluster();
auto new_storage_snapshot = storage_and_context.storage->getStorageSnapshot(storage_snapshot->metadata, storage_and_context.context);
storage_and_context.storage->read(query_plan, column_names, new_storage_snapshot, modified_query_info, storage_and_context.context, processed_stage, max_block_size, num_streams);
/// Prefer the cluster already resolved in resolveClusterRead (when remote_initiator_cluster was set).
ClusterPtr remote_initiator_cluster = resolved.remote_initiator_cluster;
if (!remote_initiator_cluster)
remote_initiator_cluster = getClusterImpl(context, remote_initiator_cluster_name);

readFromRemoteInitiator(
query_plan,
column_names,
storage_snapshot,
query_info,
context,
processed_stage,
max_block_size,
num_streams,
query_to_send,
remote_initiator_cluster,
remote_initiator_cluster_name);
return;
}

auto cluster = getClusterImpl(context, cluster_name_from_settings, isObjectStorage() ? settings[Setting::object_storage_max_nodes] : 0);
if (!cluster)
cluster = getClusterImpl(context, cluster_name_from_settings, isObjectStorage() ? settings[Setting::object_storage_max_nodes] : 0);

RestoreQualifiedNamesVisitor::Data data;
data.distributed_table = DatabaseAndTableWithAlias(*getTableExpression(query_to_send->as<ASTSelectQuery &>(), 0));
Expand Down Expand Up @@ -564,6 +711,8 @@ SinkToStoragePtr IStorageCluster::write(
{
auto cluster_name_from_settings = getClusterName(context);

// Intentionally do not apply object_storage_cluster_fallback_to_local_if_empty here.
// Cluster write is not supported; applying fallback would make INSERT depend on cluster availability.
if (cluster_name_from_settings.empty())
return writeFallBackToPure(query, metadata_snapshot, context, async_insert);

Expand Down Expand Up @@ -730,9 +879,18 @@ ContextPtr ReadFromCluster::updateSettings(const Settings & settings)
return new_context;
}

ClusterPtr IStorageCluster::getClusterImpl(ContextPtr context, const String & cluster_name_, size_t max_hosts)
ClusterPtr IStorageCluster::getClusterImpl(ContextPtr context, const String & cluster_name_, size_t max_hosts, bool allow_null)
{
return context->getCluster(cluster_name_)->getClusterWithReplicasAsShards(context->getSettingsRef(), /* max_replicas_from_shard */ 0, max_hosts);
ClusterPtr cluster = nullptr;
if (allow_null)
{
cluster = context->tryGetCluster(cluster_name_);
if (!cluster || !cluster->getAllNodeCount())
return nullptr;
}
else
cluster = context->getCluster(cluster_name_);
return cluster->getClusterWithReplicasAsShards(context->getSettingsRef(), /* max_replicas_from_shard */ 0, max_hosts);
}

}
41 changes: 40 additions & 1 deletion src/Storages/IStorageCluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,47 @@ class IStorageCluster : public IStorage

NamesAndTypesList hive_partition_columns_to_read_from_file_path;

struct ResolvedClusterRead
{
/// True for local pure read, or for pure s3()/iceberg() sent to a remote initiator (not *Cluster).
bool fallback_to_pure = false;
/// Cached shouldFallbackToLocalOnEmptyCluster(context).
bool local_fallback = false;
/// Pre-resolved object-storage cluster when local-fallback prefetch was done.
ClusterPtr object_storage_cluster;
/// Resolved remote-initiator cluster when object_storage_remote_initiator_cluster is set.
ClusterPtr remote_initiator_cluster;
};

ResolvedClusterRead resolveClusterRead(ContextPtr context) const;

/// True for alternative syntax / table engines (object_storage_cluster setting path).
/// False for explicit *Cluster(...) and for non-object-storage IStorageCluster subclasses.
virtual bool usesObjectStorageClusterSettingSyntax() const { return false; }

/// Setting enabled, setting-syntax storage, and non-empty object_storage_cluster.
bool shouldFallbackToLocalOnEmptyCluster(ContextPtr context) const;

/// Shared by read() and getQueryProcessingStage: local pure path vs throw vs remote pure send.
bool shouldReadLocallyOnFallbackToPure(const ResolvedClusterRead & resolved, ContextPtr context) const;

void readFromRemoteInitiator(
QueryPlan & query_plan,
const Names & column_names,
const StorageSnapshotPtr & storage_snapshot,
SelectQueryInfo & query_info,
ContextPtr context,
QueryProcessingStage::Enum processed_stage,
size_t max_block_size,
size_t num_streams,
ASTPtr query_to_send,
ClusterPtr remote_initiator_cluster,
const String & remote_initiator_cluster_name);

private:
static ClusterPtr getClusterImpl(ContextPtr context, const String & cluster_name_, size_t max_hosts = 0);
// With 'allow_null=true' returns nullptr when cluster does not exist or empty
// With 'allow_null=false' throws exception
static ClusterPtr getClusterImpl(ContextPtr context, const String & cluster_name_, size_t max_hosts = 0, bool allow_null = false);

virtual bool isClusterSupported() const { return true; }

Expand Down
Loading
Loading