github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/pkg/retry/strategy.go (about)

     1  // Copyright 2019 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 retry
    15  
    16  import (
    17  	"time"
    18  
    19  	tcontext "github.com/pingcap/tiflow/dm/pkg/context"
    20  	"github.com/pingcap/tiflow/dm/pkg/log"
    21  	"go.uber.org/zap"
    22  )
    23  
    24  // backoffStrategy represents enum of retry wait interval.
    25  type backoffStrategy uint8
    26  
    27  const (
    28  	// Stable represents fixed time wait retry policy, every retry should wait a fixed time.
    29  	Stable backoffStrategy = iota + 1
    30  	// LinearIncrease represents increase time wait retry policy, every retry should wait more time depends on increasing retry times.
    31  	LinearIncrease
    32  )
    33  
    34  // Params define parameters for Apply
    35  // it's a parameters union set of all implements which implement Apply.
    36  type Params struct {
    37  	RetryCount         int
    38  	FirstRetryDuration time.Duration
    39  
    40  	BackoffStrategy backoffStrategy
    41  
    42  	// IsRetryableFn tells whether we should retry when operateFn failed
    43  	// params: (number of retry, error of operation)
    44  	// return: (bool)
    45  	//   1. true: means operateFn can be retried
    46  	//   2. false: means operateFn cannot retry after receive this error
    47  	IsRetryableFn func(int, error) bool
    48  }
    49  
    50  func NewParams(retryCount int, firstRetryDuration time.Duration, backoffStrategy backoffStrategy,
    51  	isRetryableFn func(int, error) bool,
    52  ) *Params {
    53  	return &Params{
    54  		RetryCount:         retryCount,
    55  		FirstRetryDuration: firstRetryDuration,
    56  		BackoffStrategy:    backoffStrategy,
    57  		IsRetryableFn:      isRetryableFn,
    58  	}
    59  }
    60  
    61  // OperateFunc the function we can retry
    62  //
    63  //	return: (result of operation, error of operation)
    64  type OperateFunc func(*tcontext.Context) (interface{}, error)
    65  
    66  // Strategy define different kind of retry strategy.
    67  type Strategy interface {
    68  	// Apply define retry strategy
    69  	// params: (retry parameters for this strategy, a normal operation)
    70  	// return: (result of operation, number of retry, error of operation)
    71  	Apply(ctx *tcontext.Context,
    72  		params Params,
    73  		operateFn OperateFunc,
    74  	) (interface{}, int, error)
    75  }
    76  
    77  // FiniteRetryStrategy will retry `RetryCount` times when failed to operate DB.
    78  type FiniteRetryStrategy struct{}
    79  
    80  // Apply for FiniteRetryStrategy, it waits `FirstRetryDuration` before it starts first retry, and then rest of retries wait time depends on BackoffStrategy.
    81  func (*FiniteRetryStrategy) Apply(ctx *tcontext.Context, params Params, operateFn OperateFunc,
    82  ) (ret interface{}, i int, err error) {
    83  	for ; i < params.RetryCount; i++ {
    84  		ret, err = operateFn(ctx)
    85  		if err != nil {
    86  			if params.IsRetryableFn(i, err) {
    87  				duration := params.FirstRetryDuration
    88  
    89  				switch params.BackoffStrategy {
    90  				case LinearIncrease:
    91  					duration = time.Duration(i+1) * params.FirstRetryDuration
    92  				default:
    93  				}
    94  				log.L().Warn("retry strategy takes effect", zap.Error(err), zap.Int("retry_times", i), zap.Int("retry_count", params.RetryCount))
    95  
    96  				select {
    97  				case <-ctx.Context().Done():
    98  					return ret, i, err // return `ret` rather than `nil`
    99  				case <-time.After(duration):
   100  				}
   101  				continue
   102  			}
   103  		}
   104  		break
   105  	}
   106  	return ret, i, err
   107  }
   108  
   109  // Retryer retries operateFn until success or reaches retry limit
   110  // todo: merge with Strategy when refactor
   111  type Retryer interface {
   112  	Apply(ctx *tcontext.Context, operateFn OperateFunc) (interface{}, int, error)
   113  }
   114  
   115  // FiniteRetryer wraps params.
   116  type FiniteRetryer struct {
   117  	FiniteRetryStrategy
   118  	Params *Params
   119  }
   120  
   121  func (s *FiniteRetryer) Apply(ctx *tcontext.Context, operateFn OperateFunc) (ret interface{}, i int, err error) {
   122  	return s.FiniteRetryStrategy.Apply(ctx, *s.Params, operateFn)
   123  }