Skip to content

server/join: reject join reusing another member's client URL#11006

Open
bufferflies wants to merge 4 commits into
tikv:masterfrom
bufferflies:claude/github-issue-review-72bf0f
Open

server/join: reject join reusing another member's client URL#11006
bufferflies wants to merge 4 commits into
tikv:masterfrom
bufferflies:claude/github-issue-review-72bf0f

Conversation

@bufferflies

@bufferflies bufferflies commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #10999

A PD member could join a cluster with a unique peer URL while advertising a
--advertise-client-urls value already owned by an existing member. The
member list then contained two logical members sharing one client URL, which
corrupts membership and health semantics.

Root cause: MemberAdd carries only peer URLs and etcd does not enforce
client-URL uniqueness, while the self-join guard only compares --join against
the member's own advertised client URL. When --join points to a different
string (e.g. a Kubernetes Service URL instead of the duplicated per-Pod client
URL), the guard is bypassed and etcd accepts the unique peer URL.

Impact: after the orphan Pod was deleted, its member lingered and still
appeared healthy because probing its (duplicated) client URL routes to the
healthy original PD. In a reproduced 4-member cluster carrying one orphan,
isolating a single healthy PD left only 2/4 raft members available, so PD APIs
failed and control-plane SQL timed out — while a clean 3-member cluster
tolerated the same isolation.

What is changed and how does it work?

Add a standalone checkAndClaimClientURLs (server/join/client_url.go), invoked
from PrepareJoinCluster on every startup, before the local etcd (re)starts
and republishes
— both a fresh join and a restart that changed its
--advertise-client-urls. It has two layers:

  1. List members and compare against self. Fetch the current member list and
    reject if this member's advertised client URL is already used by any other
    member. "Other" is decided by peer URL (unique per member), not name, so a
    different member sharing our name is still checked.
  2. Atomic claim via an etcd transaction. For each URL, a create-if-absent
    transaction (If CreateRevision==0 Then Put(owner) Else Get) atomically
    claims the URL in a registry keyed by the (hex-encoded) client URL. Because
    the transaction is serialized through raft, two concurrent joiners cannot
    both take the same client URL — the first wins, later ones are rejected. The
    owner record stores the member's name and peer URLs, and identity is
    matched by peer URL, so even two concurrent joiners that share a name are
    correctly rejected. A restart keeps the same peer URLs and re-claims its own
    URL without error.

Because the check runs before the member's etcd (re)publishes its attributes, a
bad join/restart never enters the member list. The restart path is best-effort
on connectivity
: if the cluster is unreachable, a member is still allowed to
start from its local data directory (it is not blocked).

Known limitation (follow-up): the claim is persistent, so if a member is removed
and a different member later reuses its client URL, the stale entry must be
cleaned up first. Cleanup on member removal is tracked as follow-up work.

Tests

  • TestJoinWithDuplicateClientURL — a fresh join reusing a live member's client
    URL is rejected; membership is untouched.
  • TestRestartWithModifiedDuplicateClientURL — a restart (data dir exists) that
    changed its client URL to another member's is rejected on the data-dir path.
  • TestSimpleJoin — legitimate joins with unique URLs are unaffected.
server/join: reject join reusing another member's client URL

Verify (and atomically claim, keyed by peer URL) the advertised client
URL on every startup, before the local etcd republishes, so a duplicate
never enters the member list — whether on a fresh join, a restart that
changed the client URL, or a concurrent join sharing a name.

Issue Number: close #10999

Check List

Tests

  • Unit test
  • Integration test

Code changes

  • Has persistent data change (a new etcd registry key per advertised client URL:
    /pd/{cluster_id}/member/client-urls/{hex(url)})

Side effects

  • None

Related changes

  • None

Release note

Reject a PD member from joining or restarting into the cluster when its advertised client URL is already used by another member, preventing membership and health corruption.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented joining or restarting when a node’s advertised client URLs are already used by another member.
    • Added an early conflict check to fail before any membership changes are made.
    • Enforced uniqueness checks on restart scenarios, even when the data directory already exists.
    • Added safe client URL re-claiming when the existing owner matches the node’s advertised peer URLs.
    • Preserved existing cluster membership when duplicates are detected, and improved restart resilience on transient discovery failures.
  • Tests
    • Added regression tests for duplicate client URLs on both initial join and restart/modified-config paths.

