github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/pkg/dumpling/utils.go (about)

     1  // Copyright 2019 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  package dumpling
    15  
    16  import (
    17  	"bufio"
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/go-mysql-org/go-mysql/mysql"
    26  	brstorage "github.com/pingcap/tidb/br/pkg/storage"
    27  	"github.com/pingcap/tidb/dumpling/export"
    28  	"github.com/pingcap/tiflow/dm/pkg/binlog"
    29  	"github.com/pingcap/tiflow/dm/pkg/gtid"
    30  	"github.com/pingcap/tiflow/dm/pkg/log"
    31  	"github.com/pingcap/tiflow/dm/pkg/storage"
    32  	"github.com/pingcap/tiflow/dm/pkg/terror"
    33  	"github.com/pingcap/tiflow/dm/pkg/utils"
    34  	"github.com/spf13/pflag"
    35  )
    36  
    37  // DefaultTableFilter is the default table filter for dumpling.
    38  var DefaultTableFilter = []string{"*.*", export.DefaultTableFilter}
    39  
    40  // ParseMetaData parses mydumper's output meta file and returns binlog location.
    41  // since v2.0.0, dumpling maybe configured to output master status after connection pool is established,
    42  // we return this location as well.
    43  // If `extStorage` is nil, we will use `dir` to open a storage.
    44  func ParseMetaData(
    45  	ctx context.Context,
    46  	dir string,
    47  	filename string,
    48  	extStorage brstorage.ExternalStorage,
    49  ) (*binlog.Location, *binlog.Location, error) {
    50  	fd, err := storage.OpenFile(ctx, dir, filename, extStorage)
    51  	if err != nil {
    52  		return nil, nil, err
    53  	}
    54  	defer fd.Close()
    55  
    56  	return ParseMetaDataByReader(filename, fd)
    57  }
    58  
    59  // ParseMetaDataByReader parses mydumper's output meta file by created reader and returns binlog location.
    60  func ParseMetaDataByReader(filename string, rd io.Reader) (*binlog.Location, *binlog.Location, error) {
    61  	invalidErr := fmt.Errorf("file %s invalid format", filename)
    62  
    63  	var (
    64  		pos          mysql.Position
    65  		gtidStr      string
    66  		useLocation2 = false
    67  		pos2         mysql.Position
    68  		gtidStr2     string
    69  
    70  		locPtr  *binlog.Location
    71  		locPtr2 *binlog.Location
    72  	)
    73  
    74  	br := bufio.NewReader(rd)
    75  
    76  	parsePosAndGTID := func(pos *mysql.Position, gtid *string) error {
    77  		for {
    78  			line, err2 := br.ReadString('\n')
    79  			if err2 != nil {
    80  				return err2
    81  			}
    82  			line = strings.TrimSpace(line)
    83  			if len(line) == 0 {
    84  				return nil
    85  			}
    86  			parts := strings.SplitN(line, ":", 2)
    87  			if len(parts) != 2 {
    88  				continue
    89  			}
    90  			key := strings.TrimSpace(parts[0])
    91  			value := strings.TrimSpace(parts[1])
    92  			switch key {
    93  			case "Log":
    94  				pos.Name = value
    95  			case "Pos":
    96  				pos64, err3 := strconv.ParseUint(value, 10, 32)
    97  				if err3 != nil {
    98  					return err3
    99  				}
   100  				pos.Pos = uint32(pos64)
   101  			case "GTID":
   102  				// multiple GTID sets may cross multiple lines, continue to read them.
   103  				following, err3 := readFollowingGTIDs(br)
   104  				if err3 != nil {
   105  					return err3
   106  				}
   107  				*gtid = value + following
   108  				return nil
   109  			}
   110  		}
   111  	}
   112  
   113  	for {
   114  		line, err2 := br.ReadString('\n')
   115  		if err2 == io.EOF {
   116  			break
   117  		} else if err2 != nil {
   118  			return nil, nil, err2
   119  		}
   120  		line = strings.TrimSpace(line)
   121  		if len(line) == 0 {
   122  			continue
   123  		}
   124  
   125  		switch line {
   126  		case "SHOW MASTER STATUS:":
   127  			if err3 := parsePosAndGTID(&pos, &gtidStr); err3 != nil {
   128  				return nil, nil, err3
   129  			}
   130  		case "SHOW SLAVE STATUS:":
   131  			// ref: https://github.com/maxbube/mydumper/blob/master/mydumper.c#L434
   132  			for {
   133  				line, err3 := br.ReadString('\n')
   134  				if err3 != nil {
   135  					return nil, nil, err3
   136  				}
   137  				line = strings.TrimSpace(line)
   138  				if len(line) == 0 {
   139  					break
   140  				}
   141  			}
   142  		case "SHOW MASTER STATUS: /* AFTER CONNECTION POOL ESTABLISHED */":
   143  			useLocation2 = true
   144  			if err3 := parsePosAndGTID(&pos2, &gtidStr2); err3 != nil {
   145  				return nil, nil, err3
   146  			}
   147  		default:
   148  			// do nothing for Started dump, Finished dump...
   149  		}
   150  	}
   151  
   152  	if len(pos.Name) == 0 || pos.Pos == uint32(0) {
   153  		return nil, nil, terror.ErrMetadataNoBinlogLoc.Generate(filename)
   154  	}
   155  
   156  	gset, err := gtid.ParserGTID("", gtidStr)
   157  	if err != nil {
   158  		return nil, nil, invalidErr
   159  	}
   160  	loc := binlog.NewLocation(pos, gset)
   161  	locPtr = &loc
   162  
   163  	if useLocation2 {
   164  		if len(pos2.Name) == 0 || pos2.Pos == uint32(0) {
   165  			return nil, nil, invalidErr
   166  		}
   167  		gset2, err := gtid.ParserGTID("", gtidStr2)
   168  		if err != nil {
   169  			return nil, nil, invalidErr
   170  		}
   171  		loc2 := binlog.NewLocation(pos2, gset2)
   172  		locPtr2 = &loc2
   173  	}
   174  
   175  	return locPtr, locPtr2, nil
   176  }
   177  
   178  func readFollowingGTIDs(br *bufio.Reader) (string, error) {
   179  	var following strings.Builder
   180  	for {
   181  		line, err := br.ReadString('\n')
   182  		if err == io.EOF {
   183  			return following.String(), nil // return the previous, not including the last line.
   184  		} else if err != nil {
   185  			return "", err
   186  		}
   187  
   188  		line = strings.TrimSpace(line)
   189  		if len(line) == 0 {
   190  			return following.String(), nil // end with empty line.
   191  		}
   192  
   193  		end := len(line)
   194  		if strings.HasSuffix(line, ",") {
   195  			end = len(line) - 1
   196  		}
   197  
   198  		// try parse to verify it
   199  		_, err = gtid.ParserGTID("", line[:end])
   200  		if err != nil {
   201  			// nolint:nilerr
   202  			return following.String(), nil // return the previous, not including this non-GTID line.
   203  		}
   204  
   205  		following.WriteString(line)
   206  	}
   207  }
   208  
   209  // ParseArgLikeBash parses list arguments like bash, which helps us to run
   210  // executable command via os/exec more likely running from bash.
   211  func ParseArgLikeBash(args []string) []string {
   212  	result := make([]string, 0, len(args))
   213  	for _, arg := range args {
   214  		parsedArg := trimOutQuotes(arg)
   215  		result = append(result, parsedArg)
   216  	}
   217  	return result
   218  }
   219  
   220  // trimOutQuotes trims a pair of single quotes or a pair of double quotes from arg.
   221  func trimOutQuotes(arg string) string {
   222  	argLen := len(arg)
   223  	if argLen >= 2 {
   224  		if arg[0] == '"' && arg[argLen-1] == '"' {
   225  			return arg[1 : argLen-1]
   226  		}
   227  		if arg[0] == '\'' && arg[argLen-1] == '\'' {
   228  			return arg[1 : argLen-1]
   229  		}
   230  	}
   231  	return arg
   232  }
   233  
   234  func ParseExtraArgs(logger *log.Logger, dumpCfg *export.Config, args []string) error {
   235  	var (
   236  		dumplingFlagSet = pflag.NewFlagSet("dumpling", pflag.ContinueOnError)
   237  		fileSizeStr     string
   238  		tablesList      []string
   239  		filters         []string
   240  		noLocks         bool
   241  	)
   242  
   243  	dumplingFlagSet.StringSliceVarP(&dumpCfg.Databases, "database", "B", dumpCfg.Databases, "Database to dump")
   244  	dumplingFlagSet.StringSliceVarP(&tablesList, "tables-list", "T", nil, "Comma delimited table list to dump; must be qualified table names")
   245  	dumplingFlagSet.IntVarP(&dumpCfg.Threads, "threads", "t", dumpCfg.Threads, "Number of goroutines to use, default 4")
   246  	dumplingFlagSet.StringVarP(&fileSizeStr, "filesize", "F", "", "The approximate size of output file")
   247  	dumplingFlagSet.Uint64VarP(&dumpCfg.StatementSize, "statement-size", "s", dumpCfg.StatementSize, "Attempted size of INSERT statement in bytes")
   248  	dumplingFlagSet.StringVar(&dumpCfg.Consistency, "consistency", dumpCfg.Consistency, "Consistency level during dumping: {auto|none|flush|lock|snapshot}")
   249  	dumplingFlagSet.StringVar(&dumpCfg.Snapshot, "snapshot", dumpCfg.Snapshot, "Snapshot position. Valid only when consistency=snapshot")
   250  	dumplingFlagSet.BoolVarP(&dumpCfg.NoViews, "no-views", "W", dumpCfg.NoViews, "Do not dump views")
   251  	dumplingFlagSet.Uint64VarP(&dumpCfg.Rows, "rows", "r", dumpCfg.Rows, "Split table into chunks of this many rows, default unlimited")
   252  	dumplingFlagSet.StringVar(&dumpCfg.Where, "where", dumpCfg.Where, "Dump only selected records")
   253  	dumplingFlagSet.BoolVar(&dumpCfg.EscapeBackslash, "escape-backslash", dumpCfg.EscapeBackslash, "Use backslash to escape quotation marks")
   254  	dumplingFlagSet.StringArrayVarP(&filters, "filter", "f", DefaultTableFilter, "Filter to select which tables to dump")
   255  	dumplingFlagSet.StringVar(&dumpCfg.Security.CAPath, "ca", dumpCfg.Security.CAPath, "The path name to the certificate authority file for TLS connection")
   256  	dumplingFlagSet.StringVar(&dumpCfg.Security.CertPath, "cert", dumpCfg.Security.CertPath, "The path name to the client certificate file for TLS connection")
   257  	dumplingFlagSet.StringVar(&dumpCfg.Security.KeyPath, "key", dumpCfg.Security.KeyPath, "The path name to the client private key file for TLS connection")
   258  	dumplingFlagSet.BoolVar(&noLocks, "no-locks", false, "")
   259  	dumplingFlagSet.BoolVar(&dumpCfg.TransactionalConsistency, "transactional-consistency", true, "Only support transactional consistency")
   260  
   261  	err := dumplingFlagSet.Parse(args)
   262  	if err != nil {
   263  		return err
   264  	}
   265  
   266  	// compatibility for `--no-locks`
   267  	if noLocks {
   268  		logger.Warn("`--no-locks` is replaced by `--consistency none` since v2.0.0")
   269  		// it's default consistency or by meaning of "auto", we could overwrite it by `none`
   270  		if dumpCfg.Consistency == "auto" {
   271  			dumpCfg.Consistency = "none"
   272  		} else if dumpCfg.Consistency != "none" {
   273  			return errors.New("cannot both specify `--no-locks` and `--consistency` other than `none`")
   274  		}
   275  	}
   276  
   277  	if fileSizeStr != "" {
   278  		dumpCfg.FileSize, err = utils.ParseFileSize(fileSizeStr, export.UnspecifiedSize)
   279  		if err != nil {
   280  			return err
   281  		}
   282  	}
   283  
   284  	if len(tablesList) > 0 || !utils.NonRepeatStringsEqual(DefaultTableFilter, filters) {
   285  		ff, err2 := export.ParseTableFilter(tablesList, filters)
   286  		if err2 != nil {
   287  			return err2
   288  		}
   289  		dumpCfg.TableFilter = ff // overwrite `block-allow-list`.
   290  		logger.Warn("overwrite `block-allow-list` by `tables-list` or `filter`")
   291  	}
   292  
   293  	return nil
   294  }