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  }