Skip to content

mcs: reject registration reusing another instance's address - #11043

Open
bufferflies wants to merge 2 commits into
tikv:masterfrom
bufferflies:mcs-register-validation
Open

mcs: reject registration reusing another instance's address#11043
bufferflies wants to merge 2 commits into
tikv:masterfrom
bufferflies:mcs-register-validation

Conversation

@bufferflies

@bufferflies bufferflies commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: Close #11001, Close #10998

A microservice instance (TSO / Scheduling) started with an --advertise-listen-addr already used by another live instance could blindly overwrite that instance's etcd registry entry and then join the primary election with the same participant identity.

What is changed and how does it work?

Validate the registry key at registration time in pkg/mcs/discovery:

  • putWithTTL now claims the registry key with an atomic create-if-absent etcd transaction instead of an unconditional Put, so a duplicate advertised address can no longer overwrite another live instance's entry.
  • If the key already exists with this instance's own value (e.g. re-registering after a keepalive failure while the previous lease is still alive), it is taken over with the new lease via a value-guarded transaction; otherwise the registration is rejected with a clear error and the granted lease is revoked.
  • Register retries the claim within the lease TTL window, so a stale entry left by a crashed instance with the same address expires and the restart succeeds without manual intervention, while a genuine duplicate live instance keeps its lease refreshed and the orphan fails startup before ever entering the primary election (all MCS servers call Register before startServer).

Check List

Tests

  • Unit test: TestRegisterConflict verifies a duplicate live registration is rejected without overwriting the existing entry, and that a new instance can register after the stale lease expires.
  • Integration test: tests/integrations/mcs/discovery passes.

Release note

Reject microservice registration when the advertised address is already claimed by another live instance, preventing registry overwrite and unintended primary election takeover.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved service registration when another live instance is already using the same address.
    • Prevented duplicate registrations from replacing an active service entry.
    • Automatically retries registration until the existing lease expires or the operation is canceled.
    • Allows registration to succeed after a stale service entry is removed.

Claim the service registry key with an atomic create-if-absent
transaction so that an instance advertising a duplicate address
cannot overwrite the registry entry of another live instance and
further join the primary election with the same identity. A stale
entry left by a crashed instance expires with its lease, so the
registration retries within the lease TTL before giving up.

Signed-off-by: tongjian <1045931706@qq.com>
@ti-chi-bot ti-chi-bot Bot added 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. do-not-merge/needs-triage-completed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Jul 23, 2026
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Service registration now uses lease-backed conditional etcd transactions, rejects live duplicate addresses, retries occupied registrations within a TTL-based deadline, and revokes failed leases. Tests cover conflict rejection, lease expiry, and same-value lease ownership.

Changes

Service registration conflict handling

Layer / File(s) Summary
Registration retry loop
pkg/mcs/discovery/register.go
Tracks the registration lease, retries occupied keys until success, cancellation, another error, or the TTL-based deadline, and revokes failed leases.
Lease-backed claim validation
pkg/mcs/discovery/register.go, pkg/mcs/discovery/register_test.go
Uses explicit leases and conditional etcd transactions. New keys require absence, while re-registration requires the tracked lease. Tests cover duplicate rejection, value preservation, lease expiry, and same-value conflicts.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ServiceRegister
  participant Etcd
  participant ServiceContext
  ServiceRegister->>Etcd: Grant lease and conditionally claim registry key
  Etcd-->>ServiceRegister: Return success or errServiceAddrOccupied
  ServiceRegister->>ServiceContext: Wait for retry interval or cancellation
  ServiceRegister->>Etcd: Retry claim before the TTL-based deadline
  Etcd-->>ServiceRegister: Register after the previous lease expires
Loading

Possibly related PRs

  • tikv/pd#11017: Both changes modify lease behavior in pkg/mcs/discovery/register.go.

