github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/sink/mysql/config_test.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 "net/url" 20 "strings" 21 "testing" 22 "time" 23 24 "github.com/DATA-DOG/go-sqlmock" 25 "github.com/aws/aws-sdk-go/aws" 26 dmysql "github.com/go-sql-driver/mysql" 27 "github.com/pingcap/tiflow/cdc/model" 28 "github.com/pingcap/tiflow/pkg/config" 29 "github.com/pingcap/tiflow/pkg/util" 30 "github.com/stretchr/testify/require" 31 ) 32 33 func TestGenerateDSNByConfig(t *testing.T) { 34 t.Parallel() 35 testDefaultConfig := func() { 36 db, err := MockTestDB() 37 require.Nil(t, err) 38 defer db.Close() 39 40 dsn, err := dmysql.ParseDSN("root:123456@tcp(127.0.0.1:4000)/") 41 require.Nil(t, err) 42 cfg := NewConfig() 43 dsnStr, err := generateDSNByConfig(context.TODO(), dsn, cfg, db) 44 require.Nil(t, err) 45 expectedCfg := []string{ 46 "tidb_txn_mode=optimistic", 47 "readTimeout=2m", 48 "writeTimeout=2m", 49 "allow_auto_random_explicit_insert=1", 50 "transaction_isolation=%22READ-COMMITTED%22", 51 "charset=utf8mb4", 52 "tidb_placement_mode=%22ignore%22", 53 } 54 for _, param := range expectedCfg { 55 require.Contains(t, dsnStr, param) 56 } 57 require.False(t, strings.Contains(dsnStr, "time_zone")) 58 } 59 60 testTimezoneParam := func() { 61 db, err := MockTestDB() 62 require.Nil(t, err) 63 defer db.Close() 64 65 dsn, err := dmysql.ParseDSN("root:123456@tcp(127.0.0.1:4000)/") 66 require.Nil(t, err) 67 cfg := NewConfig() 68 cfg.Timezone = `"UTC"` 69 dsnStr, err := generateDSNByConfig(context.TODO(), dsn, cfg, db) 70 require.Nil(t, err) 71 require.True(t, strings.Contains(dsnStr, "time_zone=%22UTC%22")) 72 } 73 74 testTimeoutConfig := func() { 75 db, err := MockTestDB() 76 require.Nil(t, err) 77 defer db.Close() 78 79 dsn, err := dmysql.ParseDSN("root:123456@tcp(127.0.0.1:4000)/") 80 require.Nil(t, err) 81 uri, err := url.Parse("mysql://127.0.0.1:3306/?read-timeout=4m&write-timeout=5m&timeout=3m") 82 require.Nil(t, err) 83 cfg := NewConfig() 84 err = cfg.Apply("UTC", 85 model.DefaultChangeFeedID("123"), uri, config.GetDefaultReplicaConfig()) 86 require.Nil(t, err) 87 dsnStr, err := generateDSNByConfig(context.TODO(), dsn, cfg, db) 88 require.Nil(t, err) 89 expectedCfg := []string{ 90 "readTimeout=4m", 91 "writeTimeout=5m", 92 "timeout=3m", 93 } 94 for _, param := range expectedCfg { 95 require.True(t, strings.Contains(dsnStr, param)) 96 } 97 } 98 99 testIsolationConfig := func() { 100 db, mock, err := sqlmock.New() 101 require.Nil(t, err) 102 defer db.Close() // nolint:errcheck 103 columns := []string{"Variable_name", "Value"} 104 mock.ExpectQuery("show session variables like 'allow_auto_random_explicit_insert';").WillReturnRows( 105 sqlmock.NewRows(columns).AddRow("allow_auto_random_explicit_insert", "0"), 106 ) 107 mock.ExpectQuery("show session variables like 'tidb_txn_mode';").WillReturnRows( 108 sqlmock.NewRows(columns).AddRow("tidb_txn_mode", "pessimistic"), 109 ) 110 // simulate error 111 dsn, err := dmysql.ParseDSN("root:123456@tcp(127.0.0.1:4000)/") 112 require.Nil(t, err) 113 cfg := NewConfig() 114 var dsnStr string 115 _, err = generateDSNByConfig(context.TODO(), dsn, cfg, db) 116 require.Error(t, err) 117 118 // simulate no transaction_isolation 119 mock.ExpectQuery("show session variables like 'allow_auto_random_explicit_insert';").WillReturnRows( 120 sqlmock.NewRows(columns).AddRow("allow_auto_random_explicit_insert", "0"), 121 ) 122 mock.ExpectQuery("show session variables like 'tidb_txn_mode';").WillReturnRows( 123 sqlmock.NewRows(columns).AddRow("tidb_txn_mode", "pessimistic"), 124 ) 125 mock.ExpectQuery("show session variables like 'transaction_isolation';").WillReturnError(sql.ErrNoRows) 126 mock.ExpectQuery("show session variables like 'tidb_placement_mode';"). 127 WillReturnRows( 128 sqlmock.NewRows(columns). 129 AddRow("tidb_placement_mode", "IGNORE"), 130 ) 131 mock.ExpectQuery("show session variables like 'tidb_enable_external_ts_read';"). 132 WillReturnRows( 133 sqlmock.NewRows(columns). 134 AddRow("tidb_enable_external_ts_read", "OFF"), 135 ) 136 dsnStr, err = generateDSNByConfig(context.TODO(), dsn, cfg, db) 137 require.Nil(t, err) 138 expectedCfg := []string{ 139 "tx_isolation=%22READ-COMMITTED%22", 140 } 141 for _, param := range expectedCfg { 142 require.True(t, strings.Contains(dsnStr, param)) 143 } 144 145 // simulate transaction_isolation 146 mock.ExpectQuery("show session variables like 'allow_auto_random_explicit_insert';").WillReturnRows( 147 sqlmock.NewRows(columns).AddRow("allow_auto_random_explicit_insert", "0"), 148 ) 149 mock.ExpectQuery("show session variables like 'tidb_txn_mode';").WillReturnRows( 150 sqlmock.NewRows(columns).AddRow("tidb_txn_mode", "pessimistic"), 151 ) 152 mock.ExpectQuery("show session variables like 'transaction_isolation';").WillReturnRows( 153 sqlmock.NewRows(columns).AddRow("transaction_isolation", "REPEATED-READ"), 154 ) 155 mock.ExpectQuery("show session variables like 'tidb_placement_mode';"). 156 WillReturnRows( 157 sqlmock.NewRows(columns). 158 AddRow("tidb_placement_mode", "IGNORE"), 159 ) 160 mock.ExpectQuery("show session variables like 'tidb_enable_external_ts_read';"). 161 WillReturnRows( 162 sqlmock.NewRows(columns). 163 AddRow("tidb_enable_external_ts_read", "OFF"), 164 ) 165 dsnStr, err = generateDSNByConfig(context.TODO(), dsn, cfg, db) 166 require.Nil(t, err) 167 expectedCfg = []string{ 168 "transaction_isolation=%22READ-COMMITTED%22", 169 } 170 for _, param := range expectedCfg { 171 require.True(t, strings.Contains(dsnStr, param)) 172 } 173 } 174 175 testDefaultConfig() 176 testTimezoneParam() 177 testTimeoutConfig() 178 testIsolationConfig() 179 } 180 181 func TestApplySinkURIParamsToConfig(t *testing.T) { 182 t.Parallel() 183 184 expected := NewConfig() 185 expected.WorkerCount = 64 186 expected.MaxTxnRow = 20 187 expected.MaxMultiUpdateRowCount = 80 188 expected.MaxMultiUpdateRowSize = 512 189 expected.SafeMode = false 190 expected.Timezone = `"UTC"` 191 expected.tidbTxnMode = "pessimistic" 192 expected.CachePrepStmts = true 193 uriStr := "mysql://127.0.0.1:3306/?worker-count=64&max-txn-row=20" + 194 "&max-multi-update-row=80&max-multi-update-row-size=512" + 195 "&safe-mode=false" + 196 "&tidb-txn-mode=pessimistic" + 197 "&test-some-deprecated-config=true&test-deprecated-size-config=100" + 198 "&cache-prep-stmts=true&prep-stmt-cache-size=1000000" 199 uri, err := url.Parse(uriStr) 200 require.Nil(t, err) 201 cfg := NewConfig() 202 err = cfg.Apply("UTC", model.ChangeFeedID{}, uri, config.GetDefaultReplicaConfig()) 203 require.Nil(t, err) 204 require.Equal(t, expected, cfg) 205 } 206 207 func TestParseSinkURIOverride(t *testing.T) { 208 t.Parallel() 209 210 cases := []struct { 211 uri string 212 checker func(*Config) 213 }{{ 214 uri: "mysql://127.0.0.1:3306/?worker-count=2147483648", // int32 max 215 checker: func(sp *Config) { 216 require.EqualValues(t, sp.WorkerCount, maxWorkerCount) 217 }, 218 }, { 219 uri: "mysql://127.0.0.1:3306/?max-txn-row=2147483648", // int32 max 220 checker: func(sp *Config) { 221 require.EqualValues(t, sp.MaxTxnRow, maxMaxTxnRow) 222 }, 223 }, { 224 uri: "mysql://127.0.0.1:3306/?max-multi-update-row=2147483648", // int32 max 225 checker: func(sp *Config) { 226 require.EqualValues(t, sp.MaxMultiUpdateRowCount, maxMaxMultiUpdateRowCount) 227 }, 228 }, { 229 uri: "mysql://127.0.0.1:3306/?max-multi-update-row-size=2147483648", // int32 max 230 checker: func(sp *Config) { 231 require.EqualValues(t, sp.MaxMultiUpdateRowSize, maxMaxMultiUpdateRowSize) 232 }, 233 }, { 234 uri: "mysql://127.0.0.1:3306/?tidb-txn-mode=badmode", 235 checker: func(sp *Config) { 236 require.EqualValues(t, sp.tidbTxnMode, defaultTiDBTxnMode) 237 }, 238 }, { 239 uri: "mysql://127.0.0.1:3306/?cache-prep-stmts=false", 240 checker: func(sp *Config) { 241 require.EqualValues(t, sp.CachePrepStmts, false) 242 }, 243 }} 244 var uri *url.URL 245 var err error 246 for _, cs := range cases { 247 if cs.uri != "" { 248 uri, err = url.Parse(cs.uri) 249 require.Nil(t, err) 250 } else { 251 uri = nil 252 } 253 cfg := NewConfig() 254 err = cfg.Apply("UTC", 255 model.DefaultChangeFeedID("changefeed-01"), 256 uri, config.GetDefaultReplicaConfig()) 257 require.Nil(t, err) 258 cs.checker(cfg) 259 } 260 } 261 262 func TestParseSinkURIBadQueryString(t *testing.T) { 263 t.Parallel() 264 265 uris := []string{ 266 "", 267 "postgre://127.0.0.1:3306", 268 "mysql://127.0.0.1:3306/?worker-count=not-number", 269 "mysql://127.0.0.1:3306/?worker-count=-1", 270 "mysql://127.0.0.1:3306/?worker-count=0", 271 "mysql://127.0.0.1:3306/?max-txn-row=not-number", 272 "mysql://127.0.0.1:3306/?max-txn-row=-1", 273 "mysql://127.0.0.1:3306/?max-txn-row=0", 274 "mysql://127.0.0.1:3306/?ssl-ca=only-ca-exists", 275 "mysql://127.0.0.1:3306/?safe-mode=not-bool", 276 "mysql://127.0.0.1:3306/?time-zone=badtz", 277 "mysql://127.0.0.1:3306/?write-timeout=badduration", 278 "mysql://127.0.0.1:3306/?read-timeout=badduration", 279 "mysql://127.0.0.1:3306/?timeout=badduration", 280 } 281 var uri *url.URL 282 var err error 283 for _, uriStr := range uris { 284 if uriStr != "" { 285 uri, err = url.Parse(uriStr) 286 require.Nil(t, err) 287 } else { 288 uri = nil 289 } 290 cfg := NewConfig() 291 err = cfg.Apply("UTC", 292 model.DefaultChangeFeedID("changefeed-01"), uri, config.GetDefaultReplicaConfig()) 293 require.Error(t, err) 294 } 295 } 296 297 func TestCheckTiDBVariable(t *testing.T) { 298 t.Parallel() 299 300 db, mock, err := sqlmock.New() 301 require.Nil(t, err) 302 defer db.Close() //nolint:errcheck 303 columns := []string{"Variable_name", "Value"} 304 305 mock.ExpectQuery("show session variables like 'allow_auto_random_explicit_insert';").WillReturnRows( 306 sqlmock.NewRows(columns).AddRow("allow_auto_random_explicit_insert", "0"), 307 ) 308 val, err := checkTiDBVariable(context.TODO(), db, "allow_auto_random_explicit_insert", "1") 309 require.Nil(t, err) 310 require.Equal(t, "1", val) 311 312 mock.ExpectQuery("show session variables like 'no_exist_variable';").WillReturnError(sql.ErrNoRows) 313 val, err = checkTiDBVariable(context.TODO(), db, "no_exist_variable", "0") 314 require.Nil(t, err) 315 require.Equal(t, "", val) 316 317 mock.ExpectQuery("show session variables like 'version';").WillReturnError(sql.ErrConnDone) 318 _, err = checkTiDBVariable(context.TODO(), db, "version", "5.7.25-TiDB-v4.0.0") 319 require.NotNil(t, err) 320 require.Regexp(t, ".*"+sql.ErrConnDone.Error(), err.Error()) 321 } 322 323 func TestApplyTimezone(t *testing.T) { 324 t.Parallel() 325 326 localTimezone, err := util.GetTimezone("Local") 327 require.Nil(t, err) 328 329 for _, test := range []struct { 330 name string 331 noChangefeedTimezone bool 332 changefeedTimezone string 333 serverTimezone *time.Location 334 expected string 335 expectedHasErr bool 336 expectedErr string 337 }{ 338 { 339 name: "no changefeed timezone", 340 noChangefeedTimezone: true, 341 serverTimezone: time.UTC, 342 expected: "\"UTC\"", 343 expectedHasErr: false, 344 }, 345 { 346 name: "empty changefeed timezone", 347 noChangefeedTimezone: false, 348 changefeedTimezone: "", 349 serverTimezone: time.UTC, 350 expected: "", 351 expectedHasErr: false, 352 }, 353 { 354 name: "normal changefeed timezone", 355 noChangefeedTimezone: false, 356 changefeedTimezone: "UTC", 357 serverTimezone: time.UTC, 358 expected: "\"UTC\"", 359 expectedHasErr: false, 360 }, 361 { 362 name: "local timezone", 363 noChangefeedTimezone: false, 364 changefeedTimezone: "Local", 365 serverTimezone: localTimezone, 366 expected: "\"" + localTimezone.String() + "\"", 367 expectedHasErr: false, 368 }, 369 { 370 name: "sink-uri timezone different from server timezone", 371 noChangefeedTimezone: false, 372 changefeedTimezone: "UTC", 373 serverTimezone: localTimezone, 374 expectedHasErr: true, 375 expectedErr: "Please make sure that the timezone of the TiCDC server", 376 }, 377 { 378 name: "unsupported timezone format", 379 noChangefeedTimezone: false, 380 changefeedTimezone: "%2B08%3A00", // +08:00 381 serverTimezone: time.UTC, 382 expectedHasErr: true, 383 expectedErr: "unknown time zone +08:00", 384 }, 385 } { 386 tc := test 387 t.Run(tc.name, func(t *testing.T) { 388 t.Parallel() 389 390 cfg := NewConfig() 391 sinkURI := "mysql://127.0.0.1:3306" 392 if !tc.noChangefeedTimezone { 393 sinkURI = sinkURI + "?time-zone=" + tc.changefeedTimezone 394 } 395 uri, err := url.Parse(sinkURI) 396 require.Nil(t, err) 397 err = cfg.Apply(tc.serverTimezone.String(), 398 model.DefaultChangeFeedID("changefeed-01"), uri, config.GetDefaultReplicaConfig()) 399 if tc.expectedHasErr { 400 require.NotNil(t, err) 401 require.Contains(t, err.Error(), tc.expectedErr) 402 } else { 403 require.Nil(t, err) 404 require.Equal(t, tc.expected, cfg.Timezone) 405 } 406 }) 407 } 408 } 409 410 func TestMergeConfig(t *testing.T) { 411 uri := "mysql://topic" 412 sinkURI, err := url.Parse(uri) 413 require.NoError(t, err) 414 replicaConfig := config.GetDefaultReplicaConfig() 415 replicaConfig.Sink.MySQLConfig = &config.MySQLConfig{ 416 WorkerCount: aws.Int(13), 417 MaxTxnRow: aws.Int(100), 418 MaxMultiUpdateRowSize: aws.Int(102), 419 MaxMultiUpdateRowCount: aws.Int(103), 420 TiDBTxnMode: aws.String("pessimistic"), 421 TimeZone: aws.String("Asia/Shanghai"), 422 WriteTimeout: aws.String("1m1s"), 423 ReadTimeout: aws.String("1m2s"), 424 Timeout: aws.String("1m3s"), 425 EnableBatchDML: aws.Bool(true), 426 EnableMultiStatement: aws.Bool(true), 427 EnableCachePreparedStatement: aws.Bool(true), 428 } 429 c := NewConfig() 430 err = c.Apply("Asia/Shanghai", model.DefaultChangeFeedID("test"), sinkURI, replicaConfig) 431 require.NoError(t, err) 432 require.Equal(t, 13, c.WorkerCount) 433 require.Equal(t, 100, c.MaxTxnRow) 434 require.Equal(t, 102, c.MaxMultiUpdateRowSize) 435 require.Equal(t, 103, c.MaxMultiUpdateRowCount) 436 require.Equal(t, "pessimistic", c.tidbTxnMode) 437 require.Equal(t, "\"Asia/Shanghai\"", c.Timezone) 438 require.Equal(t, "1m1s", c.WriteTimeout) 439 require.Equal(t, "1m2s", c.ReadTimeout) 440 require.Equal(t, "1m3s", c.DialTimeout) 441 require.Equal(t, true, c.BatchDMLEnable) 442 require.Equal(t, true, c.MultiStmtEnable) 443 require.Equal(t, true, c.CachePrepStmts) 444 445 uri = "mysql://topic?" + 446 "worker-count=13&" + 447 "max-txn-row=100&" + 448 "max-multi-update-row-size=102&" + 449 "max-multi-update-row=103&" + 450 "tidb-txn-mode=pessimistic&" + 451 "time-zone=Asia/Shanghai&" + 452 "write-timeout=1m1s&" + 453 "read-timeout=1m2s&" + 454 "timeout=1m3s&" + 455 "batch-dml-enable=true&" + 456 "multi-stmt-enable=true&" + 457 "cache-prep-stmts=true" 458 sinkURI, err = url.Parse(uri) 459 require.NoError(t, err) 460 replicaConfig = config.GetDefaultReplicaConfig() 461 replicaConfig.Sink.MySQLConfig = &config.MySQLConfig{ 462 WorkerCount: aws.Int(11), 463 MaxTxnRow: aws.Int(130), 464 MaxMultiUpdateRowSize: aws.Int(142), 465 MaxMultiUpdateRowCount: aws.Int(153), 466 TiDBTxnMode: aws.String("optimistic"), 467 TimeZone: aws.String("utc"), 468 WriteTimeout: aws.String("2m1s"), 469 ReadTimeout: aws.String("3m2s"), 470 Timeout: aws.String("4m3s"), 471 EnableBatchDML: aws.Bool(false), 472 EnableMultiStatement: aws.Bool(false), 473 EnableCachePreparedStatement: aws.Bool(false), 474 } 475 c = NewConfig() 476 err = c.Apply("Asia/Shanghai", model.DefaultChangeFeedID("test"), sinkURI, replicaConfig) 477 require.NoError(t, err) 478 require.Equal(t, 13, c.WorkerCount) 479 require.Equal(t, 100, c.MaxTxnRow) 480 require.Equal(t, 102, c.MaxMultiUpdateRowSize) 481 require.Equal(t, 103, c.MaxMultiUpdateRowCount) 482 require.Equal(t, "pessimistic", c.tidbTxnMode) 483 require.Equal(t, "\"Asia/Shanghai\"", c.Timezone) 484 require.Equal(t, "1m1s", c.WriteTimeout) 485 require.Equal(t, "1m2s", c.ReadTimeout) 486 require.Equal(t, "1m3s", c.DialTimeout) 487 require.Equal(t, true, c.BatchDMLEnable) 488 require.Equal(t, true, c.MultiStmtEnable) 489 require.Equal(t, true, c.CachePrepStmts) 490 }