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 }