// Copyright 2016 Michal Witkowski. All Rights Reserved. // See LICENSE for licensing terms. package grpc_retry import ( "context" "time" "google.golang.org/grpc" "google.golang.org/grpc/codes" ) var ( // DefaultRetriableCodes is a set of well known types gRPC codes that should be retri-able. // // `ResourceExhausted` means that the user quota, e.g. per-RPC limits, have been reached. // `Unavailable` means that system is currently unavailable and the client should retry again. DefaultRetriableCodes = []codes.Code{codes.ResourceExhausted, codes.Unavailable} defaultOptions = &options{ max: 0, // disabled perCallTimeout: 0, // disabled includeHeader: true, codes: DefaultRetriableCodes, backoffFunc: BackoffFuncContext(func(ctx context.Context, attempt uint) time.Duration { return BackoffLinearWithJitter(50*time.Millisecond /*jitter*/, 0.10)(attempt) }), } ) // BackoffFunc denotes a family of functions that control the backoff duration between call retries. // // They are called with an identifier of the attempt, and should return a time the system client should // hold off for. If the time returned is longer than the `context.Context.Deadline` of the request // the deadline of the request takes precedence and the wait will be interrupted before proceeding // with the next iteration. type BackoffFunc func(attempt uint) time.Duration // BackoffFuncContext denotes a family of functions that control the backoff duration between call retries. // // They are called with an identifier of the attempt, and should return a time the system client should // hold off for. If the time returned is longer than the `context.Context.Deadline` of the request // the deadline of the request takes precedence and the wait will be interrupted before proceeding // with the next iteration. The context can be used to extract request scoped metadata and context values. type BackoffFuncContext func(ctx context.Context, attempt uint) time.Duration // Disable disables the retry behaviour on this call, or this interceptor. // // Its semantically the same to `WithMax` func Disable() CallOption { return WithMax(0) } // WithMax sets the maximum number of retries on this call, or this interceptor. func WithMax(maxRetries uint) CallOption { return CallOption{applyFunc: func(o *options) { o.max = maxRetries }} } // WithBackoff sets the `BackoffFunc` used to control time between retries. func WithBackoff(bf BackoffFunc) CallOption { return CallOption{applyFunc: func(o *options) { o.backoffFunc = BackoffFuncContext(func(ctx context.Context, attempt uint) time.Duration { return bf(attempt) }) }} } // WithBackoffContext sets the `BackoffFuncContext` used to control time between retries. func WithBackoffContext(bf BackoffFuncContext) CallOption { return CallOption{applyFunc: func(o *options) { o.backoffFunc = bf }} } // WithCodes sets which codes should be retried. // // Please *use with care*, as you may be retrying non-idempotent calls. // // You cannot automatically retry on Cancelled and Deadline, please use `WithPerRetryTimeout` for these. func WithCodes(retryCodes ...codes.Code) CallOption { return CallOption{applyFunc: func(o *options) { o.codes = retryCodes }} } // WithPerRetryTimeout sets the RPC timeout per call (including initial call) on this call, or this interceptor. // // The context.Deadline of the call takes precedence and sets the maximum time the whole invocation // will take, but WithPerRetryTimeout can be used to limit the RPC time per each call. // // For example, with context.Deadline = now + 10s, and WithPerRetryTimeout(3 * time.Seconds), each // of the retry calls (including the initial one) will have a deadline of now + 3s. // // A value of 0 disables the timeout overrides completely and returns to each retry call using the // parent `context.Deadline`. // // Note that when this is enabled, any DeadlineExceeded errors that are propagated up will be retried. func WithPerRetryTimeout(timeout time.Duration) CallOption { return CallOption{applyFunc: func(o *options) { o.perCallTimeout = timeout }} } type options struct { max uint perCallTimeout time.Duration includeHeader bool codes []codes.Code backoffFunc BackoffFuncContext } // CallOption is a grpc.CallOption that is local to grpc_retry. type CallOption struct { grpc.EmptyCallOption // make sure we implement private after() and before() fields so we don't panic. applyFunc func(opt *options) } func reuseOrNewWithCallOptions(opt *options, callOptions []CallOption) *options { if len(callOptions) == 0 { return opt } optCopy := &options{} *optCopy = *opt for _, f := range callOptions { f.applyFunc(optCopy) } return optCopy } func filterCallOptions(callOptions []grpc.CallOption) (grpcOptions []grpc.CallOption, retryOptions []CallOption) { for _, opt := range callOptions { if co, ok := opt.(CallOption); ok { retryOptions = append(retryOptions, co) } else { grpcOptions = append(grpcOptions, opt) } } return grpcOptions, retryOptions }