Skip to content

Reload cluster discovery settings from remote_servers without restart. - #2197

Open
ianton-ru wants to merge 22 commits into
antalya-26.6from
feature/antalya-26.6/cluster_discovery_update
Open

Reload cluster discovery settings from remote_servers without restart.#2197
ianton-ru wants to merge 22 commits into
antalya-26.6from
feature/antalya-26.6/cluster_discovery_update

Conversation

@ianton-ru

Copy link
Copy Markdown

Changelog category (leave one):

  • Improvement

Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):

Reload cluster discovery settings from remote_servers without restart.

Documentation entry for user-facing changes

Previously ClusterDiscovery read user/password/path and related fields only at startup, so XML changes had no effect until restart. Apply discovery config diffs in place on reload, including add/remove of discovery and multicluster paths.

CI/CD Options

Exclude tests:

  • Fast test
  • Integration Tests
  • Stateless tests
  • Stateful tests
  • Performance tests
  • Aarch64 tests
  • All with ASAN
  • All with TSAN
  • All with MSAN
  • All with UBSAN
  • All with Coverage
  • All Regression
  • Disable CI Cache

Regression jobs to run:

  • Fast suites (mostly <1h)
  • Aggregate Functions (2h)
  • Alter (1.5h)
  • Benchmark (30m)
  • ClickHouse Keeper (1h)
  • Iceberg (2h)
  • LDAP (1h)
  • OAuth (5m)
  • Parquet (1.5h)
  • RBAC (1.5h)
  • SSL Server (1h)
  • S3 (2h)
  • S3 Export (2h)
  • Swarms (30m)
  • Tiered Storage (2h)

ianton-ru and others added 4 commits August 10, 2026 14:02
Previously ClusterDiscovery read user/password/path and related fields only at startup, so XML changes had no effect until restart. Apply discovery config diffs in place on reload, including add/remove of discovery and multicluster paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
Serialize start()/ensureWorkerStarted() so concurrent config reload cannot double-assign the worker thread, and remove the ephemeral ZK node when a participant is reloaded as an observer.

Co-authored-by: Cursor <cursoragent@cursor.com>
Invisible-only config updates now schedule an upsert, and invisible upserts clear the published cluster so visibility toggles take effect immediately.

Co-authored-by: Cursor <cursoragent@cursor.com>
When a multicluster-discovered cluster already occupies a name, adding a
static <path> entry previously left watches/callbacks inconsistent.
Remove the dynamic entry first so static config wins cleanly.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ianton-ru

Copy link
Copy Markdown
Author

@codex review

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Workflow [PR], commit [49db0ee]

@ianton-ru ianton-ru changed the title Feature/antalya 26.6/cluster discovery update Reload cluster discovery settings from remote_servers without restart. Aug 10, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90d6e81a0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Interpreters/ClusterDiscovery.cpp Outdated
if (finished)
break;

consumePendingConfigUpdate();

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 Consume pending config before retrying initialization

When the initial update repeatedly fails against a Keeper path that the reload removes or corrects, the worker calls initialUpdate before reaching this pending-config consumer, so every retry continues using the obsolete configuration and the corrective reload is never applied. Process the pending update before retrying initialization so the advertised restart-free recovery works even while the old path remains unavailable.

Useful? React with 👍 / 👎.

Comment on lines +390 to +393
if (registration_changed || invisible_changed)
clusters_to_update->set(info.name);
else
rebuildClusterObject(info);

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 Refresh cached node payloads after registration changes

When a reload changes <my_hostname> or <shard> without changing the node UUID set, this only schedules upsertCluster: registerInZk updates the existing znode data, but needUpdate compares only UUIDs and the subsequent rebuild uses the old nodes_info payload. The local cluster therefore retains the old address or shard, and peers are not notified because setting child data does not trigger their children watches; invalidate and reread the payloads or otherwise notify all observers.

Useful? React with 👍 / 👎.

