Skip to content

tso, pd-ctl: support evicting all keyspace group primaries on a node - #10968

Merged
ti-chi-bot[bot] merged 1 commit into
tikv:masterfrom
bufferflies:claude/vigorous-bassi-1f06ff
Jul 9, 2026
Merged

tso, pd-ctl: support evicting all keyspace group primaries on a node#10968
ti-chi-bot[bot] merged 1 commit into
tikv:masterfrom
bufferflies:claude/vigorous-bassi-1f06ff

Conversation

@bufferflies

@bufferflies bufferflies commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: Close #10967

A TSO node exposes POST /tso/api/v1/primary/transfer to move a single
keyspace group primary away. Draining a node (restart/upgrade/decommission)
requires moving all keyspace group primaries off it, which today means
calling transfer once per group — tedious and error-prone.

What is changed and how does it work?

Add a node-local endpoint and a pd-ctl command to evict every keyspace group
primary held by a node in one step.

Add POST /tso/api/v1/primary/evict, which transfers away every keyspace
group primary currently held by the target TSO node. It iterates over the
groups this node is serving as primary and transfers each to another member
of the same group. It is best-effort: groups the node does not serve as
primary are skipped, a failure on one group does not abort the others, and a
per-group result map is returned.

Expose it through pd-ctl as `microservice tso evict-primary <address>`, which
sends the request directly to the target node since the operation is
node-local.

Check List

Tests

  • Integration test

Code changes

Release note

Support evicting all keyspace group primaries held by a TSO node via the new `POST /tso/api/v1/primary/evict` endpoint and the `pd-ctl microservice tso evict-primary` command.

Summary by CodeRabbit

  • New Features
    • Added a TSO primary eviction endpoint (POST /tso/api/v1/primary/evict) to trigger best-effort eviction of eligible keyspace-group primaries served by the target node.
    • Added a pd-ctl microservice tso evict-primary <tso_node_address> command to invoke the endpoint and display per-group outcomes.
  • Behavior Updates
    • The response includes per–keyspace-group result details; HTTP 500 if any group fails, otherwise HTTP 200.
  • Tests
    • Added an integration test to validate primary reassignment end-to-end.
    • Added unit tests for the new pd-ctl command.

@ti-chi-bot ti-chi-bot Bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. dco-signoff: yes Indicates the PR's author has signed the dco. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Jul 2, 2026
@coderabbitai

coderabbitai Bot commented Jul 2, 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 a node-local TSO primary eviction endpoint, exposes it through pd-ctl, and validates the eviction flow in integration tests.

Changes

Evict Primary Feature

Layer / File(s) Summary
Evict endpoint route and handler
pkg/mcs/tso/server/apis/v1/api.go
Registers POST /evict on the primary router and implements evictPrimary, which iterates held keyspace group primaries, transfers each primary away, and returns per-group success or error results.
pd-ctl evict-primary subcommand
tools/pd-ctl/pdctl/command/microservice_command.go, tools/pd-ctl/pdctl/command/microservice_command_test.go
Defines the TSO eviction endpoint path constant, registers evict-primary under microservice tso, implements the command, and adds unit tests for success, error, and missing-argument paths.
Integration test for eviction flow
tests/integrations/mcs/members/member_test.go
Adds TestEvictPrimary, which creates keyspace groups on all TSO nodes, invokes the eviction endpoint, and verifies primary ownership moves away from the target node.

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

Sequence Diagram(s)

sequenceDiagram
  participant pdctl as pd-ctl evict-primary
  participant api as TSO /primary/evict
  participant utils as TransferPrimary

  pdctl->>api: POST /tso/api/v1/primary/evict
  loop each primary keyspace group on the node
    api->>utils: transfer away from current primary
    utils-->>api: success or error
  end
  api-->>pdctl: per-group results map
Loading

Possibly related PRs

  • tikv/pd#10893 — Updates TSO primary transfer behavior around keyspace groups in the same API area, including keyspace-group-scoped primary handoff.

Suggested labels: ok-to-test

Suggested reviewers: JmPotato, lhy1024, rleungx

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding node-level eviction of all TSO primaries with pd-ctl support.
Description check ✅ Passed The PR description matches the template with issue number, change summary, checklist, and release note filled in.
Linked Issues check ✅ Passed The changes implement the requested node-local evict endpoint, pd-ctl command, best-effort per-group handling, and integration coverage.
Out of Scope Changes check ✅ Passed The patch stays within scope, covering only the new API, pd-ctl command, and related tests.
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.