Suggested reviewers: lhy1024

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states that registration rejects reuse of another instance's address.
Description check ✅ Passed The description includes both issue links, implementation details, tests, and a release note required by the template.
Linked Issues check ✅ Passed The lease-backed atomic registration guard addresses duplicate endpoint protection, stale-entry recovery, and election prevention for [#11001, #10998].
Out of Scope Changes check ✅ Passed The code and tests are limited to registration conflict handling and directly support the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

🧹 Nitpick comments (1)
pkg/mcs/discovery/register_test.go (1)

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

Prefer errors.Is over substring matching for the sentinel error check.

Since this is an internal test (same package), asserting errors.Is(err, errServiceAddrOccupied) directly ties the test to the actual sentinel error instead of its message wording, which is more robust against future message-copy changes.

♻️ Proposed refactor
 	err := sr2.Register()
 	re.Error(err)
-	re.Contains(err.Error(), "occupied")
+	re.ErrorIs(err, errServiceAddrOccupied)
🤖 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 `@pkg/mcs/discovery/register_test.go` around lines 109 - 111, Update the
Register test’s error assertion after sr2.Register() to use errors.Is with the
errServiceAddrOccupied sentinel instead of checking for the "occupied" message
substring, while retaining the existing error assertion.
🤖 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 `@pkg/mcs/discovery/register_test.go`:
- Around line 109-111: Update the Register test’s error assertion after
sr2.Register() to use errors.Is with the errServiceAddrOccupied sentinel instead
of checking for the "occupied" message substring, while retaining the existing
error assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b278b836-d3c2-4210-9736-a876500f995b

📥 Commits

Reviewing files that changed from the base of the PR and between f7db425 and 18f200d.

📒 Files selected for processing (2)
  • pkg/mcs/discovery/register.go
  • pkg/mcs/discovery/register_test.go

Comment thread pkg/mcs/discovery/register.go Outdated
// A stale registry entry left by a crashed instance with the same advertised
// address will be removed automatically once its lease expires, so retry
// within the lease TTL before giving up.
deadline := time.Now().Add(time.Duration(sr.ttl+1) * time.Second)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

6s might not be enough: across an etcd leader change, Promote(ElectionTimeout()) and Lease.refresh resets expiry to now + RemainingTTL + extend.

Comment thread pkg/mcs/discovery/register.go Outdated
// a keepalive failure while the previous lease has not expired yet), take
// it over with the new lease.
takeoverResp, err := sr.cli.Txn(ctx).
If(clientv3.Compare(clientv3.Value(sr.key), "=", sr.value)).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Blocker] Only comparing values doesn't tell two processes apart: the only per-process field is StartTimestamp, in whole seconds (pkg/mcs/server/server.go:58). Two instances on the same host with the same advertise addr, started in the same second, write the same value — so the second one just takes the key.

I hit this on a real 3-node PD: two scheduling instances, same advertise addr, started ~3ms apart. Both registered, neither saw occupied, both campaigned, and the loser deleted the other's leader key via participant.go:238. That's #10998 — the election ID is still just a hash of the address.

Fix: put a nonce in ServiceRegistryEntry, or compare kvs[0].Lease instead of the value.

- Compare the registry key's lease ID instead of its serialized value to
  decide whether an existing entry belongs to this instance. Two distinct
  instances can serialize an identical ServiceRegistryEntry (StartTimestamp
  has only second precision), so a value match does not prove ownership and
  could let one live instance take over another's registration.
- Extend the registration retry deadline past the raw lease TTL to account
  for etcd extending a lease's expiry on leader change (Lessor.Promote /
  Lease.refresh), so a legitimate retry does not give up before a stale
  entry's lease has actually expired.

@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

🤖 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 `@pkg/mcs/discovery/register.go`:
- Around line 59-63: Update the ownership check in the registration flow to
require sr.leaseID to be nonzero before comparing it with an existing registry
key’s lease ID. Reject unleased existing keys instead of allowing a freshly
started process to overwrite them, while preserving the current behavior for
matching nonzero leases.
🪄 Autofix

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 Plus

Run ID: e0d08bb6-7b14-42ec-a39a-b224a604a8ae

📥 Commits

Reviewing files that changed from the base of the PR and between 18f200d and a573d73.

