github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/tests/integration_tests/move_table/main.go (about) 1 // Copyright 2020 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 // This is a program that drives the CDC cluster to move a table 15 package main 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "flag" 22 "fmt" 23 "io" 24 "net/http" 25 "strings" 26 "time" 27 28 "github.com/pingcap/errors" 29 "github.com/pingcap/log" 30 "github.com/pingcap/tiflow/cdc/model" 31 cerrors "github.com/pingcap/tiflow/pkg/errors" 32 "github.com/pingcap/tiflow/pkg/etcd" 33 "github.com/pingcap/tiflow/pkg/httputil" 34 "github.com/pingcap/tiflow/pkg/retry" 35 "github.com/pingcap/tiflow/pkg/security" 36 "go.etcd.io/etcd/client/pkg/v3/logutil" 37 clientv3 "go.etcd.io/etcd/client/v3" 38 "go.uber.org/zap" 39 "go.uber.org/zap/zapcore" 40 "google.golang.org/grpc" 41 "google.golang.org/grpc/backoff" 42 ) 43 44 var ( 45 pd = flag.String("pd", "http://127.0.0.1:2379", "PD address and port") 46 logLevel = flag.String("log-level", "debug", "Set log level of the logger") 47 ) 48 49 const ( 50 maxCheckSourceEmptyRetries = 30 51 ) 52 53 // This program moves all tables replicated by a certain capture to other captures, 54 // and makes sure that the original capture becomes empty. 55 func main() { 56 flag.Parse() 57 if strings.ToLower(*logLevel) == "debug" { 58 log.SetLevel(zapcore.DebugLevel) 59 } 60 61 log.Info("table mover started") 62 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 63 defer cancel() 64 65 cluster, err := newCluster(ctx, *pd) 66 if err != nil { 67 log.Fatal("failed to create cluster info", zap.Error(err)) 68 } 69 err = retry.Do(ctx, func() error { 70 err := cluster.refreshInfo(ctx) 71 if err != nil { 72 log.Warn("error refreshing cluster info", zap.Error(err)) 73 } 74 75 log.Info("task status", zap.Reflect("status", cluster.captures)) 76 77 if len(cluster.captures) <= 1 { 78 return errors.New("too few captures") 79 } 80 return nil 81 }, retry.WithBackoffBaseDelay(100), retry.WithMaxTries(20), retry.WithIsRetryableErr(cerrors.IsRetryableError)) 82 83 if err != nil { 84 log.Fatal("Fail to get captures", zap.Error(err)) 85 } 86 87 var sourceCapture string 88 89 for capture, tables := range cluster.captures { 90 if len(tables) == 0 { 91 continue 92 } 93 sourceCapture = capture 94 break 95 } 96 97 var targetCapture string 98 99 for candidateCapture := range cluster.captures { 100 if candidateCapture != sourceCapture { 101 targetCapture = candidateCapture 102 } 103 } 104 105 if targetCapture == "" { 106 log.Fatal("no target, unexpected") 107 } 108 109 err = cluster.moveAllTables(ctx, sourceCapture, targetCapture) 110 if err != nil { 111 log.Fatal("failed to move tables", zap.Error(err)) 112 } 113 114 log.Info("all tables are moved", zap.String("sourceCapture", sourceCapture), zap.String("targetCapture", targetCapture)) 115 } 116 117 type tableInfo struct { 118 ID int64 119 Changefeed string 120 } 121 122 type cluster struct { 123 ownerAddr string 124 captures map[string][]*tableInfo 125 cdcEtcdCli *etcd.CDCEtcdClientImpl 126 } 127 128 func newCluster(ctx context.Context, pd string) (*cluster, error) { 129 logConfig := logutil.DefaultZapLoggerConfig 130 logConfig.Level = zap.NewAtomicLevelAt(zapcore.ErrorLevel) 131 132 etcdCli, err := clientv3.New(clientv3.Config{ 133 Endpoints: []string{pd}, 134 TLS: nil, 135 Context: ctx, 136 LogConfig: &logConfig, 137 DialTimeout: 5 * time.Second, 138 DialOptions: []grpc.DialOption{ 139 grpc.WithInsecure(), 140 grpc.WithBlock(), 141 grpc.WithConnectParams(grpc.ConnectParams{ 142 Backoff: backoff.Config{ 143 BaseDelay: time.Second, 144 Multiplier: 1.1, 145 Jitter: 0.1, 146 MaxDelay: 3 * time.Second, 147 }, 148 MinConnectTimeout: 3 * time.Second, 149 }), 150 }, 151 }) 152 if err != nil { 153 return nil, errors.Trace(err) 154 } 155 156 cdcEtcdCli, err := etcd.NewCDCEtcdClient(ctx, etcdCli, etcd.DefaultCDCClusterID) 157 if err != nil { 158 return nil, errors.Trace(err) 159 } 160 ret := &cluster{ 161 ownerAddr: "", 162 captures: nil, 163 cdcEtcdCli: cdcEtcdCli, 164 } 165 166 log.Info("new cluster initialized") 167 168 return ret, nil 169 } 170 171 func (c *cluster) moveAllTables(ctx context.Context, sourceCapture, targetCapture string) error { 172 // move all tables to another capture 173 for _, table := range c.captures[sourceCapture] { 174 err := moveTable(ctx, c.ownerAddr, table.Changefeed, targetCapture, table.ID) 175 if err != nil { 176 log.Warn("failed to move table", zap.Error(err)) 177 continue 178 } 179 180 log.Info("moved table successful", zap.Int64("tableID", table.ID)) 181 } 182 183 return nil 184 } 185 186 func (c *cluster) refreshInfo(ctx context.Context) error { 187 ownerID, err := c.cdcEtcdCli.GetOwnerID(ctx) 188 if err != nil { 189 return errors.Trace(err) 190 } 191 192 log.Debug("retrieved owner ID", zap.String("ownerID", ownerID)) 193 194 captureInfo, err := c.cdcEtcdCli.GetCaptureInfo(ctx, ownerID) 195 if err != nil { 196 return errors.Trace(err) 197 } 198 199 log.Debug("retrieved owner addr", zap.String("ownerAddr", captureInfo.AdvertiseAddr)) 200 c.ownerAddr = captureInfo.AdvertiseAddr 201 202 _, changefeeds, err := c.cdcEtcdCli.GetChangeFeeds(ctx) 203 if err != nil { 204 return errors.Trace(err) 205 } 206 if len(changefeeds) == 0 { 207 return errors.New("No changefeed") 208 } 209 210 log.Debug("retrieved changefeeds", zap.Reflect("changefeeds", changefeeds)) 211 var changefeed string 212 for k := range changefeeds { 213 changefeed = k.ID 214 break 215 } 216 217 c.captures = make(map[string][]*tableInfo) 218 _, captures, err := c.cdcEtcdCli.GetCaptures(ctx) 219 if err != nil { 220 return errors.Trace(err) 221 } 222 for _, capture := range captures { 223 c.captures[capture.ID] = make([]*tableInfo, 0) 224 processorDetails, err := queryProcessor(c.ownerAddr, changefeed, capture.ID) 225 if err != nil { 226 return errors.Trace(err) 227 } 228 229 log.Debug("retrieved processor details", 230 zap.String("changefeed", changefeed), 231 zap.String("captureID", capture.ID), 232 zap.Any("processorDetail", processorDetails)) 233 for _, tableID := range processorDetails.Tables { 234 c.captures[capture.ID] = append(c.captures[capture.ID], &tableInfo{ 235 ID: tableID, 236 Changefeed: changefeed, 237 }) 238 } 239 } 240 return nil 241 } 242 243 // queryProcessor invokes the following API to get the mapping from 244 // captureIDs to tableIDs: 245 // 246 // GET /api/v1/processors/{changefeed_id}/{capture_id} 247 func queryProcessor( 248 apiEndpoint string, 249 changefeed string, 250 captureID string, 251 ) (*model.ProcessorDetail, error) { 252 httpClient, err := httputil.NewClient(&security.Credential{ /* no TLS */ }) 253 if err != nil { 254 return nil, errors.Trace(err) 255 } 256 257 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 258 defer cancel() 259 requestURL := fmt.Sprintf("http://%s/api/v1/processors/%s/%s", apiEndpoint, changefeed, captureID) 260 resp, err := httpClient.Get(ctx, requestURL) 261 if err != nil { 262 return nil, errors.Trace(err) 263 } 264 defer func() { 265 _ = resp.Body.Close() 266 }() 267 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 268 return nil, errors.Trace( 269 errors.Errorf("HTTP API returned error status: %d, url: %s", resp.StatusCode, requestURL)) 270 } 271 272 bodyBytes, err := io.ReadAll(resp.Body) 273 if err != nil { 274 return nil, errors.Trace(err) 275 } 276 277 var ret model.ProcessorDetail 278 err = json.Unmarshal(bodyBytes, &ret) 279 if err != nil { 280 return nil, errors.Trace(err) 281 } 282 283 return &ret, nil 284 } 285 286 func moveTable(ctx context.Context, ownerAddr string, changefeed string, target string, tableID int64) error { 287 formStr := fmt.Sprintf("cf-id=%s&target-cp-id=%s&table-id=%d", changefeed, target, tableID) 288 log.Debug("preparing HTTP API call to owner", zap.String("formStr", formStr)) 289 rd := bytes.NewReader([]byte(formStr)) 290 req, err := http.NewRequestWithContext(ctx, "POST", "http://"+ownerAddr+"/capture/owner/move_table", rd) 291 if err != nil { 292 return errors.Trace(err) 293 } 294 295 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 296 resp, err := http.DefaultClient.Do(req) 297 if err != nil { 298 return errors.Trace(err) 299 } 300 301 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 302 body, err := io.ReadAll(resp.Body) 303 if err != nil { 304 return errors.Trace(err) 305 } 306 log.Warn("http error", zap.ByteString("body", body)) 307 return errors.New(resp.Status) 308 } 309 310 return nil 311 }