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 := makeExampleTestServer(cfg, longWorkDelay)
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 {
throttleMetrics := throttle.NewMetricsCollector("")
throttleMetrics.MustRegister()
defer throttleMetrics.Unregister()
// Configure middleware that should do global throttling ("all_reqs" tag says about that).
allReqsThrottleMiddleware := throttle.MiddlewareWithOpts(cfg, apiErrDomain, throttleMetrics, throttle.MiddlewareOpts{
Tags: []string{"all_reqs"}})
// Configure middleware that should do per-client throttling based on the username from basic auth ("authenticated_reqs" tag says about that).
authenticatedReqsThrottleMiddleware := throttle.MiddlewareWithOpts(cfg, apiErrDomain, throttleMetrics, 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
},
})
restoreTenantPathRegExp := regexp.MustCompile(`^/api/2/tenants/([\w-]{36})/?$`)
return httptest.NewServer(allReqsThrottleMiddleware(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
}
authenticatedReqsThrottleMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNoContent)
})).ServeHTTP(rw, r)
return
}
rw.WriteHeader(http.StatusNotFound)
})))
}
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
- func MiddlewareWithOpts(cfg *Config, errDomain string, mc *MetricsCollector, opts MiddlewareOpts) func(next http.Handler) http.Handler
- type Config
- type InFlightLimitZoneConfig
- type MetricsCollector
- type MiddlewareOpts
- 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 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
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
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 struct {
InFlightLimitRejects *prometheus.CounterVec
RateLimitRejects *prometheus.CounterVec
}
MetricsCollector represents collector of metrics for rate/in-flight limiting rejects.
func NewMetricsCollector ¶
func NewMetricsCollector(namespace string) *MetricsCollector
NewMetricsCollector creates a new instance of MetricsCollector.
func (*MetricsCollector) MustCurryWith ¶
func (mc *MetricsCollector) MustCurryWith(labels prometheus.Labels) *MetricsCollector
MustCurryWith curries the metrics collector with the provided labels.
func (*MetricsCollector) MustRegister ¶
func (mc *MetricsCollector) MustRegister()
MustRegister does registration of metrics collector in Prometheus and panics if any error occurs.
func (*MetricsCollector) Unregister ¶
func (mc *MetricsCollector) Unregister()
Unregister cancels registration of metrics collector in Prometheus.
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.
func (MiddlewareOpts) InFlightLimitOpts ¶
func (opts MiddlewareOpts) InFlightLimitOpts() inFlightLimitMiddlewareParams
InFlightLimitOpts returns inFlightLimitMiddlewareParams that may be used for constructing InFlightLimitMiddleware.
func (MiddlewareOpts) RateLimitOpts ¶
func (opts MiddlewareOpts) RateLimitOpts() rateLimitMiddlewareParams
RateLimitOpts returns rateLimitMiddlewareParams that may be used for constructing RateLimitMiddleware.
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.