github.com/cloudwego/kitex@v0.9.0/client/rpctimeout.go (about)

     1  /*
     2   * Copyright 2021 CloudWeGo Authors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package client
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"time"
    24  
    25  	"github.com/cloudwego/kitex/internal/wpool"
    26  	"github.com/cloudwego/kitex/pkg/endpoint"
    27  	"github.com/cloudwego/kitex/pkg/kerrors"
    28  	"github.com/cloudwego/kitex/pkg/klog"
    29  	"github.com/cloudwego/kitex/pkg/rpcinfo"
    30  	"github.com/cloudwego/kitex/pkg/rpctimeout"
    31  )
    32  
    33  // workerPool is used to reduce the timeout goroutine overhead.
    34  var workerPool *wpool.Pool
    35  
    36  func init() {
    37  	// if timeout middleware is not enabled, it will not cause any extra overhead
    38  	workerPool = wpool.New(
    39  		128,
    40  		time.Minute,
    41  	)
    42  }
    43  
    44  func makeTimeoutErr(ctx context.Context, start time.Time, timeout time.Duration) error {
    45  	ri := rpcinfo.GetRPCInfo(ctx)
    46  	to := ri.To()
    47  
    48  	errMsg := fmt.Sprintf("timeout=%v, to=%s, method=%s, location=%s",
    49  		timeout, to.ServiceName(), to.Method(), "kitex.rpcTimeoutMW")
    50  	target := to.Address()
    51  	if target != nil {
    52  		errMsg = fmt.Sprintf("%s, remote=%s", errMsg, target.String())
    53  	}
    54  
    55  	needFineGrainedErrCode := rpctimeout.LoadGlobalNeedFineGrainedErrCode()
    56  	// cancel error
    57  	if ctx.Err() == context.Canceled {
    58  		if needFineGrainedErrCode {
    59  			return kerrors.ErrCanceledByBusiness.WithCause(errors.New(errMsg))
    60  		} else {
    61  			return kerrors.ErrRPCTimeout.WithCause(fmt.Errorf("%s: %w by business", errMsg, ctx.Err()))
    62  		}
    63  	}
    64  
    65  	if ddl, ok := ctx.Deadline(); !ok {
    66  		errMsg = fmt.Sprintf("%s, %s", errMsg, "unknown error: context deadline not set?")
    67  	} else {
    68  		// Go's timer implementation is not so accurate,
    69  		// so if we need to check ctx deadline earlier than our timeout, we should consider the accuracy
    70  		if timeout <= 0 {
    71  			errMsg = fmt.Sprintf("%s, timeout by business, actual=%s", errMsg, ddl.Sub(start))
    72  		} else if roundTimeout := timeout - time.Millisecond; roundTimeout >= 0 && ddl.Before(start.Add(roundTimeout)) {
    73  			errMsg = fmt.Sprintf("%s, context deadline earlier than timeout, actual=%v", errMsg, ddl.Sub(start))
    74  		}
    75  
    76  		if needFineGrainedErrCode && isBusinessTimeout(start, timeout, ddl, rpctimeout.LoadBusinessTimeoutThreshold()) {
    77  			return kerrors.ErrTimeoutByBusiness.WithCause(errors.New(errMsg))
    78  		}
    79  	}
    80  	return kerrors.ErrRPCTimeout.WithCause(errors.New(errMsg))
    81  }
    82  
    83  func isBusinessTimeout(start time.Time, kitexTimeout time.Duration, actualDDL time.Time, threshold time.Duration) bool {
    84  	if kitexTimeout <= 0 {
    85  		return true
    86  	}
    87  	kitexDDL := start.Add(kitexTimeout)
    88  	return actualDDL.Add(threshold).Before(kitexDDL)
    89  }
    90  
    91  func rpcTimeoutMW(mwCtx context.Context) endpoint.Middleware {
    92  	var moreTimeout time.Duration
    93  	if v, ok := mwCtx.Value(rpctimeout.TimeoutAdjustKey).(*time.Duration); ok && v != nil {
    94  		moreTimeout = *v
    95  	}
    96  
    97  	return func(next endpoint.Endpoint) endpoint.Endpoint {
    98  		return func(ctx context.Context, request, response interface{}) error {
    99  			ri := rpcinfo.GetRPCInfo(ctx)
   100  			if ri.Config().InteractionMode() == rpcinfo.Streaming {
   101  				return next(ctx, request, response)
   102  			}
   103  
   104  			tm := ri.Config().RPCTimeout()
   105  			if tm > 0 {
   106  				tm += moreTimeout
   107  				var cancel context.CancelFunc
   108  				ctx, cancel = context.WithTimeout(ctx, tm)
   109  				defer cancel()
   110  			}
   111  			// Fast path for ctx without timeout
   112  			if ctx.Done() == nil {
   113  				return next(ctx, request, response)
   114  			}
   115  
   116  			var err error
   117  			start := time.Now()
   118  			done := make(chan error, 1)
   119  			workerPool.GoCtx(ctx, func() {
   120  				defer func() {
   121  					if panicInfo := recover(); panicInfo != nil {
   122  						e := rpcinfo.ClientPanicToErr(ctx, panicInfo, ri, true)
   123  						done <- e
   124  					}
   125  					if err == nil || !errors.Is(err, kerrors.ErrRPCFinish) {
   126  						// Don't regards ErrRPCFinish as normal error, it happens in retry scene,
   127  						// ErrRPCFinish means previous call returns first but is decoding.
   128  						close(done)
   129  					}
   130  				}()
   131  				err = next(ctx, request, response)
   132  				if err != nil && ctx.Err() != nil &&
   133  					!kerrors.IsTimeoutError(err) && !errors.Is(err, kerrors.ErrRPCFinish) {
   134  					// error occurs after the wait goroutine returns(RPCTimeout happens),
   135  					// we should log this error for troubleshooting, or it will be discarded.
   136  					// but ErrRPCTimeout and ErrRPCFinish can be ignored:
   137  					//    ErrRPCTimeout: it is same with outer timeout, here only care about non-timeout err.
   138  					//    ErrRPCFinish: it happens in retry scene, previous call returns first.
   139  					var errMsg string
   140  					if ri.To().Address() != nil {
   141  						errMsg = fmt.Sprintf("KITEX: to_service=%s method=%s addr=%s error=%s",
   142  							ri.To().ServiceName(), ri.To().Method(), ri.To().Address(), err.Error())
   143  					} else {
   144  						errMsg = fmt.Sprintf("KITEX: to_service=%s method=%s error=%s",
   145  							ri.To().ServiceName(), ri.To().Method(), err.Error())
   146  					}
   147  					klog.CtxErrorf(ctx, "%s", errMsg)
   148  				}
   149  			})
   150  
   151  			select {
   152  			case panicErr := <-done:
   153  				if panicErr != nil {
   154  					return panicErr
   155  				}
   156  				return err
   157  			case <-ctx.Done():
   158  				return makeTimeoutErr(ctx, start, tm)
   159  			}
   160  		}
   161  	}
   162  }