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  }