github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/pkg/utils/util.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 utils 15 16 import ( 17 "context" 18 "fmt" 19 "os" 20 "regexp" 21 "strconv" 22 "strings" 23 "time" 24 25 "github.com/go-mysql-org/go-mysql/mysql" 26 "github.com/go-mysql-org/go-mysql/replication" 27 gmysql "github.com/go-sql-driver/mysql" 28 "github.com/pingcap/errors" 29 "github.com/pingcap/tidb/pkg/errno" 30 "github.com/pingcap/tiflow/dm/pkg/log" 31 "github.com/pingcap/tiflow/dm/pkg/terror" 32 "go.uber.org/zap" 33 "golang.org/x/net/http/httpproxy" 34 ) 35 36 var ( 37 // OsExit is function placeholder for os.Exit. 38 OsExit func(int) 39 /* 40 CREATE [TEMPORARY] TABLE [IF NOT EXISTS] tbl_name 41 { LIKE old_tbl_name | (LIKE old_tbl_name) } 42 */ 43 builtInSkipDDLs = []string{ 44 // transaction 45 "^SAVEPOINT", 46 47 // skip all flush sqls 48 "^FLUSH", 49 50 // table maintenance 51 "^OPTIMIZE\\s+TABLE", 52 "^ANALYZE\\s+TABLE", 53 "^REPAIR\\s+TABLE", 54 55 // temporary table 56 "^DROP\\s+(\\/\\*\\!40005\\s+)?TEMPORARY\\s+(\\*\\/\\s+)?TABLE", 57 58 // trigger 59 "^CREATE\\s+(DEFINER\\s?=.+?)?TRIGGER", 60 "^DROP\\s+TRIGGER", 61 62 // procedure 63 "^DROP\\s+PROCEDURE", 64 "^CREATE\\s+(DEFINER\\s?=.+?)?PROCEDURE", 65 "^ALTER\\s+PROCEDURE", 66 67 // view 68 "^CREATE\\s*(OR REPLACE)?\\s+(ALGORITHM\\s?=.+?)?(DEFINER\\s?=.+?)?\\s+(SQL SECURITY DEFINER)?VIEW", 69 "^DROP\\s+VIEW", 70 "^ALTER\\s+(ALGORITHM\\s?=.+?)?(DEFINER\\s?=.+?)?(SQL SECURITY DEFINER)?VIEW", 71 72 // function 73 // user-defined function 74 "^CREATE\\s+(AGGREGATE)?\\s*?FUNCTION", 75 // stored function 76 "^CREATE\\s+(DEFINER\\s?=.+?)?FUNCTION", 77 "^ALTER\\s+FUNCTION", 78 "^DROP\\s+FUNCTION", 79 80 // tableSpace 81 "^CREATE\\s+TABLESPACE", 82 "^ALTER\\s+TABLESPACE", 83 "^DROP\\s+TABLESPACE", 84 85 // event 86 "^CREATE\\s+(DEFINER\\s?=.+?)?EVENT", 87 "^ALTER\\s+(DEFINER\\s?=.+?)?EVENT", 88 "^DROP\\s+EVENT", 89 90 // account management 91 "^GRANT", 92 "^REVOKE", 93 "^CREATE\\s+USER", 94 "^ALTER\\s+USER", 95 "^RENAME\\s+USER", 96 "^DROP\\s+USER", 97 "^SET\\s+PASSWORD", 98 } 99 builtInSkipDDLPatterns *regexp.Regexp 100 ) 101 102 func init() { 103 OsExit = os.Exit 104 builtInSkipDDLPatterns = regexp.MustCompile("(?i)" + strings.Join(builtInSkipDDLs, "|")) 105 } 106 107 // DecodeBinlogPosition parses a mysql.Position from string format. 108 func DecodeBinlogPosition(pos string) (*mysql.Position, error) { 109 if len(pos) < 3 { 110 return nil, terror.ErrInvalidBinlogPosStr.Generate(pos) 111 } 112 if pos[0] != '(' || pos[len(pos)-1] != ')' { 113 return nil, terror.ErrInvalidBinlogPosStr.Generate(pos) 114 } 115 sp := strings.Split(pos[1:len(pos)-1], ",") 116 if len(sp) != 2 { 117 return nil, terror.ErrInvalidBinlogPosStr.Generate(pos) 118 } 119 position, err := strconv.ParseUint(strings.TrimSpace(sp[1]), 10, 32) 120 if err != nil { 121 return nil, terror.ErrInvalidBinlogPosStr.Delegate(err, pos) 122 } 123 return &mysql.Position{ 124 Name: strings.TrimSpace(sp[0]), 125 Pos: uint32(position), 126 }, nil 127 } 128 129 // WaitSomething waits for something done with `true`. 130 func WaitSomething(backoff int, waitTime time.Duration, fn func() bool) bool { 131 for i := 0; i < backoff; i++ { 132 if fn() { 133 return true 134 } 135 136 time.Sleep(waitTime) 137 } 138 139 return false 140 } 141 142 // IsContextCanceledError checks whether err is context.Canceled. 143 func IsContextCanceledError(err error) bool { 144 return errors.Cause(err) == context.Canceled 145 } 146 147 // IgnoreErrorCheckpoint is used in checkpoint update. 148 func IgnoreErrorCheckpoint(err error) bool { 149 err = errors.Cause(err) // check the original error 150 mysqlErr, ok := err.(*gmysql.MySQLError) 151 if !ok { 152 return false 153 } 154 155 switch mysqlErr.Number { 156 case errno.ErrDupFieldName: 157 return true 158 default: 159 return false 160 } 161 } 162 163 // IsBuildInSkipDDL return true when checked sql that will be skipped for syncer. 164 func IsBuildInSkipDDL(sql string) bool { 165 return builtInSkipDDLPatterns.FindStringIndex(sql) != nil 166 } 167 168 // UnwrapScheme removes http or https scheme from input. 169 func UnwrapScheme(s string) string { 170 if strings.HasPrefix(s, "http://") { 171 return s[len("http://"):] 172 } else if strings.HasPrefix(s, "https://") { 173 return s[len("https://"):] 174 } 175 return s 176 } 177 178 func wrapScheme(s string, https bool) string { 179 if s == "" { 180 return s 181 } 182 s = UnwrapScheme(s) 183 if https { 184 return "https://" + s 185 } 186 return "http://" + s 187 } 188 189 // WrapSchemes adds http or https scheme to input if missing. input could be a comma-separated list. 190 func WrapSchemes(s string, https bool) string { 191 items := strings.Split(s, ",") 192 output := make([]string, 0, len(items)) 193 for _, s := range items { 194 output = append(output, wrapScheme(s, https)) 195 } 196 return strings.Join(output, ",") 197 } 198 199 // WrapSchemesForInitialCluster acts like WrapSchemes, except input is "name=URL,...". 200 func WrapSchemesForInitialCluster(s string, https bool) string { 201 items := strings.Split(s, ",") 202 output := make([]string, 0, len(items)) 203 for _, item := range items { 204 kv := strings.Split(item, "=") 205 if len(kv) != 2 { 206 output = append(output, item) 207 continue 208 } 209 210 output = append(output, kv[0]+"="+wrapScheme(kv[1], https)) 211 } 212 return strings.Join(output, ",") 213 } 214 215 // IsFakeRotateEvent return true if is this event is a fake rotate event 216 // If log pos equals zero then the received event is a fake rotate event and 217 // contains only a name of the next binlog file 218 // See https://github.com/mysql/mysql-server/blob/8e797a5d6eb3a87f16498edcb7261a75897babae/sql/rpl_binlog_sender.h#L235 219 // and https://github.com/mysql/mysql-server/blob/8cc757da3d87bf4a1f07dcfb2d3c96fed3806870/sql/rpl_binlog_sender.cc#L899 220 func IsFakeRotateEvent(header *replication.EventHeader) bool { 221 return header.Timestamp == 0 || header.LogPos == 0 222 } 223 224 // LogHTTPProxies logs HTTP proxy relative environment variables. 225 func LogHTTPProxies(useLogger bool) { 226 if fields := proxyFields(); len(fields) > 0 { 227 if useLogger { 228 log.L().Warn("using proxy config", fields...) 229 } else { 230 filedsStr := make([]string, 0, len(fields)) 231 for _, field := range fields { 232 filedsStr = append(filedsStr, field.Key+"="+field.String) 233 } 234 fmt.Printf("\n[Warning] [using proxy config] [%v]\n", strings.Join(filedsStr, ", ")) 235 } 236 } 237 } 238 239 func proxyFields() []zap.Field { 240 proxyCfg := httpproxy.FromEnvironment() 241 fields := make([]zap.Field, 0, 3) 242 if proxyCfg.HTTPProxy != "" { 243 fields = append(fields, zap.String("http_proxy", proxyCfg.HTTPProxy)) 244 } 245 if proxyCfg.HTTPSProxy != "" { 246 fields = append(fields, zap.String("https_proxy", proxyCfg.HTTPSProxy)) 247 } 248 if proxyCfg.NoProxy != "" { 249 fields = append(fields, zap.String("no_proxy", proxyCfg.NoProxy)) 250 } 251 return fields 252 } 253 254 // SetToSlice converts a map of struct{} value to a slice to pretty print. 255 func SetToSlice(set map[string]struct{}) []string { 256 slice := make([]string, 0, len(set)) 257 for key := range set { 258 slice = append(slice, key) 259 } 260 return slice 261 } 262 263 func NewStoppedTimer() *time.Timer { 264 // stopped timer should be Reset with correct duration, so use 0 here 265 t := time.NewTimer(0) 266 if !t.Stop() { 267 <-t.C 268 } 269 return t 270 }