github.com/ydb-platform/ydb-go-sdk/v3@v3.57.0/retry/retry.go (about) 1 package retry 2 3 import ( 4 "context" 5 "fmt" 6 7 "github.com/ydb-platform/ydb-go-sdk/v3/internal/backoff" 8 "github.com/ydb-platform/ydb-go-sdk/v3/internal/stack" 9 "github.com/ydb-platform/ydb-go-sdk/v3/internal/wait" 10 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext" 11 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors" 12 "github.com/ydb-platform/ydb-go-sdk/v3/trace" 13 ) 14 15 // retryOperation is the interface that holds an operation for retry. 16 // if retryOperation returns not nil - operation will retry 17 // if retryOperation returns nil - retry loop will break 18 type retryOperation func(context.Context) (err error) 19 20 type retryOptions struct { 21 label string 22 call call 23 trace *trace.Retry 24 idempotent bool 25 stackTrace bool 26 fastBackoff backoff.Backoff 27 slowBackoff backoff.Backoff 28 29 panicCallback func(e interface{}) 30 } 31 32 type Option interface { 33 ApplyRetryOption(opts *retryOptions) 34 } 35 36 var _ Option = labelOption("") 37 38 type labelOption string 39 40 func (label labelOption) ApplyDoOption(opts *doOptions) { 41 opts.retryOptions = append(opts.retryOptions, WithLabel(string(label))) 42 } 43 44 func (label labelOption) ApplyDoTxOption(opts *doTxOptions) { 45 opts.retryOptions = append(opts.retryOptions, WithLabel(string(label))) 46 } 47 48 func (label labelOption) ApplyRetryOption(opts *retryOptions) { 49 opts.label = string(label) 50 } 51 52 // WithLabel applies label for identification call Retry in trace.Retry.OnRetry 53 func WithLabel(label string) labelOption { 54 return labelOption(label) 55 } 56 57 var _ Option = (*callOption)(nil) 58 59 type callOption struct { 60 call 61 } 62 63 func (call callOption) ApplyDoOption(opts *doOptions) { 64 opts.retryOptions = append(opts.retryOptions, withCaller(call)) 65 } 66 67 func (call callOption) ApplyDoTxOption(opts *doTxOptions) { 68 opts.retryOptions = append(opts.retryOptions, withCaller(call)) 69 } 70 71 func (call callOption) ApplyRetryOption(opts *retryOptions) { 72 opts.call = call 73 } 74 75 type call interface { 76 FunctionID() string 77 } 78 79 func withCaller(call call) callOption { 80 return callOption{call} 81 } 82 83 var _ Option = stackTraceOption{} 84 85 type stackTraceOption struct{} 86 87 func (stackTraceOption) ApplyRetryOption(opts *retryOptions) { 88 opts.stackTrace = true 89 } 90 91 func (stackTraceOption) ApplyDoOption(opts *doOptions) { 92 opts.retryOptions = append(opts.retryOptions, WithStackTrace()) 93 } 94 95 func (stackTraceOption) ApplyDoTxOption(opts *doTxOptions) { 96 opts.retryOptions = append(opts.retryOptions, WithStackTrace()) 97 } 98 99 // WithStackTrace wraps errors with stacktrace from Retry call 100 func WithStackTrace() stackTraceOption { 101 return stackTraceOption{} 102 } 103 104 var _ Option = traceOption{} 105 106 type traceOption struct { 107 t *trace.Retry 108 } 109 110 func (t traceOption) ApplyRetryOption(opts *retryOptions) { 111 opts.trace = opts.trace.Compose(t.t) 112 } 113 114 func (t traceOption) ApplyDoOption(opts *doOptions) { 115 opts.retryOptions = append(opts.retryOptions, WithTrace(t.t)) 116 } 117 118 func (t traceOption) ApplyDoTxOption(opts *doTxOptions) { 119 opts.retryOptions = append(opts.retryOptions, WithTrace(t.t)) 120 } 121 122 // WithTrace returns trace option 123 func WithTrace(t *trace.Retry) traceOption { 124 return traceOption{t: t} 125 } 126 127 var _ Option = idempotentOption(false) 128 129 type idempotentOption bool 130 131 func (idempotent idempotentOption) ApplyRetryOption(opts *retryOptions) { 132 opts.idempotent = bool(idempotent) 133 } 134 135 func (idempotent idempotentOption) ApplyDoOption(opts *doOptions) { 136 opts.retryOptions = append(opts.retryOptions, WithIdempotent(bool(idempotent))) 137 } 138 139 func (idempotent idempotentOption) ApplyDoTxOption(opts *doTxOptions) { 140 opts.retryOptions = append(opts.retryOptions, WithIdempotent(bool(idempotent))) 141 } 142 143 // WithIdempotent applies idempotent flag to retry operation 144 func WithIdempotent(idempotent bool) idempotentOption { 145 return idempotentOption(idempotent) 146 } 147 148 var _ Option = fastBackoffOption{} 149 150 type fastBackoffOption struct { 151 backoff backoff.Backoff 152 } 153 154 func (o fastBackoffOption) ApplyRetryOption(opts *retryOptions) { 155 if o.backoff != nil { 156 opts.fastBackoff = o.backoff 157 } 158 } 159 160 func (o fastBackoffOption) ApplyDoOption(opts *doOptions) { 161 opts.retryOptions = append(opts.retryOptions, WithFastBackoff(o.backoff)) 162 } 163 164 func (o fastBackoffOption) ApplyDoTxOption(opts *doTxOptions) { 165 opts.retryOptions = append(opts.retryOptions, WithFastBackoff(o.backoff)) 166 } 167 168 // WithFastBackoff replaces default fast backoff 169 func WithFastBackoff(b backoff.Backoff) fastBackoffOption { 170 return fastBackoffOption{backoff: b} 171 } 172 173 var _ Option = slowBackoffOption{} 174 175 type slowBackoffOption struct { 176 backoff backoff.Backoff 177 } 178 179 func (o slowBackoffOption) ApplyRetryOption(opts *retryOptions) { 180 if o.backoff != nil { 181 opts.slowBackoff = o.backoff 182 } 183 } 184 185 func (o slowBackoffOption) ApplyDoOption(opts *doOptions) { 186 opts.retryOptions = append(opts.retryOptions, WithSlowBackoff(o.backoff)) 187 } 188 189 func (o slowBackoffOption) ApplyDoTxOption(opts *doTxOptions) { 190 opts.retryOptions = append(opts.retryOptions, WithSlowBackoff(o.backoff)) 191 } 192 193 // WithSlowBackoff replaces default slow backoff 194 func WithSlowBackoff(b backoff.Backoff) slowBackoffOption { 195 return slowBackoffOption{backoff: b} 196 } 197 198 var _ Option = panicCallbackOption{} 199 200 type panicCallbackOption struct { 201 callback func(e interface{}) 202 } 203 204 func (o panicCallbackOption) ApplyRetryOption(opts *retryOptions) { 205 opts.panicCallback = o.callback 206 } 207 208 func (o panicCallbackOption) ApplyDoOption(opts *doOptions) { 209 opts.retryOptions = append(opts.retryOptions, WithPanicCallback(o.callback)) 210 } 211 212 func (o panicCallbackOption) ApplyDoTxOption(opts *doTxOptions) { 213 opts.retryOptions = append(opts.retryOptions, WithPanicCallback(o.callback)) 214 } 215 216 // WithPanicCallback returns panic callback option 217 // If not defined - panic would not intercept with driver 218 func WithPanicCallback(panicCallback func(e interface{})) panicCallbackOption { 219 return panicCallbackOption{callback: panicCallback} 220 } 221 222 // Retry provide the best effort fo retrying operation 223 // 224 // Retry implements internal busy loop until one of the following conditions is met: 225 // 226 // - context was canceled or deadlined 227 // 228 // - retry operation returned nil as error 229 // 230 // Warning: if deadline without deadline or cancellation func Retry will be worked infinite 231 // 232 // If you need to retry your op func on some logic errors - you must return RetryableError() from retryOperation 233 func Retry(ctx context.Context, op retryOperation, opts ...Option) (finalErr error) { 234 options := &retryOptions{ 235 call: stack.FunctionID(""), 236 trace: &trace.Retry{}, 237 fastBackoff: backoff.Fast, 238 slowBackoff: backoff.Slow, 239 } 240 for _, opt := range opts { 241 if opt != nil { 242 opt.ApplyRetryOption(options) 243 } 244 } 245 if options.idempotent { 246 ctx = xcontext.WithIdempotent(ctx, options.idempotent) 247 } 248 defer func() { 249 if finalErr != nil && options.stackTrace { 250 finalErr = xerrors.WithStackTrace(finalErr, 251 xerrors.WithSkipDepth(2), // 1 - exit from defer, 1 - exit from Retry call 252 ) 253 } 254 }() 255 var ( 256 i int 257 attempts int 258 259 code = int64(0) 260 onIntermediate = trace.RetryOnRetry(options.trace, &ctx, 261 options.label, options.call, options.label, options.idempotent, xcontext.IsNestedCall(ctx), 262 ) 263 ) 264 defer func() { 265 onIntermediate(finalErr)(attempts, finalErr) 266 }() 267 for { 268 i++ 269 attempts++ 270 select { 271 case <-ctx.Done(): 272 return xerrors.WithStackTrace( 273 fmt.Errorf("retry failed on attempt No.%d: %w", 274 attempts, ctx.Err(), 275 ), 276 ) 277 278 default: 279 err := func() (err error) { 280 if options.panicCallback != nil { 281 defer func() { 282 if e := recover(); e != nil { 283 options.panicCallback(e) 284 err = xerrors.WithStackTrace( 285 fmt.Errorf("panic recovered: %v", e), 286 ) 287 } 288 }() 289 } 290 291 return op(ctx) 292 }() 293 294 if err == nil { 295 return nil 296 } 297 298 if ctxErr := ctx.Err(); ctxErr != nil { 299 return xerrors.WithStackTrace( 300 xerrors.Join( 301 fmt.Errorf("context error occurred on attempt No.%d", attempts), 302 ctxErr, err, 303 ), 304 ) 305 } 306 307 m := Check(err) 308 309 if m.StatusCode() != code { 310 i = 0 311 } 312 313 if !m.MustRetry(options.idempotent) { 314 return xerrors.WithStackTrace( 315 fmt.Errorf("non-retryable error occurred on attempt No.%d (idempotent=%v): %w", 316 attempts, options.idempotent, err, 317 ), 318 ) 319 } 320 321 if e := wait.Wait(ctx, options.fastBackoff, options.slowBackoff, m.BackoffType(), i); e != nil { 322 return xerrors.WithStackTrace( 323 xerrors.Join( 324 fmt.Errorf("wait exit on attempt No.%d", 325 attempts, 326 ), e, err, 327 ), 328 ) 329 } 330 331 code = m.StatusCode() 332 333 onIntermediate(err) 334 } 335 } 336 } 337 338 // Check returns retry mode for queryErr. 339 func Check(err error) (m retryMode) { 340 code, errType, backoffType, deleteSession := xerrors.Check(err) 341 342 return retryMode{ 343 code: code, 344 errType: errType, 345 backoff: backoffType, 346 deleteSession: deleteSession, 347 } 348 }