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  }