-
Notifications
You must be signed in to change notification settings - Fork 778
evict: support new_primary in /primary/evict endpoint #11121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
696ec9e
48f7146
1ac1ec0
e181dc8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ import ( | |
|
|
||
| "github.com/tikv/pd/pkg/errs" | ||
| "github.com/tikv/pd/pkg/keyspace/constant" | ||
| "github.com/tikv/pd/pkg/mcs/discovery" | ||
| tsoserver "github.com/tikv/pd/pkg/mcs/tso/server" | ||
| "github.com/tikv/pd/pkg/mcs/utils" | ||
| mcs "github.com/tikv/pd/pkg/mcs/utils/constant" | ||
|
|
@@ -389,28 +390,58 @@ func transferPrimary(c *gin.Context) { | |
| } | ||
|
|
||
| // evictPrimary transfers away every keyspace group primary currently held by this | ||
| // node, moving each to another member of the same group. It is a best-effort, | ||
| // node-local operation: groups that this node is not serving as primary are | ||
| // skipped, and a failure on one group does not stop the others. | ||
| // node, moving each to another member of the same group. Groups that this node is | ||
| // not serving as primary are skipped. | ||
| // | ||
| // The request is rejected as a whole if this node is the primary of any | ||
| // splitting keyspace group: a split target must campaign on the same TSO node as | ||
| // its split source, but eviction transfers each group to an independently chosen | ||
| // member, which could break that invariant and leave the target keyspaces | ||
| // without a primary. A single TransferPrimary is not atomic and the groups are | ||
| // iterated in no particular order, so the split state is checked for every | ||
| // candidate group up front before transferring anything; otherwise a normal | ||
| // group could be moved before a later splitting group aborts the loop, leaving | ||
| // partial side effects. Since splitting is transient, the caller should retry | ||
| // once it finishes. | ||
| // The request is rejected as a whole, before any group is touched, if this node | ||
| // is the primary of any splitting keyspace group or if new_primary does not | ||
| // identify a member of every candidate group: a split target must campaign on | ||
| // the same TSO node as its split source, but eviction transfers each group to an | ||
| // independently chosen member, which could break that invariant and leave the | ||
| // target keyspaces without a primary; an invalid new_primary would otherwise only | ||
| // be discovered mid-loop, after other groups had already been moved. A single | ||
| // TransferPrimary is not atomic and the groups are iterated in no particular | ||
| // order, so both conditions are checked for every candidate group up front; | ||
| // otherwise a normal group could be moved before a later group aborts the loop, | ||
| // leaving partial side effects. Since splitting is transient, the caller should | ||
| // retry once it finishes. Once past this pre-check, a transfer failure for an | ||
| // individual group (e.g. a transient etcd error) is best-effort and does not stop | ||
| // the others. | ||
| // @Tags primary | ||
| // @Summary Evict all keyspace group primaries held by this node. | ||
| // @Produce json | ||
| // @Success 200 {object} map[string]string | ||
| // @Failure 500 {object} map[string]string | ||
| // @Param new_primary body string false "new primary name" | ||
| // @Success 200 {object} map[string]string | ||
| // @Failure 400 {string} string "invalid request" | ||
| // @Failure 500 {object} map[string]string | ||
| // @Router /primary/evict [post] | ||
| func evictPrimary(c *gin.Context) { | ||
| svr := c.MustGet(multiservicesapi.ServiceContextKey).(*tsoserver.Service) | ||
|
|
||
| body, err := io.ReadAll(c.Request.Body) | ||
| if err != nil { | ||
| c.String(http.StatusBadRequest, err.Error()) | ||
| return | ||
| } | ||
| var input struct { | ||
| NewPrimary string `json:"new_primary"` | ||
| } | ||
| if len(body) > 0 { | ||
| if err := json.Unmarshal(body, &input); err != nil { | ||
| c.String(http.StatusBadRequest, err.Error()) | ||
| return | ||
| } | ||
| } | ||
| // The node being evicted cannot also be its own replacement: TransferPrimary | ||
| // treats a target equal to the current primary as a self-transfer and silently | ||
| // no-ops, which would report "success" while leaving the node undrained. Match | ||
| // on both name and service address since IsValidPrimaryCandidate and | ||
| // TransferPrimary accept either as an identifier for new_primary. | ||
| if input.NewPrimary != "" && (input.NewPrimary == svr.Name() || input.NewPrimary == svr.GetAdvertiseListenAddr()) { | ||
| c.String(http.StatusBadRequest, "new_primary must not be the node being evicted") | ||
| return | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| kgm := svr.GetKeyspaceGroupManager() | ||
|
|
||
| // Collect the keyspace groups this node is currently the primary of. There is | ||
|
|
@@ -424,15 +455,36 @@ func evictPrimary(c *gin.Context) { | |
| primaryGroupIDs = append(primaryGroupIDs, keyspaceGroupID) | ||
| } | ||
|
|
||
| // Resolve the service registry once and reuse it for every candidate group | ||
| // below, instead of re-fetching per group. | ||
| var entries []discovery.ServiceRegistryEntry | ||
| if input.NewPrimary != "" { | ||
| entries, err = discovery.GetMSMembers(mcs.TSOServiceName, svr.GetClient()) | ||
| if err != nil { | ||
| c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error()) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| // Pre-check every candidate group before transferring anything so the | ||
| // operation is all-or-nothing: reject the whole request if any of them is | ||
| // splitting, instead of moving some groups first and only then aborting. | ||
| // splitting or new_primary is not one of its members, instead of moving some | ||
| // groups first and only then aborting. | ||
| for _, keyspaceGroupID := range primaryGroupIDs { | ||
| if group := kgm.GetKeyspaceGroupByID(keyspaceGroupID); group != nil && group.IsSplitting() { | ||
| group := kgm.GetKeyspaceGroupByID(keyspaceGroupID) | ||
| if group == nil { | ||
| continue | ||
| } | ||
| if group.IsSplitting() { | ||
| c.AbortWithStatusJSON(http.StatusInternalServerError, | ||
| errs.ErrKeyspaceGroupInSplit.FastGenByArgs(keyspaceGroupID).Error()) | ||
| return | ||
| } | ||
| if !utils.IsValidPrimaryCandidate(entries, input.NewPrimary, keyspaceGroupMemberMap(group)) { | ||
| c.String(http.StatusBadRequest, | ||
| fmt.Sprintf("new_primary %q is not a member of keyspace group %d", input.NewPrimary, keyspaceGroupID)) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| // results maps a keyspace group ID to the transfer outcome ("success" or the | ||
|
|
@@ -458,10 +510,7 @@ func evictPrimary(c *gin.Context) { | |
| } | ||
|
|
||
| // only members of the specific group are valid primary candidates. | ||
| memberMap := make(map[string]bool, len(group.Members)) | ||
| for _, member := range group.Members { | ||
| memberMap[member.Address] = true | ||
| } | ||
| memberMap := keyspaceGroupMemberMap(group) | ||
|
|
||
| participant, ok := allocator.GetMember().(*member.Participant) | ||
| if !ok { | ||
|
|
@@ -471,9 +520,8 @@ func evictPrimary(c *gin.Context) { | |
| // has a higher priority for the group, the priority checker will move the | ||
| // primary back to it, so the eviction does not durably drain the node. | ||
| // Priority handling is being reworked, so revisit this when needed. | ||
| // An empty new primary lets TransferPrimary pick a random other member. | ||
| if err := utils.TransferPrimary(svr.GetClient(), participant, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment @ Line 446 said: "the operation is all-or-nothing". What if
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a P1 operational-correctness issue.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a pre-check pass before any transfer: for every candidate group, |
||
| mcs.TSOServiceName, svr.Name(), "", keyspaceGroupID, memberMap); err != nil { | ||
| mcs.TSOServiceName, svr.Name(), input.NewPrimary, keyspaceGroupID, memberMap); err != nil { | ||
| log.Warn("failed to evict keyspace group primary", | ||
| zap.Uint32("keyspace-group-id", keyspaceGroupID), errs.ZapError(err)) | ||
| results[keyspaceGroupID] = err.Error() | ||
|
|
@@ -490,6 +538,16 @@ func evictPrimary(c *gin.Context) { | |
| c.IndentedJSON(http.StatusOK, results) | ||
| } | ||
|
|
||
| // keyspaceGroupMemberMap returns the set of service addresses that are members of | ||
| // the group, so callers can filter transfer candidates down to that group. | ||
| func keyspaceGroupMemberMap(group *endpoint.KeyspaceGroup) map[string]bool { | ||
| memberMap := make(map[string]bool, len(group.Members)) | ||
| for _, member := range group.Members { | ||
| memberMap[member.Address] = true | ||
| } | ||
| return memberMap | ||
| } | ||
|
|
||
| // forwardToGroupPrimary forwards the transfer primary request to the primary of | ||
| // the given keyspace group, replaying the original request body. The request is | ||
| // fully handled (forwarded or aborted with an error) when this returns. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: tikv/pd
Length of output: 1678
🏁 Script executed:
Repository: tikv/pd
Length of output: 18869
Document the JSON object request body and the 400 response.
@Param new_primary body stringdescribes a JSON string, butevictPrimaryunmarshals an object withnew_primary. Generated clients can send"node-1"and receive HTTP 400 fromjson.Unmarshal.Use a named request model in the Swagger annotation. Add an HTTP 400 response definition that matches the plaintext error body. Regenerate the Swagger specification with
make swagger-spec(SWAGGER=1) after the annotation change.🤖 Prompt for AI Agents
Source: Coding guidelines
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
@Failure 400 {string} string "invalid request"to document the 400 response, matching the convention used elsewhere in this file (e.g.resetTS). Kept the existing@Param new_primary body stringannotation instead of introducing a named request model, sincetransferPrimaryright above uses the same convention for the same JSON shape and this file doesn't use named Swagger models elsewhere.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bufferflies, the added@Failure 400resolves the response-documentation part.The request schema issue remains.
evictPrimaryaccepts a JSON object such as{"new_primary":"node-1"}, but@Param new_primary body stringdescribes the complete body as a JSON string. Generated clients can therefore send"node-1", which the handler rejects.The nearby
transferPrimaryannotation has the same mismatch. It is not a reason to preserve it here. Please use a request model for/primary/evictand regenerate the Swagger specification.✏️ Learnings added
You are interacting with an AI system.