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 }