Skip to content

Force-apply topology on reconcile and add a per-cluster sync endpoint (#395)#400

Open
bocharov wants to merge 1 commit into
apache:unstablefrom
bocharov:feat/force-sync-and-endpoint
Open

Force-apply topology on reconcile and add a per-cluster sync endpoint (#395)#400
bocharov wants to merge 1 commit into
apache:unstablefrom
bocharov:feat/force-sync-and-endpoint

Conversation

@bocharov

@bocharov bocharov commented Jul 18, 2026

Copy link
Copy Markdown

Problem

Fixes #395. Kvrocks cluster nodes do not gossip — the controller is the sole source of topology and must push a complete, identical view to every node. Today a node can silently diverge and never recover:

  • The probe loop re-pushes CLUSTERX SETNODES only when a node's epoch is behind the stored version. A node that has drifted at an equal epoch (a dropped push, a partial view, a stale/phantom node entry) is never repaired.
  • SETNODES is sent without force, so even a re-push at an equal version is a no-op on the server (the version gate rejects a lower version and no-ops an equal one). The only prior recovery was CLUSTER RESET, which requires an empty DB and so is unusable once a node holds data. Controller's cluster_nodes push silently drops when a data node is unreachable, leaving cluster meta stale forever #395 reports exactly this: divergent cluster_nodes snapshots that never converge, with no operator-facing way to force a re-push.

Changes

  1. SyncClusterInfo(ctx, cluster, force) — when force is set, send the SETNODES force flag so the topology applies unconditionally. Because SETNODES replaces the node's whole topology, a forced push also clears stale/phantom entries without a CLUSTER RESET and without data loss. Wrapped in a bounded retry-with-backoff (a push dropped by a transient blip is otherwise not retried until the next tick).

  2. Divergence detection at an equal epoch — the reconcile loop parses cluster_known_nodes and cluster_slots_ok from CLUSTER INFO and force-pushes a node whose peer count or slot coverage disagrees with the desired topology, not only one whose epoch lags. Coverage is compared against the desired covered-slot count (not a hardcoded 16384), so a cluster that is intentionally mid-scale is not seen as drifted; a node that omits these fields falls back to epoch-only reconcile. The existing "node is ahead" adopt branch is preserved.

  3. POST /namespaces/{namespace}/clusters/{cluster}/sync — force-push the stored topology to every node and report per-node results. This is the operator-facing re-push Controller's cluster_nodes push silently drops when a data node is unreachable, leaving cluster meta stale forever #395 asks for. It performs no CLUSTER RESET, so it is safe to run on a populated cluster.

  4. Reject blank/port-less node addresses in CheckNewNodes (the create/add-node boundary) and Shard.ToSlotsString (serialization), so a half-resolved address fails loudly instead of registering a phantom, unreachable node.

Safety

The controller is the single writer of topology (failover routes through it), so "newest complete view wins" holds. Force fires only when a node's version is not greater than the store's; a node legitimately ahead keeps the existing adopt-from-node branch. Verified against Apache Kvrocks 2.16.0 that CLUSTERX SETNODES <str> <ver> force bypasses the equal-version no-op.

Tests

make test passing. New unit tests cover the CLUSTER INFO parsing (incl. sentinel/malformed values), the divergence check, address validation, the blank-address guard, and the /sync handler. A behavioral test asserts the reconcile fix by exercising the real probe path.

Kvrocks cluster nodes do not gossip: the controller is the sole source of
topology and must reliably push one complete, identical view to every node.
Three gaps let a node's view drift and never recover (see apache#395):

1. The probe loop re-pushed only a node whose epoch was behind the store, so a
   node that had drifted at an equal epoch (a dropped push, a partial view, a
   stale/phantom node entry) was never repaired.
2. CLUSTERX SETNODES was sent without force, so even a re-push at an equal
   version is a no-op on the server (the version gate rejects a lower version and
   no-ops an equal one). The only prior recovery was CLUSTER RESET, which needs
   an empty DB and is unusable once a node holds data.
3. A push dropped by a transient error was not retried before the next tick.

Changes:

- SyncClusterInfo takes a force argument and, when set, sends the SETNODES force
  flag so the topology applies unconditionally. A forced push replaces the node's
  whole topology, so it also clears stale/phantom entries without a CLUSTER RESET
  and without data loss. Wrapped in a bounded retry-with-backoff.

- The reconcile loop parses cluster_known_nodes and cluster_slots_ok from CLUSTER
  INFO and force-pushes a node whose peer count or slot coverage disagrees with
  the desired topology, not only one whose epoch lags. Coverage is compared
  against the desired covered-slot count; a node that omits these fields falls
  back to epoch-only reconcile. The node-ahead adopt branch is preserved.

- New endpoint POST /namespaces/{namespace}/clusters/{cluster}/sync force-pushes
  the stored topology to every node and reports per-node results. It performs no
  CLUSTER RESET, so it is safe to run on a populated cluster.

- CheckNewNodes and Shard.ToSlotsString reject a blank or port-less node address,
  so a half-resolved address fails loudly instead of being pushed as a phantom.

Unit tests cover the CLUSTER INFO parsing, address validation, the blank-address
guard, the divergence check, and the sync handler.
@bocharov
bocharov force-pushed the feat/force-sync-and-endpoint branch from 998ffa0 to 3abbe98 Compare July 18, 2026 00:58
@git-hulk
git-hulk self-requested a review July 18, 2026 02:04
@codecov-commenter

codecov-commenter commented Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.00000% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.89%. Comparing base (6c56470) to head (3abbe98).
⚠️ Report is 109 commits behind head on unstable.

Files with missing lines Patch % Lines
store/cluster_node.go 62.06% 9 Missing and 2 partials ⚠️
store/cluster_mock_node.go 0.00% 4 Missing and 1 partial ⚠️
controller/cluster.go 82.60% 2 Missing and 2 partials ⚠️
server/api/cluster.go 87.50% 2 Missing and 1 partial ⚠️
server/api/shard.go 0.00% 0 Missing and 2 partials ⚠️
server/route.go 0.00% 1 Missing ⚠️
store/cluster.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           unstable     #400      +/-   ##
============================================
+ Coverage     43.38%   49.89%   +6.50%     
============================================
  Files            37       45       +8     
  Lines          2971     4167    +1196     
============================================
+ Hits           1289     2079     +790     
- Misses         1544     1859     +315     
- Partials        138      229      +91     
Flag Coverage Δ
unittests 49.89% <70.00%> (+6.50%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Controller's cluster_nodes push silently drops when a data node is unreachable, leaving cluster meta stale forever

2 participants