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  }