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  }