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 }