github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/p2p/grpc_client_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 p2p
    15  
    16  import (
    17  	"context"
    18  	"math"
    19  	"sync"
    20  	"sync/atomic"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/pingcap/errors"
    25  	"github.com/pingcap/tiflow/pkg/security"
    26  	"github.com/pingcap/tiflow/proto/p2p"
    27  	"github.com/stretchr/testify/mock"
    28  	"github.com/stretchr/testify/require"
    29  	"google.golang.org/grpc"
    30  )
    31  
    32  type mockClientBatchSender struct {
    33  	mock.Mock
    34  
    35  	sendCnt int32 // atomic
    36  }
    37  
    38  func (s *mockClientBatchSender) Append(msg MessageEntry) error {
    39  	args := s.Called(msg)
    40  	atomic.AddInt32(&s.sendCnt, 1)
    41  	return args.Error(0)
    42  }
    43  
    44  func (s *mockClientBatchSender) Flush() error {
    45  	args := s.Called()
    46  	return args.Error(0)
    47  }
    48  
    49  type mockClientConnector struct {
    50  	mock.Mock
    51  }
    52  
    53  func (c *mockClientConnector) Connect(opts clientConnectOptions) (p2p.CDCPeerToPeerClient, cancelFn, error) {
    54  	args := c.Called(opts)
    55  	return args.Get(0).(p2p.CDCPeerToPeerClient), args.Get(1).(cancelFn), args.Error(2)
    56  }
    57  
    58  type testMessage struct {
    59  	Value int `json:"value"`
    60  }
    61  
    62  var clientConfigForUnitTesting = &MessageClientConfig{
    63  	SendChannelSize:         1,
    64  	BatchSendInterval:       128 * time.Hour, // essentially disables flushing
    65  	MaxBatchBytes:           math.MaxInt64,
    66  	MaxBatchCount:           math.MaxInt64,
    67  	RetryRateLimitPerSecond: 999.0,
    68  	ClientVersion:           "v5.4.0", // a fake version
    69  	AdvertisedAddr:          "fake-addr:8300",
    70  	MaxRecvMsgSize:          4 * 1024 * 1024, // 4MB
    71  }
    72  
    73  func TestMessageClientBasics(t *testing.T) {
    74  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
    75  	defer cancel()
    76  
    77  	client := NewGrpcMessageClient("node-1", clientConfigForUnitTesting)
    78  	sender := &mockClientBatchSender{}
    79  	client.newSenderFn = func(stream MessageClientStream) clientBatchSender[MessageEntry] {
    80  		return sender
    81  	}
    82  	connector := &mockClientConnector{}
    83  	grpcClient := &mockCDCPeerToPeerClient{}
    84  	client.connector = connector
    85  	var closed int32
    86  	connector.On("Connect", mock.Anything).Return(
    87  		grpcClient,
    88  		func() {
    89  			require.Equal(t, int32(0), atomic.LoadInt32(&closed))
    90  			atomic.StoreInt32(&closed, 1)
    91  		},
    92  		nil,
    93  	).Run(func(_ mock.Arguments) {
    94  		atomic.StoreInt32(&closed, 0)
    95  	})
    96  
    97  	// Test point 1: Connecting to the server and sends the meta
    98  	grpcStream := newMockSendMessageClient(ctx)
    99  	grpcClient.On("SendMessage", mock.Anything, []grpc.CallOption(nil)).Return(
   100  		grpcStream,
   101  		nil,
   102  	)
   103  	grpcStream.On("Send", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
   104  		packet := args.Get(0).(*p2p.MessagePacket)
   105  		require.EqualValues(t, &p2p.StreamMeta{
   106  			SenderId:             "node-1",
   107  			ReceiverId:           "node-2",
   108  			Epoch:                1, // 1 is the initial epoch
   109  			ClientVersion:        "v5.4.0",
   110  			SenderAdvertisedAddr: "fake-addr:8300",
   111  		}, packet.Meta)
   112  	})
   113  	grpcStream.On("Recv").Return(nil, nil)
   114  
   115  	var wg sync.WaitGroup
   116  	wg.Add(1)
   117  	go func() {
   118  		defer wg.Done()
   119  		err := client.Run(ctx, "", "", "node-2", &security.Credential{})
   120  		require.Error(t, err)
   121  		require.Regexp(t, "context canceled", err.Error())
   122  	}()
   123  
   124  	// wait for the stream meta to be received
   125  	require.Eventuallyf(t, func() bool {
   126  		return atomic.LoadInt32(&grpcStream.msgCount) > 0
   127  	}, time.Second*1, time.Millisecond*10, "meta should have been received")
   128  
   129  	connector.AssertExpectations(t)
   130  	grpcClient.AssertExpectations(t)
   131  
   132  	// Test point 2: Send a message
   133  	sender.On("Append", &p2p.MessageEntry{
   134  		Topic:    "topic-1",
   135  		Content:  []byte(`{"value":1}`),
   136  		Sequence: 1,
   137  	}).Return(nil)
   138  	seq, err := client.TrySendMessage(ctx, "topic-1", &testMessage{Value: 1})
   139  	require.NoError(t, err)
   140  	require.Equal(t, int64(1), seq)
   141  	require.Eventuallyf(t, func() bool {
   142  		return atomic.LoadInt32(&sender.sendCnt) == 1
   143  	}, time.Second*1, time.Millisecond*10, "message should have been received")
   144  	sender.AssertExpectations(t)
   145  
   146  	// Test point 3: CurrentAck works for a known topic
   147  	ack, ok := client.CurrentAck("topic-1")
   148  	require.True(t, ok)
   149  	require.Equal(t, int64(0), ack) // we have not ack'ed the message, so the current ack is 0.
   150  
   151  	// Test point 4: CurrentAck works for an unknown topic
   152  	_, ok = client.CurrentAck("topic-2" /* unknown topic */)
   153  	require.False(t, ok)
   154  
   155  	// Test point 5: CurrentAck should advance as expected
   156  	grpcStream.replyCh <- &p2p.SendMessageResponse{
   157  		ExitReason: p2p.ExitReason_OK,
   158  		Ack: []*p2p.Ack{{
   159  			Topic:   "topic-1",
   160  			LastSeq: 1,
   161  		}},
   162  	}
   163  	require.Eventually(t, func() bool {
   164  		ack, ok := client.CurrentAck("topic-1")
   165  		require.True(t, ok)
   166  		return ack == 1
   167  	}, time.Second*1, time.Millisecond*10)
   168  
   169  	// Test point 6: Send another message (blocking)
   170  	sender.On("Append", &p2p.MessageEntry{
   171  		Topic:    "topic-1",
   172  		Content:  []byte(`{"value":2}`),
   173  		Sequence: 2,
   174  	}).Return(nil)
   175  	seq, err = client.SendMessage(ctx, "topic-1", &testMessage{Value: 2})
   176  	require.NoError(t, err)
   177  	require.Equal(t, int64(2), seq)
   178  	require.Eventuallyf(t, func() bool {
   179  		return atomic.LoadInt32(&sender.sendCnt) == 2
   180  	}, time.Second*1, time.Millisecond*10, "message should have been received")
   181  	sender.AssertExpectations(t)
   182  
   183  	// Test point 7: Interrupt the connection
   184  	grpcStream.ResetMock()
   185  
   186  	sender.ExpectedCalls = nil
   187  	sender.Calls = nil
   188  
   189  	// Test point 8: We expect the message to be resent
   190  	sender.On("Append", &p2p.MessageEntry{
   191  		Topic:    "topic-1",
   192  		Content:  []byte(`{"value":2}`),
   193  		Sequence: 2,
   194  	}).Return(nil)
   195  	// We should flush the sender after resending the messages.
   196  	sender.On("Flush").Return(nil)
   197  
   198  	grpcStream.On("Recv").Return(nil, errors.New("fake error")).Once()
   199  	grpcStream.On("Recv").Return(nil, nil)
   200  	grpcStream.On("Send", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
   201  		packet := args.Get(0).(*p2p.MessagePacket)
   202  		require.EqualValues(t, &p2p.StreamMeta{
   203  			SenderId:             "node-1",
   204  			ReceiverId:           "node-2",
   205  			Epoch:                2, // the epoch should be increased
   206  			ClientVersion:        "v5.4.0",
   207  			SenderAdvertisedAddr: "fake-addr:8300",
   208  		}, packet.Meta)
   209  	}).Once()
   210  
   211  	// Resets the sentCnt
   212  	atomic.StoreInt32(&sender.sendCnt, 0)
   213  	grpcStream.replyCh <- &p2p.SendMessageResponse{
   214  		ExitReason: p2p.ExitReason_OK,
   215  		Ack: []*p2p.Ack{{
   216  			Topic:   "topic-1",
   217  			LastSeq: 1,
   218  		}},
   219  	}
   220  	// We expect the message to be resent
   221  	require.Eventually(t, func() bool {
   222  		return atomic.LoadInt32(&sender.sendCnt) == 1
   223  	}, time.Second*1, time.Millisecond*10, "message should have been resent")
   224  	sender.AssertExpectations(t)
   225  
   226  	cancel()
   227  	wg.Wait()
   228  }
   229  
   230  func TestClientPermanentFailure(t *testing.T) {
   231  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   232  	defer cancel()
   233  
   234  	client := NewGrpcMessageClient("node-1", clientConfigForUnitTesting)
   235  	sender := &mockClientBatchSender{}
   236  	client.newSenderFn = func(stream MessageClientStream) clientBatchSender[MessageEntry] {
   237  		return sender
   238  	}
   239  	connector := &mockClientConnector{}
   240  	grpcClient := &mockCDCPeerToPeerClient{}
   241  	client.connector = connector
   242  	connector.On("Connect", mock.Anything).Return(grpcClient, func() {}, nil)
   243  
   244  	grpcStream := newMockSendMessageClient(ctx)
   245  	grpcClient.On("SendMessage", mock.Anything, []grpc.CallOption(nil)).Return(
   246  		grpcStream,
   247  		nil,
   248  	)
   249  	grpcStream.On("Send", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
   250  		packet := args.Get(0).(*p2p.MessagePacket)
   251  		require.EqualValues(t, &p2p.StreamMeta{
   252  			SenderId:             "node-1",
   253  			ReceiverId:           "node-2",
   254  			Epoch:                1, // 1 is the initial epoch
   255  			ClientVersion:        "v5.4.0",
   256  			SenderAdvertisedAddr: "fake-addr:8300",
   257  		}, packet.Meta)
   258  	})
   259  
   260  	grpcStream.On("Recv").Return(&p2p.SendMessageResponse{
   261  		ExitReason:   p2p.ExitReason_CAPTURE_ID_MISMATCH,
   262  		ErrorMessage: "test message",
   263  	}, nil)
   264  
   265  	var wg sync.WaitGroup
   266  	wg.Add(1)
   267  	go func() {
   268  		defer wg.Done()
   269  		err := client.Run(ctx, "", "", "node-2", &security.Credential{})
   270  		require.Error(t, err)
   271  		require.Regexp(t, ".*ErrPeerMessageClientPermanentFail.*", err.Error())
   272  	}()
   273  
   274  	wg.Wait()
   275  
   276  	connector.AssertExpectations(t)
   277  	grpcStream.AssertExpectations(t)
   278  	sender.AssertExpectations(t)
   279  }
   280  
   281  func TestClientSendAnomalies(t *testing.T) {
   282  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   283  	defer cancel()
   284  
   285  	// copies the config
   286  	config := &*clientConfigForUnitTesting
   287  	// disables flushing to make this case deterministic.
   288  	config.BatchSendInterval = 999 * time.Second
   289  
   290  	client := NewGrpcMessageClient("node-1", config)
   291  	sender := &mockClientBatchSender{}
   292  
   293  	runCtx, closeClient := context.WithCancel(ctx)
   294  	defer closeClient()
   295  
   296  	client.newSenderFn = func(stream MessageClientStream) clientBatchSender[MessageEntry] {
   297  		<-runCtx.Done()
   298  		return sender
   299  	}
   300  	connector := &mockClientConnector{}
   301  	grpcClient := &mockCDCPeerToPeerClient{}
   302  	client.connector = connector
   303  	connector.On("Connect", mock.Anything).Return(grpcClient, func() {}, nil)
   304  
   305  	grpcStream := newMockSendMessageClient(runCtx)
   306  	grpcClient.On("SendMessage", mock.Anything, []grpc.CallOption(nil)).Return(
   307  		grpcStream,
   308  		nil,
   309  	)
   310  
   311  	grpcStream.On("Send", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
   312  		packet := args.Get(0).(*p2p.MessagePacket)
   313  		require.EqualValues(t, &p2p.StreamMeta{
   314  			SenderId:             "node-1",
   315  			ReceiverId:           "node-2",
   316  			Epoch:                1, // 1 is the initial epoch
   317  			ClientVersion:        "v5.4.0",
   318  			SenderAdvertisedAddr: "fake-addr:8300",
   319  		}, packet.Meta)
   320  	})
   321  
   322  	grpcStream.On("Recv").Return(nil, nil)
   323  	sender.On("Flush").Return(nil)
   324  	sender.On("Append", mock.Anything).Return(nil)
   325  
   326  	var wg sync.WaitGroup
   327  	wg.Add(1)
   328  	go func() {
   329  		defer wg.Done()
   330  		err := client.Run(runCtx, "", "", "node-2", &security.Credential{})
   331  		require.Error(t, err)
   332  		require.Regexp(t, ".*context canceled.*", err.Error())
   333  	}()
   334  
   335  	// Test point 1: ErrPeerMessageSendTryAgain
   336  	_, err := client.TrySendMessage(ctx, "test-topic", &testMessage{Value: 1})
   337  	require.NoError(t, err)
   338  
   339  	_, err = client.TrySendMessage(ctx, "test-topic", &testMessage{Value: 1})
   340  	require.Error(t, err)
   341  	require.Regexp(t, ".*ErrPeerMessageSendTryAgain.*", err.Error())
   342  
   343  	// Test point 2: close the client while SendMessage is blocking.
   344  	go func() {
   345  		time.Sleep(100 * time.Millisecond)
   346  		closeClient()
   347  	}()
   348  	_, _ = client.SendMessage(ctx, "test-topic", &testMessage{Value: 1})
   349  	// There is no need to check for error here, because when a client is closing,
   350  	// message loss is expected because sending the message is fully asynchronous.
   351  	// The client implementation is considered correct if `SendMessage` does not
   352  	// block infinitely.
   353  
   354  	wg.Wait()
   355  
   356  	// Test point 3: call SendMessage after the client is closed.
   357  	_, err = client.SendMessage(ctx, "test-topic", &testMessage{Value: 1})
   358  	require.Error(t, err)
   359  	require.Regexp(t, ".*ErrPeerMessageClientClosed.*", err.Error())
   360  }