github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/sink/mysql/db_helper.go (about) 1 // Copyright 2022 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 mysql 15 16 import ( 17 "context" 18 "database/sql" 19 "encoding/base64" 20 "fmt" 21 "net" 22 "net/url" 23 "strconv" 24 25 dmysql "github.com/go-sql-driver/mysql" 26 "github.com/pingcap/errors" 27 "github.com/pingcap/log" 28 "github.com/pingcap/tidb/pkg/parser/charset" 29 "github.com/pingcap/tidb/pkg/parser/mysql" 30 tmysql "github.com/pingcap/tidb/pkg/parser/mysql" 31 dmutils "github.com/pingcap/tiflow/dm/pkg/conn" 32 cerror "github.com/pingcap/tiflow/pkg/errors" 33 "go.uber.org/zap" 34 ) 35 36 // CreateMySQLDBConn creates a mysql database connection with the given dsn. 37 func CreateMySQLDBConn(ctx context.Context, dsnStr string) (*sql.DB, error) { 38 db, err := sql.Open("mysql", dsnStr) 39 if err != nil { 40 return nil, cerror.ErrMySQLConnectionError.Wrap(err).GenWithStack("fail to open MySQL connection") 41 } 42 43 err = db.PingContext(ctx) 44 if err != nil { 45 // close db to recycle resources 46 if closeErr := db.Close(); closeErr != nil { 47 log.Warn("close db failed", zap.Error(err)) 48 } 49 return nil, cerror.ErrMySQLConnectionError.Wrap(err).GenWithStack("fail to open MySQL connection") 50 } 51 52 return db, nil 53 } 54 55 // GenerateDSN generates the dsn with the given config. 56 func GenerateDSN(ctx context.Context, sinkURI *url.URL, cfg *Config, dbConnFactory Factory) (dsnStr string, err error) { 57 // dsn format of the driver: 58 // [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] 59 dsn, err := GenBasicDSN(sinkURI, cfg) 60 if err != nil { 61 return "", err 62 } 63 64 var testDB *sql.DB 65 testDB, err = GetTestDB(ctx, dsn, dbConnFactory) 66 if err != nil { 67 return 68 } 69 defer testDB.Close() 70 71 // we use default sql mode for downstream because all dmls generated and ddls in ticdc 72 // are based on default sql mode. 73 dsn.Params["sql_mode"], err = dmutils.AdjustSQLModeCompatible(mysql.DefaultSQLMode) 74 if err != nil { 75 return 76 } 77 // NOTE: quote the string is necessary to avoid ambiguities. 78 dsn.Params["sql_mode"] = strconv.Quote(dsn.Params["sql_mode"]) 79 80 dsnStr, err = generateDSNByConfig(ctx, dsn, cfg, testDB) 81 if err != nil { 82 return 83 } 84 85 // check if GBK charset is supported by downstream 86 var gbkSupported bool 87 gbkSupported, err = checkCharsetSupport(ctx, testDB, charset.CharsetGBK) 88 if err != nil { 89 return 90 } 91 if !gbkSupported { 92 log.Warn("GBK charset is not supported by the downstream. "+ 93 "Some types of DDLs may fail to execute", 94 zap.String("host", dsn.Addr)) 95 } 96 97 return 98 } 99 100 func generateDSNByConfig( 101 ctx context.Context, 102 dsnCfg *dmysql.Config, 103 cfg *Config, 104 testDB *sql.DB, 105 ) (string, error) { 106 if dsnCfg.Params == nil { 107 dsnCfg.Params = make(map[string]string, 1) 108 } 109 dsnCfg.DBName = "" 110 dsnCfg.InterpolateParams = true 111 dsnCfg.MultiStatements = true 112 // if timezone is empty string, we don't pass this variable in dsn 113 if cfg.Timezone != "" { 114 dsnCfg.Params["time_zone"] = cfg.Timezone 115 } 116 dsnCfg.Params["readTimeout"] = cfg.ReadTimeout 117 dsnCfg.Params["writeTimeout"] = cfg.WriteTimeout 118 dsnCfg.Params["timeout"] = cfg.DialTimeout 119 // auto fetch max_allowed_packet on every new connection 120 dsnCfg.Params["maxAllowedPacket"] = "0" 121 122 autoRandom, err := checkTiDBVariable(ctx, testDB, "allow_auto_random_explicit_insert", "1") 123 if err != nil { 124 return "", err 125 } 126 if autoRandom != "" { 127 dsnCfg.Params["allow_auto_random_explicit_insert"] = autoRandom 128 } 129 130 txnMode, err := checkTiDBVariable(ctx, testDB, "tidb_txn_mode", cfg.tidbTxnMode) 131 if err != nil { 132 return "", err 133 } 134 if txnMode != "" { 135 dsnCfg.Params["tidb_txn_mode"] = txnMode 136 } 137 138 // Since we don't need select, just set default isolation level to read-committed 139 // transaction_isolation is mysql newly introduced variable and will vary from MySQL5.7/MySQL8.0/Mariadb 140 isolation, err := checkTiDBVariable(ctx, testDB, "transaction_isolation", defaultTxnIsolationRC) 141 if err != nil { 142 return "", err 143 } 144 if isolation != "" { 145 dsnCfg.Params["transaction_isolation"] = fmt.Sprintf(`"%s"`, defaultTxnIsolationRC) 146 } else { 147 dsnCfg.Params["tx_isolation"] = fmt.Sprintf(`"%s"`, defaultTxnIsolationRC) 148 } 149 150 // equals to executing "SET NAMES utf8mb4" 151 dsnCfg.Params["charset"] = defaultCharacterSet 152 153 // disable foreign_key_checks 154 dsnCfg.Params["foreign_key_checks"] = "0" 155 156 tidbPlacementMode, err := checkTiDBVariable(ctx, testDB, "tidb_placement_mode", "ignore") 157 if err != nil { 158 return "", err 159 } 160 if tidbPlacementMode != "" { 161 dsnCfg.Params["tidb_placement_mode"] = fmt.Sprintf(`"%s"`, tidbPlacementMode) 162 } 163 tidbEnableExternalTSRead, err := checkTiDBVariable(ctx, testDB, "tidb_enable_external_ts_read", "OFF") 164 if err != nil { 165 return "", err 166 } 167 if tidbEnableExternalTSRead != "" { 168 // set the `tidb_enable_external_ts_read` to `OFF`, so cdc could write to the sink 169 dsnCfg.Params["tidb_enable_external_ts_read"] = fmt.Sprintf(`"%s"`, tidbEnableExternalTSRead) 170 } 171 dsnClone := dsnCfg.Clone() 172 dsnClone.Passwd = "******" 173 log.Info("sink uri is configured", zap.String("dsn", dsnClone.FormatDSN())) 174 175 return dsnCfg.FormatDSN(), nil 176 } 177 178 // check whether the target charset is supported 179 func checkCharsetSupport(ctx context.Context, db *sql.DB, charsetName string) (bool, error) { 180 // validate charsetName 181 _, err := charset.GetCharsetInfo(charsetName) 182 if err != nil { 183 return false, errors.Trace(err) 184 } 185 186 var characterSetName string 187 querySQL := "select character_set_name from information_schema.character_sets " + 188 "where character_set_name = '" + charsetName + "';" 189 err = db.QueryRowContext(ctx, querySQL).Scan(&characterSetName) 190 if err != nil && err != sql.ErrNoRows { 191 return false, cerror.WrapError(cerror.ErrMySQLQueryError, err) 192 } 193 if err != nil { 194 return false, nil 195 } 196 197 return true, nil 198 } 199 200 func checkTiDBVariable(ctx context.Context, db *sql.DB, variableName, defaultValue string) (string, error) { 201 var name string 202 var value string 203 querySQL := fmt.Sprintf("show session variables like '%s';", variableName) 204 err := db.QueryRowContext(ctx, querySQL).Scan(&name, &value) 205 if err != nil && err != sql.ErrNoRows { 206 errMsg := "fail to query session variable " + variableName 207 return "", cerror.ErrMySQLQueryError.Wrap(err).GenWithStack(errMsg) 208 } 209 // session variable works, use given default value 210 if err == nil { 211 return defaultValue, nil 212 } 213 // session variable not exists, return "" to ignore it 214 return "", nil 215 } 216 217 // GetTestDB checks and adjusts the password of the given DSN, 218 // it will return a DB instance opened with the adjusted password. 219 func GetTestDB(ctx context.Context, dbConfig *dmysql.Config, dbConnFactory Factory) (*sql.DB, error) { 220 password := dbConfig.Passwd 221 if dbConnFactory == nil { 222 dbConnFactory = CreateMySQLDBConn 223 } 224 testDB, err := dbConnFactory(ctx, dbConfig.FormatDSN()) 225 if err != nil { 226 // If access is denied and password is encoded by base64, try to decoded password. 227 if mysqlErr, ok := errors.Cause(err).(*dmysql.MySQLError); ok && mysqlErr.Number == tmysql.ErrAccessDenied { 228 if dePassword, decodeErr := base64.StdEncoding.DecodeString(password); decodeErr == nil && string(dePassword) != password { 229 dbConfig.Passwd = string(dePassword) 230 testDB, err = dbConnFactory(ctx, dbConfig.FormatDSN()) 231 } 232 } 233 } 234 return testDB, err 235 } 236 237 // GenBasicDSN generates a basic DSN from the given config. 238 func GenBasicDSN(sinkURI *url.URL, cfg *Config) (*dmysql.Config, error) { 239 // dsn format of the driver: 240 // [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] 241 username := sinkURI.User.Username() 242 if username == "" { 243 username = "root" 244 } 245 password, _ := sinkURI.User.Password() 246 247 hostName := sinkURI.Hostname() 248 port := sinkURI.Port() 249 if port == "" { 250 port = "4000" 251 } 252 253 // This will handle the IPv6 address format. 254 var dsn *dmysql.Config 255 var err error 256 host := net.JoinHostPort(hostName, port) 257 dsnStr := fmt.Sprintf("%s:%s@tcp(%s)/%s", username, password, host, cfg.TLS) 258 if dsn, err = dmysql.ParseDSN(dsnStr); err != nil { 259 return nil, errors.Trace(err) 260 } 261 262 // create test db used for parameter detection 263 // Refer https://github.com/go-sql-driver/mysql#parameters 264 if dsn.Params == nil { 265 dsn.Params = make(map[string]string, 1) 266 } 267 if cfg.Timezone != "" { 268 dsn.Params["time_zone"] = cfg.Timezone 269 } 270 dsn.Params["readTimeout"] = cfg.ReadTimeout 271 dsn.Params["writeTimeout"] = cfg.WriteTimeout 272 dsn.Params["timeout"] = cfg.DialTimeout 273 return dsn, nil 274 } 275 276 // CheckIfBDRModeIsSupported checks if the downstream supports BDR mode. 277 func CheckIfBDRModeIsSupported(ctx context.Context, db *sql.DB) (bool, error) { 278 isTiDB, err := CheckIsTiDB(ctx, db) 279 if err != nil || !isTiDB { 280 return false, err 281 } 282 testSourceID := 1 283 // downstream is TiDB, set system variables. 284 // We should always try to set this variable, and ignore the error if 285 // downstream does not support this variable, it is by design. 286 query := fmt.Sprintf("SET SESSION %s = %d", "tidb_cdc_write_source", testSourceID) 287 _, err = db.ExecContext(ctx, query) 288 if err != nil { 289 if mysqlErr, ok := errors.Cause(err).(*dmysql.MySQLError); ok && 290 mysqlErr.Number == tmysql.ErrUnknownSystemVariable { 291 return false, nil 292 } 293 return false, err 294 } 295 return true, nil 296 } 297 298 // CheckIsTiDB checks if the downstream is TiDB. 299 func CheckIsTiDB(ctx context.Context, db *sql.DB) (bool, error) { 300 var tidbVer string 301 // check if downstream is TiDB 302 row := db.QueryRowContext(ctx, "select tidb_version()") 303 err := row.Scan(&tidbVer) 304 if err != nil { 305 log.Error("check tidb version error", zap.Error(err)) 306 // downstream is not TiDB, do nothing 307 if mysqlErr, ok := errors.Cause(err).(*dmysql.MySQLError); ok && (mysqlErr.Number == tmysql.ErrNoDB || 308 mysqlErr.Number == tmysql.ErrSpDoesNotExist || mysqlErr.Number == tmysql.ErrDBaccessDenied) { 309 return false, nil 310 } 311 return false, errors.Trace(err) 312 } 313 return true, nil 314 } 315 316 // QueryMaxPreparedStmtCount gets the value of max_prepared_stmt_count 317 func QueryMaxPreparedStmtCount(ctx context.Context, db *sql.DB) (int, error) { 318 row := db.QueryRowContext(ctx, "select @@global.max_prepared_stmt_count;") 319 var maxPreparedStmtCount sql.NullInt32 320 err := row.Scan(&maxPreparedStmtCount) 321 if err != nil { 322 err = cerror.WrapError(cerror.ErrMySQLQueryError, err) 323 } 324 return int(maxPreparedStmtCount.Int32), err 325 } 326 327 // QueryMaxAllowedPacket gets the value of max_allowed_packet 328 func QueryMaxAllowedPacket(ctx context.Context, db *sql.DB) (int64, error) { 329 row := db.QueryRowContext(ctx, "select @@global.max_allowed_packet;") 330 var maxAllowedPacket sql.NullInt64 331 if err := row.Scan(&maxAllowedPacket); err != nil { 332 return 0, cerror.WrapError(cerror.ErrMySQLQueryError, err) 333 } 334 return maxAllowedPacket.Int64, nil 335 } 336 337 // SetWriteSource sets write source for the transaction. 338 func SetWriteSource(ctx context.Context, cfg *Config, txn *sql.Tx) error { 339 // we only set write source when donwstream is TiDB and write source is existed. 340 if !cfg.IsWriteSourceExisted { 341 return nil 342 } 343 // downstream is TiDB, set system variables. 344 // We should always try to set this variable, and ignore the error if 345 // downstream does not support this variable, it is by design. 346 query := fmt.Sprintf("SET SESSION %s = %d", "tidb_cdc_write_source", cfg.SourceID) 347 _, err := txn.ExecContext(ctx, query) 348 if err != nil { 349 if mysqlErr, ok := errors.Cause(err).(*dmysql.MySQLError); ok && 350 mysqlErr.Number == mysql.ErrUnknownSystemVariable { 351 return nil 352 } 353 return err 354 } 355 return nil 356 }