github.com/klaytn/klaytn@v1.10.2/datasync/chaindatafetcher/kafka/kafka_test.go (about)

     1  // Copyright 2020 The klaytn Authors
     2  // This file is part of the klaytn library.
     3  //
     4  // The klaytn library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The klaytn library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the klaytn library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package kafka
    18  
    19  import (
    20  	"context"
    21  	"encoding/binary"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"math/rand"
    26  	"reflect"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/stretchr/testify/assert"
    34  
    35  	"github.com/Shopify/sarama"
    36  	"github.com/klaytn/klaytn/common"
    37  	"github.com/stretchr/testify/suite"
    38  )
    39  
    40  type KafkaSuite struct {
    41  	suite.Suite
    42  	conf     *KafkaConfig
    43  	kfk      *Kafka
    44  	consumer sarama.Consumer
    45  	topic    string
    46  }
    47  
    48  // In order to test KafkaSuite, any available kafka broker must be connectable with "kafka:9094".
    49  // If no kafka broker is available, the KafkaSuite tests are skipped.
    50  func (s *KafkaSuite) SetupTest() {
    51  	s.conf = GetDefaultKafkaConfig()
    52  	s.conf.Brokers = []string{"kafka:9094"}
    53  	kfk, err := NewKafka(s.conf)
    54  	if err == sarama.ErrOutOfBrokers {
    55  		s.T().Log("Failed connecting to brokers", s.conf.Brokers)
    56  		s.T().Skip()
    57  	}
    58  	s.NoError(err)
    59  	s.kfk = kfk
    60  
    61  	consumer, err := sarama.NewConsumer(s.conf.Brokers, s.conf.SaramaConfig)
    62  	s.NoError(err)
    63  	s.consumer = consumer
    64  	s.topic = "test-topic"
    65  }
    66  
    67  func (s *KafkaSuite) TearDownTest() {
    68  	if s.kfk != nil {
    69  		s.kfk.Close()
    70  	}
    71  }
    72  
    73  func (s *KafkaSuite) TestKafka_split() {
    74  	segmentSizeBytes := 3
    75  	s.kfk.config.SegmentSizeBytes = segmentSizeBytes
    76  
    77  	// test with the size less than the segment size
    78  	bytes := common.MakeRandomBytes(segmentSizeBytes - 1)
    79  	parts, size := s.kfk.split(bytes)
    80  	s.Equal(bytes, parts[0])
    81  	s.Equal(1, size)
    82  
    83  	// test with the given segment size
    84  	bytes = common.MakeRandomBytes(segmentSizeBytes)
    85  	parts, size = s.kfk.split(bytes)
    86  	s.Equal(bytes, parts[0])
    87  	s.Equal(1, size)
    88  
    89  	// test with the size greater than the segment size
    90  	bytes = common.MakeRandomBytes(2*segmentSizeBytes + 2)
    91  	parts, size = s.kfk.split(bytes)
    92  	s.Equal(bytes[:segmentSizeBytes], parts[0])
    93  	s.Equal(bytes[segmentSizeBytes:2*segmentSizeBytes], parts[1])
    94  	s.Equal(bytes[2*segmentSizeBytes:], parts[2])
    95  	s.Equal(3, size)
    96  }
    97  
    98  func (s *KafkaSuite) TestKafka_makeProducerV1Message() {
    99  	// make test data
   100  	data := common.MakeRandomBytes(100)
   101  	rand.Seed(time.Now().UnixNano())
   102  	totalSegments := rand.Uint64()
   103  	idx := rand.Uint64() % totalSegments
   104  
   105  	// make a producer message with the random input
   106  	msg := s.kfk.makeProducerMessage(s.topic, "", data, idx, totalSegments)
   107  
   108  	// compare the data is correctly inserted
   109  	s.Equal(s.topic, msg.Topic)
   110  	s.Equal(sarama.ByteEncoder(data), msg.Value)
   111  	s.Equal(totalSegments, binary.BigEndian.Uint64(msg.Headers[MsgHeaderTotalSegments].Value))
   112  	s.Equal(idx, binary.BigEndian.Uint64(msg.Headers[MsgHeaderSegmentIdx].Value))
   113  	s.Equal(s.kfk.config.MsgVersion, string(msg.Headers[MsgHeaderVersion].Value))
   114  	s.Equal(s.kfk.config.ProducerId, string(msg.Headers[MsgHeaderProducerId].Value))
   115  }
   116  
   117  func (s *KafkaSuite) TestKafka_makeProducerMessage() {
   118  	// make test data
   119  	data := common.MakeRandomBytes(100)
   120  	rand.Seed(time.Now().UnixNano())
   121  	totalSegments := rand.Uint64()
   122  	idx := rand.Uint64() % totalSegments
   123  
   124  	// make a producer message with the random input
   125  	msg := s.kfk.makeProducerMessage(s.topic, "", data, idx, totalSegments)
   126  
   127  	// compare the data is correctly inserted
   128  	s.Equal(s.topic, msg.Topic)
   129  	s.Equal(sarama.ByteEncoder(data), msg.Value)
   130  	s.Equal(totalSegments, binary.BigEndian.Uint64(msg.Headers[MsgHeaderTotalSegments].Value))
   131  	s.Equal(idx, binary.BigEndian.Uint64(msg.Headers[MsgHeaderSegmentIdx].Value))
   132  }
   133  
   134  func (s *KafkaSuite) TestKafka_setupTopic() {
   135  	topicName := "test-setup-topic"
   136  
   137  	// create a new topic
   138  	err := s.kfk.setupTopic(topicName)
   139  	s.NoError(err)
   140  
   141  	// try to create duplicated topic
   142  	err = s.kfk.setupTopic(topicName)
   143  	s.NoError(err)
   144  }
   145  
   146  func (s *KafkaSuite) TestKafka_setupTopicConcurrency() {
   147  	topicName := "test-setup-concurrency-topic"
   148  	wg := sync.WaitGroup{}
   149  	for i := 0; i < 10; i++ {
   150  		wg.Add(1)
   151  		go func() {
   152  			defer wg.Done()
   153  			kaf, err := NewKafka(s.conf)
   154  			s.NoError(err)
   155  
   156  			err = kaf.setupTopic(topicName)
   157  			s.NoError(err)
   158  		}()
   159  	}
   160  	wg.Wait()
   161  }
   162  
   163  func (s *KafkaSuite) TestKafka_CreateAndDeleteTopic() {
   164  	// no topic to be deleted
   165  	err := s.kfk.DeleteTopic(s.topic)
   166  	s.Error(err)
   167  	s.True(strings.Contains(err.Error(), sarama.ErrUnknownTopicOrPartition.Error()))
   168  
   169  	// created a topic successfully
   170  	err = s.kfk.CreateTopic(s.topic)
   171  	s.NoError(err)
   172  
   173  	// failed to create a duplicated topic
   174  	err = s.kfk.CreateTopic(s.topic)
   175  	s.Error(err)
   176  	s.True(strings.Contains(err.Error(), sarama.ErrTopicAlreadyExists.Error()))
   177  
   178  	// deleted a topic successfully
   179  	s.Nil(s.kfk.DeleteTopic(s.topic))
   180  
   181  	topics, err := s.kfk.ListTopics()
   182  	if _, exist := topics[s.topic]; exist {
   183  		s.Fail("topic must not exist")
   184  	}
   185  }
   186  
   187  type kafkaData struct {
   188  	Number int
   189  	Data   []byte `json:"data"`
   190  }
   191  
   192  func (d *kafkaData) Key() string {
   193  	return fmt.Sprintf("%v", d.Number)
   194  }
   195  
   196  func publishRandomData(t *testing.T, producer *Kafka, topic string, numTests, testBytesSize int) []*kafkaData {
   197  	var expected []*kafkaData
   198  	for i := 0; i < numTests; i++ {
   199  		testData := &kafkaData{i, common.MakeRandomBytes(testBytesSize)}
   200  		assert.NoError(t, producer.Publish(topic, testData))
   201  		expected = append(expected, testData)
   202  	}
   203  	return expected
   204  }
   205  
   206  func (s *KafkaSuite) subscribeData(topic, groupId string, numTests int, handler func(message *sarama.ConsumerMessage) error) {
   207  	numCheckCh := make(chan struct{}, numTests)
   208  
   209  	// make a test consumer group
   210  	s.kfk.config.SaramaConfig.Consumer.Offsets.Initial = sarama.OffsetOldest
   211  	consumer, err := NewConsumer(s.kfk.config, groupId)
   212  	s.NoError(err)
   213  	defer consumer.Close()
   214  
   215  	// add handler for the test event group
   216  	consumer.topics = append(consumer.topics, topic)
   217  	consumer.handlers[topic] = func(message *sarama.ConsumerMessage) error {
   218  		err := handler(message)
   219  		numCheckCh <- struct{}{}
   220  		return err
   221  	}
   222  
   223  	// subscribe the added topics
   224  	go func() {
   225  		err := consumer.Subscribe(context.Background())
   226  		s.NoError(err)
   227  	}()
   228  
   229  	// wait for all data to be consumed
   230  	timeout := time.NewTimer(5 * time.Second)
   231  	for i := 0; i < numTests; i++ {
   232  		select {
   233  		case <-numCheckCh:
   234  			s.T().Logf("test count: %v, total tests: %v", i+1, numTests)
   235  		case <-timeout.C:
   236  			s.FailNow("timeout")
   237  		}
   238  	}
   239  }
   240  
   241  func (s *KafkaSuite) TestKafka_Publish() {
   242  	numTests := 10
   243  	testBytesSize := 100
   244  
   245  	s.kfk.CreateTopic(s.topic)
   246  
   247  	expected := publishRandomData(s.T(), s.kfk, s.topic, numTests, testBytesSize)
   248  
   249  	// consume from the first partition and the first item
   250  	partitionConsumer, err := s.consumer.ConsumePartition(s.topic, int32(0), int64(0))
   251  	s.NoError(err)
   252  
   253  	var actual []*kafkaData
   254  	i := 0
   255  	for msg := range partitionConsumer.Messages() {
   256  		var dec *kafkaData
   257  		json.Unmarshal(msg.Value, &dec)
   258  		actual = append(actual, dec)
   259  		i++
   260  		if i == numTests {
   261  			break
   262  		}
   263  	}
   264  
   265  	s.True(len(actual) == numTests)
   266  	for idx, v := range expected {
   267  		s.Equal(v, actual[idx])
   268  	}
   269  }
   270  
   271  func (s *KafkaSuite) TestKafka_Subscribe() {
   272  	numTests := 10
   273  	testBytesSize := 100
   274  
   275  	topic := "test-subscribe"
   276  	s.kfk.CreateTopic(topic)
   277  
   278  	expected := publishRandomData(s.T(), s.kfk, topic, numTests, testBytesSize)
   279  
   280  	var actual []*kafkaData
   281  	s.subscribeData(topic, "test-group-id", numTests, func(message *sarama.ConsumerMessage) error {
   282  		var d *kafkaData
   283  		json.Unmarshal(message.Value, &d)
   284  		actual = append(actual, d)
   285  		return nil
   286  	})
   287  
   288  	// compare the results with the published data
   289  	s.Equal(expected, actual)
   290  }
   291  
   292  func (s *KafkaSuite) TestKafka_PubSubWith2Partitions() {
   293  	numTests := 10
   294  	testBytesSize := 100
   295  
   296  	s.kfk.config.Partitions = 2
   297  	defer func() { s.kfk.config.Partitions = DefaultPartitions }()
   298  
   299  	topicPartition := "test-2-partition-topic"
   300  	s.kfk.CreateTopic(topicPartition)
   301  
   302  	// publish random data
   303  	expected := publishRandomData(s.T(), s.kfk, topicPartition, numTests, testBytesSize)
   304  
   305  	var actual []*kafkaData
   306  	s.subscribeData(topicPartition, "test-group-id", numTests, func(message *sarama.ConsumerMessage) error {
   307  		var d *kafkaData
   308  		json.Unmarshal(message.Value, &d)
   309  		actual = append(actual, d)
   310  		return nil
   311  	})
   312  
   313  	// the number of partitions is not 1, so the order may be changed after subscription.
   314  	// compare the results with the published data
   315  	s.Equal(len(expected), len(actual))
   316  
   317  	for _, e := range expected {
   318  		has := false
   319  		for _, a := range actual {
   320  			if reflect.DeepEqual(e, a) {
   321  				has = true
   322  			}
   323  		}
   324  		if !has {
   325  			s.Fail("the expected data is not contained in the actual data", "expected", e)
   326  		}
   327  	}
   328  }
   329  
   330  func (s *KafkaSuite) TestKafka_PubSubWith2DifferentGroups() {
   331  	numTests := 10
   332  	testBytesSize := 100
   333  
   334  	topic := "test-different-groups"
   335  	s.kfk.CreateTopic(topic)
   336  
   337  	// publish random data
   338  	expected := publishRandomData(s.T(), s.kfk, topic, numTests, testBytesSize)
   339  
   340  	var actual []*kafkaData
   341  	s.subscribeData(topic, "test-group-id-1", numTests, func(message *sarama.ConsumerMessage) error {
   342  		var d *kafkaData
   343  		json.Unmarshal(message.Value, &d)
   344  		actual = append(actual, d)
   345  		return nil
   346  	})
   347  
   348  	var actual2 []*kafkaData
   349  	s.subscribeData(topic, "test-group-id-2", numTests, func(message *sarama.ConsumerMessage) error {
   350  		var d *kafkaData
   351  		json.Unmarshal(message.Value, &d)
   352  		actual2 = append(actual2, d)
   353  		return nil
   354  	})
   355  
   356  	// the number of partitions is not 1, so the order may be changed after subscription.
   357  	// compare the results with the published data
   358  	s.Equal(expected, actual)
   359  	s.Equal(expected, actual2)
   360  }
   361  
   362  func (s *KafkaSuite) TestKafka_PubSubWithV1Segments() {
   363  	numProducers := 3
   364  	numTests := 10
   365  	testBytesSize := 31
   366  	segmentSize := 3
   367  
   368  	// make multi producers
   369  	var producers []*Kafka
   370  	for i := 0; i < numProducers; i++ {
   371  		config := GetDefaultKafkaConfig()
   372  		config.Brokers = s.kfk.config.Brokers
   373  		config.SegmentSizeBytes = segmentSize
   374  		config.SaramaConfig.Producer.RequiredAcks = -1
   375  		kfk, err := NewKafka(config)
   376  		s.NoError(err)
   377  		producers = append(producers, kfk)
   378  	}
   379  
   380  	topic := "test-multi-producer-segments"
   381  	s.kfk.CreateTopic(topic)
   382  
   383  	var expected []*kafkaData
   384  	{ // produce messages
   385  		wg := sync.WaitGroup{}
   386  		dataLock := sync.Mutex{}
   387  		for _, p := range producers {
   388  			wg.Add(1)
   389  			go func(producer *Kafka) {
   390  				data := publishRandomData(s.T(), producer, topic, numTests, testBytesSize)
   391  				dataLock.Lock()
   392  				expected = append(expected, data...)
   393  				dataLock.Unlock()
   394  				wg.Done()
   395  			}(p)
   396  		}
   397  		wg.Wait()
   398  	}
   399  
   400  	var actual []*kafkaData
   401  	s.subscribeData(topic, "test-multi-producers-consumer", numTests*numProducers, func(message *sarama.ConsumerMessage) error {
   402  		var d *kafkaData
   403  		json.Unmarshal(message.Value, &d)
   404  		actual = append(actual, d)
   405  		return nil
   406  	})
   407  
   408  	for _, expectedData := range expected {
   409  		exist := false
   410  		for _, actualData := range actual {
   411  			if reflect.DeepEqual(actualData, expectedData) {
   412  				exist = true
   413  				break
   414  			}
   415  		}
   416  		assert.True(s.T(), exist)
   417  	}
   418  }
   419  
   420  func (s *KafkaSuite) TestKafka_PubSubWithSegments() {
   421  	numTests := 5
   422  	testBytesSize := 10
   423  	segmentSize := 3
   424  
   425  	s.kfk.config.SegmentSizeBytes = segmentSize
   426  	topic := "test-message-segments"
   427  	s.kfk.CreateTopic(topic)
   428  
   429  	// publish random data
   430  	expected := publishRandomData(s.T(), s.kfk, topic, numTests, testBytesSize)
   431  
   432  	var actual []*kafkaData
   433  	s.subscribeData(topic, "test-group-id", numTests, func(message *sarama.ConsumerMessage) error {
   434  		var d *kafkaData
   435  		json.Unmarshal(message.Value, &d)
   436  		actual = append(actual, d)
   437  		return nil
   438  	})
   439  	s.Equal(expected, actual)
   440  }
   441  
   442  func (s *KafkaSuite) TestKafka_PubSubWithSegements_BufferOverflow() {
   443  	// create a topic
   444  	topic := "test-message-segments-buffer-overflow"
   445  	err := s.kfk.setupTopic(topic)
   446  	s.NoError(err)
   447  
   448  	// insert incomplete message segments
   449  	for i := 0; i < 3; i++ {
   450  		msg := s.kfk.makeProducerMessage(topic, "test-key-"+strconv.Itoa(i), common.MakeRandomBytes(10), 0, 2)
   451  		_, _, err = s.kfk.producer.SendMessage(msg)
   452  		s.NoError(err)
   453  	}
   454  
   455  	// setup consumer to handle errors
   456  	s.kfk.config.MaxMessageNumber = 1 // if buffer size is greater than 1, then it returns an error
   457  	s.kfk.config.SaramaConfig.Consumer.Return.Errors = true
   458  	s.kfk.config.SaramaConfig.Consumer.Offsets.Initial = sarama.OffsetOldest
   459  	consumer, err := NewConsumer(s.kfk.config, "test-group-id")
   460  	s.NoError(err)
   461  	consumer.topics = append(consumer.topics, topic)
   462  	consumer.handlers[topic] = func(message *sarama.ConsumerMessage) error { return nil }
   463  	errCh := consumer.Errors()
   464  
   465  	go func() {
   466  		err = consumer.Subscribe(context.Background())
   467  		s.NoError(err)
   468  	}()
   469  
   470  	// checkout the returned error is buffer overflow error
   471  	timeout := time.NewTimer(3 * time.Second)
   472  	select {
   473  	case <-timeout.C:
   474  		s.Fail("timeout")
   475  	case err := <-errCh:
   476  		s.True(strings.Contains(err.Error(), bufferOverflowErrorMsg))
   477  	}
   478  }
   479  
   480  func (s *KafkaSuite) TestKafka_PubSubWithSegments_ErrCallBack() {
   481  	// create a topic
   482  	topic := "test-message-segments-error-callback"
   483  	err := s.kfk.setupTopic(topic)
   484  	s.NoError(err)
   485  
   486  	_ = publishRandomData(s.T(), s.kfk, topic, 1, 1)
   487  
   488  	// setup consumer to handle errors with callback method
   489  	s.kfk.config.SaramaConfig.Consumer.Return.Errors = true
   490  	s.kfk.config.SaramaConfig.Consumer.Offsets.Initial = sarama.OffsetOldest
   491  	callbackErr := errors.New("callback error")
   492  	s.kfk.config.ErrCallback = func(string) error { return callbackErr }
   493  
   494  	// create a consumer structure
   495  	consumer, err := NewConsumer(s.kfk.config, "test-group-id")
   496  	s.NoError(err)
   497  	consumer.topics = append(consumer.topics, topic)
   498  	consumer.handlers[topic] = func(message *sarama.ConsumerMessage) error { return errors.New("test error") }
   499  
   500  	go func() {
   501  		err = consumer.Subscribe(context.Background())
   502  		s.NoError(err)
   503  	}()
   504  
   505  	// checkout the returned error is callback error
   506  	timeout := time.NewTimer(3 * time.Second)
   507  	select {
   508  	case <-timeout.C:
   509  		s.Fail("timeout")
   510  	case err := <-consumer.Errors():
   511  		s.Error(err)
   512  		s.True(strings.Contains(err.Error(), callbackErr.Error()))
   513  	}
   514  }
   515  
   516  func (s *KafkaSuite) TestKafka_PubSubWithSegments_MessageTimeout() {
   517  	// create a topic
   518  	topic := "test-message-segments-expiration"
   519  	err := s.kfk.setupTopic(topic)
   520  	s.NoError(err)
   521  
   522  	// produce incomplete message
   523  	msg := s.kfk.makeProducerMessage(topic, "test-key", common.MakeRandomBytes(10), 0, 2)
   524  	_, _, err = s.kfk.producer.SendMessage(msg)
   525  	s.NoError(err)
   526  
   527  	// setup consumer to handle errors with callback method
   528  	s.kfk.config.SaramaConfig.Consumer.Return.Errors = true
   529  	s.kfk.config.SaramaConfig.Consumer.Offsets.Initial = sarama.OffsetOldest
   530  	s.kfk.config.ExpirationTime = 300 * time.Millisecond
   531  
   532  	// create a consumer structure
   533  	consumer, err := NewConsumer(s.kfk.config, "test-group-id")
   534  	s.NoError(err)
   535  	consumer.topics = append(consumer.topics, topic)
   536  	consumer.handlers[topic] = func(message *sarama.ConsumerMessage) error {
   537  		// sleep for message expiration
   538  		time.Sleep(1 * time.Second)
   539  		return nil
   540  	}
   541  
   542  	go func() {
   543  		err = consumer.Subscribe(context.Background())
   544  		s.NoError(err)
   545  	}()
   546  
   547  	// checkout the returned error is callback error
   548  	timeout := time.NewTimer(3 * time.Second)
   549  	select {
   550  	case <-timeout.C:
   551  		s.Fail("timeout")
   552  	case err := <-consumer.Errors():
   553  		s.Error(err)
   554  		s.True(strings.Contains(err.Error(), msgExpiredErrorMsg))
   555  	}
   556  }
   557  
   558  func (s *KafkaSuite) TestKafka_Consumer_AddTopicAndHandler() {
   559  	consumer, err := NewConsumer(s.kfk.config, "test-group-id")
   560  	s.NoError(err)
   561  
   562  	blockGroupHandler := func(msg *sarama.ConsumerMessage) error { return nil }
   563  	s.NoError(consumer.AddTopicAndHandler(EventBlockGroup, blockGroupHandler))
   564  	traceGroupHandler := func(msg *sarama.ConsumerMessage) error { return nil }
   565  	s.NoError(consumer.AddTopicAndHandler(EventTraceGroup, traceGroupHandler))
   566  
   567  	blockGroupTopic := s.kfk.config.GetTopicName(EventBlockGroup)
   568  	traceGroupTopic := s.kfk.config.GetTopicName(EventTraceGroup)
   569  	expectedTopics := []string{blockGroupTopic, traceGroupTopic}
   570  	s.Equal(expectedTopics, consumer.topics)
   571  	s.Equal(reflect.ValueOf(blockGroupHandler).Pointer(), reflect.ValueOf(consumer.handlers[blockGroupTopic]).Pointer())
   572  	s.Equal(reflect.ValueOf(traceGroupHandler).Pointer(), reflect.ValueOf(consumer.handlers[traceGroupTopic]).Pointer())
   573  }
   574  
   575  func (s *KafkaSuite) TestKafka_Consumer_AddTopicAndHandler_Error() {
   576  	consumer, err := NewConsumer(s.kfk.config, "test-group-id")
   577  	s.NoError(err)
   578  
   579  	err = consumer.AddTopicAndHandler("not-available-event", nil)
   580  	s.Error(err)
   581  	s.True(strings.Contains(err.Error(), eventNameErrorMsg))
   582  }
   583  
   584  func TestKafkaSuite(t *testing.T) {
   585  	suite.Run(t, new(KafkaSuite))
   586  }