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, >idStr); 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, >idStr2); 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 }