Commit 55e16faa authored by Dmitry Smirnov's avatar Dmitry Smirnov

New upstream version 2.2.1

parents
.DS_Store
*.swp
*.swo
*.test
*.out
sudo: false
language: go
go:
- 1.7
- 1.8
- 1.9
- "1.10"
# 1.x builds the latest in that series. Also try to add other versions here
# as they come up so that we're pretty sure that we're maintaining
# backwards compatibility.
- 1.x
notifications:
email: false
services:
- redis-server
install: make get-deps
script: make
# Changelog
## Unreleased
## 2.2.1 - 2018-03-21
* [#40](https://github.com/throttled/throttled/pull/40) Replace unmaintained `garyburd/redigo` with `gomodule/redigo`
## 2.2.0 - 2018-03-19
* [#37](https://github.com/throttled/throttled/pull/37) Add `go-redis` support
## 2.1.0 - 2017-10-23
* [#27](https://github.com/throttled/throttled/pull/27) Never assign a Redis key's TTL to zero
* [#32](https://github.com/throttled/throttled/pull/32) Stop using `gopkg.in`
## 2.0.3 - 2015-09-09
* [#15](https://github.com/throttled/throttled/pull/15) Use non-HTTP example for `GCRARateLimiter`
## 2.0.2 - 2015-09-07
* [#14](https://github.com/throttled/throttled/pull/14) Add example demonstrating granular use of `RateLimit`
## 2.0.1 - 2015-09-01
* [#12](https://github.com/throttled/throttled/pull/12) Fix parsing of `TIME` in `redigostore`
## 2.0.0 - 2015-08-28
* [#9](https://github.com/throttled/throttled/pull/9) Substantially rebuild the APIs and implementation of the `throttled` package (wholly not backwards compatible)
(There are other versions, but this is the beginning of `CHANGELOG.md`.)
<!--
# vim: set tw=0:
-->
Copyright (c) 2014, Martin Angers and Contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.PHONY: test test-cover bench lint get-deps .go-test .go-test-cover
test: .go-test bench lint
test-cover: .go-test-cover bench lint
bench:
go test -race -bench=. -cpu=1,2,4
lint:
gofmt -l .
go vet ./...
which golint # Fail if golint doesn't exist
-golint . # Don't fail on golint warnings themselves
-golint store # Don't fail on golint warnings themselves
get-deps:
go get github.com/gomodule/redigo/redis
go get github.com/hashicorp/golang-lru
go get github.com/golang/lint/golint
go get github.com/go-redis/redis
.go-test:
go test ./...
.go-test-cover:
go test -coverprofile=throttled.coverage.out .
go test -coverprofile=store.coverage.out ./store
# Throttled [![build status](https://secure.travis-ci.org/throttled/throttled.svg)](https://travis-ci.org/throttled/throttled) [![GoDoc](https://godoc.org/github.com/throttled/throttled?status.svg)](https://godoc.org/github.com/throttled/throttled)
Package throttled implements rate limiting using the [generic cell rate
algorithm][gcra] to limit access to resources such as HTTP endpoints.
The 2.0.0 release made some major changes to the throttled API. If
this change broke your code in problematic ways or you wish a feature
of the old API had been retained, please open an issue. We don't
guarantee any particular changes but would like to hear more about
what our users need. Thanks!
## Installation
```sh
go get -u github.com/throttled/throttled
```
## Documentation
API documentation is available on [godoc.org][doc]. The following
example demonstrates the usage of HTTPLimiter for rate-limiting access
to an http.Handler to 20 requests per path per minute with bursts of
up to 5 additional requests:
```go
store, err := memstore.New(65536)
if err != nil {
log.Fatal(err)
}
quota := throttled.RateQuota{throttled.PerMin(20), 5}
rateLimiter, err := throttled.NewGCRARateLimiter(store, quota)
if err != nil {
log.Fatal(err)
}
httpRateLimiter := throttled.HTTPRateLimiter{
RateLimiter: rateLimiter,
VaryBy: &throttled.VaryBy{Path: true},
}
http.ListenAndServe(":8080", httpRateLimiter.RateLimit(myHandler))
```
## Related Projects
See [throttled/gcra][throttled-gcra] for a list of other projects related to
rate limiting and GCRA.
## Release
1. Update `CHANGELOG.md`. Please use semantic versioning and the existing
conventions established in the file. Commit the changes with a message like
`Bump version to 2.2.0`.
2. Tag `master` with a new version prefixed with `v`. For example, `v2.2.0`.
3. `git push origin master --tags`.
4. Publish a new release on the [releases] page. Copy the body from the
contents of `CHANGELOG.md` for the version and follow other conventions from
previous releases.
## License
The [BSD 3-clause license][bsd]. Copyright (c) 2014 Martin Angers and contributors.
[blog]: http://0value.com/throttled--guardian-of-the-web-server
[bsd]: https://opensource.org/licenses/BSD-3-Clause
[doc]: https://godoc.org/github.com/throttled/throttled
[gcra]: https://en.wikipedia.org/wiki/Generic_cell_rate_algorithm
[puerkitobio]: https://github.com/puerkitobio/
[pr]: https://github.com/throttled/throttled/compare
[releases]: https://github.com/throttled/throttled/releases
[throttled-gcra]: https://github.com/throttled/gcra
<!--
# vim: set tw=79:
-->
package throttled
import (
"net/http"
"time"
)
// DEPRECATED. Quota returns the number of requests allowed and the custom time window.
func (q Rate) Quota() (int, time.Duration) {
return q.count, q.period * time.Duration(q.count)
}
// DEPRECATED. Q represents a custom quota.
type Q struct {
Requests int
Window time.Duration
}
// DEPRECATED. Quota returns the number of requests allowed and the custom time window.
func (q Q) Quota() (int, time.Duration) {
return q.Requests, q.Window
}
// DEPRECATED. The Quota interface defines the method to implement to describe
// a time-window quota, as required by the RateLimit throttler.
type Quota interface {
// Quota returns a number of requests allowed, and a duration.
Quota() (int, time.Duration)
}
// DEPRECATED. Throttler is a backwards-compatible alias for HTTPLimiter.
type Throttler struct {
HTTPRateLimiter
}
// DEPRECATED. Throttle is an alias for HTTPLimiter#Limit
func (t *Throttler) Throttle(h http.Handler) http.Handler {
return t.RateLimit(h)
}
// DEPRECATED. RateLimit creates a Throttler that conforms to the given
// rate limits
func RateLimit(q Quota, vary *VaryBy, store GCRAStore) *Throttler {
count, period := q.Quota()
if count < 1 {
count = 1
}
if period <= 0 {
period = time.Second
}
rate := Rate{period: period / time.Duration(count)}
limiter, err := NewGCRARateLimiter(store, RateQuota{rate, count - 1})
// This panic in unavoidable because the original interface does
// not support returning an error.
if err != nil {
panic(err)
}
return &Throttler{
HTTPRateLimiter{
RateLimiter: limiter,
VaryBy: vary,
},
}
}
// DEPRECATED. Store is an alias for GCRAStore
type Store interface {
GCRAStore
}
package throttled_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/throttled/throttled"
"github.com/throttled/throttled/store"
)
// Ensure that the current implementation remains compatible with the
// supported but deprecated usage until the next major version.
func TestDeprecatedUsage(t *testing.T) {
// Declare interfaces to statically check that names haven't changed
var st throttled.Store
var thr *throttled.Throttler
var q throttled.Quota
st = store.NewMemStore(100)
vary := &throttled.VaryBy{Path: true}
q = throttled.PerMin(2)
thr = throttled.RateLimit(q, vary, st)
handler := thr.Throttle(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
cases := []struct {
path string
code int
headers map[string]string
}{
{"/foo", 200, map[string]string{"X-Ratelimit-Limit": "2", "X-Ratelimit-Remaining": "1", "X-Ratelimit-Reset": "30"}},
{"/foo", 200, map[string]string{"X-Ratelimit-Limit": "2", "X-Ratelimit-Remaining": "0", "X-Ratelimit-Reset": "60"}},
{"/foo", 429, map[string]string{"X-Ratelimit-Limit": "2", "X-Ratelimit-Remaining": "0", "X-Ratelimit-Reset": "60", "Retry-After": "30"}},
{"/bar", 200, map[string]string{"X-Ratelimit-Limit": "2", "X-Ratelimit-Remaining": "1", "X-Ratelimit-Reset": "30"}},
}
for i, c := range cases {
req, err := http.NewRequest("GET", c.path, nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if have, want := rr.Code, c.code; have != want {
t.Errorf("Expected request %d at %s to return %d but got %d",
i, c.path, want, have)
}
for name, want := range c.headers {
if have := rr.HeaderMap.Get(name); have != want {
t.Errorf("Expected request %d at %s to have header '%s: %s' but got '%s'",
i, c.path, name, want, have)
}
}
}
}
// Package throttled implements rate limiting access to resources such
// as HTTP endpoints.
package throttled // import "github.com/throttled/throttled"
package throttled_test
import (
"fmt"
"log"
"net/http"
"github.com/throttled/throttled"
"github.com/throttled/throttled/store/memstore"
)
var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi there!"))
})
// ExampleHTTPRateLimiter demonstrates the usage of HTTPRateLimiter
// for rate-limiting access to an http.Handler to 20 requests per path
// per minute with a maximum burst of 5 requests.
func ExampleHTTPRateLimiter() {
store, err := memstore.New(65536)
if err != nil {
log.Fatal(err)
}
// Maximum burst of 5 which refills at 20 tokens per minute.
quota := throttled.RateQuota{MaxRate: throttled.PerMin(20), MaxBurst: 5}
rateLimiter, err := throttled.NewGCRARateLimiter(store, quota)
if err != nil {
log.Fatal(err)
}
httpRateLimiter := throttled.HTTPRateLimiter{
RateLimiter: rateLimiter,
VaryBy: &throttled.VaryBy{Path: true},
}
http.ListenAndServe(":8080", httpRateLimiter.RateLimit(myHandler))
}
// Demonstrates direct use of GCRARateLimiter's RateLimit function (and the
// more general RateLimiter interface). This should be used anywhere where
// granular control over rate limiting is required.
func ExampleGCRARateLimiter() {
store, err := memstore.New(65536)
if err != nil {
log.Fatal(err)
}
// Maximum burst of 5 which refills at 1 token per hour.
quota := throttled.RateQuota{MaxRate: throttled.PerHour(1), MaxBurst: 5}
rateLimiter, err := throttled.NewGCRARateLimiter(store, quota)
if err != nil {
log.Fatal(err)
}
// Bucket according to the number i / 10 (so 1 falls into the bucket 0
// while 11 falls into the bucket 1). This has the effect of allowing a
// burst of 5 plus 1 (a single emission interval) on every ten iterations
// of the loop. See the output for better clarity here.
//
// We also refill the bucket at 1 token per hour, but that has no effect
// for the purposes of this example.
for i := 0; i < 20; i++ {
bucket := fmt.Sprintf("by-order:%v", i/10)
limited, result, err := rateLimiter.RateLimit(bucket, 1)
if err != nil {
log.Fatal(err)
}
if limited {
fmt.Printf("Iteration %2v; bucket %v: FAILED. Rate limit exceeded.\n",
i, bucket)
} else {
fmt.Printf("Iteration %2v; bucket %v: Operation successful (remaining=%v).\n",
i, bucket, result.Remaining)
}
}
// Output:
// Iteration 0; bucket by-order:0: Operation successful (remaining=5).
// Iteration 1; bucket by-order:0: Operation successful (remaining=4).
// Iteration 2; bucket by-order:0: Operation successful (remaining=3).
// Iteration 3; bucket by-order:0: Operation successful (remaining=2).
// Iteration 4; bucket by-order:0: Operation successful (remaining=1).
// Iteration 5; bucket by-order:0: Operation successful (remaining=0).
// Iteration 6; bucket by-order:0: FAILED. Rate limit exceeded.
// Iteration 7; bucket by-order:0: FAILED. Rate limit exceeded.
// Iteration 8; bucket by-order:0: FAILED. Rate limit exceeded.
// Iteration 9; bucket by-order:0: FAILED. Rate limit exceeded.
// Iteration 10; bucket by-order:1: Operation successful (remaining=5).
// Iteration 11; bucket by-order:1: Operation successful (remaining=4).
// Iteration 12; bucket by-order:1: Operation successful (remaining=3).
// Iteration 13; bucket by-order:1: Operation successful (remaining=2).
// Iteration 14; bucket by-order:1: Operation successful (remaining=1).
// Iteration 15; bucket by-order:1: Operation successful (remaining=0).
// Iteration 16; bucket by-order:1: FAILED. Rate limit exceeded.
// Iteration 17; bucket by-order:1: FAILED. Rate limit exceeded.
// Iteration 18; bucket by-order:1: FAILED. Rate limit exceeded.
// Iteration 19; bucket by-order:1: FAILED. Rate limit exceeded.
}
package throttled
import (
"errors"
"math"
"net/http"
"strconv"
)
var (
// DefaultDeniedHandler is the default DeniedHandler for an
// HTTPRateLimiter. It returns a 429 status code with a generic
// message.
DefaultDeniedHandler = http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "limit exceeded", 429)
}))
// DefaultError is the default Error function for an HTTPRateLimiter.
// It returns a 500 status code with a generic message.
DefaultError = func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, "internal error", http.StatusInternalServerError)
}
)
// HTTPRateLimiter faciliates using a Limiter to limit HTTP requests.
type HTTPRateLimiter struct {
// DeniedHandler is called if the request is disallowed. If it is
// nil, the DefaultDeniedHandler variable is used.
DeniedHandler http.Handler
// Error is called if the RateLimiter returns an error. If it is
// nil, the DefaultErrorFunc is used.
Error func(w http.ResponseWriter, r *http.Request, err error)
// Limiter is call for each request to determine whether the
// request is permitted and update internal state. It must be set.
RateLimiter RateLimiter
// VaryBy is called for each request to generate a key for the
// limiter. If it is nil, all requests use an empty string key.
VaryBy interface {
Key(*http.Request) string
}
}
// RateLimit wraps an http.Handler to limit incoming requests.
// Requests that are not limited will be passed to the handler
// unchanged. Limited requests will be passed to the DeniedHandler.
// X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and
// Retry-After headers will be written to the response based on the
// values in the RateLimitResult.
func (t *HTTPRateLimiter) RateLimit(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if t.RateLimiter == nil {
t.error(w, r, errors.New("You must set a RateLimiter on HTTPRateLimiter"))
}
var k string
if t.VaryBy != nil {
k = t.VaryBy.Key(r)
}
limited, context, err := t.RateLimiter.RateLimit(k, 1)
if err != nil {
t.error(w, r, err)
return
}
setRateLimitHeaders(w, context)
if !limited {
h.ServeHTTP(w, r)
} else {
dh := t.DeniedHandler
if dh == nil {
dh = DefaultDeniedHandler
}
dh.ServeHTTP(w, r)
}
})
}
func (t *HTTPRateLimiter) error(w http.ResponseWriter, r *http.Request, err error) {
e := t.Error
if e == nil {
e = DefaultError
}
e(w, r, err)
}
func setRateLimitHeaders(w http.ResponseWriter, context RateLimitResult) {
if v := context.Limit; v >= 0 {
w.Header().Add("X-RateLimit-Limit", strconv.Itoa(v))
}
if v := context.Remaining; v >= 0 {
w.Header().Add("X-RateLimit-Remaining", strconv.Itoa(v))
}
if v := context.ResetAfter; v >= 0 {
vi := int(math.Ceil(v.Seconds()))
w.Header().Add("X-RateLimit-Reset", strconv.Itoa(vi))
}
if v := context.RetryAfter; v >= 0 {
vi := int(math.Ceil(v.Seconds()))
w.Header().Add("Retry-After", strconv.Itoa(vi))
}
}
package throttled_test
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/throttled/throttled"
)
type stubLimiter struct {
}
func (sl *stubLimiter) RateLimit(key string, quantity int) (bool, throttled.RateLimitResult, error) {
switch key {
case "limit":
result := throttled.RateLimitResult{
Limit: -1,
Remaining: -1,
ResetAfter: -1,
RetryAfter: time.Minute,
}
return true, result, nil
case "error":
result := throttled.RateLimitResult{}
return false, result, errors.New("stubLimiter error")
default:
result := throttled.RateLimitResult{
Limit: 1,
Remaining: 2,
ResetAfter: time.Minute,
RetryAfter: -1,
}
return false, result, nil
}
}
type pathGetter struct{}
func (*pathGetter) Key(r *http.Request) string {
return r.URL.Path
}
type httpTestCase struct {
path string
code int
headers map[string]string
}
func TestHTTPRateLimiter(t *testing.T) {
limiter := throttled.HTTPRateLimiter{
RateLimiter: &stubLimiter{},
VaryBy: &pathGetter{},
}
handler := limiter.RateLimit(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
runHTTPTestCases(t, handler, []httpTestCase{
{"ok", 200, map[string]string{"X-Ratelimit-Limit": "1", "X-Ratelimit-Remaining": "2", "X-Ratelimit-Reset": "60"}},
{"error", 500, map[string]string{}},
{"limit", 429, map[string]string{"Retry-After": "60"}},
})
}
func TestCustomHTTPRateLimiterHandlers(t *testing.T) {
limiter := throttled.HTTPRateLimiter{
RateLimiter: &stubLimiter{},
VaryBy: &pathGetter{},
DeniedHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "custom limit exceeded", 400)
}),
Error: func(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, "custom internal error", 501)
},
}
handler := limiter.RateLimit(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
runHTTPTestCases(t, handler, []httpTestCase{
{"limit", 400, map[string]string{}},
{"error", 501, map[string]string{}},
})
}
func runHTTPTestCases(t *testing.T, h http.Handler, cs []httpTestCase) {
for i, c := range cs {
req, err := http.NewRequest("GET", c.path, nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if have, want := rr.Code, c.code; have != want {
t.Errorf("Expected request %d at %s to return %d but got %d",
i, c.path, want, have)
}
for name, want := range c.headers {
if have := rr.HeaderMap.Get(name); have != want {
t.Errorf("Expected request %d at %s to have header '%s: %s' but got '%s'",
i, c.path, name, want, have)
}
}
}
}
package throttled
import (
"fmt"
"time"
)
const (
// Maximum number of times to retry SetIfNotExists/CompareAndSwap operations
// before returning an error.
maxCASAttempts = 10
)
// A RateLimiter manages limiting the rate of actions by key.
type RateLimiter interface {
// RateLimit checks whether a particular key has exceeded a rate
// limit. It also returns a RateLimitResult to provide additional
// information about the state of the RateLimiter.
//
// If the rate limit has not been exceeded, the underlying storage
// is updated by the supplied quantity. For example, a quantity of
// 1 might be used to rate limit a single request while a greater
// quantity could rate limit based on the size of a file upload in
// megabytes. If quantity is 0, no update is performed allowing
// you to "peek" at the state of the RateLimiter for a given key.
RateLimit(key string, quantity int) (bool, RateLimitResult, error)
}
// RateLimitResult represents the state of the RateLimiter for a
// given key at the time of the query. This state can be used, for
// example, to communicate information to the client via HTTP
// headers. Negative values indicate that the attribute is not
// relevant to the implementation or state.
type RateLimitResult struct {
// Limit is the maximum number of requests that could be permitted
// instantaneously for this key starting from an empty state. For
// example, if a rate limiter allows 10 requests per second per
// key, Limit would always be 10.
Limit int
// Remaining is the maximum number of requests that could be
// permitted instantaneously for this key given the current
// state. For example, if a rate limiter allows 10 requests per
// second and has already received 6 requests for this key this
// second, Remaining would be 4.
Remaining int
// ResetAfter is the time until the RateLimiter returns to its
// initial state for a given key. For example, if a rate limiter
// manages requests per second and received one request 200ms ago,
// Reset would return 800ms. You can also think of this as the time
// until Limit and Remaining will be equal.
ResetAfter time.Duration
// RetryAfter is the time until the next request will be permitted.
// It should be -1 unless the rate limit has been exceeded.
RetryAfter time.Duration
}
type limitResult struct {
limited bool
}
func (r *limitResult) Limited() bool { return r.limited }
type rateLimitResult struct {
limitResult
limit, remaining int
reset, retryAfter time.Duration
}
func (r *rateLimitResult) Limit() int { return r.limit }
func (r *rateLimitResult) Remaining() int { return r.remaining }
func (r *rateLimitResult) Reset() time.Duration { return r.reset }
func (r *rateLimitResult) RetryAfter() time.Duration { return r.retryAfter }
// Rate describes a frequency of an activity such as the number of requests
// allowed per minute.
type Rate struct {
period time.Duration // Time between equally spaced requests at the rate
count int // Used internally for deprecated `RateLimit` interface only
}
// RateQuota describes the number of requests allowed per time period.
// MaxRate specified the maximum sustained rate of requests and must
// be greater than zero. MaxBurst defines the number of requests that
// will be allowed to exceed the rate in a single burst and must be
// greater than or equal to zero.
//
// Rate{PerSec(1), 0} would mean that after each request, no more
// requests will be permitted for that client for one second.
// Rate{PerSec(2), 0} permits one request per 0.5 seconds rather than
// two requests in one second. In practice, you probably want to set
// MaxBurst >0 to provide some flexibility to clients that only need
// to make a handful of requests. In fact a MaxBurst of zero will
// *never* permit a request with a quantity greater than one because
// it will immediately exceed the limit.