📒 Files selected for processing (2)
  • pkg/mcs/discovery/register.go
  • pkg/mcs/discovery/register_test.go

Comment on lines +59 to +63
// leaseID is the lease this instance most recently registered the key
// with, used to prove ownership of an existing entry on re-registration.
// Zero (clientv3.NoLease) until the first successful put, so a freshly
// started process can never match an existing key's lease by accident.
leaseID clientv3.LeaseID

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate register.go and etcd deps =="
git ls-files | rg '(^|/)register\.go$|go\.mod$|go\.sum$'
echo
echo "== relevant snippet =="
if [ -f pkg/mcs/discovery/register.go ]; then
  nl -ba pkg/mcs/discovery/register.go | sed -n '1,240p'
fi

echo
echo "== etcd/client deps =="
if [ -f go.mod ]; then
  rg 'go.etcd.io/etcd|clientv3|leases|registry' go.mod go.sum | head -n 120
fi

Repository: tikv/pd

Length of output: 398


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the repository has any local etcd client source that can be inspected for LeaseValue semantics.
for d in module go mod cache; do
  if [ -d "${d}" ]; then
    echo "directory ${d} exists"
    find "${d}" -path '*etcd*client*' -type f | head -n 80 || true
  fi
done

echo
echo "== search LeaseValue definitions/usages in repo =="
rg -n "func LeaseValue|LeaseValue\(|NoLease|LeaseID\\(" -S . | head -n 200

Repository: tikv/pd

Length of output: 1001


🌐 Web query:

go etcd clientv3 LeaseValue response Lease zero NoLease semantics

💡 Result:

In the etcd clientv3 library and etcd API, a lease ID of 0 is a special value defined as NoLease [1][2]. This value represents the absence of a lease [1][3]. Key semantic details include: 1. NoLease Semantics: The constant NoLease (defined as 0) is used to explicitly indicate that no lease is attached to a specific key-value pair [4][5][6]. In the etcd key-value store, each key may be attached to at most one lease; if the lease ID field for a key is 0, the key is effectively unassociated with any lease and will not be automatically deleted by lease expiration or revocation [4][5][6]. 2. LeaseValue Helper: In the clientv3 package, the LeaseValue function allows developers to compare a key's current lease ID against a specific value within a transaction (Txn) [7][8]. When using this helper, comparing against 0 (NoLease) is the standard way to verify that a key does not currently have a lease attached [7][9]. 3. LeaseGrant API: While a client can request a specific lease ID, setting the ID to 0 in a LeaseGrantRequest instructs the etcd cluster to automatically generate and assign a unique lease ID [4][5]. 4. Internal Consistency: Both the client-side (clientv3) and the server-side (lessor) packages use NoLease = 0 to maintain consistent semantics across the system [1][3][2]. Attempting to perform lease-specific operations using the NoLease ID will typically result in an error, such as ErrLeaseNotFound [3][10].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
export LANG=C

echo "== register.go relevant section =="
awk 'NR>=40 && NR<=70 {printf "%6d\t%s\n", NR, $0} NR>=180 && NR<=215 {printf "%6d\t%s\n", NR, $0}' pkg/mcs/discovery/register.go

echo
echo "== dependencies =="
awk '/go\.etcd\.io\/etcd|client\.v3|etcd (client)|registry/ {printf "%s\n", $0}' go.mod go.sum | sed -n '1,160p'

echo
echo "== deterministic semantic probe using repository comment/condition context =="
python3 - <<'PY'
from pathlib import Path
p=Path('pkg/mcs/discovery/register.go').read_text()
checks=[
    ("fresh leaseID zero", "sr.leaseID = clientv3.NoLease" in p or "leaseID clientv3.LeaseID" in p),
    ("current ownership condition compares lease", "clientv3.LeaseID(kvs[0].Lease) != sr.leaseID" in p),
    ("current transaction compares LeaseValue to leaseID", "clientv3.LeaseValue(sr.key)" in p and "sr.leaseID" in p),
    ("NoLease constant likely zero per source comment", "clientv3.NoLease" in p),
]
for name, ok in checks:
    print(f"{name}: {ok}")