🧹 Nitpick comments (2)
tests/integrations/mcs/members/member_test.go (1)

299-325: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant polling and untested response body.

Lines 312-321 poll twice for the same condition: a manual Eventually loop checking any non-primary node is serving, immediately followed by tests.WaitForPrimaryServing, whose return value is also discarded. These can be consolidated into a single WaitForPrimaryServing(re, nodes) call. Additionally, the test never inspects the eviction response body (the per-group results map), so a regression that returns malformed/empty results with status 200 would go undetected.

♻️ Simplify polling
-	testutil.Eventually(re, func() bool {
-		for _, member := range nodes {
-			if member.GetAddr() != primary && member.IsServing() {
-				return true
-			}
-		}
-		return false
-	}, testutil.WithWaitFor(5*time.Second), testutil.WithTickInterval(50*time.Millisecond))
-
-	tests.WaitForPrimaryServing(re, nodes)
+	tests.WaitForPrimaryServing(re, nodes)
🤖 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/integrations/mcs/members/member_test.go` around lines 299 - 325, The
TestEvictPrimary flow is doing redundant polling and not validating the eviction
response payload. In memberTestSuite.TestEvictPrimary, remove the manual
testutil.Eventually check and rely on tests.WaitForPrimaryServing(re, nodes)
once to wait for the new primary. Also inspect the response body from the POST
to /tso/api/v1/primary/evict and assert the per-group results map is present and
well-formed, so a 200 response with an empty or malformed body is caught.
pkg/mcs/tso/server/apis/v1/api.go (1)

397-433: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Sequential per-group transfers with no overall timeout/cancellation.

evictPrimary iterates every serving keyspace group and performs blocking etcd operations (client.Grant, lease.Close, markExpectedPrimaryFlag via utils.TransferPrimary) sequentially, on the request goroutine, with no context deadline or early-exit on client disconnect. On a node hosting many keyspace groups, this endpoint can block for a long time, tying up the HTTP handler with no way to bound total latency.

Consider checking c.Request.Context() (or a derived timeout context) between iterations and aborting the remaining work if it's done/cancelled, so a single slow etcd interaction or large group count cannot indefinitely stall the request.

🔧 Example mitigation
 	results := make(map[uint32]string)
 	failed := false
 	for keyspaceGroupID, group := range kgm.GetServingKeyspaceGroups() {
+		if c.Request.Context().Err() != nil {
+			break
+		}
 		allocator, err := svr.GetTSOAllocator(keyspaceGroupID)
🤖 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/tso/server/apis/v1/api.go` around lines 397 - 433, The evictPrimary
handler currently processes all serving keyspace groups sequentially with no
cancellation or overall deadline, so it can block the request goroutine for too
long. Update evictPrimary in api.go to check c.Request.Context() (or a derived
timeout context) before and/or between iterations over
kgm.GetServingKeyspaceGroups(), and stop the loop early when the context is done
or the client disconnects. Keep the existing per-group transfer logic using
utils.TransferPrimary, but abort remaining work and return the partial results
when cancellation is detected.
🤖 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/tso/server/apis/v1/api.go`:
- Around line 397-433: The evictPrimary handler currently processes all serving
keyspace groups sequentially with no cancellation or overall deadline, so it can
block the request goroutine for too long. Update evictPrimary in api.go to check
c.Request.Context() (or a derived timeout context) before and/or between
iterations over kgm.GetServingKeyspaceGroups(), and stop the loop early when the
context is done or the client disconnects. Keep the existing per-group transfer
logic using utils.TransferPrimary, but abort remaining work and return the
partial results when cancellation is detected.

In `@tests/integrations/mcs/members/member_test.go`:
- Around line 299-325: The TestEvictPrimary flow is doing redundant polling and
not validating the eviction response payload. In
memberTestSuite.TestEvictPrimary, remove the manual testutil.Eventually check
and rely on tests.WaitForPrimaryServing(re, nodes) once to wait for the new
primary. Also inspect the response body from the POST to
/tso/api/v1/primary/evict and assert the per-group results map is present and
well-formed, so a 200 response with an empty or malformed body is caught.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b52a9eea-8095-4bda-9aa7-36b92e0fc605

📥 Commits

Reviewing files that changed from the base of the PR and between d71c039 and 7fb8d3f.

📒 Files selected for processing (3)
  • pkg/mcs/tso/server/apis/v1/api.go
  • tests/integrations/mcs/members/member_test.go
  • tools/pd-ctl/pdctl/command/microservice_command.go

@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch from 7fb8d3f to 16157c3 Compare July 2, 2026 13:19

@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)
tests/integrations/mcs/members/member_test.go (1)

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

Consider cleaning up the created keyspace groups after the test.

Groups with hardcoded IDs 1 and 2 are created but never deleted. If other tests in memberTestSuite (sharing the same cluster) later use the same IDs, or run after this test without an isolated cluster, this could leak state and cause flaky collisions.

♻️ Suggested cleanup
+	defer func() {
+		for _, id := range groupIDs {
+			handlersutil.MustDeleteKeyspaceGroup(re, suite.server, id)
+		}
+	}()
+
 	for i, id := range groupIDs {
 		handlersutil.MustCreateKeyspaceGroup(re, suite.server, &handlers.CreateKeyspaceGroupParams{
🤖 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/integrations/mcs/members/member_test.go` around lines 319 - 335, This
test creates keyspace groups with hardcoded IDs via
handlersutil.MustCreateKeyspaceGroup and never removes them, which can leak
state into later memberTestSuite cases. Add cleanup in the test itself (for
example with defer or suite teardown) to delete the created groups after the
assertions complete, using the same IDs and the keyspace group management
helpers so the test leaves the cluster in a clean state.
🤖 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 `@tests/integrations/mcs/members/member_test.go`:
- Around line 319-335: This test creates keyspace groups with hardcoded IDs via
handlersutil.MustCreateKeyspaceGroup and never removes them, which can leak
state into later memberTestSuite cases. Add cleanup in the test itself (for
example with defer or suite teardown) to delete the created groups after the
assertions complete, using the same IDs and the keyspace group management
helpers so the test leaves the cluster in a clean state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de2deec2-1688-4fef-aa18-9545e92c815c

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb8d3f and 16157c3.

