Skip to content

Commit

Permalink
Retries with policy options
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewesteves committed Dec 26, 2024
1 parent 6d7fa55 commit 2d02ab6
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 114 deletions.
191 changes: 169 additions & 22 deletions httpclient/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,54 @@ package httpclient
import (
"errors"
"github.com/acronis/go-appkit/config"
"github.com/acronis/go-appkit/retry"
"github.com/cenkalti/backoff/v4"
"time"
)

const (
// DefaultClientWaitTimeout is a default timeout for a client to wait for a request.
DefaultClientWaitTimeout = 10 * time.Second

// RetryPolicyExponential is a policy for exponential retries.
RetryPolicyExponential = "exponential"

// RetryPolicyConstant is a policy for constant retries.
RetryPolicyConstant = "constant"

// configuration properties
cfgKeyRetriesEnabled = "retries.enabled"
cfgKeyRetriesMax = "retries.maxAttempts"
cfgKeyRateLimitsEnabled = "rateLimits.enabled"
cfgKeyRateLimitsLimit = "rateLimits.limit"
cfgKeyRateLimitsBurst = "rateLimits.burst"
cfgKeyRateLimitsWaitTimeout = "rateLimits.waitTimeout"
cfgKeyLoggerEnabled = "logger.enabled"
cfgKeyLoggerMode = "logger.mode"
cfgKeyLoggerSlowRequestThreshold = "logger.slowRequestThreshold"
cfgKeyMetricsEnabled = "metrics.enabled"
cfgKeyTimeout = "timeout"
cfgKeyRetriesEnabled = "retries.enabled"
cfgKeyRetriesMax = "retries.maxAttempts"
cfgKeyRetriesPolicyStrategy = "retries.policy.strategy"
cfgKeyRetriesPolicyExponentialInitialInterval = "retries.policy.exponentialBackoffInitialInterval"
cfgKeyRetriesPolicyExponentialMultiplier = "retries.policy.exponentialBackoffMultiplier"
cfgKeyRetriesPolicyConstantInternal = "retries.policy.constantBackoffInterval"
cfgKeyRateLimitsEnabled = "rateLimits.enabled"
cfgKeyRateLimitsLimit = "rateLimits.limit"
cfgKeyRateLimitsBurst = "rateLimits.burst"
cfgKeyRateLimitsWaitTimeout = "rateLimits.waitTimeout"
cfgKeyLoggerEnabled = "logger.enabled"
cfgKeyLoggerMode = "logger.mode"
cfgKeyLoggerSlowRequestThreshold = "logger.slowRequestThreshold"
cfgKeyMetricsEnabled = "metrics.enabled"
cfgKeyTimeout = "timeout"
)

var _ config.Config = (*Config)(nil)
var _ config.KeyPrefixProvider = (*Config)(nil)

// RateLimitConfig represents configuration options for HTTP client rate limits.
type RateLimitConfig struct {
Enabled bool `mapstructure:"enabled"`
Limit int `mapstructure:"limit"`
Burst int `mapstructure:"burst"`
// Enabled is a flag that enables rate limiting.
Enabled bool `mapstructure:"enabled"`

// Limit is the maximum number of requests that can be made.
Limit int `mapstructure:"limit"`

// Burst allow temporary spikes in request rate.
Burst int `mapstructure:"burst"`

// WaitTimeout is the maximum time to wait for a request to be made.
WaitTimeout time.Duration `mapstructure:"waitTimeout"`
}

Expand All @@ -48,6 +68,10 @@ func (c *RateLimitConfig) Set(dp config.DataProvider) (err error) {
}
c.Enabled = enabled

if !c.Enabled {
return nil
}

limit, err := dp.GetInt(cfgKeyRateLimitsLimit)
if err != nil {
return err
Expand Down Expand Up @@ -89,10 +113,104 @@ func (c *RateLimitConfig) TransportOpts() RateLimitingRoundTripperOpts {
}
}

// PolicyConfig represents configuration options for policy retry.
type PolicyConfig struct {
// Strategy is a strategy for retry policy.
Strategy string `mapstructure:"strategy"`

// ExponentialBackoffInitialInterval is the initial interval for exponential backoff.
ExponentialBackoffInitialInterval time.Duration `mapstructure:"exponentialBackoffInitialInterval"`

// ExponentialBackoffMultiplier is the multiplier for exponential backoff.
ExponentialBackoffMultiplier float64 `mapstructure:"exponentialBackoffMultiplier"`

// ConstantBackoffInterval is the interval for constant backoff.
ConstantBackoffInterval time.Duration `mapstructure:"constantBackoffInterval"`
}