A new PD member can join with a unique peer URL while advertising a
client URL already owned by an existing member. The self-join check
only compares --join against the member's own advertise-client-urls,
so when --join points to a different string (e.g. a Service URL) it is
bypassed, and etcd accepts the unique peer URL. The member list then
contains two members sharing one client URL.

Add an integration test that starts such an orphan member and asserts
that two members end up advertising the same client URL, reproducing
the membership corruption.

Issue Number: ref tikv#10999

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: tongjian <1045931706@qq.com>
@ti-chi-bot ti-chi-bot Bot added do-not-merge/needs-triage-completed release-note-none Denotes a PR that doesn't merit a release note. dco-signoff: yes Indicates the PR's author has signed the dco. labels Jul 14, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign qiuyesuifeng for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot Bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Jul 14, 2026
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds etcd-backed advertised client URL ownership checks, rejects duplicate URLs before membership changes, adds restart-aware join handling, and tests both fresh-join and restart scenarios.

Changes

Duplicate client URL membership

Layer / File(s) Summary
Client URL registry and validation
server/join/client_url.go
Defines ownership records and encoded etcd keys, detects conflicts with existing members, and atomically claims advertised client URLs.
Restart-aware join control flow
server/join/join.go
Handles existing-data joins with retries and best-effort failures, validates client URL conflicts, and claims URLs before MemberAdd.
Regression coverage
tests/server/join/join_test.go
Tests duplicate URL rejection for new joins, unchanged membership, and restart after configuration modification.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PrepareJoinCluster
  participant retryListEtcdMembers
  participant checkClientURLConflict
  participant claimClientURLs
  participant Etcd
  participant MemberAdd
  PrepareJoinCluster->>retryListEtcdMembers: list members for restart-aware join
  retryListEtcdMembers->>Etcd: request current members
  Etcd-->>retryListEtcdMembers: member list or failure
  PrepareJoinCluster->>checkClientURLConflict: validate advertised client URLs
  checkClientURLConflict->>Etcd: inspect member URLs
  Etcd-->>checkClientURLConflict: ownership candidates
  PrepareJoinCluster->>claimClientURLs: claim URLs on fresh join
  claimClientURLs->>Etcd: create ownership keys transactionally
  Etcd-->>claimClientURLs: claim result
  PrepareJoinCluster->>MemberAdd: add member after successful validation
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: rejecting joins that reuse another member's client URL.
Description check ✅ Passed The PR description matches the template with issue number, change summary, checklist items, tests, and release note.
Linked Issues check ✅ Passed The changes reject duplicate advertised client URLs on join and restart, matching issue #10999's stated problem and expected behavior.
Out of Scope Changes check ✅ Passed The restart retry logic and persistent client-URL registry are directly supporting the stated fix and don't appear unrelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/server/join/join_test.go (1)

206-212: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Prevent double-counting member IDs.

If a member's ClientURLs list somehow contains the duplicate URL multiple times, its ID would be appended to dupOwners more than once, potentially satisfying the re.Len(dupOwners, 2) assertion with only a single member. Adding a break ensures each member is only counted once.

