github.com/pingcap/ticdc@v0.0.0-20220526033649-485a10ef2652/cdc/sink/codec/schema_registry_test.go (about) 1 // Copyright 2020 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 codec 15 16 import ( 17 "bytes" 18 "context" 19 "encoding/json" 20 "io/ioutil" 21 "net/http" 22 "sync" 23 "time" 24 25 "github.com/jarcoal/httpmock" 26 "github.com/linkedin/goavro/v2" 27 "github.com/pingcap/check" 28 "github.com/pingcap/ticdc/cdc/model" 29 "github.com/pingcap/ticdc/pkg/security" 30 "github.com/pingcap/ticdc/pkg/util/testleak" 31 ) 32 33 type AvroSchemaRegistrySuite struct { 34 } 35 36 var _ = check.Suite(&AvroSchemaRegistrySuite{}) 37 38 type mockRegistry struct { 39 mu sync.Mutex 40 subjects map[string]*mockRegistrySchema 41 newID int 42 } 43 44 type mockRegistrySchema struct { 45 content string 46 version int 47 ID int 48 } 49 50 func startHTTPInterceptForTestingRegistry(c *check.C) { 51 httpmock.Activate() 52 53 registry := mockRegistry{ 54 subjects: make(map[string]*mockRegistrySchema), 55 newID: 1, 56 } 57 58 httpmock.RegisterResponder("GET", "http://127.0.0.1:8081", httpmock.NewStringResponder(200, "{}")) 59 60 httpmock.RegisterResponder("POST", `=~^http://127.0.0.1:8081/subjects/(.+)/versions`, 61 func(req *http.Request) (*http.Response, error) { 62 subject, err := httpmock.GetSubmatch(req, 1) 63 if err != nil { 64 return nil, err 65 } 66 reqBody, err := ioutil.ReadAll(req.Body) 67 if err != nil { 68 return nil, err 69 } 70 var reqData registerRequest 71 err = json.Unmarshal(reqBody, &reqData) 72 if err != nil { 73 return nil, err 74 } 75 76 // c.Assert(reqData.SchemaType, check.Equals, "AVRO") 77 78 var respData registerResponse 79 registry.mu.Lock() 80 item, exists := registry.subjects[subject] 81 if !exists { 82 item = &mockRegistrySchema{ 83 content: reqData.Schema, 84 version: 0, 85 ID: registry.newID, 86 } 87 registry.subjects[subject] = item 88 respData.ID = registry.newID 89 } else { 90 if item.content == reqData.Schema { 91 respData.ID = item.ID 92 } else { 93 item.content = reqData.Schema 94 item.version++ 95 item.ID = registry.newID 96 respData.ID = registry.newID 97 } 98 } 99 registry.newID++ 100 registry.mu.Unlock() 101 return httpmock.NewJsonResponse(200, &respData) 102 }) 103 104 httpmock.RegisterResponder("GET", `=~^http://127.0.0.1:8081/subjects/(.+)/versions/latest`, 105 func(req *http.Request) (*http.Response, error) { 106 subject, err := httpmock.GetSubmatch(req, 1) 107 if err != nil { 108 return httpmock.NewStringResponse(500, "Internal Server Error"), err 109 } 110 111 registry.mu.Lock() 112 item, exists := registry.subjects[subject] 113 registry.mu.Unlock() 114 if !exists { 115 return httpmock.NewStringResponse(404, ""), nil 116 } 117 118 var respData lookupResponse 119 respData.Schema = item.content 120 respData.Name = subject 121 respData.RegistryID = item.ID 122 123 return httpmock.NewJsonResponse(200, &respData) 124 }) 125 126 httpmock.RegisterResponder("DELETE", `=~^http://127.0.0.1:8081/subjects/(.+)`, 127 func(req *http.Request) (*http.Response, error) { 128 subject, err := httpmock.GetSubmatch(req, 1) 129 if err != nil { 130 return nil, err 131 } 132 133 registry.mu.Lock() 134 defer registry.mu.Unlock() 135 _, exists := registry.subjects[subject] 136 if !exists { 137 return httpmock.NewStringResponse(404, ""), nil 138 } 139 140 delete(registry.subjects, subject) 141 return httpmock.NewStringResponse(200, ""), nil 142 }) 143 144 failCounter := 0 145 httpmock.RegisterResponder("POST", `=~^http://127.0.0.1:8081/may-fail`, 146 func(req *http.Request) (*http.Response, error) { 147 data, _ := ioutil.ReadAll(req.Body) 148 c.Assert(len(data), check.Greater, 0) 149 c.Assert(int64(len(data)), check.Equals, req.ContentLength) 150 if failCounter < 3 { 151 failCounter++ 152 return httpmock.NewStringResponse(422, ""), nil 153 } 154 return httpmock.NewStringResponse(200, ""), nil 155 }) 156 } 157 158 func stopHTTPInterceptForTestingRegistry() { 159 httpmock.DeactivateAndReset() 160 } 161 162 func (s *AvroSchemaRegistrySuite) SetUpSuite(c *check.C) { 163 startHTTPInterceptForTestingRegistry(c) 164 } 165 166 func (s *AvroSchemaRegistrySuite) TearDownSuite(c *check.C) { 167 stopHTTPInterceptForTestingRegistry() 168 } 169 170 func getTestingContext() context.Context { 171 // nolint:govet 172 ctx, _ := context.WithTimeout(context.Background(), time.Second*3) 173 return ctx 174 } 175 176 func (s *AvroSchemaRegistrySuite) TestSchemaRegistry(c *check.C) { 177 defer testleak.AfterTest(c)() 178 table := model.TableName{ 179 Schema: "testdb", 180 Table: "test1", 181 } 182 183 manager, err := NewAvroSchemaManager(getTestingContext(), &security.Credential{}, "http://127.0.0.1:8081", "-value") 184 c.Assert(err, check.IsNil) 185 186 err = manager.ClearRegistry(getTestingContext(), table) 187 c.Assert(err, check.IsNil) 188 189 _, _, err = manager.Lookup(getTestingContext(), table, 1) 190 c.Assert(err, check.ErrorMatches, `.*not\sfound.*`) 191 192 codec, err := goavro.NewCodec(`{ 193 "type": "record", 194 "name": "test", 195 "fields": 196 [ 197 { 198 "type": "string", 199 "name": "field1" 200 } 201 ] 202 }`) 203 c.Assert(err, check.IsNil) 204 205 _, err = manager.Register(getTestingContext(), table, codec) 206 c.Assert(err, check.IsNil) 207 208 var id int 209 for i := 0; i < 2; i++ { 210 _, id, err = manager.Lookup(getTestingContext(), table, 1) 211 c.Assert(err, check.IsNil) 212 c.Assert(id, check.Greater, 0) 213 } 214 215 codec, err = goavro.NewCodec(`{ 216 "type": "record", 217 "name": "test", 218 "fields": 219 [ 220 { 221 "type": "string", 222 "name": "field1" 223 }, 224 { 225 "type": [ 226 "null", 227 "string" 228 ], 229 "default": null, 230 "name": "field2" 231 } 232 ] 233 }`) 234 c.Assert(err, check.IsNil) 235 _, err = manager.Register(getTestingContext(), table, codec) 236 c.Assert(err, check.IsNil) 237 238 codec2, id2, err := manager.Lookup(getTestingContext(), table, 999) 239 c.Assert(err, check.IsNil) 240 c.Assert(id2, check.Not(check.Equals), id) 241 c.Assert(codec.CanonicalSchema(), check.Equals, codec2.CanonicalSchema()) 242 } 243 244 func (s *AvroSchemaRegistrySuite) TestSchemaRegistryBad(c *check.C) { 245 defer testleak.AfterTest(c)() 246 _, err := NewAvroSchemaManager(getTestingContext(), &security.Credential{}, "http://127.0.0.1:808", "-value") 247 c.Assert(err, check.NotNil) 248 249 _, err = NewAvroSchemaManager(getTestingContext(), &security.Credential{}, "https://127.0.0.1:8080", "-value") 250 c.Assert(err, check.NotNil) 251 } 252 253 func (s *AvroSchemaRegistrySuite) TestSchemaRegistryIdempotent(c *check.C) { 254 defer testleak.AfterTest(c)() 255 table := model.TableName{ 256 Schema: "testdb", 257 Table: "test1", 258 } 259 260 manager, err := NewAvroSchemaManager(getTestingContext(), &security.Credential{}, "http://127.0.0.1:8081", "-value") 261 c.Assert(err, check.IsNil) 262 for i := 0; i < 20; i++ { 263 err = manager.ClearRegistry(getTestingContext(), table) 264 c.Assert(err, check.IsNil) 265 } 266 codec, err := goavro.NewCodec(`{ 267 "type": "record", 268 "name": "test", 269 "fields": 270 [ 271 { 272 "type": "string", 273 "name": "field1" 274 }, 275 { 276 "type": [ 277 "null", 278 "string" 279 ], 280 "default": null, 281 "name": "field2" 282 } 283 ] 284 }`) 285 c.Assert(err, check.IsNil) 286 287 id := 0 288 for i := 0; i < 20; i++ { 289 id1, err := manager.Register(getTestingContext(), table, codec) 290 c.Assert(err, check.IsNil) 291 c.Assert(id == 0 || id == id1, check.IsTrue) 292 id = id1 293 } 294 } 295 296 func (s *AvroSchemaRegistrySuite) TestGetCachedOrRegister(c *check.C) { 297 defer testleak.AfterTest(c)() 298 table := model.TableName{ 299 Schema: "testdb", 300 Table: "test1", 301 } 302 303 manager, err := NewAvroSchemaManager(getTestingContext(), &security.Credential{}, "http://127.0.0.1:8081", "-value") 304 c.Assert(err, check.IsNil) 305 306 called := 0 307 //nolint:unparam 308 schemaGen := func() (string, error) { 309 called++ 310 return `{ 311 "type": "record", 312 "name": "test", 313 "fields": 314 [ 315 { 316 "type": "string", 317 "name": "field1" 318 }, 319 { 320 "type": [ 321 "null", 322 "string" 323 ], 324 "default": null, 325 "name": "field2" 326 } 327 ] 328 }`, nil 329 } 330 331 codec, id, err := manager.GetCachedOrRegister(getTestingContext(), table, 1, schemaGen) 332 c.Assert(err, check.IsNil) 333 c.Assert(id, check.Greater, 0) 334 c.Assert(codec, check.NotNil) 335 c.Assert(called, check.Equals, 1) 336 337 codec1, _, err := manager.GetCachedOrRegister(getTestingContext(), table, 1, schemaGen) 338 c.Assert(err, check.IsNil) 339 c.Assert(codec1, check.Equals, codec) 340 c.Assert(called, check.Equals, 1) 341 342 codec2, _, err := manager.GetCachedOrRegister(getTestingContext(), table, 2, schemaGen) 343 c.Assert(err, check.IsNil) 344 c.Assert(codec2, check.Not(check.Equals), codec) 345 c.Assert(called, check.Equals, 2) 346 347 schemaGen = func() (string, error) { 348 return `{ 349 "type": "record", 350 "name": "test", 351 "fields": 352 [ 353 { 354 "type": "string", 355 "name": "field1" 356 }, 357 { 358 "type": [ 359 "null", 360 "string" 361 ], 362 "default": null, 363 "name": "field2" 364 } 365 ] 366 }`, nil 367 } 368 369 var wg sync.WaitGroup 370 for i := 0; i < 20; i++ { 371 finalI := i 372 wg.Add(1) 373 go func() { 374 defer wg.Done() 375 for j := 0; j < 100; j++ { 376 codec, id, err := manager.GetCachedOrRegister(getTestingContext(), table, uint64(finalI), schemaGen) 377 c.Assert(err, check.IsNil) 378 c.Assert(id, check.Greater, 0) 379 c.Assert(codec, check.NotNil) 380 } 381 }() 382 } 383 wg.Wait() 384 } 385 386 func (s *AvroSchemaRegistrySuite) TestHTTPRetry(c *check.C) { 387 defer testleak.AfterTest(c)() 388 payload := []byte("test") 389 req, err := http.NewRequest("POST", "http://127.0.0.1:8081/may-fail", bytes.NewReader(payload)) 390 c.Assert(err, check.IsNil) 391 392 ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) 393 defer cancel() 394 395 resp, err := httpRetry(ctx, nil, req, false) 396 c.Assert(err, check.IsNil) 397 _ = resp.Body.Close() 398 }