github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/config/replica_config_test.go (about)

     1  // Copyright 2021 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 config
    15  
    16  import (
    17  	"bytes"
    18  	"encoding/json"
    19  	"net/url"
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/aws/aws-sdk-go/aws"
    24  	"github.com/pingcap/tiflow/pkg/compression"
    25  	cerror "github.com/pingcap/tiflow/pkg/errors"
    26  	"github.com/pingcap/tiflow/pkg/integrity"
    27  	"github.com/pingcap/tiflow/pkg/util"
    28  	"github.com/stretchr/testify/require"
    29  )
    30  
    31  func mustIndentJSON(t *testing.T, j string) string {
    32  	var buf bytes.Buffer
    33  	err := json.Indent(&buf, []byte(j), "", "  ")
    34  	require.Nil(t, err)
    35  	return buf.String()
    36  }
    37  
    38  func TestReplicaConfigMarshal(t *testing.T) {
    39  	t.Parallel()
    40  	conf := GetDefaultReplicaConfig()
    41  	conf.CaseSensitive = false
    42  	conf.ForceReplicate = true
    43  	conf.Filter.Rules = []string{"1.1"}
    44  	conf.Mounter.WorkerNum = 3
    45  	conf.Sink.Protocol = util.AddressOf("canal-json")
    46  	conf.Sink.ColumnSelectors = []*ColumnSelector{
    47  		{
    48  			Matcher: []string{"1.1"},
    49  			Columns: []string{"a", "b"},
    50  		},
    51  	}
    52  	conf.Sink.CSVConfig = &CSVConfig{
    53  		Delimiter:            ",",
    54  		Quote:                "\"",
    55  		NullString:           `\N`,
    56  		IncludeCommitTs:      true,
    57  		BinaryEncodingMethod: BinaryEncodingBase64,
    58  	}
    59  	conf.Sink.TxnAtomicity = util.AddressOf(unknownTxnAtomicity)
    60  	conf.Sink.DateSeparator = util.AddressOf("month")
    61  	conf.Sink.EnablePartitionSeparator = util.AddressOf(true)
    62  	conf.Sink.EnableKafkaSinkV2 = util.AddressOf(true)
    63  	conf.Scheduler.EnableTableAcrossNodes = true
    64  	conf.Scheduler.RegionThreshold = 100001
    65  	conf.Scheduler.WriteKeyThreshold = 100001
    66  
    67  	conf.Sink.OnlyOutputUpdatedColumns = aws.Bool(true)
    68  	conf.Sink.DeleteOnlyOutputHandleKeyColumns = aws.Bool(true)
    69  	conf.Sink.ContentCompatible = aws.Bool(true)
    70  	conf.Sink.SafeMode = aws.Bool(true)
    71  	conf.Sink.AdvanceTimeoutInSec = util.AddressOf(uint(150))
    72  	conf.Sink.DebeziumDisableSchema = util.AddressOf(false)
    73  	conf.Sink.KafkaConfig = &KafkaConfig{
    74  		PartitionNum:                 aws.Int32(1),
    75  		ReplicationFactor:            aws.Int16(1),
    76  		KafkaVersion:                 aws.String("version"),
    77  		MaxMessageBytes:              aws.Int(1),
    78  		Compression:                  aws.String("gzip"),
    79  		KafkaClientID:                aws.String("client-id"),
    80  		AutoCreateTopic:              aws.Bool(true),
    81  		DialTimeout:                  aws.String("1m"),
    82  		WriteTimeout:                 aws.String("1m"),
    83  		ReadTimeout:                  aws.String("1m"),
    84  		RequiredAcks:                 aws.Int(1),
    85  		SASLUser:                     aws.String("user"),
    86  		SASLPassword:                 aws.String("password"),
    87  		SASLMechanism:                aws.String("mechanism"),
    88  		SASLGssAPIAuthType:           aws.String("type"),
    89  		SASLGssAPIKeytabPath:         aws.String("path"),
    90  		SASLGssAPIKerberosConfigPath: aws.String("path"),
    91  		SASLGssAPIServiceName:        aws.String("service"),
    92  		SASLGssAPIUser:               aws.String("user"),
    93  		SASLGssAPIPassword:           aws.String("password"),
    94  		SASLGssAPIRealm:              aws.String("realm"),
    95  		SASLGssAPIDisablePafxfast:    aws.Bool(true),
    96  		EnableTLS:                    aws.Bool(true),
    97  		CA:                           aws.String("ca"),
    98  		Cert:                         aws.String("cert"),
    99  		Key:                          aws.String("key"),
   100  		CodecConfig: &CodecConfig{
   101  			EnableTiDBExtension:            aws.Bool(true),
   102  			MaxBatchSize:                   aws.Int(100000),
   103  			AvroEnableWatermark:            aws.Bool(true),
   104  			AvroDecimalHandlingMode:        aws.String("string"),
   105  			AvroBigintUnsignedHandlingMode: aws.String("string"),
   106  			EncodingFormat:                 aws.String("json"),
   107  		},
   108  		LargeMessageHandle: &LargeMessageHandleConfig{
   109  			LargeMessageHandleOption: LargeMessageHandleOptionHandleKeyOnly,
   110  		},
   111  		GlueSchemaRegistryConfig: &GlueSchemaRegistryConfig{
   112  			Region:       "region",
   113  			RegistryName: "registry",
   114  		},
   115  	}
   116  	conf.Sink.PulsarConfig = &PulsarConfig{
   117  		PulsarVersion:           aws.String("v2.10.0"),
   118  		AuthenticationToken:     aws.String("token"),
   119  		TLSTrustCertsFilePath:   aws.String("TLSTrustCertsFilePath_path"),
   120  		ConnectionTimeout:       NewTimeSec(18),
   121  		OperationTimeout:        NewTimeSec(8),
   122  		BatchingMaxPublishDelay: NewTimeMill(5000),
   123  	}
   124  	conf.Sink.MySQLConfig = &MySQLConfig{
   125  		WorkerCount:                  aws.Int(8),
   126  		MaxTxnRow:                    aws.Int(100000),
   127  		MaxMultiUpdateRowSize:        aws.Int(100000),
   128  		MaxMultiUpdateRowCount:       aws.Int(100000),
   129  		TiDBTxnMode:                  aws.String("pessimistic"),
   130  		SSLCa:                        aws.String("ca"),
   131  		SSLCert:                      aws.String("cert"),
   132  		SSLKey:                       aws.String("key"),
   133  		TimeZone:                     aws.String("UTC"),
   134  		WriteTimeout:                 aws.String("1m"),
   135  		ReadTimeout:                  aws.String("1m"),
   136  		Timeout:                      aws.String("1m"),
   137  		EnableBatchDML:               aws.Bool(true),
   138  		EnableMultiStatement:         aws.Bool(true),
   139  		EnableCachePreparedStatement: aws.Bool(true),
   140  	}
   141  	conf.Sink.CloudStorageConfig = &CloudStorageConfig{
   142  		WorkerCount:    aws.Int(8),
   143  		FlushInterval:  aws.String("1m"),
   144  		FileSize:       aws.Int(1024),
   145  		OutputColumnID: aws.Bool(false),
   146  	}
   147  	conf.Sink.Debezium = &DebeziumConfig{
   148  		OutputOldValue: true,
   149  	}
   150  	conf.Sink.OpenProtocol = &OpenProtocolConfig{
   151  		OutputOldValue: true,
   152  	}
   153  
   154  	b, err := conf.Marshal()
   155  	require.NoError(t, err)
   156  	b = mustIndentJSON(t, b)
   157  	require.JSONEq(t, testCfgTestReplicaConfigMarshal1, b)
   158  
   159  	conf2 := new(ReplicaConfig)
   160  	err = conf2.UnmarshalJSON([]byte(testCfgTestReplicaConfigMarshal2))
   161  	require.NoError(t, err)
   162  	require.Equal(t, conf, conf2)
   163  }
   164  
   165  func TestReplicaConfigClone(t *testing.T) {
   166  	t.Parallel()
   167  	conf := GetDefaultReplicaConfig()
   168  	conf.CaseSensitive = false
   169  	conf.ForceReplicate = true
   170  	conf.Filter.Rules = []string{"1.1"}
   171  	conf.Mounter.WorkerNum = 3
   172  	conf2 := conf.Clone()
   173  	require.Equal(t, conf, conf2)
   174  	conf2.Mounter.WorkerNum = 4
   175  	require.Equal(t, 3, conf.Mounter.WorkerNum)
   176  }
   177  
   178  func TestReplicaConfigOutDated(t *testing.T) {
   179  	t.Parallel()
   180  	conf2 := new(ReplicaConfig)
   181  	err := conf2.UnmarshalJSON([]byte(testCfgTestReplicaConfigOutDated))
   182  	require.NoError(t, err)
   183  
   184  	conf := GetDefaultReplicaConfig()
   185  	conf.CaseSensitive = false
   186  	conf.ForceReplicate = true
   187  	conf.Filter.Rules = []string{"1.1"}
   188  	conf.Mounter.WorkerNum = 3
   189  	conf.Sink.Protocol = util.AddressOf("canal-json")
   190  	conf.Sink.DispatchRules = []*DispatchRule{
   191  		{Matcher: []string{"a.b"}, DispatcherRule: "r1"},
   192  		{Matcher: []string{"a.c"}, DispatcherRule: "r2"},
   193  		{Matcher: []string{"a.d"}, DispatcherRule: "r2"},
   194  	}
   195  	conf.Sink.CSVConfig = nil
   196  	require.Equal(t, conf, conf2)
   197  }
   198  
   199  func TestReplicaConfigValidate(t *testing.T) {
   200  	t.Parallel()
   201  	conf := GetDefaultReplicaConfig()
   202  
   203  	sinkURL, err := url.Parse("blackhole://")
   204  	require.NoError(t, err)
   205  	require.NoError(t, conf.ValidateAndAdjust(sinkURL))
   206  
   207  	conf = GetDefaultReplicaConfig()
   208  	conf.Sink.DispatchRules = []*DispatchRule{
   209  		{Matcher: []string{"a.b"}, DispatcherRule: "d1", PartitionRule: "r1"},
   210  	}
   211  	err = conf.ValidateAndAdjust(sinkURL)
   212  	require.Regexp(t, ".*dispatcher and partition cannot be configured both.*", err)
   213  
   214  	// Correct sink configuration.
   215  	conf = GetDefaultReplicaConfig()
   216  	conf.Sink.DispatchRules = []*DispatchRule{
   217  		{Matcher: []string{"a.b"}, DispatcherRule: "d1"},
   218  		{Matcher: []string{"a.c"}, PartitionRule: "p1"},
   219  		{Matcher: []string{"a.d"}},
   220  	}
   221  	err = conf.ValidateAndAdjust(sinkURL)
   222  	require.NoError(t, err)
   223  	rules := conf.Sink.DispatchRules
   224  	require.Equal(t, "d1", rules[0].PartitionRule)
   225  	require.Equal(t, "p1", rules[1].PartitionRule)
   226  	require.Equal(t, "", rules[2].PartitionRule)
   227  
   228  	// Test memory quota can be adjusted
   229  	conf = GetDefaultReplicaConfig()
   230  	conf.MemoryQuota = 0
   231  	err = conf.ValidateAndAdjust(sinkURL)
   232  	require.NoError(t, err)
   233  	require.Equal(t, uint64(DefaultChangefeedMemoryQuota), conf.MemoryQuota)
   234  
   235  	conf.MemoryQuota = uint64(1024)
   236  	err = conf.ValidateAndAdjust(sinkURL)
   237  	require.NoError(t, err)
   238  	require.Equal(t, uint64(1024), conf.MemoryQuota)
   239  
   240  	conf.Scheduler = &ChangefeedSchedulerConfig{
   241  		EnableTableAcrossNodes: true,
   242  		RegionThreshold:        -1,
   243  	}
   244  	err = conf.ValidateAndAdjust(sinkURL)
   245  	require.Error(t, err)
   246  }
   247  
   248  func TestValidateIntegrity(t *testing.T) {
   249  	sinkURL, err := url.Parse("kafka://topic?protocol=avro")
   250  	require.NoError(t, err)
   251  
   252  	cfg := GetDefaultReplicaConfig()
   253  	cfg.Integrity.IntegrityCheckLevel = integrity.CheckLevelCorrectness
   254  	cfg.Sink.ColumnSelectors = []*ColumnSelector{
   255  		{
   256  			Matcher: []string{"a.b"}, Columns: []string{"c"},
   257  		},
   258  	}
   259  
   260  	err = cfg.ValidateAndAdjust(sinkURL)
   261  	require.ErrorIs(t, err, cerror.ErrInvalidReplicaConfig)
   262  }
   263  
   264  func TestValidateAndAdjust(t *testing.T) {
   265  	cfg := GetDefaultReplicaConfig()
   266  
   267  	require.False(t, util.GetOrZero(cfg.EnableSyncPoint))
   268  	sinkURL, err := url.Parse("blackhole://")
   269  	require.NoError(t, err)
   270  
   271  	require.NoError(t, cfg.ValidateAndAdjust(sinkURL))
   272  
   273  	cfg.EnableSyncPoint = util.AddressOf(true)
   274  	require.NoError(t, cfg.ValidateAndAdjust(sinkURL))
   275  
   276  	cfg.SyncPointInterval = util.AddressOf(time.Second * 29)
   277  	require.Error(t, cfg.ValidateAndAdjust(sinkURL))
   278  
   279  	cfg.SyncPointInterval = util.AddressOf(time.Second * 30)
   280  	cfg.SyncPointRetention = util.AddressOf(time.Minute * 10)
   281  	require.Error(t, cfg.ValidateAndAdjust(sinkURL))
   282  
   283  	cfg.Sink.EncoderConcurrency = util.AddressOf(-1)
   284  	require.Error(t, cfg.ValidateAndAdjust(sinkURL))
   285  
   286  	cfg = GetDefaultReplicaConfig()
   287  	cfg.Scheduler = nil
   288  	require.Nil(t, cfg.ValidateAndAdjust(sinkURL))
   289  	require.False(t, cfg.Scheduler.EnableTableAcrossNodes)
   290  
   291  	// enable the checksum verification, but use blackhole sink
   292  	cfg = GetDefaultReplicaConfig()
   293  	cfg.Integrity.IntegrityCheckLevel = integrity.CheckLevelCorrectness
   294  	require.NoError(t, cfg.ValidateAndAdjust(sinkURL))
   295  	require.Equal(t, integrity.CheckLevelNone, cfg.Integrity.IntegrityCheckLevel)
   296  
   297  	// changefeed error stuck duration is less than 30 minutes
   298  	cfg = GetDefaultReplicaConfig()
   299  	duration := minChangeFeedErrorStuckDuration - time.Second*1
   300  	cfg.ChangefeedErrorStuckDuration = &duration
   301  	err = cfg.ValidateAndAdjust(sinkURL)
   302  	require.Error(t, err)
   303  	require.Contains(t, err.Error(), "The ChangefeedErrorStuckDuration")
   304  	duration = minChangeFeedErrorStuckDuration
   305  	cfg.ChangefeedErrorStuckDuration = &duration
   306  	require.NoError(t, cfg.ValidateAndAdjust(sinkURL))
   307  }
   308  
   309  func TestIsSinkCompatibleWithSpanReplication(t *testing.T) {
   310  	t.Parallel()
   311  
   312  	tests := []struct {
   313  		name       string
   314  		uri        string
   315  		compatible bool
   316  	}{
   317  		{
   318  			name:       "MySQL URI",
   319  			uri:        "mysql://root:111@foo.bar:3306/",
   320  			compatible: false,
   321  		},
   322  		{
   323  			name:       "TiDB URI",
   324  			uri:        "tidb://root:111@foo.bar:3306/",
   325  			compatible: false,
   326  		},
   327  		{
   328  			name:       "MySQL URI",
   329  			uri:        "mysql+ssl://root:111@foo.bar:3306/",
   330  			compatible: false,
   331  		},
   332  		{
   333  			name:       "TiDB URI",
   334  			uri:        "tidb+ssl://root:111@foo.bar:3306/",
   335  			compatible: false,
   336  		},
   337  		{
   338  			name:       "Kafka URI",
   339  			uri:        "kafka://foo.bar:3306/topic",
   340  			compatible: true,
   341  		},
   342  		{
   343  			name:       "Kafka URI",
   344  			uri:        "kafka+ssl://foo.bar:3306/topic",
   345  			compatible: true,
   346  		},
   347  		{
   348  			name:       "Blackhole URI",
   349  			uri:        "blackhole://foo.bar:3306/topic",
   350  			compatible: true,
   351  		},
   352  		{
   353  			name:       "Unknown URI",
   354  			uri:        "unknown://foo.bar:3306",
   355  			compatible: false,
   356  		},
   357  	}
   358  
   359  	for _, tt := range tests {
   360  		u, e := url.Parse(tt.uri)
   361  		require.Nil(t, e)
   362  		compatible := isSinkCompatibleWithSpanReplication(u)
   363  		require.Equal(t, compatible, tt.compatible, tt.name)
   364  	}
   365  }
   366  
   367  func TestValidateAndAdjustLargeMessageHandle(t *testing.T) {
   368  	cfg := GetDefaultReplicaConfig()
   369  	cfg.Sink.KafkaConfig = &KafkaConfig{
   370  		LargeMessageHandle: NewDefaultLargeMessageHandleConfig(),
   371  	}
   372  	cfg.Sink.KafkaConfig.LargeMessageHandle.LargeMessageHandleOption = ""
   373  	cfg.Sink.KafkaConfig.LargeMessageHandle.LargeMessageHandleCompression = ""
   374  
   375  	rawURL := "kafka://127.0.0.1:9092/canal-json-test?protocol=canal-json&enable-tidb-extension=true"
   376  	sinkURL, err := url.Parse(rawURL)
   377  	require.NoError(t, err)
   378  
   379  	err = cfg.ValidateAndAdjust(sinkURL)
   380  	require.NoError(t, err)
   381  
   382  	require.Equal(t, LargeMessageHandleOptionNone, cfg.Sink.KafkaConfig.LargeMessageHandle.LargeMessageHandleOption)
   383  	require.Equal(t, compression.None, cfg.Sink.KafkaConfig.LargeMessageHandle.LargeMessageHandleCompression)
   384  }
   385  
   386  func TestMaskSensitiveData(t *testing.T) {
   387  	config := ReplicaConfig{
   388  		Sink:       nil,
   389  		Consistent: nil,
   390  	}
   391  	config.MaskSensitiveData()
   392  	require.Nil(t, config.Sink)
   393  	require.Nil(t, config.Consistent)
   394  	config.Sink = &SinkConfig{}
   395  	config.Sink.KafkaConfig = &KafkaConfig{
   396  		SASLOAuthTokenURL:     aws.String("http://abc.com?password=bacd"),
   397  		SASLOAuthClientSecret: aws.String("bacd"),
   398  		SASLPassword:          aws.String("bacd"),
   399  		SASLGssAPIPassword:    aws.String("bacd"),
   400  		Key:                   aws.String("bacd"),
   401  		GlueSchemaRegistryConfig: &GlueSchemaRegistryConfig{
   402  			AccessKey:       "abc",
   403  			SecretAccessKey: "def",
   404  			Token:           "aaa",
   405  		},
   406  	}
   407  	config.Sink.SchemaRegistry = aws.String("http://abc.com?password=bacd")
   408  	config.Consistent = &ConsistentConfig{
   409  		Storage: "http://abc.com?password=bacd",
   410  	}
   411  	config.MaskSensitiveData()
   412  	require.Equal(t, "http://abc.com?password=xxxxx", *config.Sink.SchemaRegistry)
   413  	require.Equal(t, "http://abc.com?password=xxxxx", config.Consistent.Storage)
   414  	require.Equal(t, "http://abc.com?password=xxxxx", *config.Sink.KafkaConfig.SASLOAuthTokenURL)
   415  	require.Equal(t, "******", *config.Sink.KafkaConfig.SASLOAuthClientSecret)
   416  	require.Equal(t, "******", *config.Sink.KafkaConfig.Key)
   417  	require.Equal(t, "******", *config.Sink.KafkaConfig.SASLPassword)
   418  	require.Equal(t, "******", *config.Sink.KafkaConfig.SASLGssAPIPassword)
   419  	require.Equal(t, "******", config.Sink.KafkaConfig.GlueSchemaRegistryConfig.SecretAccessKey)
   420  	require.Equal(t, "******", config.Sink.KafkaConfig.GlueSchemaRegistryConfig.Token)
   421  	require.Equal(t, "******", config.Sink.KafkaConfig.GlueSchemaRegistryConfig.AccessKey)
   422  }