📒 Files selected for processing (3)
  • pkg/mcs/tso/server/apis/v1/api.go
  • tests/integrations/mcs/members/member_test.go
  • tools/pd-ctl/pdctl/command/microservice_command.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/mcs/tso/server/apis/v1/api.go

@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch from 16157c3 to d57a3d8 Compare July 2, 2026 13:41
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.26733% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.27%. Comparing base (aa5a988) to head (4fb19f6).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #10968      +/-   ##
==========================================
+ Coverage   79.21%   79.27%   +0.06%     
==========================================
  Files         541      541              
  Lines       75677    75965     +288     
==========================================
+ Hits        59949    60224     +275     
- Misses      11491    11509      +18     
+ Partials     4237     4232       -5     
Flag Coverage Δ
unittests 79.27% <73.26%> (+0.06%) ⬆️

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.

resp, err := tests.TestDialClient.Post(target.GetAddr()+"/tso/api/v1/primary/evict",
"application/json", nil)
re.NoError(err)
re.Equal(http.StatusOK, resp.StatusCode)

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.

Please also decode and assert the response body, so this test verifies the per-group result map instead of only the status code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. The test now reads the evict response body, unmarshals it into the per-group result map, and asserts every group the target held (all 12 created groups) reports success.

Short: "show the tso members status",
Run: getMembersCommandFunc,
})
d.AddCommand(&cobra.Command{

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.

Please add a pd-ctl test for this new subcommand, so the direct-node endpoint path and argument handling are covered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added microservice_command_test.go with unit tests covering the success path (asserting the request is a POST to <node>/tso/api/v1/primary/evict), the transport-error path, and the missing-argument path.

@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch from d57a3d8 to 39e0522 Compare July 3, 2026 07:38
@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 3, 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 (1)
tools/pd-ctl/pdctl/command/microservice_command_test.go (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Possible duplication between recordingRoundTripper and mockRoundTripper.

If mockRoundTripper already exists in the package, consider consolidating with recordingRoundTripper (e.g., add an err field to one struct) rather than maintaining two similar http.RoundTripper test doubles.

Also applies to: 67-71

🤖 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 `@tools/pd-ctl/pdctl/command/microservice_command_test.go` around lines 28 -
39, The test file has two very similar http.RoundTripper doubles,
recordingRoundTripper and mockRoundTripper, which should be consolidated. Update
the existing test double used by the microservice command tests so it can both
capture the request and return a canned response/error, then replace the
duplicate helper with that single implementation. Keep the behavior centered
around RoundTrip and the request-recording fields so the tests still assert the
destination while avoiding duplicated structs.
🤖 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 `@tools/pd-ctl/pdctl/command/microservice_command_test.go`:
- Around line 28-39: The test file has two very similar http.RoundTripper
doubles, recordingRoundTripper and mockRoundTripper, which should be
consolidated. Update the existing test double used by the microservice command
tests so it can both capture the request and return a canned response/error,
then replace the duplicate helper with that single implementation. Keep the
behavior centered around RoundTrip and the request-recording fields so the tests
still assert the destination while avoiding duplicated structs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 821efd1c-0e8d-4277-94ee-c6748028312b

📥 Commits

Reviewing files that changed from the base of the PR and between d57a3d8 and 39e0522.

📒 Files selected for processing (4)
  • pkg/mcs/tso/server/apis/v1/api.go
  • tests/integrations/mcs/members/member_test.go
  • tools/pd-ctl/pdctl/command/microservice_command.go
  • tools/pd-ctl/pdctl/command/microservice_command_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • tools/pd-ctl/pdctl/command/microservice_command.go
  • pkg/mcs/tso/server/apis/v1/api.go

@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch from 39e0522 to e5befe8 Compare July 3, 2026 07:44

@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)
tools/pd-ctl/pdctl/command/microservice_command_test.go (1)

28-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Possible duplication with an existing mock round tripper.

recordingRoundTripper overlaps in purpose with mockRoundTripper referenced at line 70 (both implement RoundTrip to return a canned response/error, and this one additionally records the request). If mockRoundTripper can be extended to optionally capture the request, the two types could be consolidated to avoid maintaining two near-identical mocks in the same package.

🤖 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 `@tools/pd-ctl/pdctl/command/microservice_command_test.go` around lines 28 -
39, The test helper `recordingRoundTripper` duplicates most of the behavior of
`mockRoundTripper` in `microservice_command_test.go`; consolidate them by
extending `mockRoundTripper` to optionally store the incoming request while
still returning the canned response/error. Update the affected tests to use the
shared mock and keep any request assertions working through the captured request
field so there is only one `RoundTrip` mock implementation in the package.
tests/integrations/mcs/members/member_test.go (1)

373-393: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant duplicate assertion loops.

Lines 388-390 and 391-393 both assert results[id] == "success" for effectively the same data — the second loop iterating the whole map adds no coverage beyond a length check, since every entry it visits was already validated in the first loop (assuming results doesn't contain unexpected group IDs). Consider dropping one loop, or replacing the second with an explicit re.Len(results, len(groupIDs)) to make the extra-entries check explicit.

♻️ Simplify redundant checks
 		results := make(map[uint32]string)
 		re.NoError(json.Unmarshal(body, &results), string(body))
+		re.Len(results, len(groupIDs))
 		for _, id := range groupIDs {
 			re.Equalf("success", results[id], "group %d result: %q", id, results[id])
 		}
-		for id, status := range results {
-			re.Equalf("success", status, "group %d result: %q", id, status)
-		}
🤖 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/integrations/mcs/members/member_test.go` around lines 373 - 393, The
eviction response validation has redundant duplicate assertions: the loop over
groupIDs already checks each expected group’s status in results, so the second
loop over results adds no new coverage. In the member_test.go eviction test
block, either remove the second loop entirely or replace it with an explicit
length check on results against groupIDs to make unexpected extra entries a
deliberate assertion. Keep the existing json.Unmarshal and per-group success
checks, but avoid asserting the same success condition twice.
🤖 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 `@tests/integrations/mcs/members/member_test.go`:
- Around line 373-393: The eviction response validation has redundant duplicate
assertions: the loop over groupIDs already checks each expected group’s status
in results, so the second loop over results adds no new coverage. In the
member_test.go eviction test block, either remove the second loop entirely or
replace it with an explicit length check on results against groupIDs to make
unexpected extra entries a deliberate assertion. Keep the existing
json.Unmarshal and per-group success checks, but avoid asserting the same
success condition twice.

In `@tools/pd-ctl/pdctl/command/microservice_command_test.go`:
- Around line 28-39: The test helper `recordingRoundTripper` duplicates most of
the behavior of `mockRoundTripper` in `microservice_command_test.go`;
consolidate them by extending `mockRoundTripper` to optionally store the
incoming request while still returning the canned response/error. Update the
affected tests to use the shared mock and keep any request assertions working
through the captured request field so there is only one `RoundTrip` mock
implementation in the package.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4bba15b4-7c08-4155-9df3-f270a9e8abea

📥 Commits

Reviewing files that changed from the base of the PR and between 39e0522 and e5befe8.

📒 Files selected for processing (4)
  • pkg/mcs/tso/server/apis/v1/api.go
  • tests/integrations/mcs/members/member_test.go
  • tools/pd-ctl/pdctl/command/microservice_command.go
  • tools/pd-ctl/pdctl/command/microservice_command_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • tools/pd-ctl/pdctl/command/microservice_command.go
  • pkg/mcs/tso/server/apis/v1/api.go

@bufferflies
bufferflies requested review from lhy1024 and rleungx July 6, 2026 07:47
Comment thread pkg/mcs/tso/server/apis/v1/api.go Outdated

lease := allocator.GetExpectedPrimaryLease()
// An empty new primary lets TransferPrimary pick a random other member.
if err := utils.TransferPrimary(svr.GetClient(), lease,

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.

Bulk eviction can temporarily leave a split target without a primary. During an in-progress split, the target group must campaign on the same TSO node as its split source, but this loop transfers each local group independently and TransferPrimary(..., "", ...) chooses a random next primary per group. If the drained node owns both the source and target, they can get different expected-primary flags, so /evict can report success while the target keyspaces have no TSO primary until that flag expires.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Fixed by rejecting the whole request up front (before transferring anything) when this node is the primary of a splitting keyspace group, returning 500 with ErrKeyspaceGroupInSplit. Since a split target must campaign on the same node as its source, evicting mid-split could otherwise leave the target keyspaces without a primary; splitting is transient, so the caller can retry after it finishes. Added TestEvictPrimaryRejectedWhileSplitting (holds the split via the pauseFinishSplitBeforeTxn failpoint, asserts the 500, then confirms eviction succeeds once the split completes).

@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch from e5befe8 to b7c5bf9 Compare July 6, 2026 11:44
Short: "show the tso members status",
Run: getMembersCommandFunc,
})
d.AddCommand(newEvictTSOPrimaryCommand())

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.

Nit: since this adds another TSO subcommand, please also update the parent usage string to include evict-primary; otherwise pd-ctl microservice tso --help still advertises only <primary|members>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — updated the parent Use string to tso <primary|members|evict-primary>.

Comment thread pkg/mcs/tso/server/apis/v1/api.go Outdated
// error message), so the caller can see the per-group result.
results := make(map[uint32]string)
failed := false
for keyspaceGroupID, group := range servingGroups {

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.

The split guard can be stale by the time this loop transfers a group. If a split starts after the pre-check and before this group is processed, /evict can still move the split source away, leaving the split target on a node whose local source allocator is no longer serving; checkTSOSplit then fails to generate TSO for the target keyspaces.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed by re-checking the split state right before each transfer: the loop now re-fetches the latest group meta via GetKeyspaceGroupByID and aborts with 500 (ErrKeyspaceGroupInSplit) if the group is splitting, in addition to the up-front pre-check. This narrows the window to the gap between that per-group check and the TransferPrimary etcd op; a split starting inside that tiny gap is still not covered. Fully closing it would require serializing eviction with the split state machine (a larger change in kgm/allocator) — happy to do that if you prefer, but note the residual case self-heals: TransferPrimary points the source's expected-primary at the new node, and the split target's campaign checker re-elects it onto that same node once the source serves there, so the target keyspaces only see a brief TSO gap rather than a permanent stall.

@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch 2 times, most recently from de84a65 to afd608b Compare July 7, 2026 13:08
@bufferflies
bufferflies requested review from lhy1024 and rleungx July 8, 2026 02:12
@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch 2 times, most recently from d3c30d3 to f7a9075 Compare July 8, 2026 02:43
@ti-chi-bot ti-chi-bot Bot added needs-1-more-lgtm Indicates a PR needs 1 more LGTM. approved labels Jul 8, 2026
re.Contains(result, "mock error")
}

func TestEvictTSOPrimaryCommand_MissingArg(t *testing.T) {

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.

Please change the test name.

Comment thread pkg/mcs/tso/server/apis/v1/api.go Outdated
// error message), so the caller can see the per-group result.
results := make(map[uint32]string)
failed := false
for keyspaceGroupID := range servingGroups {

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.

I think the split guard still allows partial side effects before the request is rejected. This loop both checks group.IsSplitting() and calls TransferPrimary, and servingGroups is a Go map with nondeterministic iteration order. If this node is primary for both a normal group and a splitting group, /primary/evict may transfer the normal group first, then hit the splitting group and return 500. That contradicts the comment/previous discussion that the request is rejected for splitting groups, and it also skips the per-group result map promised by the issue/API contract.

Can we pre-collect the candidate primary groups and check all of them for split state before calling any TransferPrimary? Alternatively, if this is intended to be best-effort even in split state, the split case should be recorded in results and the handler should return the result map consistently instead of aborting with a bare error string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Restored the all-or-nothing pre-check: evictPrimary now pre-collects the groups this node is primary of and checks every one of them for split state before calling any TransferPrimary. If any is splitting, the whole request is rejected with 500/ErrKeyspaceGroupInSplit and nothing is transferred, so a normal group can no longer be moved before a later splitting group is hit. The per-transfer re-check is kept as a narrow TOCTOU guard for a split that starts mid-eviction.

}, testutil.WithWaitFor(30*time.Second), testutil.WithTickInterval(200*time.Millisecond))
re.NotEmpty(primaryAddr)

resp, err := tests.TestDialClient.Post(primaryAddr+"/tso/api/v1/primary/evict",

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.

Can we extend this regression test to cover the partial-eviction case? Right now the test only creates the splitting source/target, so it cannot catch the case where the handler transfers a non-splitting group first and only later encounters the splitting group.

A useful test shape would be: create one additional normal keyspace group, transfer both the normal group and the splitting source to the same target node, pause the split, call /primary/evict, then assert the normal group is still primary on the target node if the intended behavior is to reject the whole request while any local primary group is splitting. This would verify that the split rejection happens before any transfer side effect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. TestEvictPrimaryRejectedWhileSplitting now also creates a normal keyspace group, transfers its primary onto the same node as the split source, calls /primary/evict, asserts the 500 rejection, and then asserts the normal group is still primary on that node — verifying the rejection happens before any transfer side effect.

@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch 3 times, most recently from 81ce0ca to 5dd4b71 Compare July 8, 2026 07:24
Comment thread server/apiv2/handlers/microservice.go Outdated
}
isMember := false
for _, entry := range entries {
if entry.ServiceAddr == node {

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.

Can we normalize the node address before this membership check and before building the proxy target? Existing TSO config/tests allow advertise-listen-addr without a scheme, and nearby keyspace-group code uses typeutil.EqualBaseURLs for node-address matching. With the current exact string comparison, pd-ctl ... evict-primary 127.0.0.1:3379 is rejected when the registry contains http://127.0.0.1:3379; conversely, if the registry itself stores 127.0.0.1:3379, this check can pass but url.Parse(node) below does not produce a usable host/scheme for NewCustomReverseProxies, so the proxy request fails.

Please match against registered TSO members using the existing scheme-insensitive comparison, then construct a normalized URL for the matched registry address before forwarding (for example by defaulting a missing scheme from the server/client TLS scheme). This keeps the open-proxy guard while accepting the address formats the rest of the code already treats as equivalent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. The membership check now uses typeutil.EqualBaseURLs (same helper as KeyspaceGroupMember.IsAddressEquivalent) instead of an exact string compare, so scheme/advertise-listen-addr differences no longer cause a false negative, and PD proxies to the matched member's canonical ServiceAddr rather than the raw client input.

@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch from 5dd4b71 to de41597 Compare July 8, 2026 08:51
@bufferflies
bufferflies requested a review from lhy1024 July 8, 2026 08:52
continue
}
// An empty new primary lets TransferPrimary pick a random other member.
if err := utils.TransferPrimary(svr.GetClient(), participant,

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 eviction is still only a one-shot TransferPrimary; it does not make the priority checker avoid the evicted node. If this node has a higher priority in a keyspace group, the primary can be moved back by primaryPriorityCheckLoop after /evict succeeds.

I verified this locally with a small test: make the higher-priority node the primary, use the same TransferPrimary(..., newPrimary="", ...) shape as /primary/evict to move it away, and then the priority checker moves the primary back to the higher-priority node. So the current behavior is closer to a one-shot transfer than to the drain workflow described in the issue/PR for restart/upgrade/decommission.

If this API is expected to support draining, could we make the priority checker aware of the draining/evicted node, or temporarily exclude/lower this node as a primary candidate, and add a regression test where evicting a higher-priority node does not move the primary back? If the intended semantics are only best-effort one-shot transfer, please narrow the docs/PR description so users do not assume it durably drains the node.

The full test I used:

// Additional imports needed by this test:
//
//     "github.com/tikv/pd/pkg/mcs/utils"
//     "github.com/tikv/pd/pkg/member"

func (suite *keyspaceGroupManagerTestSuite) TestEvictStyleTransferToLowerPriorityIsRevertedByPriorityChecker() {
	re := suite.Require()

	defaultPriority := mcs.DefaultKeyspaceGroupReplicaPriority
	cfg1 := suite.createConfig()
	cfg2 := suite.createConfig()
	cfg1.Name = "tso-evict-low-priority"
	cfg2.Name = "tso-evict-high-priority"
	svcAddr1 := cfg1.GetAdvertiseListenAddr()
	svcAddr2 := cfg2.GetAdvertiseListenAddr()

	re.NoError(suite.registerTSOServer(re, svcAddr1, cfg1))
	defer func() {
		re.NoError(suite.deregisterTSOServer(svcAddr1))
	}()
	re.NoError(suite.registerTSOServer(re, svcAddr2, cfg2))
	defer func() {
		re.NoError(suite.deregisterTSOServer(svcAddr2))
	}()

	const groupID = uint32(1234)
	re.NoError(addKeyspaceGroupAssignment(
		suite.ctx, suite.etcdClient, groupID,
		[]string{svcAddr1, svcAddr2}, []int{defaultPriority, defaultPriority + 1}, []uint32{groupID}))
	defer func() {
		re.NoError(deleteKeyspaceGroupInEtcd(suite.ctx, suite.etcdClient, groupID))
	}()

	mgr1 := suite.newKeyspaceGroupManager(1, cfg1)
	re.NotNil(mgr1)
	mgr1.primaryPriorityCheckInterval = 200 * time.Millisecond
	defer mgr1.Close()
	re.NoError(mgr1.Initialize())
	mgr2 := suite.newKeyspaceGroupManager(1, cfg2)
	re.NotNil(mgr2)
	mgr2.primaryPriorityCheckInterval = 200 * time.Millisecond
	defer mgr2.Close()
	re.NoError(mgr2.Initialize())

	waitForPrimariesServing(re, []*KeyspaceGroupManager{mgr2}, []uint32{groupID})

	allocator, err := mgr2.GetAllocator(groupID)
	re.NoError(err)
	participant, ok := allocator.GetMember().(*member.Participant)
	re.True(ok)
	memberMap := map[string]bool{
		svcAddr1: true,
		svcAddr2: true,
	}

	// This is the same one-shot transfer shape used by /primary/evict: the
	// current primary is moved to any other member, without marking this node as
	// drained or changing replica priority.
	re.NoError(utils.TransferPrimary(suite.etcdClient, participant,
		mcs.TSOServiceName, cfg2.Name, "", groupID, memberMap))
	waitForPrimariesServing(re, []*KeyspaceGroupManager{mgr1}, []uint32{groupID})

	// Since mgr2 still has the higher priority, the priority checker moves the
	// primary back. This is the behavior that makes the current evict API a
	// one-shot transfer rather than a durable drain.
	waitForPrimariesServing(re, []*KeyspaceGroupManager{mgr2}, []uint32{groupID})
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point — with equal priorities (the default) the transfer sticks since the checker only moves to a strictly higher-priority member, but if the evicted node has a higher priority the checker will move the primary back, so this doesn't durably drain such a node. We'd rather not build priority-aware eviction into this PR: priority has had a number of production issues and is largely deprecated right now, so we'll revisit a durable-drain mechanism if/when it's needed. I've left a TODO at the transfer site to reconsider priority here.

@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch from de41597 to 3445382 Compare July 8, 2026 12:58
Comment thread server/apiv2/handlers/microservice.go Outdated
c.AbortWithStatusJSON(http.StatusBadRequest, fmt.Sprintf("%s is not a tso node", node))
return
}
u, err := url.Parse(target)

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.

We still need to normalize the matched registry address into a URL that is valid for the reverse proxy. The current update fixes the membership match with EqualBaseURLs, but after target = entry.ServiceAddr it still calls url.Parse(target) directly. If the registry stores a valid bare advertise-listen-addr, such as 127.0.0.1:3379 or localhost:3379, url.Parse will not produce a usable Host/Scheme, and NewCustomReverseProxies will still forward with a broken request URL.

This is a supported configuration today: pkg/mcs/tso/server/config_test.go already covers advertise addresses without a scheme. Could we normalize entry.ServiceAddr after it is matched, defaulting a missing scheme to http:// or https:// according to the current server/client TLS setup, before passing it to the reverse proxy? It would also be useful to add a test where the registered TSO member address has no scheme and the pd-ctl input, with or without a scheme, still forwards successfully.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. After matching, PD now normalizes the target via a small resolveProxyURL helper: if the registered ServiceAddr has no scheme (a bare advertise-listen-addr like 127.0.0.1:3379 or localhost:3379, where url.Parse yields an empty Host), it prepends the scheme derived from the server's TLS config (https when a cert is configured, http otherwise) before building the reverse-proxy target; an address that already carries a scheme is kept as-is. Added a unit test (TestResolveProxyURL / TestHTTPSchemeFromTLS) covering scheme-less and scheme'd addresses under both schemes, and the pd-ctl integration test now passes the node address without a scheme so the end-to-end forward still succeeds.

@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch from 3445382 to d827623 Compare July 9, 2026 02:01
@ti-chi-bot ti-chi-bot Bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Jul 9, 2026
"github.com/tikv/pd/pkg/utils/grpcutil"
)

func TestHTTPSchemeFromTLS(t *testing.T) {

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.

plz fix lint

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The lint failure was leakcheck: this is the first test file in the handlers package, so the package needs a TestMain with goleak.VerifyTestMain(m, testutil.LeakOptions...). Added it; leakcheck now passes locally.

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

the rest LGTM

@ti-chi-bot ti-chi-bot Bot added the lgtm label Jul 9, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: lhy1024, rleungx

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

The pull request process is described 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 removed the needs-1-more-lgtm Indicates a PR needs 1 more LGTM. label Jul 9, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

[LGTM Timeline notifier]

Timeline:

  • 2026-07-08 03:14:58.741855314 +0000 UTC m=+165084.777950380: ☑️ agreed by rleungx.
  • 2026-07-09 09:01:08.233612894 +0000 UTC m=+272254.269707960: ☑️ agreed by lhy1024.

Add POST /tso/api/v1/primary/evict, which transfers away every keyspace
group primary currently held by the target TSO node. It is a best-effort,
node-local operation: groups the node does not serve as primary are
skipped, a failure on one group does not abort the others, and a per-group
result map is returned.

Expose it through pd-ctl as `microservice tso evict-primary <address>`,
sending the request directly to the target node.

Issue Number: close tikv#10967

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: tongjian <1045931706@qq.com>
@bufferflies
bufferflies force-pushed the claude/vigorous-bassi-1f06ff branch from d827623 to 4fb19f6 Compare July 9, 2026 09:08
@bufferflies

Copy link
Copy Markdown
Contributor Author

/retest

@ti-chi-bot
ti-chi-bot Bot merged commit c2a47d8 into tikv:master Jul 9, 2026
30 of 32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tso: support evicting all keyspace group primaries held by a node

3 participants