// Set is part of config interface implementation.
func (c *PolicyConfig) Set(dp config.DataProvider) (err error) {
strategy, err := dp.GetString(cfgKeyRetriesPolicyStrategy)
if err != nil {
return err
}
c.Strategy = strategy

if c.Strategy != "" && c.Strategy != RetryPolicyExponential && c.Strategy != RetryPolicyConstant {
return errors.New("client retry policy must be one of: [exponential, constant]")
}

if c.Strategy == RetryPolicyExponential {
var interval time.Duration
interval, err = dp.GetDuration(cfgKeyRetriesPolicyExponentialInitialInterval)
if err != nil {
return nil
}
if interval < 0 {
return errors.New("client exponential backoff initial interval must be positive")
}
c.ExponentialBackoffInitialInterval = interval

var multiplier float64
multiplier, err = dp.GetFloat64(cfgKeyRetriesPolicyExponentialMultiplier)
if err != nil {
return err
}
if multiplier <= 1 {
return errors.New("client exponential backoff multiplier must be greater than 1")
}
c.ExponentialBackoffMultiplier = multiplier

return nil
} else if c.Strategy == RetryPolicyConstant {
var interval time.Duration
interval, err = dp.GetDuration(cfgKeyRetriesPolicyConstantInternal)
if err != nil {
return err
}
if interval < 0 {
return errors.New("client constant backoff interval must be positive")
}
c.ConstantBackoffInterval = interval
}

return nil
}

// SetProviderDefaults is part of config interface implementation.
func (c *PolicyConfig) SetProviderDefaults(_ config.DataProvider) {}

// RetriesConfig represents configuration options for HTTP client retries policy.
type RetriesConfig struct {
Enabled bool `mapstructure:"enabled"`
MaxAttempts int `mapstructure:"maxAttempts"`
// Enabled is a flag that enables retries.
Enabled bool `mapstructure:"enabled"`

// MaxAttempts is the maximum number of attempts to retry the request.
MaxAttempts int `mapstructure:"maxAttempts"`

// Policy of a retry: [exponential, constant]. default is exponential.
Policy PolicyConfig `mapstructure:"policy"`
}

// GetPolicy returns a retry policy based on strategy or nil if none is provided.
func (c *RetriesConfig) GetPolicy() retry.Policy {
if c.Policy.Strategy == RetryPolicyExponential {
return retry.PolicyFunc(func() backoff.BackOff {
bf := backoff.NewExponentialBackOff()
bf.InitialInterval = c.Policy.ExponentialBackoffInitialInterval
bf.Multiplier = c.Policy.ExponentialBackoffMultiplier
bf.Reset()
return bf
})
} else if c.Policy.Strategy == RetryPolicyConstant {
return retry.PolicyFunc(func() backoff.BackOff {
bf := backoff.NewConstantBackOff(c.Policy.ConstantBackoffInterval)
bf.Reset()
return bf
})
}

return nil
}

// Set is part of config interface implementation.
Expand All @@ -103,6 +221,10 @@ func (c *RetriesConfig) Set(dp config.DataProvider) error {
}
c.Enabled = enabled

if !c.Enabled {
return nil
}

maxAttempts, err := dp.GetInt(cfgKeyRetriesMax)
if err != nil {
return err
Expand All @@ -112,6 +234,11 @@ func (c *RetriesConfig) Set(dp config.DataProvider) error {
}
c.MaxAttempts = maxAttempts

err = c.Policy.Set(config.NewKeyPrefixedDataProvider(dp, ""))
if err != nil {
return err
}

return nil
}

Expand All @@ -125,9 +252,14 @@ func (c *RetriesConfig) TransportOpts() RetryableRoundTripperOpts {

// LoggerConfig represents configuration options for HTTP client logs.
type LoggerConfig struct {
Enabled bool `mapstructure:"enabled"`
// Enabled is a flag that enables logging.
Enabled bool `mapstructure:"enabled"`

// SlowRequestThreshold is a threshold for slow requests.
SlowRequestThreshold time.Duration `mapstructure:"slowRequestThreshold"`
Mode string `mapstructure:"mode"`

// Mode of logging.
Mode string `mapstructure:"mode"`
}

// Set is part of config interface implementation.
Expand All @@ -138,6 +270,10 @@ func (c *LoggerConfig) Set(dp config.DataProvider) error {
}
c.Enabled = enabled

if !c.Enabled {
return nil
}

slowRequestThreshold, err := dp.GetDuration(cfgKeyLoggerSlowRequestThreshold)
if err != nil {
return err
Expand Down Expand Up @@ -172,6 +308,7 @@ func (c *LoggerConfig) TransportOpts() LoggingRoundTripperOpts {

// MetricsConfig represents configuration options for HTTP client logs.
type MetricsConfig struct {
// Enabled is a flag that enables metrics.
Enabled bool `mapstructure:"enabled"`
}

Expand All @@ -191,12 +328,22 @@ func (c *MetricsConfig) SetProviderDefaults(_ config.DataProvider) {}

// Config represents options for HTTP client configuration.
type Config struct {
Retries RetriesConfig `mapstructure:"retries"`
// Retries is a configuration for HTTP client retries policy.
Retries RetriesConfig `mapstructure:"retries"`

// RateLimits is a configuration for HTTP client rate limits.
RateLimits RateLimitConfig `mapstructure:"rateLimits"`
Logger LoggerConfig `mapstructure:"logger"`
Metrics MetricsConfig `mapstructure:"metrics"`
Timeout time.Duration `mapstructure:"timeout"`

// Logger is a configuration for HTTP client logs.
Logger LoggerConfig `mapstructure:"logger"`

// Metrics is a configuration for HTTP client metrics.
Metrics MetricsConfig `mapstructure:"metrics"`

// Timeout is the maximum time to wait for a request to be made.
Timeout time.Duration `mapstructure:"timeout"`

// keyPrefix is a prefix for configuration parameters.
keyPrefix string
}

Expand Down
Loading

0 comments on commit 2d02ab6

Please sign in to comment.