Documentation
¶
Overview ¶
Package throttle provides configurable middleware for throttling HTTP requests on the server side.
Example ¶
package main import ( "bytes" "fmt" stdlog "log" "net/http" "net/http/httptest" "regexp" "strconv" "time" "github.com/acronis/go-appkit/config" "github.com/acronis/go-appkit/httpserver/middleware/throttle" ) const apiErrDomain = "MyService" func main() { configReader := bytes.NewReader([]byte(` rateLimitZones: rl_zone1: rateLimit: 1/s burstLimit: 0 responseStatusCode: 503 responseRetryAfter: auto dryRun: false rl_zone2: rateLimit: 5/m burstLimit: 0 responseStatusCode: 429 responseRetryAfter: auto key: type: "identity" dryRun: false inFlightLimitZones: ifl_zone1: inFlightLimit: 1 backlogLimit: 0 backlogTimeout: 15s responseStatusCode: 503 dryRun: false rules: - routes: - path: "/hello-world" methods: GET excludedRoutes: - path: "/healthz" rateLimits: - zone: rl_zone1 tags: all_reqs - routes: - path: "= /long-work" methods: POST inFlightLimits: - zone: ifl_zone1 tags: all_reqs - routes: - path: ~^/api/2/tenants/([\w\-]{36})/?$ methods: PUT rateLimits: - zone: rl_zone2 tags: authenticated_reqs `)) configLoader := config.NewLoader(config.NewViperAdapter()) cfg := &throttle.Config{} if err := configLoader.LoadFromReader(configReader, config.DataTypeYAML, cfg); err != nil { stdlog.Fatal(err) return } const longWorkDelay = time.Second srv, err := makeExampleTestServer(cfg, longWorkDelay) if err != nil { stdlog.Fatal(err) return } defer srv.Close() // Rate limiting. // 1st request finished successfully. resp1, _ := http.Get(srv.URL + "/hello-world") _ = resp1.Body.Close() fmt.Println("[1] GET /hello-world " + strconv.Itoa(resp1.StatusCode)) // 2nd request is throttled. resp2, _ := http.Get(srv.URL + "/hello-world") _ = resp2.Body.Close() fmt.Println("[2] GET /hello-world " + strconv.Itoa(resp2.StatusCode)) // In-flight limiting. // 3rd request finished successfully. resp3code := make(chan int) go func() { resp3, _ := http.Post(srv.URL+"/long-work", "", nil) _ = resp3.Body.Close() resp3code <- resp3.StatusCode }() time.Sleep(longWorkDelay / 2) // 4th request is throttled. resp4, _ := http.Post(srv.URL+"/long-work", "", nil) _ = resp4.Body.Close() fmt.Println("[3] POST /long-work " + strconv.Itoa(<-resp3code)) fmt.Println("[4] POST /long-work " + strconv.Itoa(resp4.StatusCode)) // Unmatched (unspecified) routes are not limited. resp5code := make(chan int) go func() { resp5, _ := http.Post(srv.URL+"/long-work-without-limits", "", nil) _ = resp5.Body.Close() resp5code <- resp5.StatusCode }() time.Sleep(longWorkDelay / 2) resp6, _ := http.Post(srv.URL+"/long-work", "", nil) _ = resp6.Body.Close() fmt.Println("[5] POST /long-work-without-limits " + strconv.Itoa(<-resp5code)) fmt.Println("[6] POST /long-work-without-limits " + strconv.Itoa(resp6.StatusCode)) // Throttle authenticated requests by username from basic auth. const tenantPath = "/api/2/tenants/446507ba-2f9b-4347-adbc-63581383ba25" doReqWithBasicAuth := func(username string) *http.Response { req, _ := http.NewRequest(http.MethodPut, srv.URL+tenantPath, http.NoBody) req.SetBasicAuth(username, username+"-password") resp, _ := http.DefaultClient.Do(req) return resp } // 7th request is not throttled. resp7 := doReqWithBasicAuth("ba27afb7-ad60-4077-956e-366e77358b92") _ = resp7.Body.Close() fmt.Printf("[7] PUT %s %d\n", tenantPath, resp7.StatusCode) // 8th request is throttled (the same username as in the previous request, and it's rate-limited). resp8 := doReqWithBasicAuth("ba27afb7-ad60-4077-956e-366e77358b92") _ = resp8.Body.Close() fmt.Printf("[8] PUT %s %d\n", tenantPath, resp8.StatusCode) // 9th request is not throttled (the different username is used). resp9 := doReqWithBasicAuth("97d8d1e6-948d-4c41-91d6-495dcc8c7b1a") _ = resp9.Body.Close() fmt.Printf("[9] PUT %s %d\n", tenantPath, resp9.StatusCode) } func makeExampleTestServer(cfg *throttle.Config, longWorkDelay time.Duration) (*httptest.Server, error) { promMetrics := throttle.NewPrometheusMetrics() promMetrics.MustRegister() defer promMetrics.Unregister() // Configure middleware that should do global throttling ("all_reqs" tag says about that). globalThrottleMiddleware, err := throttle.MiddlewareWithOpts(cfg, apiErrDomain, promMetrics, throttle.MiddlewareOpts{ Tags: []string{"all_reqs"}}) if err != nil { return nil, fmt.Errorf("create global throttling middleware: %w", err) } // Configure middleware that should do per-client throttling based on the username from basic auth ("authenticated_reqs" tag says about that). clientThrottleMiddleware, err := throttle.MiddlewareWithOpts(cfg, apiErrDomain, promMetrics, throttle.MiddlewareOpts{ Tags: []string{"authenticated_reqs"}, GetKeyIdentity: func(r *http.Request) (key string, bypass bool, err error) { username, _, ok := r.BasicAuth() if !ok { return "", true, fmt.Errorf("no basic auth") } return username, false, nil }, }) if err != nil { return nil, fmt.Errorf("create client throttling middleware: %w", err) } restoreTenantPathRegExp := regexp.MustCompile(`^/api/2/tenants/([\w-]{36})/?$`) return httptest.NewServer(globalThrottleMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/long-work": if r.Method != http.MethodPost { rw.WriteHeader(http.StatusMethodNotAllowed) return } time.Sleep(longWorkDelay) // Emulate long work. rw.WriteHeader(http.StatusOK) return case "/hello-world": if r.Method != http.MethodGet { rw.WriteHeader(http.StatusMethodNotAllowed) return } rw.WriteHeader(http.StatusOK) _, _ = rw.Write([]byte("Hello world!")) return case "/long-work-without-limits": if r.Method != http.MethodPost { rw.WriteHeader(http.StatusMethodNotAllowed) return } time.Sleep(longWorkDelay) // Emulate long work. rw.WriteHeader(http.StatusOK) return } if restoreTenantPathRegExp.MatchString(r.URL.Path) { if r.Method != http.MethodPut { rw.WriteHeader(http.StatusMethodNotAllowed) return } clientThrottleMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusNoContent) })).ServeHTTP(rw, r) return } rw.WriteHeader(http.StatusNotFound) }))), nil }
Output: [1] GET /hello-world 200 [2] GET /hello-world 503 [3] POST /long-work 200 [4] POST /long-work 503 [5] POST /long-work-without-limits 200 [6] POST /long-work-without-limits 200 [7] PUT /api/2/tenants/446507ba-2f9b-4347-adbc-63581383ba25 204 [8] PUT /api/2/tenants/446507ba-2f9b-4347-adbc-63581383ba25 429 [9] PUT /api/2/tenants/446507ba-2f9b-4347-adbc-63581383ba25 204
Index ¶
- Constants
- func Middleware(cfg *Config, errDomain string, mc MetricsCollector) (func(next http.Handler) http.Handler, error)
- func MiddlewareWithOpts(cfg *Config, errDomain string, mc MetricsCollector, opts MiddlewareOpts) (func(next http.Handler) http.Handler, error)
- type Config
- type InFlightLimitZoneConfig
- type MetricsCollector
- type MiddlewareOpts
- type PrometheusMetrics
- func (pm *PrometheusMetrics) IncInFlightLimitRejects(ruleName string, dryRun bool, backlogged bool)
- func (pm *PrometheusMetrics) IncRateLimitRejects(ruleName string, dryRun bool)
- func (pm *PrometheusMetrics) MustCurryWith(labels prometheus.Labels) *PrometheusMetrics
- func (pm *PrometheusMetrics) MustRegister()
- func (pm *PrometheusMetrics) Unregister()
- type PrometheusMetricsOpts
- type RateLimitRetryAfterValue
- type RateLimitValue
- type RateLimitZoneConfig
- type RuleConfig
- type RuleInFlightLimit
- type RuleRateLimit
- type ZoneConfig
- type ZoneKeyConfig
- type ZoneKeyType
Examples ¶
Constants ¶
const ( RateLimitAlgLeakyBucket = "leaky_bucket" RateLimitAlgSlidingWindow = "sliding_window" )
Rate-limiting algorithms.
const RuleLogFieldName = "throttle_rule"
RuleLogFieldName is a logged field that contains the name of the throttling rule.
Variables ¶
This section is empty.
Functions ¶
func Middleware ¶
func Middleware(cfg *Config, errDomain string, mc MetricsCollector) (func(next http.Handler) http.Handler, error)
Middleware is a middleware that throttles incoming HTTP requests based on the passed configuration.
func MiddlewareWithOpts ¶
func MiddlewareWithOpts( cfg *Config, errDomain string, mc MetricsCollector, opts MiddlewareOpts, ) (func(next http.Handler) http.Handler, error)
MiddlewareWithOpts is a more configurable version of Middleware.
Types ¶
type Config ¶
type Config struct { // RateLimitZones contains rate limiting zones. // Key is a zone's name, and value is a zone's configuration. RateLimitZones map[string]RateLimitZoneConfig `mapstructure:"rateLimitZones" yaml:"rateLimitZones"` // InFlightLimitZones contains in-flight limiting zones. // Key is a zone's name, and value is a zone's configuration. InFlightLimitZones map[string]InFlightLimitZoneConfig `mapstructure:"inFlightLimitZones" yaml:"inFlightLimitZones"` // Rules contains list of so-called throttling rules. // Basically, throttling rule represents a route (or multiple routes), // and rate/in-flight limiting zones based on which all matched HTTP requests will be throttled. Rules []RuleConfig `mapstructure:"rules" yaml:"rules"` // contains filtered or unexported fields }
Config represents a configuration for throttling of HTTP requests on the server side.
func NewConfigWithKeyPrefix ¶
NewConfigWithKeyPrefix creates a new instance of the Config. Allows specifying key prefix which will be used for parsing configuration parameters.
func (*Config) KeyPrefix ¶
KeyPrefix returns a key prefix with which all configuration parameters should be presented.
func (*Config) Set ¶
func (c *Config) Set(dp config.DataProvider) error
Set sets throttling configuration values from config.DataProvider.
func (*Config) SetProviderDefaults ¶
func (c *Config) SetProviderDefaults(_ config.DataProvider)
SetProviderDefaults sets default configuration values for logger in config.DataProvider.
type InFlightLimitZoneConfig ¶
type InFlightLimitZoneConfig struct { ZoneConfig `mapstructure:",squash" yaml:",inline"` InFlightLimit int `mapstructure:"inFlightLimit" yaml:"inFlightLimit"` BacklogLimit int `mapstructure:"backlogLimit" yaml:"backlogLimit"` BacklogTimeout time.Duration `mapstructure:"backlogTimeout" yaml:"backlogTimeout"` ResponseRetryAfter time.Duration `mapstructure:"responseRetryAfter" yaml:"responseRetryAfter"` }
InFlightLimitZoneConfig represents zone configuration for in-flight limiting.
func (*InFlightLimitZoneConfig) Validate ¶
func (c *InFlightLimitZoneConfig) Validate() error
Validate validates zone configuration for in-flight limiting.
type MetricsCollector ¶
type MetricsCollector interface { // IncInFlightLimitRejects increments the counter of rejected requests due to in-flight limit exceeded. IncInFlightLimitRejects(ruleName string, dryRun bool, backlogged bool) // IncRateLimitRejects increments the counter of rejected requests due to rate limit exceeded. IncRateLimitRejects(ruleName string, dryRun bool) }
MetricsCollector represents a collector of metrics for rate/in-flight limiting rejects.
type MiddlewareOpts ¶
type MiddlewareOpts struct { // GetKeyIdentity is a function that returns identity string representation. // The returned string is used as a key for zone when key.type is "identity". GetKeyIdentity func(r *http.Request) (key string, bypass bool, err error) // RateLimitOnReject is a callback called for rejecting HTTP request when the rate limit is exceeded. RateLimitOnReject middleware.RateLimitOnRejectFunc // RateLimitOnRejectInDryRun is a callback called for rejecting HTTP request in the dry-run mode // when the rate limit is exceeded. RateLimitOnRejectInDryRun middleware.RateLimitOnRejectFunc // RateLimitOnError is a callback called in case of any error that may occur during the rate limiting. RateLimitOnError middleware.RateLimitOnErrorFunc // InFlightLimitOnReject is a callback called for rejecting HTTP request when the in-flight limit is exceeded. InFlightLimitOnReject middleware.InFlightLimitOnRejectFunc // RateLimitOnRejectInDryRun is a callback called for rejecting HTTP request in the dry-run mode // when the in-flight limit is exceeded. InFlightLimitOnRejectInDryRun middleware.InFlightLimitOnRejectFunc // RateLimitOnError is a callback called in case of any error that may occur during the in-flight limiting. InFlightLimitOnError middleware.InFlightLimitOnErrorFunc // Tags is a list of tags for filtering throttling rules from the config. If it's empty, all rules can be applied. Tags []string // BuildHandlerAtInit determines where the final handler will be constructed. // If true, it will be done at the initialization step (i.e., in the constructor), // false (default) - right in the ServeHTTP() method (gorilla/mux case). BuildHandlerAtInit bool }
MiddlewareOpts represents an options for Middleware.
type PrometheusMetrics ¶ added in v1.3.0
type PrometheusMetrics struct { InFlightLimitRejects *prometheus.CounterVec RateLimitRejects *prometheus.CounterVec }
PrometheusMetrics represents a collector of Prometheus metrics for rate/in-flight limiting rejects.
func NewPrometheusMetrics ¶ added in v1.3.0
func NewPrometheusMetrics() *PrometheusMetrics
NewPrometheusMetrics creates a new instance of PrometheusMetrics.
func NewPrometheusMetricsWithOpts ¶ added in v1.3.0
func NewPrometheusMetricsWithOpts(opts PrometheusMetricsOpts) *PrometheusMetrics
NewPrometheusMetricsWithOpts creates a new instance of PrometheusMetrics with the provided options.
func (*PrometheusMetrics) IncInFlightLimitRejects ¶ added in v1.3.0
func (pm *PrometheusMetrics) IncInFlightLimitRejects(ruleName string, dryRun bool, backlogged bool)
IncInFlightLimitRejects increments the counter of rejected requests due to in-flight limit exceeded.
func (*PrometheusMetrics) IncRateLimitRejects ¶ added in v1.3.0
func (pm *PrometheusMetrics) IncRateLimitRejects(ruleName string, dryRun bool)
IncRateLimitRejects increments the counter of rejected requests due to rate limit exceeded.
func (*PrometheusMetrics) MustCurryWith ¶ added in v1.3.0
func (pm *PrometheusMetrics) MustCurryWith(labels prometheus.Labels) *PrometheusMetrics
MustCurryWith curries the metrics collector with the provided labels.
func (*PrometheusMetrics) MustRegister ¶ added in v1.3.0
func (pm *PrometheusMetrics) MustRegister()
MustRegister does registration of metrics collector in Prometheus and panics if any error occurs.
func (*PrometheusMetrics) Unregister ¶ added in v1.3.0
func (pm *PrometheusMetrics) Unregister()
Unregister cancels registration of metrics collector in Prometheus.
type PrometheusMetricsOpts ¶ added in v1.3.0
type PrometheusMetricsOpts struct { // Namespace is a namespace for metrics. It will be prepended to all metric names. Namespace string // ConstLabels is a set of labels that will be applied to all metrics. ConstLabels prometheus.Labels // CurriedLabelNames is a list of label names that will be curried with the provided labels. // See PrometheusMetrics.MustCurryWith method for more details. // Keep in mind that if this list is not empty, // PrometheusMetrics.MustCurryWith method must be called further with the same labels. // Otherwise, the collector will panic. CurriedLabelNames []string }
PrometheusMetricsOpts represents options for PrometheusMetrics.
type RateLimitRetryAfterValue ¶
RateLimitRetryAfterValue represents structured retry-after value for rate limiting.
func (*RateLimitRetryAfterValue) UnmarshalText ¶
func (ra *RateLimitRetryAfterValue) UnmarshalText(text []byte) error
UnmarshalText implements the encoding.TextUnmarshaler interface.
type RateLimitValue ¶
RateLimitValue represents value for rate limiting.
func (*RateLimitValue) UnmarshalText ¶
func (rl *RateLimitValue) UnmarshalText(text []byte) error
UnmarshalText implements the encoding.TextUnmarshaler interface.
type RateLimitZoneConfig ¶
type RateLimitZoneConfig struct { ZoneConfig `mapstructure:",squash" yaml:",inline"` Alg string `mapstructure:"alg" yaml:"alg"` RateLimit RateLimitValue `mapstructure:"rateLimit" yaml:"rateLimit"` BurstLimit int `mapstructure:"burstLimit" yaml:"burstLimit"` BacklogLimit int `mapstructure:"backlogLimit" yaml:"backlogLimit"` BacklogTimeout time.Duration `mapstructure:"backlogTimeout" yaml:"backlogTimeout"` ResponseRetryAfter RateLimitRetryAfterValue `mapstructure:"responseRetryAfter" yaml:"responseRetryAfter"` }
RateLimitZoneConfig represents zone configuration for rate limiting.
func (*RateLimitZoneConfig) Validate ¶
func (c *RateLimitZoneConfig) Validate() error
Validate validates zone configuration for rate limiting.
type RuleConfig ¶
type RuleConfig struct { // Alias is an alternative name for the rule. It will be used as a label in metrics. Alias string `mapstructure:"alias" yaml:"alias"` // Routes contains a list of routes (HTTP verb + URL path) for which the rule will be applied. Routes []restapi.RouteConfig `mapstructure:"routes" yaml:"routes"` // ExcludedRoutes contains list of routes (HTTP verb + URL path) to be excluded from throttling limitations. // The following service endpoints fit should typically be added to this list: // - healthcheck endpoint serving as readiness probe // - status endpoint serving as liveness probe ExcludedRoutes []restapi.RouteConfig `mapstructure:"excludedRoutes" yaml:"excludedRoutes"` // Tags is useful when the different rules of the same config should be used by different middlewares. // As example let's suppose we would like to have 2 different throttling rules: // 1) for absolutely all requests; // 2) for all identity-aware (authorized) requests. // In the code, we will have 2 middlewares that will be executed on the different steps of the HTTP request serving, // and each one should do only its own throttling. // We can achieve this using different tags for rules and passing needed tag in the MiddlewareOpts. Tags []string `mapstructure:"tags" yaml:"tags"` // RateLimits contains a list of the rate limiting zones that are used in the rule. RateLimits []RuleRateLimit `mapstructure:"rateLimits" yaml:"rateLimits"` // InFlightLimits contains a list of the in-flight limiting zones that are used in the rule. InFlightLimits []RuleInFlightLimit `mapstructure:"inFlightLimits" yaml:"inFlightLimits"` }
RuleConfig represents configuration for throttling rule.
func (*RuleConfig) Validate ¶
func (c *RuleConfig) Validate( rateLimitZones map[string]RateLimitZoneConfig, inFlightLimitZones map[string]InFlightLimitZoneConfig, ) error
Validate validates throttling rule configuration.
type RuleInFlightLimit ¶
type RuleInFlightLimit struct {
Zone string `mapstructure:"zone" yaml:"zone"`
}
RuleInFlightLimit represents rule's in-flight limiting parameters.
type RuleRateLimit ¶
type RuleRateLimit struct {
Zone string `mapstructure:"zone" yaml:"zone"`
}
RuleRateLimit represents rule's rate limiting parameters.
type ZoneConfig ¶
type ZoneConfig struct { Key ZoneKeyConfig `mapstructure:"key" yaml:"key"` MaxKeys int `mapstructure:"maxKeys" yaml:"maxKeys"` ResponseStatusCode int `mapstructure:"responseStatusCode" yaml:"responseStatusCode"` DryRun bool `mapstructure:"dryRun" yaml:"dryRun"` IncludedKeys []string `mapstructure:"includedKeys" yaml:"includedKeys"` ExcludedKeys []string `mapstructure:"excludedKeys" yaml:"excludedKeys"` }
ZoneConfig represents a basic zone configuration.
func (*ZoneConfig) Validate ¶
func (c *ZoneConfig) Validate() error
Validate validates zone configuration.
type ZoneKeyConfig ¶
type ZoneKeyConfig struct { // Type determines type of key that will be used for throttling. Type ZoneKeyType `mapstructure:"type" yaml:"type"` // HeaderName is a name of the HTTP request header which value will be used as a key. // Matters only when Type is a "header". HeaderName string `mapstructure:"headerName" yaml:"headerName"` // NoBypassEmpty specifies whether throttling will be used if the value obtained by the key is empty. NoBypassEmpty bool `mapstructure:"noBypassEmpty" yaml:"noBypassEmpty"` }
ZoneKeyConfig represents a configuration of zone's key.
func (*ZoneKeyConfig) Validate ¶
func (c *ZoneKeyConfig) Validate() error
Validate validates keys zone configuration.
type ZoneKeyType ¶
type ZoneKeyType string
ZoneKeyType is a type of keys zone.
const ( ZoneKeyTypeNoKey ZoneKeyType = "" ZoneKeyTypeIdentity ZoneKeyType = "identity" ZoneKeyTypeHTTPHeader ZoneKeyType = "header" ZoneKeyTypeRemoteAddr ZoneKeyType = "remote_addr" )
Zone key types.