♻️ Proposed refactor
 	for _, m := range members.Members {
 		for _, u := range m.ClientURLs {
 			if u == dupClientURL {
 				dupOwners = append(dupOwners, m.ID)
+				break
 			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server/join/join_test.go` around lines 206 - 212, Update the nested
ClientURLs loop in the duplicate-owner collection to break after appending a
member ID for dupClientURL, ensuring each member contributes at most once to
dupOwners while preserving the existing matching behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/server/join/join_test.go`:
- Around line 176-178: In the test setup around tests.NewTestCluster, verify err
with re.NoError before registering cleanup. Move defer cluster.Destroy() after
the assertion so cleanup is only scheduled when cluster creation succeeds and
failures preserve the original error.

---

Nitpick comments:
In `@tests/server/join/join_test.go`:
- Around line 206-212: Update the nested ClientURLs loop in the duplicate-owner
collection to break after appending a member ID for dupClientURL, ensuring each
member contributes at most once to dupOwners while preserving the existing
matching behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f0ec241-ba42-4878-9e32-3e8aa7c1e815

📥 Commits

Reviewing files that changed from the base of the PR and between 6a2787b and fa03d1a.

📒 Files selected for processing (1)
  • tests/server/join/join_test.go

Comment on lines +176 to +178
cluster, err := tests.NewTestCluster(ctx, 1)
defer cluster.Destroy()
re.NoError(err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Prevent nil pointer panic on test failure.

If tests.NewTestCluster encounters an error, cluster will be nil. Calling defer cluster.Destroy() before verifying the error will result in a nil pointer panic during test cleanup, which masks the original failure reason. Move the defer to execute only after asserting that no error occurred.

🐛 Proposed fix
 	cluster, err := tests.NewTestCluster(ctx, 1)
-	defer cluster.Destroy()
 	re.NoError(err)
+	defer cluster.Destroy()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cluster, err := tests.NewTestCluster(ctx, 1)
defer cluster.Destroy()
re.NoError(err)
cluster, err := tests.NewTestCluster(ctx, 1)
re.NoError(err)
defer cluster.Destroy()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server/join/join_test.go` around lines 176 - 178, In the test setup
around tests.NewTestCluster, verify err with re.NoError before registering
cleanup. Move defer cluster.Destroy() after the assertion so cleanup is only
scheduled when cluster creation succeeds and failures preserve the original
error.

@ti-chi-bot ti-chi-bot Bot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Jul 15, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/duplicate-client-url-membership.md`:
- Around line 60-64: Align the design document’s stated correctness goal with
the implementation actually selected: either narrow the goal to sequential joins
on upgraded nodes, or revise the initial A1 scope to include an authoritative,
concurrency-safe mechanism covering concurrent joins and bypassed join paths.
Update the corresponding goal and scope sections, including the referenced
A1/A1'/A2-c discussion and later guarantees, so they consistently describe the
chosen behavior without claiming deferred guarantees.
- Around line 88-95: Resolve the contradictory A2-c rollout decision throughout
the design document. Make the decision, recommendation, proposed solution,
rollout plan, implementation scope, testing expectations, and operational
guidance consistently state whether A2-c is included in this PR; if deferred,
remove language recommending shipment and clearly keep it as future work.
- Around line 217-227: Update the pseudocode fence around the member
reconciliation example to include an explicit language identifier such as text
or pseudocode, while preserving the existing example content.
- Around line 266-276: Clarify the C2 health semantics in the design section by
explicitly stating how duplicate client URLs affect individual member health,
aggregate health, and quorum decisions without C1; avoid ambiguity about whether
all sharers are marked unhealthy or a separate duplicate condition is reported.
Add tests covering the chosen behavior, including a duplicate URL where one
responder is otherwise healthy.
- Around line 101-108: The design must require the duplicate client-URL check to
run before PrepareJoinCluster performs MemberAdd, preventing rejected fresh
joins from leaving membership behind; alternatively, explicitly define rollback
cleanup and a regression test proving membership is unchanged after rejection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8ff3fa7-0bd9-4f46-b2db-c2e6b1233c09

📥 Commits

Reviewing files that changed from the base of the PR and between fa03d1a and aabb2cb.

📒 Files selected for processing (1)
  • docs/design/duplicate-client-url-membership.md

Comment on lines +60 to +64
1. **Prevent** the common case (sequential join with a duplicated client URL)
cheaply and with a clear, early error.
2. **Guarantee** correctness under concurrent joins and when the join path is
bypassed.
3. **Make existing corruption visible and safe** — an orphan must never be

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Align the stated correctness goal with the selected implementation scope.

The design goal requires correctness under concurrent joins and bypassed join paths, but A1 explicitly does not provide those guarantees, while A1' and A2-c are deferred. Either narrow the goal to sequential joins on upgraded nodes or include an authoritative/concurrency-safe mechanism in the initial fix.

Also applies to: 121-131, 314-321

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design/duplicate-client-url-membership.md` around lines 60 - 64, Align
the design document’s stated correctness goal with the implementation actually
selected: either narrow the goal to sequential joins on upgraded nodes, or
revise the initial A1 scope to include an authoritative, concurrency-safe
mechanism covering concurrent joins and bypassed join paths. Update the
corresponding goal and scope sections, including the referenced A1/A1'/A2-c
discussion and later guarantees, so they consistently describe the chosen
behavior without claiming deferred guarantees.

Comment on lines +88 to +95
- **C2 rides along with A1** (make any residual duplicate visibly unhealthy).
**C1 / A2** are left as possible future work, not part of the initial fix.

## Proposed solution (layered)

The layers are complementary defense-in-depth. Per the decision above, the
initial fix is **A1 (mandatory) + C2**; A2 and C1 are documented but deferred.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Resolve the contradictory A2-c rollout decision.

The decision says A2-c is deferred, the recommendation says to ship it, and the rollout plan defers it again. Clarify whether A2-c is part of this PR; otherwise implementation, testing, and operational expectations are inconsistent.

Also applies to: 209-210, 280-289, 314-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design/duplicate-client-url-membership.md` around lines 88 - 95, Resolve
the contradictory A2-c rollout decision throughout the design document. Make the
decision, recommendation, proposed solution, rollout plan, implementation scope,
testing expectations, and operational guidance consistently state whether A2-c
is included in this PR; if deferred, remove language recommending shipment and
clearly keep it as future work.

Comment thread docs/design/duplicate-client-url-membership.md Outdated
Comment on lines +217 to +227
```
loop every memberReconcileInterval (leader only):
members = cluster.GetMembers(s.GetClient())
owners = map[clientURL] -> []member // reverse index
for url, ms in owners where len(ms) > 1:
trueID = probeSelfID(url) // C1 endpoint; who actually answers
if trueID unknown:
warn + metric; continue // cannot identify -> do not remove
for m in ms where m.ID != trueID: // every other sharer is an orphan
quarantine(m) // mark unhealthy + warn + metric
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to the pseudocode fence.

Use a fence such as text or pseudocode so Markdown lint does not report MD040.

Proposed fix
-```
+```text
 loop every memberReconcileInterval (leader only):
   members = cluster.GetMembers(s.GetClient())
   owners  = map[clientURL] -> []member
   ...
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
loop every memberReconcileInterval (leader only):
members = cluster.GetMembers(s.GetClient())
owners = map[clientURL] -> []member // reverse index
for url, ms in owners where len(ms) > 1:
trueID = probeSelfID(url) // C1 endpoint; who actually answers
if trueID unknown:
warn + metric; continue // cannot identify -> do not remove
for m in ms where m.ID != trueID: // every other sharer is an orphan
quarantine(m) // mark unhealthy + warn + metric
```
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 217-217: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design/duplicate-client-url-membership.md` around lines 217 - 227,
Update the pseudocode fence around the member reconciliation example to include
an explicit language identifier such as text or pseudocode, while preserving the
existing example content.

Source: Linters/SAST tools

Comment on lines +266 to +276
### C2 — Do not report duplicates as healthy (health hardening, backstop)

Independent of C1, harden `CheckHealth` / `GetHealthStatus`:

- Pre-scan the member list; when two members share a client URL, log a warning
and export a metric.
- Do not credit a single `200` on a shared URL to multiple members. Credit it
only to the identity-matching member (with C1), or mark the conflicting
members unhealthy to surface the problem (without C1).

C2 immediately removes the "orphan appears healthy" illusion and is zero-risk.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='docs/design/duplicate-client-url-membership.md'

echo '--- file size ---'
wc -l "$file"

echo '--- surrounding context around lines 220-320 ---'
sed -n '220,320p' "$file" | cat -n

echo '--- search for C1/C2 and health-related wording ---'
rg -n 'C1|C2|health|healthy|unhealthy|duplicate|client URL|quorum|availability' "$file"

Repository: tikv/pd

Length of output: 10903


Define C2’s health semantics explicitly.

Without C1, “mark the conflicting members unhealthy” can also penalize the healthy responder, turning a duplicate URL into a false availability/quorum failure in small clusters. Spell out whether C2 reports a separate duplicate condition, affects only aggregate health, or intentionally marks every sharer unhealthy, and add test coverage for that behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/design/duplicate-client-url-membership.md` around lines 266 - 276,
Clarify the C2 health semantics in the design section by explicitly stating how
duplicate client URLs affect individual member health, aggregate health, and
quorum decisions without C1; avoid ambiguity about whether all sharers are
marked unhealthy or a separate duplicate condition is reported. Add tests
covering the chosen behavior, including a duplicate URL where one responder is
otherwise healthy.

@bufferflies bufferflies force-pushed the claude/github-issue-review-72bf0f branch from aabb2cb to fa03d1a Compare July 15, 2026 08:01
@ti-chi-bot ti-chi-bot Bot added size/M Denotes a PR that changes 30-99 lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jul 15, 2026
A joining member could pass with a unique peer URL while advertising a
client URL already owned by an existing member, because MemberAdd only
carries peer URLs and etcd does not enforce client-URL uniqueness. The
result was two members sharing one client URL, corrupting membership and
health semantics.

Add a standalone checkAndClaimClientURLs invoked before MemberAdd. It
lists the current members and rejects the join if the advertised client
URL is already used by another member, then atomically claims each URL
via an etcd transaction so concurrent joiners cannot both take the same
URL. Flip the reproduction test into a regression test asserting the
join is rejected and membership is untouched.

Issue Number: close tikv#10999

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: tongjian <1045931706@qq.com>
@ti-chi-bot ti-chi-bot Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. release-note Denotes a PR that will be considered when it comes time to generate release notes. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. release-note-none Denotes a PR that doesn't merit a release note. labels Jul 15, 2026
@bufferflies bufferflies changed the title tests: reproduce join with duplicate advertise-client-urls server/join: reject join reusing another member's client URL Jul 15, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/server/join/join_test.go (1)

187-198: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Exercise the registry transaction rather than only the member-list guard.

Because dupClientURL already appears in pd1.ClientURLs, this test returns from layer 1 and never tests atomic claiming. Add concurrent joiners targeting an initially unowned URL and assert exactly one reaches membership; also cover cleanup after an injected MemberAdd failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server/join/join_test.go` around lines 187 - 198, Extend the join tests
around cluster.Join to use concurrent joiners targeting an initially unowned
client URL, asserting exactly one succeeds and appears in membership to exercise
atomic registry claiming. Add a separate failure-injection case for MemberAdd
and verify the claimed URL is released afterward, allowing a subsequent join to
succeed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/join/client_url.go`:
- Around line 53-55: Update the URL-claim ownership checks in the join flow,
including the logic around the registry entry and MemberAdd, so claim identity
is based on the member name plus stable advertised peer URL identity rather than
the name alone. Allow reclaiming an existing claim only when both the name and
peer URL(s) match; treat same-name claims with different or concurrent peer
identities as conflicts.
- Around line 85-110: Make advertised URL ownership transactional across join
completion: in server/join/client_url.go:85-110, update the client URL claim
flow to claim the complete URL set atomically and provide a conditional release
operation for this incarnation’s claims. In server/join/join.go:159-167, invoke
that release operation whenever MemberAdd fails, preserving claims owned by
other members.
- Around line 60-66: Update checkAndClaimClientURLs to accept a caller context,
pass the context from PrepareJoinCluster through the join path, and replace
NewSlowLogTxn with NewSlowLogTxnWithContext using that context so cancellation
is honored.

---

Nitpick comments:
In `@tests/server/join/join_test.go`:
- Around line 187-198: Extend the join tests around cluster.Join to use
concurrent joiners targeting an initially unowned client URL, asserting exactly
one succeeds and appears in membership to exercise atomic registry claiming. Add
a separate failure-injection case for MemberAdd and verify the claimed URL is
released afterward, allowing a subsequent join to succeed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 59e6ffa4-fc7f-4aca-88e9-40ec700a9393

📥 Commits

Reviewing files that changed from the base of the PR and between aabb2cb and 8ed5fab.

📒 Files selected for processing (3)
  • server/join/client_url.go
  • server/join/join.go
  • tests/server/join/join_test.go

Comment thread server/join/client_url.go Outdated
Comment thread server/join/client_url.go Outdated
Comment on lines +60 to +66
func checkAndClaimClientURLs(
client *clientv3.Client,
clusterID uint64,
name string,
advertiseClientURLs []string,
members []*etcdserverpb.Member,
) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- server/join/client_url.go (lines 1-220) ---'
sed -n '1,220p' server/join/client_url.go | cat -n

echo
echo '--- search for checkAndClaimClientURLs ---'
rg -n "checkAndClaimClientURLs|ClaimClientURLs|clientURL" server -S

Repository: tikv/pd

Length of output: 5792


🏁 Script executed:

#!/bin/bash
set -euo pipefail
git ls-files 'server/join/client_url.go' && echo FOUND

Repository: tikv/pd

Length of output: 177


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- server/join/join.go (around call site) ---'
sed -n '130,210p' server/join/join.go | cat -n

echo
echo '--- pkg/storage/kv search ---'
rg -n "func NewSlowLogTxn|type .*Txn|func .*Commit\(|context.Context|WithContext|NewTxn|SlowLogTxn" pkg/storage/kv -S

Repository: tikv/pd

Length of output: 5754


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '135,185p' pkg/storage/kv/etcd_kv.go | cat -n

Repository: tikv/pd

Length of output: 1997


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' server/join/join.go | cat -n

Repository: tikv/pd

Length of output: 5740


Thread a caller context through this join path. This helper still falls back to client.Ctx() via NewSlowLogTxn, so it won’t honor caller cancellation. Pass ctx from PrepareJoinCluster and use NewSlowLogTxnWithContext(ctx, client).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/join/client_url.go` around lines 60 - 66, Update
checkAndClaimClientURLs to accept a caller context, pass the context from
PrepareJoinCluster through the join path, and replace NewSlowLogTxn with
NewSlowLogTxnWithContext using that context so cancellation is honored.

Source: Coding guidelines

Comment thread server/join/client_url.go Outdated
Comment on lines +85 to +110
// Layer 2: atomically claim each URL.
for _, url := range advertiseClientURLs {
key := clientURLRegistryPath(clusterID, url)
resp, err := kv.NewSlowLogTxn(client).
If(clientv3.Compare(clientv3.CreateRevision(key), "=", 0)).
Then(clientv3.OpPut(key, name)).
Else(clientv3.OpGet(key)).
Commit()
if err != nil {
return errors.WithStack(err)
}
if resp.Succeeded {
continue // freshly claimed
}
// The key already exists; make sure we are the owner.
kvs := resp.Responses[0].GetResponseRange().Kvs
if len(kvs) == 0 {
return errors.Errorf("failed to claim advertise-client-urls %q, please retry", url)
}
if owner := string(kvs[0].Value); owner != name {
return errors.Errorf(
"advertise-client-urls %q is already claimed by member %q", url, owner)
}
log.Info("re-claimed an advertised client URL owned by this member",
zap.String("name", name), zap.String("client-url", url))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Tie persistent URL ownership to successful join completion.

Claims currently survive partial validation and downstream join failures, leaving registry entries without corresponding members.

  • server/join/client_url.go#L85-L110: claim the complete URL set atomically and expose conditional release.
  • server/join/join.go#L159-L167: release this incarnation’s claims whenever MemberAdd fails.
📍 Affects 2 files
  • server/join/client_url.go#L85-L110 (this comment)
  • server/join/join.go#L159-L167
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/join/client_url.go` around lines 85 - 110, Make advertised URL
ownership transactional across join completion: in
server/join/client_url.go:85-110, update the client URL claim flow to claim the
complete URL set atomically and provide a conditional release operation for this
incarnation’s claims. In server/join/join.go:159-167, invoke that release
operation whenever MemberAdd fails, preserving claims owned by other members.

… url

Extend the duplicate advertise-client-urls guard to also run on a
restart that changed its client URL (data-dir path), before the local
etcd republishes, so such a change cannot slip a duplicate into the
member list. The restart path is best-effort: an unreachable cluster
does not block a member from starting off its local data.

Key the client-url registry claim by the owner's peer URLs (globally
unique, stable across restarts) instead of its name, so two concurrent
joiners sharing a name are still rejected. Match self by peer URL in the
member-list comparison for the same reason.

Add a regression test for a restart with a modified, duplicated client
URL.

Issue Number: close tikv#10999

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: tongjian <1045931706@qq.com>

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
server/join/join.go (1)

129-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Check the error returned by client.Close().

As per coding guidelines, resources should be closed with defer and their errors should be checked. The client.Close() method returns an error that is currently being silently ignored.

♻️ Proposed refactor
-	defer client.Close()
+	defer func() {
+		if err := client.Close(); err != nil {
+			log.Error("failed to close etcd client", errs.ZapError(err))
+		}
+	}()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/join/join.go` at line 129, Update the deferred cleanup around
client.Close in the join flow to capture and handle the error returned by
client.Close instead of silently discarding it, using the surrounding function’s
established error-reporting or return pattern.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/join/join.go`:
- Line 129: Update the deferred cleanup around client.Close in the join flow to
capture and handle the error returned by client.Close instead of silently
discarding it, using the surrounding function’s established error-reporting or
return pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b7b9773-506b-48f1-b6bf-b7e233bca9c5

📥 Commits

Reviewing files that changed from the base of the PR and between 8ed5fab and 517b819.

📒 Files selected for processing (3)
  • server/join/client_url.go
  • server/join/join.go
  • tests/server/join/join_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/server/join/join_test.go

…estart

Split the client-URL guard so the write path cannot block cluster
recovery: the read-only conflict check runs on every startup, while the
etcd registry claim (a write needing quorum) runs only on the fresh-join
path where quorum is required anyway. Otherwise a member restarting
during a quorum loss could hang on the claim and fail to start.

On the restart path, retry ListEtcdMembers with exponential backoff
(3 times, ~20s) before skipping the best-effort check, so a transient
failure does not silently bypass duplicate detection.

Issue Number: close tikv#10999

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: tongjian <1045931706@qq.com>
@ti-chi-bot ti-chi-bot Bot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Jul 16, 2026

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (2)
server/join/join.go (2)

57-77: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Implement context-aware backoff for retries. As per coding guidelines, functions making external effects should take a context.Context as their first parameter, and retries should use context-aware timeouts and backoffs. Currently, time.Sleep blocks without respecting context cancellation.

  • server/join/join.go#L57-L77: Add ctx context.Context as the first parameter and replace time.Sleep(backoff) with select { case <-ctx.Done(): return nil, ctx.Err(); case <-time.After(backoff): }. Pass ctx to etcdutil.ListEtcdMembers.
  • server/join/join.go#L173-L173: Update the call site to pass the client context: retryListEtcdMembers(client.Ctx(), client).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/join/join.go` around lines 57 - 77, Make retryListEtcdMembers
context-aware by adding a context.Context first parameter, replacing time.Sleep
with a context cancellation select that returns ctx.Err(), and passing ctx to
etcdutil.ListEtcdMembers. Update server/join/join.go lines 57-77 accordingly; at
server/join/join.go line 173, pass client.Ctx() to retryListEtcdMembers.

Source: Coding guidelines


188-194: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse parsed URL slices to avoid repeated string splitting. cfg.AdvertiseClientUrls and cfg.AdvertisePeerUrls are repeatedly split into slices across the function. Parsing them once before their first use improves clarity and avoids redundant allocations.

  • server/join/join.go#L188-L194: Hoist the strings.Split calls into local variables (advertiseClientURLs and advertisePeerURLs) right before checkClientURLConflict and pass the variables.
  • server/join/join.go#L209-L209: Remove the inline declaration of advertisePeerURLs since it is already defined above.
  • server/join/join.go#L236-L245: Pass the reused advertiseClientURLs and advertisePeerURLs variables into claimClientURLs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/join/join.go` around lines 188 - 194, Parse cfg.AdvertiseClientUrls
and cfg.AdvertisePeerUrls once into advertiseClientURLs and advertisePeerURLs
before checkClientURLConflict in server/join/join.go:188-194, then reuse both
variables for claimClientURLs at server/join/join.go:236-245. Remove the later
inline advertisePeerURLs declaration at server/join/join.go:209 because the
earlier parsed variable replaces it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/join/join.go`:
- Around line 57-77: Make retryListEtcdMembers context-aware by adding a
context.Context first parameter, replacing time.Sleep with a context
cancellation select that returns ctx.Err(), and passing ctx to
etcdutil.ListEtcdMembers. Update server/join/join.go lines 57-77 accordingly; at
server/join/join.go line 173, pass client.Ctx() to retryListEtcdMembers.
- Around line 188-194: Parse cfg.AdvertiseClientUrls and cfg.AdvertisePeerUrls
once into advertiseClientURLs and advertisePeerURLs before
checkClientURLConflict in server/join/join.go:188-194, then reuse both variables
for claimClientURLs at server/join/join.go:236-245. Remove the later inline
advertisePeerURLs declaration at server/join/join.go:209 because the earlier
parsed variable replaces it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1eca7846-0d4d-43dc-a406-1875f1f5418b

📥 Commits

Reviewing files that changed from the base of the PR and between 517b819 and d31d372.

📒 Files selected for processing (2)
  • server/join/client_url.go
  • server/join/join.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/join/client_url.go

@ti-chi-bot

ti-chi-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@bufferflies: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-unit-test-next-gen-2 d31d372 link true /test pull-unit-test-next-gen-2
pull-unit-test-next-gen-3 d31d372 link true /test pull-unit-test-next-gen-3

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@JmPotato JmPotato left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Inline comments cover the blocking issues found at d31d372.

Comment thread server/join/join.go
// Atomically claim the advertised client URLs before MemberAdd so two
// concurrent joiners cannot both take the same client URL. Only the
// fresh-join path reaches here, where quorum is required anyway.
if err := claimClientURLs(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This write also runs when joinedFailedToStart is true. That recovery path may already be at 1/2 quorum, so the transaction fails and prevents the missing member from starting. TestFailedToStartPDAfterSuccessfulJoin already reproduces this; keep this recovery path write-free.

Comment thread server/join/client_url.go
if err != nil {
return errors.WithStack(err)
}
for _, url := range advertiseClientURLs {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

URLs are claimed one transaction at a time and never released if a later claim or MemberAdd fails, or when the member is removed. This leaves permanent orphan claims and blocks legitimate replacements. Claim the full set atomically and clean it up with the member lifecycle.

Comment thread server/join/join.go
cfg.InitialClusterState = embed.ClusterStateFlagExisting
return nil
}
dataExists := isDataExist(filepath.Join(cfg.DataDir, "member"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

PrepareJoinCluster returns above when cfg.Join == "", so this check does not actually run on every startup. A member bootstrapped with initial-cluster can still restart with another member client URL. Please cover that path or narrow the claimed guarantee.

Comment thread server/join/join.go
// data directory, nothing more to prepare here. The registry claim is
// intentionally skipped on this path — it writes to etcd (needs quorum), and
// a restart must not depend on quorum.
if dataExists {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is still a check-then-publish race: restarting members skip the registry claim, so two restarts—or a restart and a fresh join—can both pass before either publishes the new URL. The PR can still create duplicate client URLs under concurrency.

Comment thread server/join/join.go
// another member. This is read-only (no quorum needed) and runs before the
// local etcd (re)starts and republishes its attributes, so a duplicate never
// enters the member list. See issue #10999.
if err := checkClientURLConflict(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

#10999 also calls for health checks to verify the responding member identity. /pd/api/v1/ping still exposes no identity, so an orphan sharing a live URL remains falsely healthy. Please address this before closing the issue, or track it separately and change this PR to ref.

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

Labels

dco-signoff: yes Indicates the PR's author has signed the dco. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prevent a joining PD member from publishing a client URL already used by another member

2 participants