Comment thread src/Interpreters/ClusterDiscovery.cpp Outdated
Comment on lines +887 to +889
catch (...)
{
tryLogCurrentException(log, "Error while unregistering node from cluster '" + info.name + "'");

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 Propagate failed discovery unregistration

If Keeper is temporarily unavailable while a participant cluster is removed or its path is replaced, swallowing this exception lets removeStaticCluster erase all local tracking and treat the reload as applied even though the old ephemeral registration may remain for the live session. Other servers can consequently keep routing queries or writes to a node that was removed from that cluster, with no later retry; fail the update and retry unregistration instead.

AGENTS.md reference: AGENTS.md:L153-L153

Useful? React with 👍 / 👎.

ianton-ru and others added 10 commits August 10, 2026 18:57
Registration changes only update ephemeral payload data, which does not
fire children watches, and needUpdate skipped getNodes when UUIDs were
unchanged. Clear nodes_info on registration change, recreate the
ephemeral when payload differs so peers are notified, and always refresh
payloads in upsertCluster.

Co-authored-by: Cursor <cursoragent@cursor.com>
Previously, the first ClusterDiscovery constructed on config reload skipped
updateFromConfig and never called start(), so enabling discovery post-startup
left registration and watches inactive until restart. Start the worker when
the server is already up, including the allow-flag-only reload path where
remote_servers is unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
Invalid discovery XML (e.g. both password and secret) used to update
clusters_config first and only then fail in updateFromConfig, leaving
Clusters and discovery out of sync. Parse/validate discovery before
mutating shared cluster state so SYSTEM RELOAD CONFIG fails closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
Unrelated config reloads were always calling notifyHostIDsUpdated after the
early-return path was removed for allow-flag discovery start. Notify only when
clusters changed, discovery was updated, or discovery was just created.

Co-authored-by: Cursor <cursoragent@cursor.com>
An existing ClusterDiscovery is still updated when allow_experimental_cluster_discovery
is disabled, so invalid discovery XML could commit Clusters and then fail in
updateFromConfig. Validate whenever remote_servers changes and discovery is
enabled or already constructed.

Co-authored-by: Cursor <cursoragent@cursor.com>
If initialUpdate keeps failing, the worker never reached consumePendingConfigUpdate
in the main loop, so a corrective remote_servers reload stayed stuck. Drain the
pending update before each init attempt (and before startImpl's first init).

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep applying the local cluster removal when Keeper cleanup fails, queue the ephemeral path for worker retry, and cover it with a failpoint integration test instead of failing the whole reload.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drop queued ZK cleanup for a path on participant re-add/register, and skip retries while an active participant owns that path, so a delayed unregister cannot remove a freshly re-registered ephemeral.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drop redundant cancel helpers and fold unregister into one function; keep skipping retries when a participant owns the path again, and merge the failpoint coverage into a single test.

Co-authored-by: Cursor <cursoragent@cursor.com>
Also stop marking Flags::cv mutable; only the mutex needs mutable for isStopped.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ianton-ru

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cde1440d06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +352 to +354
clusters_to_update->remove(name);
get_nodes_callbacks.erase(name);
clusters_info.erase(it);

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 Rescan roots after removing a shadowing static cluster

When a static observer cluster has replaced a same-named cluster discovered through a still-configured multicluster_root_path, removing that static entry erases the name here without marking the root's need_update. The generic wake from updateFromConfig is insufficient because findDynamicClusters skips roots whose flag is false, and the worker uses an untimed wait, so with no Keeper child event the dynamic cluster can remain absent indefinitely. Mark the remaining multicluster roots for rescan when removing a static shadow.

Useful? React with 👍 / 👎.

When a static cluster that shadowed a same-named dynamic entry is removed, mark remaining multicluster roots for update so findDynamicClusters rediscovers it without waiting for the force-refresh interval.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ianton-ru

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 0a3163a9fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ianton-ru
ianton-ru marked this pull request as ready for review August 11, 2026 09:58
ianton-ru and others added 4 commits August 11, 2026 22:00
Erase impl when a name enters discovery and drop automatic_clusters when it returns to static or is removed, so Context no longer prefers a stale static Cluster after ownership transitions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Flags: :wait and multicluster/register bits were cleared before Keeper I/O, so a one-shot throw left the worker blocked until an unrelated event; restore consumed signals and re-arm wakeup for retry.
Co-authored-by: Cursor <cursoragent@cursor.com>
ianton-ru and others added 3 commits August 12, 2026 19:12
…isabled.

Reloading the allow flag from 1 to 0 left the worker registered and clusters published; tear down ClusterDiscovery with synchronous unregister so reload matches a restart with discovery off.

Co-authored-by: Cursor <cursoragent@cursor.com>
Participant aliases on the same Keeper path share one ephemeral; skip unregister (and observer tryRemove) while another non-observer still owns that path so peers do not temporarily lose the retained replica.

Co-authored-by: Cursor <cursoragent@cursor.com>
Watch callbacks used Flags::set, which could reinsert a removed cluster name and cause perpetual Unknown cluster scans; use setIfPresent and drop unknown keys in the worker loop.

Co-authored-by: Cursor <cursoragent@cursor.com>

@mkmkme mkmkme left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm in general, left three comments

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not sure if this test actually makes sense to add. IIUC you changed the behaviour of repeating start() / updateFromConfig() so it doesn't abort anymore. Does this really need to be always checked in the unit test suite?


auto & info = it->second;
auto zk = context->getDefaultOrAuxiliaryZooKeeper(info.zk_name);
registerInZk(zk, info);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It seems that the first thing upsertCluster does is registerInZk so the lines 1061-1062 seem to be redundant

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm also not sure about this unit test tbh. It has a any_need_update field which is not used (but I assume it's just taken from ClusterDiscovery::Flags it's mirroring).

But if we have a look at the rest of it, this test:

  • Has its own class to test and doesn't test any of the actual production code, so any change that would break the class it mirrors would not be caught by the test
  • It basically just tests the underlying std::unordered_map which we can assume to work just fine since it's part of the standard C++ library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants