Skip to content

Commit 18f16ca

Browse files
authored
Merge pull request #1240 from planetscale/nick/planned-reparent-shard
Add planned-reparent command to pscale branch vtctld
2 parents 51a487e + e25a2aa commit 18f16ca

5 files changed

Lines changed: 232 additions & 3 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require (
2525
github.com/mattn/go-shellwords v1.0.12
2626
github.com/mitchellh/go-homedir v1.1.0
2727
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
28-
github.com/planetscale/planetscale-go v0.157.0
28+
github.com/planetscale/planetscale-go v0.159.0
2929
github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4
3030
github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7
3131
github.com/spf13/cobra v1.10.2

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL
176176
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
177177
github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e h1:MZ8D+Z3m2vvqGZLvoQfpaGg/j1fNDr4j03s3PRz4rVY=
178178
github.com/planetscale/noglog v0.2.1-0.20210421230640-bea75fcd2e8e/go.mod h1:hwAsSPQdvPa3WcfKfzTXxtEq/HlqwLjQasfO6QbGo4Q=
179-
github.com/planetscale/planetscale-go v0.157.0 h1:b0kWxC39F4/FQw2/Y+5/H4tRWUAzvl2ZukimrsTYP7M=
180-
github.com/planetscale/planetscale-go v0.157.0/go.mod h1:paQCI5SgquuoewvMQM7R+r1XJO868bdP6/ihGidYRM0=
179+
github.com/planetscale/planetscale-go v0.159.0 h1:qqyZjG/z5k/w5gihfSwxssVu+mIsRTKqXFIeVJa/7hI=
180+
github.com/planetscale/planetscale-go v0.159.0/go.mod h1:paQCI5SgquuoewvMQM7R+r1XJO868bdP6/ihGidYRM0=
181181
github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4 h1:Xv5pj20Rhfty1Tv0OVcidg4ez4PvGrpKvb6rvUwQgDs=
182182
github.com/planetscale/psdb v0.0.0-20250717190954-65c6661ab6e4/go.mod h1:M52h5IWxAcbdQ1hSZrLAGQC4ZXslxEsK/Wh9nu3wdWs=
183183
github.com/planetscale/psdbproxy v0.0.0-20250728082226-3f4ea3a74ec7 h1:aRd6vdE1fyuSI4RVj7oCr8lFmgqXvpnPUmN85VbZCp8=
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package vtctld
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"time"
9+
10+
"github.com/planetscale/cli/internal/cmdutil"
11+
ps "github.com/planetscale/planetscale-go/planetscale"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
var (
16+
plannedReparentOperationPollInterval = time.Second
17+
plannedReparentOperationTimeoutBuffer = 30 * time.Second
18+
plannedReparentOperationDefaultTimeout = 10 * time.Minute
19+
)
20+
21+
func PlannedReparentShardCmd(ch *cmdutil.Helper) *cobra.Command {
22+
var flags struct {
23+
keyspace string
24+
shard string
25+
newPrimary string
26+
wait bool
27+
}
28+
29+
cmd := &cobra.Command{
30+
Use: "planned-reparent-shard <database> <branch>",
31+
Short: "Reparent a shard to a new primary",
32+
Long: `Reparent a shard to a new primary using Vitess PlannedReparentShard.
33+
Both the old and new primaries must be up and running.
34+
35+
To check on an existing operation, use the "status" subcommand:
36+
pscale branch vtctld planned-reparent-shard status <db> <branch> <operation-id>`,
37+
Args: cmdutil.RequiredArgs("database", "branch"),
38+
RunE: func(cmd *cobra.Command, args []string) error {
39+
ctx := cmd.Context()
40+
database, branch := args[0], args[1]
41+
42+
client, err := ch.Client()
43+
if err != nil {
44+
return err
45+
}
46+
47+
end := ch.Printer.PrintProgress(
48+
fmt.Sprintf("Executing PlannedReparentShard on %s\u2026",
49+
progressTarget(ch.Config.Organization, database, branch)))
50+
defer end()
51+
52+
operation, err := client.PlannedReparentShard.Create(ctx, &ps.PlannedReparentShardRequest{
53+
Organization: ch.Config.Organization,
54+
Database: database,
55+
Branch: branch,
56+
Keyspace: flags.keyspace,
57+
Shard: flags.shard,
58+
NewPrimary: flags.newPrimary,
59+
})
60+
if err != nil {
61+
return cmdutil.HandleError(err)
62+
}
63+
64+
if !flags.wait {
65+
end()
66+
return ch.Printer.PrintJSON(map[string]string{"id": operation.ID})
67+
}
68+
69+
result, err := waitForPlannedReparentResult(ctx, client, ch.Config.Organization, database, branch, operation)
70+
if err != nil {
71+
return cmdutil.HandleError(err)
72+
}
73+
74+
end()
75+
return ch.Printer.PrettyPrintJSON(result)
76+
},
77+
}
78+
79+
cmd.Flags().StringVar(&flags.keyspace, "keyspace", "", "Keyspace name")
80+
cmd.Flags().StringVar(&flags.shard, "shard", "", "Shard range (e.g., '-80', '80-', or '-' for unsharded)")
81+
cmd.Flags().StringVar(&flags.newPrimary, "new-primary", "", "Tablet alias to promote as the new primary")
82+
cmd.Flags().BoolVar(&flags.wait, "wait", true, "Wait for the operation to complete")
83+
cmd.MarkFlagRequired("keyspace") // nolint:errcheck
84+
cmd.MarkFlagRequired("shard") // nolint:errcheck
85+
cmd.MarkFlagRequired("new-primary") // nolint:errcheck
86+
87+
cmd.AddCommand(plannedReparentShardStatusCmd(ch))
88+
89+
return cmd
90+
}
91+
92+
func plannedReparentShardStatusCmd(ch *cmdutil.Helper) *cobra.Command {
93+
cmd := &cobra.Command{
94+
Use: "status <database> <branch> <operation-id>",
95+
Short: "Check the status of a planned reparent shard operation",
96+
Args: cmdutil.RequiredArgs("database", "branch", "operation-id"),
97+
RunE: func(cmd *cobra.Command, args []string) error {
98+
ctx := cmd.Context()
99+
database, branch, id := args[0], args[1], args[2]
100+
101+
client, err := ch.Client()
102+
if err != nil {
103+
return err
104+
}
105+
106+
end := ch.Printer.PrintProgress(
107+
fmt.Sprintf("Getting PlannedReparentShard operation on %s\u2026",
108+
progressTarget(ch.Config.Organization, database, branch)))
109+
defer end()
110+
111+
operation, err := client.PlannedReparentShard.Get(ctx, &ps.GetPlannedReparentShardRequest{
112+
Organization: ch.Config.Organization,
113+
Database: database,
114+
Branch: branch,
115+
ID: id,
116+
})
117+
if err != nil {
118+
return cmdutil.HandleError(err)
119+
}
120+
121+
end()
122+
return ch.Printer.PrintJSON(operation)
123+
},
124+
}
125+
126+
return cmd
127+
}
128+
129+
func waitForPlannedReparentResult(ctx context.Context, client *ps.Client, organization, database, branch string, operation *ps.VtctldOperation) (json.RawMessage, error) {
130+
result, done, err := plannedReparentOperationResult(operation)
131+
if done || err != nil {
132+
return result, err
133+
}
134+
135+
request := &ps.GetPlannedReparentShardRequest{
136+
Organization: organization,
137+
Database: database,
138+
Branch: branch,
139+
ID: operation.ID,
140+
}
141+
142+
pollCtx, cancel := context.WithTimeout(ctx, plannedReparentOperationTimeout(operation))
143+
defer cancel()
144+
ticker := time.NewTicker(plannedReparentOperationPollInterval)
145+
defer ticker.Stop()
146+
147+
for {
148+
select {
149+
case <-pollCtx.Done():
150+
if errors.Is(pollCtx.Err(), context.DeadlineExceeded) {
151+
return nil, fmt.Errorf("timed out waiting for planned reparent operation %s to finish", operation.ID)
152+
}
153+
154+
return nil, pollCtx.Err()
155+
case <-ticker.C:
156+
}
157+
158+
op, err := client.PlannedReparentShard.Get(pollCtx, request)
159+
if err != nil {
160+
if errors.Is(err, context.DeadlineExceeded) {
161+
return nil, fmt.Errorf("timed out waiting for planned reparent operation %s to finish", operation.ID)
162+
}
163+
164+
return nil, err
165+
}
166+
167+
result, done, err = plannedReparentOperationResult(op)
168+
if done || err != nil {
169+
return result, err
170+
}
171+
}
172+
}
173+
174+
func plannedReparentOperationResult(operation *ps.VtctldOperation) (json.RawMessage, bool, error) {
175+
if !operation.Completed {
176+
return nil, false, nil
177+
}
178+
179+
switch operation.State {
180+
case "completed":
181+
if len(operation.Result) == 0 {
182+
return json.RawMessage(`{}`), true, nil
183+
}
184+
185+
return operation.Result, true, nil
186+
case "failed", "cancelled":
187+
if operation.Error != "" {
188+
return nil, true, errors.New(operation.Error)
189+
}
190+
191+
return nil, true, fmt.Errorf("planned reparent operation %s ended in state %q", operation.ID, operation.State)
192+
default:
193+
return nil, true, fmt.Errorf("planned reparent operation %s reached unexpected terminal state %q", operation.ID, operation.State)
194+
}
195+
}
196+
197+
func plannedReparentOperationTimeout(operation *ps.VtctldOperation) time.Duration {
198+
if operation.Timeout > 0 {
199+
return time.Duration(operation.Timeout)*time.Second + plannedReparentOperationTimeoutBuffer
200+
}
201+
202+
return plannedReparentOperationDefaultTimeout
203+
}

internal/cmd/branch/vtctld/vtctld.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func VtctldCmd(ch *cmdutil.Helper) *cobra.Command {
1717
cmd.AddCommand(VDiffCmd(ch))
1818
cmd.AddCommand(LookupVindexCmd(ch))
1919
cmd.AddCommand(MoveTablesCmd(ch))
20+
cmd.AddCommand(PlannedReparentShardCmd(ch))
2021
cmd.AddCommand(ListWorkflowsCmd(ch))
2122
cmd.AddCommand(ListKeyspacesCmd(ch))
2223
cmd.AddCommand(StartWorkflowCmd(ch))
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package mock
2+
3+
import (
4+
"context"
5+
6+
ps "github.com/planetscale/planetscale-go/planetscale"
7+
)
8+
9+
type PlannedReparentShardService struct {
10+
CreateFn func(context.Context, *ps.PlannedReparentShardRequest) (*ps.VtctldOperation, error)
11+
CreateFnInvoked bool
12+
13+
GetFn func(context.Context, *ps.GetPlannedReparentShardRequest) (*ps.VtctldOperation, error)
14+
GetFnInvoked bool
15+
}
16+
17+
func (s *PlannedReparentShardService) Create(ctx context.Context, req *ps.PlannedReparentShardRequest) (*ps.VtctldOperation, error) {
18+
s.CreateFnInvoked = true
19+
return s.CreateFn(ctx, req)
20+
}
21+
22+
func (s *PlannedReparentShardService) Get(ctx context.Context, req *ps.GetPlannedReparentShardRequest) (*ps.VtctldOperation, error) {
23+
s.GetFnInvoked = true
24+
return s.GetFn(ctx, req)
25+
}

0 commit comments

Comments
 (0)