PY

Repository: tikv/pd

Length of output: 5423


🏁 Script executed:

#!/bin/bash
set -euo pipefail
export LANG=C

echo "== register.go relevant section =="
awk 'NR>=1 && NR<=240 {printf "%6d\t%s\n", NR, $0}' pkg/mcs/discovery/register.go

echo
echo "== dependency lines =="
grep -E 'go\.etcd\.io/etcd|client\.v3|etcd \(client\)|registry|lease' go.mod go.sum | sed -n '1,180p' || true

echo
echo "== exact semantic probe from source =="
python3 - <<'PY'
from pathlib import Path
p=Path('pkg/mcs/discovery/register.go').read_text()
print("fresh leaseID field:", "leaseID clientv3.LeaseID" in p)
print("init zero leaseID:", "sr.leaseID = clientv3.NoLease" in p)
print("ownership compares existing lease to register leaseID:", "clientv3.LeaseID(kvs[0].Lease) != sr.leaseID" in p)
print("transaction compares current LeaseValue to register leaseID:", "clientv3.LeaseValue(sr.key)" in p)
print("comment explains NoLease/zero:", "Zero (clientv3.NoLease)" in p)
PY

Repository: tikv/pd

Length of output: 11681


🏁 Script executed:

#!/bin/bash
set -euo pipefail
export LANG=C

echo "== register.go relevant section =="
awk 'NR>=1 && NR<=240 {printf "%6d\t%s\n", NR, $0}' pkg/mcs/discovery/register.go

echo
echo "== dependency lines =="
grep -E 'go\.etcd\.io/etcd|client\.v3|etcd \(client\)|registry|lease' go.mod go.sum | sed - n '1,180p' || true

echo
	echo "== exact semantic probe from source =="
python3 - <<'PY'
from pathlib import Path
p=Path('pkg/mcs/discovery/register.go').read_text()
print("fresh leaseID field:", "leaseID clientv3.LeaseID" in p)
print("init zero leaseID:", "sr.leaseID = clientv3.NoLease" in p)
print("ownership compares existing lease to register leaseID:", "clientv3.LeaseID(kvs[0].Lease) != sr.leaseID" in p)
print("transaction compares current LeaseValue to register leaseID:", "clientv3.LeaseValue(sr.key)" in p)
print("comment explains NoLease/zero:", "Zero (clientv3.NoLease)" in p)
PY

Repository: tikv/pd

Length of output: 10114


Reject unleased existing registry keys.

sr.leaseID starts as clientv3.NoLease, and an existing etcd key without a lease also has lease ID zero. The ownership check can then treat that key as owned by a freshly started process and allow re-registration to overwrite it. Require sr.leaseID to be nonzero before accepting lease ownership.

🤖 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 `@pkg/mcs/discovery/register.go` around lines 59 - 63, Update the ownership
check in the registration flow to require sr.leaseID to be nonzero before
comparing it with an existing registry key’s lease ID. Reject unleased existing
keys instead of allowing a freshly started process to overwrite them, while
preserving the current behavior for matching nonzero leases.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.07407% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.34%. Comparing base (39b6220) to head (a573d73).
⚠️ Report is 18 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11043      +/-   ##
==========================================
+ Coverage   79.25%   79.34%   +0.09%     
==========================================
  Files         541      542       +1     
  Lines       76037    77058    +1021     
==========================================
+ Hits        60262    61145     +883     
- Misses      11534    11602      +68     
- Partials     4241     4311      +70     
Flag Coverage Δ
unittests 79.34% <74.07%> (+0.09%) ⬆️

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ti-chi-bot

ti-chi-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@YuhaoZhang00: adding LGTM is restricted to approvers and reviewers in OWNERS files.

Details

In response to this:

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.

@ti-chi-bot

ti-chi-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: YuhaoZhang00
Once this PR has been reviewed and has the lgtm label, please assign overvenus 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

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. do-not-merge/needs-triage-completed release-note Denotes a PR that will be considered when it comes time to generate release notes. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

2 participants