github.com/hxx258456/ccgo@v0.0.5-0.20230213014102-48b35f46f66f/go-grpc-middleware/retry/retry_test.go (about) 1 // Copyright 2016 Michal Witkowski. All Rights Reserved. 2 // See LICENSE for licensing terms. 3 4 package grpc_retry_test 5 6 import ( 7 "io" 8 "sync" 9 "testing" 10 "time" 11 12 pb_testproto "github.com/hxx258456/ccgo/go-grpc-middleware/testing/testproto" 13 14 grpc_middleware "github.com/hxx258456/ccgo/go-grpc-middleware" 15 grpc_retry "github.com/hxx258456/ccgo/go-grpc-middleware/retry" 16 grpc_testing "github.com/hxx258456/ccgo/go-grpc-middleware/testing" 17 18 "github.com/hxx258456/ccgo/grpc" 19 "github.com/hxx258456/ccgo/grpc/codes" 20 "github.com/hxx258456/ccgo/net/context" 21 "github.com/stretchr/testify/assert" 22 "github.com/stretchr/testify/require" 23 "github.com/stretchr/testify/suite" 24 ) 25 26 var ( 27 retriableErrors = []codes.Code{codes.Unavailable, codes.DataLoss} 28 goodPing = &pb_testproto.PingRequest{Value: "something"} 29 noSleep = 0 * time.Second 30 retryTimeout = 50 * time.Millisecond 31 ) 32 33 type failingService struct { 34 pb_testproto.TestServiceServer 35 mu sync.Mutex 36 37 reqCounter uint 38 reqModulo uint 39 reqSleep time.Duration 40 reqError codes.Code 41 } 42 43 func (s *failingService) resetFailingConfiguration(modulo uint, errorCode codes.Code, sleepTime time.Duration) { 44 s.mu.Lock() 45 defer s.mu.Unlock() 46 47 s.reqCounter = 0 48 s.reqModulo = modulo 49 s.reqError = errorCode 50 s.reqSleep = sleepTime 51 } 52 53 func (s *failingService) requestCount() uint { 54 s.mu.Lock() 55 defer s.mu.Unlock() 56 return s.reqCounter 57 } 58 59 func (s *failingService) maybeFailRequest() error { 60 s.mu.Lock() 61 defer s.mu.Unlock() 62 s.reqCounter += 1 63 if (s.reqModulo > 0) && (s.reqCounter%s.reqModulo == 0) { 64 return nil 65 } 66 time.Sleep(s.reqSleep) 67 return grpc.Errorf(s.reqError, "maybeFailRequest: failing it") 68 } 69 70 func (s *failingService) Ping(ctx context.Context, ping *pb_testproto.PingRequest) (*pb_testproto.PingResponse, error) { 71 if err := s.maybeFailRequest(); err != nil { 72 return nil, err 73 } 74 return s.TestServiceServer.Ping(ctx, ping) 75 } 76 77 func (s *failingService) PingList(ping *pb_testproto.PingRequest, stream pb_testproto.TestService_PingListServer) error { 78 if err := s.maybeFailRequest(); err != nil { 79 return err 80 } 81 return s.TestServiceServer.PingList(ping, stream) 82 } 83 84 func (s *failingService) PingStream(stream pb_testproto.TestService_PingStreamServer) error { 85 if err := s.maybeFailRequest(); err != nil { 86 return err 87 } 88 return s.TestServiceServer.PingStream(stream) 89 } 90 91 func TestRetrySuite(t *testing.T) { 92 service := &failingService{ 93 TestServiceServer: &grpc_testing.TestPingService{T: t}, 94 } 95 unaryInterceptor := grpc_retry.UnaryClientInterceptor( 96 grpc_retry.WithCodes(retriableErrors...), 97 grpc_retry.WithMax(3), 98 grpc_retry.WithBackoff(grpc_retry.BackoffLinear(retryTimeout)), 99 ) 100 streamInterceptor := grpc_retry.StreamClientInterceptor( 101 grpc_retry.WithCodes(retriableErrors...), 102 grpc_retry.WithMax(3), 103 grpc_retry.WithBackoff(grpc_retry.BackoffLinear(retryTimeout)), 104 ) 105 s := &RetrySuite{ 106 srv: service, 107 InterceptorTestSuite: &grpc_testing.InterceptorTestSuite{ 108 TestService: service, 109 ClientOpts: []grpc.DialOption{ 110 grpc.WithStreamInterceptor(streamInterceptor), 111 grpc.WithUnaryInterceptor(unaryInterceptor), 112 }, 113 }, 114 } 115 suite.Run(t, s) 116 } 117 118 type RetrySuite struct { 119 *grpc_testing.InterceptorTestSuite 120 srv *failingService 121 } 122 123 func (s *RetrySuite) SetupTest() { 124 s.srv.resetFailingConfiguration( /* don't fail */ 0, codes.OK, noSleep) 125 } 126 127 func (s *RetrySuite) TestUnary_FailsOnNonRetriableError() { 128 s.srv.resetFailingConfiguration(5, codes.Internal, noSleep) 129 _, err := s.Client.Ping(s.SimpleCtx(), goodPing) 130 require.Error(s.T(), err, "error must occur from the failing service") 131 require.Equal(s.T(), codes.Internal, grpc.Code(err), "failure code must come from retrier") 132 } 133 134 func (s *RetrySuite) TestCallOptionsDontPanicWithoutInterceptor() { 135 // Fix for https://github.com/grpc-ecosystem/go-grpc-middleware/issues/37 136 // If this code doesn't panic, that's good. 137 s.srv.resetFailingConfiguration(100, codes.DataLoss, noSleep) // doesn't matter all requests should fail 138 nonMiddlewareClient := s.NewClient() 139 _, err := nonMiddlewareClient.Ping(s.SimpleCtx(), goodPing, 140 grpc_retry.WithMax(5), 141 grpc_retry.WithBackoff(grpc_retry.BackoffLinear(1*time.Millisecond)), 142 grpc_retry.WithCodes(codes.DataLoss), 143 grpc_retry.WithPerRetryTimeout(1*time.Millisecond), 144 ) 145 require.Error(s.T(), err) 146 } 147 148 func (s *RetrySuite) TestServerStream_FailsOnNonRetriableError() { 149 s.srv.resetFailingConfiguration(5, codes.Internal, noSleep) 150 stream, err := s.Client.PingList(s.SimpleCtx(), goodPing) 151 require.NoError(s.T(), err, "should not fail on establishing the stream") 152 _, err = stream.Recv() 153 require.Error(s.T(), err, "error must occur from the failing service") 154 require.Equal(s.T(), codes.Internal, grpc.Code(err), "failure code must come from retrier") 155 } 156 157 func (s *RetrySuite) TestUnary_SucceedsOnRetriableError() { 158 s.srv.resetFailingConfiguration(3, codes.DataLoss, noSleep) // see retriable_errors 159 out, err := s.Client.Ping(s.SimpleCtx(), goodPing) 160 require.NoError(s.T(), err, "the third invocation should succeed") 161 require.NotNil(s.T(), out, "Pong must be not nill") 162 require.EqualValues(s.T(), 3, s.srv.requestCount(), "three requests should have been made") 163 } 164 165 func (s *RetrySuite) TestUnary_OverrideFromDialOpts() { 166 s.srv.resetFailingConfiguration(5, codes.ResourceExhausted, noSleep) // default is 3 and retriable_errors 167 out, err := s.Client.Ping(s.SimpleCtx(), goodPing, grpc_retry.WithCodes(codes.ResourceExhausted), grpc_retry.WithMax(5)) 168 require.NoError(s.T(), err, "the fifth invocation should succeed") 169 require.NotNil(s.T(), out, "Pong must be not nill") 170 require.EqualValues(s.T(), 5, s.srv.requestCount(), "five requests should have been made") 171 } 172 173 func (s *RetrySuite) TestUnary_PerCallDeadline_Succeeds() { 174 // This tests 5 requests, with first 4 sleeping for 10 millisecond, and the retry logic firing 175 // a retry call with a 5 millisecond deadline. The 5th one doesn't sleep and succeeds. 176 deadlinePerCall := 5 * time.Millisecond 177 s.srv.resetFailingConfiguration(5, codes.NotFound, 2*deadlinePerCall) 178 out, err := s.Client.Ping(s.SimpleCtx(), goodPing, grpc_retry.WithPerRetryTimeout(deadlinePerCall), 179 grpc_retry.WithMax(5)) 180 require.NoError(s.T(), err, "the fifth invocation should succeed") 181 require.NotNil(s.T(), out, "Pong must be not nill") 182 require.EqualValues(s.T(), 5, s.srv.requestCount(), "five requests should have been made") 183 } 184 185 func (s *RetrySuite) TestUnary_PerCallDeadline_FailsOnParent() { 186 // This tests that the parent context (passed to the invocation) takes precedence over retries. 187 // The parent context has 150 milliseconds of deadline. 188 // Each failed call sleeps for 100milliseconds, and there is 5 milliseconds between each one. 189 // This means that unlike in TestUnary_PerCallDeadline_Succeeds, the fifth successful call won't 190 // be made. 191 parentDeadline := 150 * time.Millisecond 192 deadlinePerCall := 50 * time.Millisecond 193 // All 0-4 requests should have 10 millisecond sleeps and deadline, while the last one works. 194 s.srv.resetFailingConfiguration(5, codes.NotFound, 2*deadlinePerCall) 195 ctx, _ := context.WithTimeout(context.TODO(), parentDeadline) 196 _, err := s.Client.Ping(ctx, goodPing, grpc_retry.WithPerRetryTimeout(deadlinePerCall), 197 grpc_retry.WithMax(5)) 198 require.Error(s.T(), err, "the retries must fail due to context deadline exceeded") 199 require.Equal(s.T(), codes.DeadlineExceeded, grpc.Code(err), "failre code must be a gRPC error of Deadline class") 200 } 201 202 func (s *RetrySuite) TestServerStream_SucceedsOnRetriableError() { 203 s.srv.resetFailingConfiguration(3, codes.DataLoss, noSleep) // see retriable_errors 204 stream, err := s.Client.PingList(s.SimpleCtx(), goodPing) 205 require.NoError(s.T(), err, "establishing the connection must always succeed") 206 s.assertPingListWasCorrect(stream) 207 require.EqualValues(s.T(), 3, s.srv.requestCount(), "three requests should have been made") 208 } 209 210 func (s *RetrySuite) TestServerStream_OverrideFromContext() { 211 s.srv.resetFailingConfiguration(5, codes.ResourceExhausted, noSleep) // default is 3 and retriable_errors 212 stream, err := s.Client.PingList(s.SimpleCtx(), goodPing, grpc_retry.WithCodes(codes.ResourceExhausted), grpc_retry.WithMax(5)) 213 require.NoError(s.T(), err, "establishing the connection must always succeed") 214 s.assertPingListWasCorrect(stream) 215 require.EqualValues(s.T(), 5, s.srv.requestCount(), "three requests should have been made") 216 } 217 218 func (s *RetrySuite) TestServerStream_PerCallDeadline_Succeeds() { 219 // This tests 5 requests, with first 4 sleeping for 100 millisecond, and the retry logic firing 220 // a retry call with a 50 millisecond deadline. The 5th one doesn't sleep and succeeds. 221 deadlinePerCall := 50 * time.Millisecond 222 s.srv.resetFailingConfiguration(5, codes.NotFound, 2*deadlinePerCall) 223 stream, err := s.Client.PingList(s.SimpleCtx(), goodPing, grpc_retry.WithPerRetryTimeout(deadlinePerCall), 224 grpc_retry.WithMax(5)) 225 require.NoError(s.T(), err, "establishing the connection must always succeed") 226 s.assertPingListWasCorrect(stream) 227 require.EqualValues(s.T(), 5, s.srv.requestCount(), "three requests should have been made") 228 } 229 230 func (s *RetrySuite) TestServerStream_PerCallDeadline_FailsOnParent() { 231 // This tests that the parent context (passed to the invocation) takes precedence over retries. 232 // The parent context has 150 milliseconds of deadline. 233 // Each failed call sleeps for 50milliseconds, and there is 25 milliseconds between each one. 234 // This means that unlike in TestServerStream_PerCallDeadline_Succeeds, the fifth successful call won't 235 // be made. 236 parentDeadline := 150 * time.Millisecond 237 deadlinePerCall := 50 * time.Millisecond 238 // All 0-4 requests should have 10 millisecond sleeps and deadline, while the last one works. 239 s.srv.resetFailingConfiguration(5, codes.NotFound, 2*deadlinePerCall) 240 parentCtx, _ := context.WithTimeout(context.TODO(), parentDeadline) 241 stream, err := s.Client.PingList(parentCtx, goodPing, grpc_retry.WithPerRetryTimeout(deadlinePerCall), 242 grpc_retry.WithMax(5)) 243 require.NoError(s.T(), err, "establishing the connection must always succeed") 244 _, err = stream.Recv() 245 require.Equal(s.T(), codes.DeadlineExceeded, grpc.Code(err), "failre code must be a gRPC error of Deadline class") 246 } 247 248 func (s *RetrySuite) TestServerStream_CallFailsOnOutOfRetries() { 249 restarted := s.RestartServer(3 * retryTimeout) 250 _, err := s.Client.PingList(s.SimpleCtx(), goodPing) 251 252 require.Error(s.T(), err, "establishing the connection should not succeed") 253 assert.Equal(s.T(), codes.Unavailable, grpc.Code(err)) 254 255 <-restarted 256 } 257 258 func (s *RetrySuite) TestServerStream_CallFailsOnDeadlineExceeded() { 259 restarted := s.RestartServer(3 * retryTimeout) 260 ctx, _ := context.WithTimeout(context.TODO(), retryTimeout) 261 _, err := s.Client.PingList(ctx, goodPing) 262 263 require.Error(s.T(), err, "establishing the connection should not succeed") 264 assert.Equal(s.T(), codes.DeadlineExceeded, grpc.Code(err)) 265 266 <-restarted 267 } 268 269 func (s *RetrySuite) TestServerStream_CallRetrySucceeds() { 270 restarted := s.RestartServer(retryTimeout) 271 272 _, err := s.Client.PingList(s.SimpleCtx(), goodPing, 273 grpc_retry.WithMax(40), 274 ) 275 276 assert.NoError(s.T(), err, "establishing the connection should succeed") 277 <-restarted 278 } 279 280 func (s *RetrySuite) assertPingListWasCorrect(stream pb_testproto.TestService_PingListClient) { 281 count := 0 282 for { 283 pong, err := stream.Recv() 284 if err == io.EOF { 285 break 286 } 287 require.NotNil(s.T(), pong, "received values must not be nill") 288 require.NoError(s.T(), err, "no errors during receive on client side") 289 require.Equal(s.T(), goodPing.Value, pong.Value, "the returned pong contained the outgoing ping") 290 count += 1 291 } 292 require.EqualValues(s.T(), grpc_testing.ListResponseCount, count, "should have received all ping items") 293 } 294 295 type trackedInterceptor struct { 296 called int 297 } 298 299 func (ti *trackedInterceptor) UnaryClientInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { 300 ti.called++ 301 return invoker(ctx, method, req, reply, cc, opts...) 302 } 303 304 func (ti *trackedInterceptor) StreamClientInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { 305 ti.called++ 306 return streamer(ctx, desc, cc, method, opts...) 307 } 308 309 func TestChainedRetrySuite(t *testing.T) { 310 service := &failingService{ 311 TestServiceServer: &grpc_testing.TestPingService{T: t}, 312 } 313 preRetryInterceptor := &trackedInterceptor{} 314 postRetryInterceptor := &trackedInterceptor{} 315 s := &ChainedRetrySuite{ 316 srv: service, 317 preRetryInterceptor: preRetryInterceptor, 318 postRetryInterceptor: postRetryInterceptor, 319 InterceptorTestSuite: &grpc_testing.InterceptorTestSuite{ 320 TestService: service, 321 ClientOpts: []grpc.DialOption{ 322 grpc.WithUnaryInterceptor(grpc_middleware.ChainUnaryClient(preRetryInterceptor.UnaryClientInterceptor, grpc_retry.UnaryClientInterceptor(), postRetryInterceptor.UnaryClientInterceptor)), 323 grpc.WithStreamInterceptor(grpc_middleware.ChainStreamClient(preRetryInterceptor.StreamClientInterceptor, grpc_retry.StreamClientInterceptor(), postRetryInterceptor.StreamClientInterceptor)), 324 }, 325 }, 326 } 327 suite.Run(t, s) 328 } 329 330 type ChainedRetrySuite struct { 331 *grpc_testing.InterceptorTestSuite 332 srv *failingService 333 preRetryInterceptor *trackedInterceptor 334 postRetryInterceptor *trackedInterceptor 335 } 336 337 func (s *ChainedRetrySuite) SetupTest() { 338 s.srv.resetFailingConfiguration( /* don't fail */ 0, codes.OK, noSleep) 339 s.preRetryInterceptor.called = 0 340 s.postRetryInterceptor.called = 0 341 } 342 343 func (s *ChainedRetrySuite) TestUnaryWithChainedInterceptors_NoFailure() { 344 _, err := s.Client.Ping(s.SimpleCtx(), goodPing, grpc_retry.WithMax(2)) 345 require.NoError(s.T(), err, "the invocation should succeed") 346 require.EqualValues(s.T(), 1, s.srv.requestCount(), "one request should have been made") 347 require.EqualValues(s.T(), 1, s.preRetryInterceptor.called, "pre-retry interceptor should be called once") 348 require.EqualValues(s.T(), 1, s.postRetryInterceptor.called, "post-retry interceptor should be called once") 349 } 350 351 func (s *ChainedRetrySuite) TestUnaryWithChainedInterceptors_WithRetry() { 352 s.srv.resetFailingConfiguration(2, codes.Unavailable, noSleep) 353 _, err := s.Client.Ping(s.SimpleCtx(), goodPing, grpc_retry.WithMax(2)) 354 require.NoError(s.T(), err, "the second invocation should succeed") 355 require.EqualValues(s.T(), 2, s.srv.requestCount(), "two requests should have been made") 356 require.EqualValues(s.T(), 1, s.preRetryInterceptor.called, "pre-retry interceptor should be called once") 357 require.EqualValues(s.T(), 2, s.postRetryInterceptor.called, "post-retry interceptor should be called twice") 358 } 359 360 func (s *ChainedRetrySuite) TestStreamWithChainedInterceptors_NoFailure() { 361 stream, err := s.Client.PingList(s.SimpleCtx(), goodPing, grpc_retry.WithMax(2)) 362 require.NoError(s.T(), err, "the invocation should succeed") 363 _, err = stream.Recv() 364 require.NoError(s.T(), err, "the Recv should succeed") 365 require.EqualValues(s.T(), 1, s.srv.requestCount(), "one request should have been made") 366 require.EqualValues(s.T(), 1, s.preRetryInterceptor.called, "pre-retry interceptor should be called once") 367 require.EqualValues(s.T(), 1, s.postRetryInterceptor.called, "post-retry interceptor should be called once") 368 } 369 370 func (s *ChainedRetrySuite) TestStreamWithChainedInterceptors_WithRetry() { 371 s.srv.resetFailingConfiguration(2, codes.Unavailable, noSleep) 372 stream, err := s.Client.PingList(s.SimpleCtx(), goodPing, grpc_retry.WithMax(2)) 373 require.NoError(s.T(), err, "the second invocation should succeed") 374 _, err = stream.Recv() 375 require.NoError(s.T(), err, "the Recv should succeed") 376 require.EqualValues(s.T(), 2, s.srv.requestCount(), "two requests should have been made") 377 require.EqualValues(s.T(), 1, s.preRetryInterceptor.called, "pre-retry interceptor should be called once") 378 require.EqualValues(s.T(), 2, s.postRetryInterceptor.called, "post-retry interceptor should be called twice") 379 }