summaryrefslogtreecommitdiffstats
path: root/vendor/gopkg.in/throttled/throttled.v2
diff options
context:
space:
mode:
authorChristopher Speller <crspeller@gmail.com>2016-09-23 10:17:51 -0400
committerGitHub <noreply@github.com>2016-09-23 10:17:51 -0400
commit2ca0e8f9a0f9863555a26e984cde15efff9ef8f8 (patch)
treedaae1ee67b14a3d0a84424f2a304885d9e75ce2b /vendor/gopkg.in/throttled/throttled.v2
parent6d62d65b2dc85855aabea036cbd44f6059e19d13 (diff)
downloadchat-2ca0e8f9a0f9863555a26e984cde15efff9ef8f8.tar.gz
chat-2ca0e8f9a0f9863555a26e984cde15efff9ef8f8.tar.bz2
chat-2ca0e8f9a0f9863555a26e984cde15efff9ef8f8.zip
Updating golang dependancies (#4075)
Diffstat (limited to 'vendor/gopkg.in/throttled/throttled.v2')
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/.gitignore5
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/.travis.yml24
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/LICENSE12
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/Makefile31
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/README.md85
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/deprecated.go73
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/deprecated_test.go59
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/doc.go3
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/example_test.go103
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/http.go110
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/http_test.go99
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/rate.go239
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/rate_test.go128
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/store.go34
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/store/deprecated.go32
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/store/memstore/memstore.go127
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/store/memstore/memstore_test.go40
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/store/redigostore/redigostore.go156
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/store/redigostore/redisstore_test.go85
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/store/storetest/doc.go2
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/store/storetest/storetest.go176
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/varyby.go78
-rw-r--r--vendor/gopkg.in/throttled/throttled.v2/varyby_test.go58
23 files changed, 1759 insertions, 0 deletions
diff --git a/vendor/gopkg.in/throttled/throttled.v2/.gitignore b/vendor/gopkg.in/throttled/throttled.v2/.gitignore
new file mode 100644
index 000000000..96c4e10b7
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/.gitignore
@@ -0,0 +1,5 @@
+.DS_Store
+*.swp
+*.swo
+*.test
+*.out
diff --git a/vendor/gopkg.in/throttled/throttled.v2/.travis.yml b/vendor/gopkg.in/throttled/throttled.v2/.travis.yml
new file mode 100644
index 000000000..29225d7af
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/.travis.yml
@@ -0,0 +1,24 @@
+sudo: false
+
+language: go
+
+go:
+ - 1.3
+ - tip
+
+notifications:
+ email: false
+
+services:
+ - redis-server
+
+install:
+ - make get-deps
+ # Move to the gopkg location that rather than the default clone location
+ # otherwise Go 1.4+ complains about incorrect import paths.
+ - export GOPKG_DIR=$GOPATH/src/gopkg.in/throttled
+ - mkdir -p $GOPKG_DIR
+ - mv $TRAVIS_BUILD_DIR $GOPKG_DIR/throttled.v2
+ - cd $GOPKG_DIR/throttled.v2
+
+script: make
diff --git a/vendor/gopkg.in/throttled/throttled.v2/LICENSE b/vendor/gopkg.in/throttled/throttled.v2/LICENSE
new file mode 100644
index 000000000..f9616483e
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/LICENSE
@@ -0,0 +1,12 @@
+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.
diff --git a/vendor/gopkg.in/throttled/throttled.v2/Makefile b/vendor/gopkg.in/throttled/throttled.v2/Makefile
new file mode 100644
index 000000000..cfc235c31
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/Makefile
@@ -0,0 +1,31 @@
+
+
+.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 .
+ifneq ($(TRAVIS_GO_VERSION),1.3) # go vet doesn't play nicely on 1.3
+ go vet ./...
+endif
+ 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/garyburd/redigo/redis
+ go get github.com/hashicorp/golang-lru
+ go get github.com/golang/lint/golint
+
+.go-test:
+ go test ./...
+
+.go-test-cover:
+ go test -coverprofile=throttled.coverage.out .
+ go test -coverprofile=store.coverage.out ./store
diff --git a/vendor/gopkg.in/throttled/throttled.v2/README.md b/vendor/gopkg.in/throttled/throttled.v2/README.md
new file mode 100644
index 000000000..b18dcbcc6
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/README.md
@@ -0,0 +1,85 @@
+# Throttled [![build status](https://secure.travis-ci.org/throttled/throttled.png)](https://travis-ci.org/throttled/throttled) [![GoDoc](https://godoc.org/gopkg.in/throttled/throttled.v2?status.png)](https://godoc.org/gopkg.in/throttled/throttled.v2)
+
+Package throttled implements rate limiting 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
+
+throttled uses gopkg.in for semantic versioning:
+`go get gopkg.in/throttled/throttled.v2`
+
+As of July 27, 2015, the package is located under its own Github
+organization. Please adjust your imports to
+`gopkg.in/throttled/throttled.v2`.
+
+The 1.x release series is compatible with the original, unversioned
+library written by [Martin Angers][puerkitobio]. There is a
+[blog post explaining that version's usage on 0value.com][blog].
+
+## 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:
+
+ 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))
+
+## Contributing
+
+Since throttled uses gopkg.in for versioning, running `go get` against
+a fork or cloning from Github to the default path will break
+imports. Instead, use the following process for setting up your
+environment and contributing:
+
+```sh
+# Retrieve the source and dependencies.
+go get gopkg.in/throttled/throttled.v2/...
+
+# Fork the project on Github. For all following directions replace
+# <username> with your Github username. Add your fork as a remote.
+cd $GOPATH/src/gopkg.in/throttled/throttled.v2
+git remote add fork git@github.com:<username>/throttled.git
+
+# Create a branch, make your changes, test them and commit.
+git checkout -b my-new-feature
+# <do some work>
+make test
+git commit -a
+git push -u fork my-new-feature
+```
+
+When your changes are ready, [open a pull request][pr] using "compare
+across forks".
+
+## 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/gopkg.in/throttled/throttled.v2
+[puerkitobio]: https://github.com/puerkitobio/
+[pr]: https://github.com/throttled/throttled/compare
diff --git a/vendor/gopkg.in/throttled/throttled.v2/deprecated.go b/vendor/gopkg.in/throttled/throttled.v2/deprecated.go
new file mode 100644
index 000000000..f2c648a3e
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/deprecated.go
@@ -0,0 +1,73 @@
+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
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/deprecated_test.go b/vendor/gopkg.in/throttled/throttled.v2/deprecated_test.go
new file mode 100644
index 000000000..93406648f
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/deprecated_test.go
@@ -0,0 +1,59 @@
+package throttled_test
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "gopkg.in/throttled/throttled.v2"
+ "gopkg.in/throttled/throttled.v2/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)
+ }
+ }
+ }
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/doc.go b/vendor/gopkg.in/throttled/throttled.v2/doc.go
new file mode 100644
index 000000000..302c2bed7
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/doc.go
@@ -0,0 +1,3 @@
+// Package throttled implements rate limiting access to resources such
+// as HTTP endpoints.
+package throttled // import "gopkg.in/throttled/throttled.v2"
diff --git a/vendor/gopkg.in/throttled/throttled.v2/example_test.go b/vendor/gopkg.in/throttled/throttled.v2/example_test.go
new file mode 100644
index 000000000..66e6374be
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/example_test.go
@@ -0,0 +1,103 @@
+package throttled_test
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+
+ "gopkg.in/throttled/throttled.v2"
+ "gopkg.in/throttled/throttled.v2/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{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))
+}
+
+// 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{throttled.PerHour(1), 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.
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/http.go b/vendor/gopkg.in/throttled/throttled.v2/http.go
new file mode 100644
index 000000000..4c513a81d
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/http.go
@@ -0,0 +1,110 @@
+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))
+ }
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/http_test.go b/vendor/gopkg.in/throttled/throttled.v2/http_test.go
new file mode 100644
index 000000000..42761da09
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/http_test.go
@@ -0,0 +1,99 @@
+package throttled_test
+
+import (
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "gopkg.in/throttled/throttled.v2"
+)
+
+type stubLimiter struct {
+}
+
+func (sl *stubLimiter) RateLimit(key string, quantity int) (bool, throttled.RateLimitResult, error) {
+ switch key {
+ case "limit":
+ return true, throttled.RateLimitResult{-1, -1, -1, time.Minute}, nil
+ case "error":
+ return false, throttled.RateLimitResult{}, errors.New("stubLimiter error")
+ default:
+ return false, throttled.RateLimitResult{1, 2, time.Minute, -1}, 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)
+ }
+ }
+ }
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/rate.go b/vendor/gopkg.in/throttled/throttled.v2/rate.go
new file mode 100644
index 000000000..8c11cdb47
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/rate.go
@@ -0,0 +1,239 @@
+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. 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.
+type RateQuota struct {
+ MaxRate Rate
+ MaxBurst int
+}
+
+// PerSec represents a number of requests per second.
+func PerSec(n int) Rate { return Rate{time.Second / time.Duration(n), n} }
+
+// PerMin represents a number of requests per minute.
+func PerMin(n int) Rate { return Rate{time.Minute / time.Duration(n), n} }
+
+// PerHour represents a number of requests per hour.
+func PerHour(n int) Rate { return Rate{time.Hour / time.Duration(n), n} }
+
+// PerDay represents a number of requests per day.
+func PerDay(n int) Rate { return Rate{24 * time.Hour / time.Duration(n), n} }
+
+// GCRARateLimiter is a RateLimiter that users the generic cell-rate
+// algorithm. The algorithm has been slightly modified from its usual
+// form to support limiting with an additional quantity parameter, such
+// as for limiting the number of bytes uploaded.
+type GCRARateLimiter struct {
+ limit int
+ // Think of the DVT as our flexibility:
+ // How far can you deviate from the nominal equally spaced schedule?
+ // If you like leaky buckets, think about it as the size of your bucket.
+ delayVariationTolerance time.Duration
+ // Think of the emission interval as the time between events
+ // in the nominal equally spaced schedule. If you like leaky buckets,
+ // think of it as how frequently the bucket leaks one unit.
+ emissionInterval time.Duration
+
+ store GCRAStore
+}
+
+// NewGCRARateLimiter creates a GCRARateLimiter. quota.Count defines
+// the maximum number of requests permitted in an instantaneous burst
+// and quota.Count / quota.Period defines the maximum sustained
+// rate. For example, PerMin(60) permits 60 requests instantly per key
+// followed by one request per second indefinitely whereas PerSec(1)
+// only permits one request per second with no tolerance for bursts.
+func NewGCRARateLimiter(st GCRAStore, quota RateQuota) (*GCRARateLimiter, error) {
+ if quota.MaxBurst < 0 {
+ return nil, fmt.Errorf("Invalid RateQuota %#v. MaxBurst must be greater than zero.", quota)
+ }
+ if quota.MaxRate.period <= 0 {
+ return nil, fmt.Errorf("Invalid RateQuota %#v. MaxRate must be greater than zero.", quota)
+ }
+
+ return &GCRARateLimiter{
+ delayVariationTolerance: quota.MaxRate.period * (time.Duration(quota.MaxBurst) + 1),
+ emissionInterval: quota.MaxRate.period,
+ limit: quota.MaxBurst + 1,
+ store: st,
+ }, nil
+}
+
+// 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.
+func (g *GCRARateLimiter) RateLimit(key string, quantity int) (bool, RateLimitResult, error) {
+ var tat, newTat, now time.Time
+ var ttl time.Duration
+ rlc := RateLimitResult{Limit: g.limit, RetryAfter: -1}
+ limited := false
+
+ i := 0
+ for {
+ var err error
+ var tatVal int64
+ var updated bool
+
+ // tat refers to the theoretical arrival time that would be expected
+ // from equally spaced requests at exactly the rate limit.
+ tatVal, now, err = g.store.GetWithTime(key)
+ if err != nil {
+ return false, rlc, err
+ }
+
+ if tatVal == -1 {
+ tat = now
+ } else {
+ tat = time.Unix(0, tatVal)
+ }
+
+ increment := time.Duration(quantity) * g.emissionInterval
+ if now.After(tat) {
+ newTat = now.Add(increment)
+ } else {
+ newTat = tat.Add(increment)
+ }
+
+ // Block the request if the next permitted time is in the future
+ allowAt := newTat.Add(-(g.delayVariationTolerance))
+ if diff := now.Sub(allowAt); diff < 0 {
+ if increment <= g.delayVariationTolerance {
+ rlc.RetryAfter = -diff
+ }
+ ttl = tat.Sub(now)
+ limited = true
+ break
+ }
+
+ ttl = newTat.Sub(now)
+
+ if tatVal == -1 {
+ updated, err = g.store.SetIfNotExistsWithTTL(key, newTat.UnixNano(), ttl)
+ } else {
+ updated, err = g.store.CompareAndSwapWithTTL(key, tatVal, newTat.UnixNano(), ttl)
+ }
+
+ if err != nil {
+ return false, rlc, err
+ }
+ if updated {
+ break
+ }
+
+ i++
+ if i > maxCASAttempts {
+ return false, rlc, fmt.Errorf(
+ "Failed to store updated rate limit data for key %s after %d attempts",
+ key, i,
+ )
+ }
+ }
+
+ next := g.delayVariationTolerance - ttl
+ if next > -g.emissionInterval {
+ rlc.Remaining = int(next / g.emissionInterval)
+ }
+ rlc.ResetAfter = ttl
+
+ return limited, rlc, nil
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/rate_test.go b/vendor/gopkg.in/throttled/throttled.v2/rate_test.go
new file mode 100644
index 000000000..292a050bc
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/rate_test.go
@@ -0,0 +1,128 @@
+package throttled_test
+
+import (
+ "testing"
+ "time"
+
+ "gopkg.in/throttled/throttled.v2"
+ "gopkg.in/throttled/throttled.v2/store/memstore"
+)
+
+const deniedStatus = 429
+
+type testStore struct {
+ store throttled.GCRAStore
+
+ clock time.Time
+ failUpdates bool
+}
+
+func (ts *testStore) GetWithTime(key string) (int64, time.Time, error) {
+ v, _, e := ts.store.GetWithTime(key)
+ return v, ts.clock, e
+}
+
+func (ts *testStore) SetIfNotExistsWithTTL(key string, value int64, ttl time.Duration) (bool, error) {
+ if ts.failUpdates {
+ return false, nil
+ }
+ return ts.store.SetIfNotExistsWithTTL(key, value, ttl)
+}
+
+func (ts *testStore) CompareAndSwapWithTTL(key string, old, new int64, ttl time.Duration) (bool, error) {
+ if ts.failUpdates {
+ return false, nil
+ }
+ return ts.store.CompareAndSwapWithTTL(key, old, new, ttl)
+}
+
+func TestRateLimit(t *testing.T) {
+ limit := 5
+ rq := throttled.RateQuota{throttled.PerSec(1), limit - 1}
+ start := time.Unix(0, 0)
+ cases := []struct {
+ now time.Time
+ volume, remaining int
+ reset, retry time.Duration
+ limited bool
+ }{
+ // You can never make a request larger than the maximum
+ 0: {start, 6, 5, 0, -1, true},
+ // Rate limit normal requests appropriately
+ 1: {start, 1, 4, time.Second, -1, false},
+ 2: {start, 1, 3, 2 * time.Second, -1, false},
+ 3: {start, 1, 2, 3 * time.Second, -1, false},
+ 4: {start, 1, 1, 4 * time.Second, -1, false},
+ 5: {start, 1, 0, 5 * time.Second, -1, false},
+ 6: {start, 1, 0, 5 * time.Second, time.Second, true},
+ 7: {start.Add(3000 * time.Millisecond), 1, 2, 3000 * time.Millisecond, -1, false},
+ 8: {start.Add(3100 * time.Millisecond), 1, 1, 3900 * time.Millisecond, -1, false},
+ 9: {start.Add(4000 * time.Millisecond), 1, 1, 4000 * time.Millisecond, -1, false},
+ 10: {start.Add(8000 * time.Millisecond), 1, 4, 1000 * time.Millisecond, -1, false},
+ 11: {start.Add(9500 * time.Millisecond), 1, 4, 1000 * time.Millisecond, -1, false},
+ // Zero-volume request just peeks at the state
+ 12: {start.Add(9500 * time.Millisecond), 0, 4, time.Second, -1, false},
+ // High-volume request uses up more of the limit
+ 13: {start.Add(9500 * time.Millisecond), 2, 2, 3 * time.Second, -1, false},
+ // Large requests cannot exceed limits
+ 14: {start.Add(9500 * time.Millisecond), 5, 2, 3 * time.Second, 3 * time.Second, true},
+ }
+
+ mst, err := memstore.New(0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ st := testStore{store: mst}
+
+ rl, err := throttled.NewGCRARateLimiter(&st, rq)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Start the server
+ for i, c := range cases {
+ st.clock = c.now
+
+ limited, context, err := rl.RateLimit("foo", c.volume)
+ if err != nil {
+ t.Fatalf("%d: %#v", i, err)
+ }
+
+ if limited != c.limited {
+ t.Errorf("%d: expected Limited to be %t but got %t", i, c.limited, limited)
+ }
+
+ if have, want := context.Limit, limit; have != want {
+ t.Errorf("%d: expected Limit to be %d but got %d", i, want, have)
+ }
+
+ if have, want := context.Remaining, c.remaining; have != want {
+ t.Errorf("%d: expected Remaining to be %d but got %d", i, want, have)
+ }
+
+ if have, want := context.ResetAfter, c.reset; have != want {
+ t.Errorf("%d: expected ResetAfter to be %s but got %s", i, want, have)
+ }
+
+ if have, want := context.RetryAfter, c.retry; have != want {
+ t.Errorf("%d: expected RetryAfter to be %d but got %d", i, want, have)
+ }
+ }
+}
+
+func TestRateLimitUpdateFailures(t *testing.T) {
+ rq := throttled.RateQuota{throttled.PerSec(1), 1}
+ mst, err := memstore.New(0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ st := testStore{store: mst, failUpdates: true}
+ rl, err := throttled.NewGCRARateLimiter(&st, rq)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, _, err := rl.RateLimit("foo", 1); err == nil {
+ t.Error("Expected limiting to fail when store updates fail")
+ }
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/store.go b/vendor/gopkg.in/throttled/throttled.v2/store.go
new file mode 100644
index 000000000..a26bbc2c5
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/store.go
@@ -0,0 +1,34 @@
+package throttled
+
+import (
+ "time"
+)
+
+// GCRAStore is the interface to implement to store state for a GCRA
+// rate limiter
+type GCRAStore interface {
+ // GetWithTime returns the value of the key if it is in the store
+ // or -1 if it does not exist. It also returns the current time at
+ // the Store. The time must be representable as a positive int64
+ // of nanoseconds since the epoch.
+ //
+ // GCRA assumes that all instances sharing the same Store also
+ // share the same clock. Using separate clocks will work if the
+ // skew is small but not recommended in practice unless you're
+ // lucky enough to be hooked up to GPS or atomic clocks.
+ GetWithTime(key string) (int64, time.Time, error)
+
+ // SetIfNotExistsWithTTL sets the value of key only if it is not
+ // already set in the store it returns whether a new value was
+ // set. If the store supports expiring keys and a new value was
+ // set, the key will expire after the provided ttl.
+ SetIfNotExistsWithTTL(key string, value int64, ttl time.Duration) (bool, error)
+
+ // CompareAndSwapWithTTL atomically compares the value at key to
+ // the old value. If it matches, it sets it to the new value and
+ // returns true. Otherwise, it returns false. If the key does not
+ // exist in the store, it returns false with no error. If the
+ // store supports expiring keys and the swap succeeded, the key
+ // will expire after the provided ttl.
+ CompareAndSwapWithTTL(key string, old, new int64, ttl time.Duration) (bool, error)
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/store/deprecated.go b/vendor/gopkg.in/throttled/throttled.v2/store/deprecated.go
new file mode 100644
index 000000000..5476e87ac
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/store/deprecated.go
@@ -0,0 +1,32 @@
+// Package store contains deprecated aliases for subpackages
+package store // import "gopkg.in/throttled/throttled.v2/store"
+
+import (
+ "github.com/garyburd/redigo/redis"
+
+ "gopkg.in/throttled/throttled.v2/store/memstore"
+ "gopkg.in/throttled/throttled.v2/store/redigostore"
+)
+
+// DEPRECATED. NewMemStore is a compatible alias for mem.New
+func NewMemStore(maxKeys int) *memstore.MemStore {
+ st, err := memstore.New(maxKeys)
+ if err != nil {
+ // As of this writing, `lru.New` can only return an error if you pass
+ // maxKeys <= 0 so this should never occur.
+ panic(err)
+ }
+ return st
+}
+
+// DEPRECATED. NewMemStore is a compatible alias for redis.New
+func NewRedisStore(pool *redis.Pool, keyPrefix string, db int) *redigostore.RedigoStore {
+ st, err := redigostore.New(pool, keyPrefix, db)
+ if err != nil {
+ // As of this writing, creating a Redis store never returns an error
+ // so this should be safe while providing some ability to return errors
+ // in the future.
+ panic(err)
+ }
+ return st
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/store/memstore/memstore.go b/vendor/gopkg.in/throttled/throttled.v2/store/memstore/memstore.go
new file mode 100644
index 000000000..5d8fee8b5
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/store/memstore/memstore.go
@@ -0,0 +1,127 @@
+// Package memstore offers an in-memory store implementation for throttled.
+package memstore // import "gopkg.in/throttled/throttled.v2/store/memstore"
+
+import (
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/hashicorp/golang-lru"
+)
+
+// MemStore is an in-memory store implementation for throttled. It
+// supports evicting the least recently used keys to control memory
+// usage. It is stored in memory in the current process and thus
+// doesn't share state with other rate limiters.
+type MemStore struct {
+ sync.RWMutex
+ keys *lru.Cache
+ m map[string]*int64
+}
+
+// New initializes a Store. If maxKeys > 0, the number of different
+// keys is restricted to the specified amount. In this case, it uses
+// an LRU algorithm to evict older keys to make room for newer
+// ones. If maxKeys <= 0, there is no limit on the number of keys,
+// which may use an unbounded amount of memory.
+func New(maxKeys int) (*MemStore, error) {
+ var m *MemStore
+
+ if maxKeys > 0 {
+ keys, err := lru.New(maxKeys)
+ if err != nil {
+ return nil, err
+ }
+
+ m = &MemStore{
+ keys: keys,
+ }
+ } else {
+ m = &MemStore{
+ m: make(map[string]*int64),
+ }
+ }
+ return m, nil
+}
+
+// GetWithTime returns the value of the key if it is in the store or
+// -1 if it does not exist. It also returns the current local time on
+// the machine.
+func (ms *MemStore) GetWithTime(key string) (int64, time.Time, error) {
+ now := time.Now()
+ valP, ok := ms.get(key, false)
+
+ if !ok {
+ return -1, now, nil
+ }
+
+ return atomic.LoadInt64(valP), now, nil
+}
+
+// SetIfNotExistsWithTTL sets the value of key only if it is not
+// already set in the store it returns whether a new value was set. It
+// ignores the ttl.
+func (ms *MemStore) SetIfNotExistsWithTTL(key string, value int64, _ time.Duration) (bool, error) {
+ _, ok := ms.get(key, false)
+
+ if ok {
+ return false, nil
+ }
+
+ ms.Lock()
+ defer ms.Unlock()
+
+ _, ok = ms.get(key, true)
+
+ if ok {
+ return false, nil
+ }
+
+ // Store a pointer to a new instance so that the caller
+ // can't mutate the value after setting
+ v := value
+
+ if ms.keys != nil {
+ ms.keys.Add(key, &v)
+ } else {
+ ms.m[key] = &v
+ }
+
+ return true, nil
+}
+
+// CompareAndSwapWithTTL atomically compares the value at key to the
+// old value. If it matches, it sets it to the new value and returns
+// true. Otherwise, it returns false. If the key does not exist in the
+// store, it returns false with no error. It ignores the ttl.
+func (ms *MemStore) CompareAndSwapWithTTL(key string, old, new int64, _ time.Duration) (bool, error) {
+ valP, ok := ms.get(key, false)
+
+ if !ok {
+ return false, nil
+ }
+
+ return atomic.CompareAndSwapInt64(valP, old, new), nil
+}
+
+func (ms *MemStore) get(key string, locked bool) (*int64, bool) {
+ var valP *int64
+ var ok bool
+
+ if ms.keys != nil {
+ var valI interface{}
+
+ valI, ok = ms.keys.Get(key)
+ if ok {
+ valP = valI.(*int64)
+ }
+ } else {
+ if !locked {
+ ms.RLock()
+ defer ms.RUnlock()
+ }
+ valP, ok = ms.m[key]
+ }
+
+ return valP, ok
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/store/memstore/memstore_test.go b/vendor/gopkg.in/throttled/throttled.v2/store/memstore/memstore_test.go
new file mode 100644
index 000000000..ef003d3de
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/store/memstore/memstore_test.go
@@ -0,0 +1,40 @@
+package memstore_test
+
+import (
+ "testing"
+
+ "gopkg.in/throttled/throttled.v2/store/memstore"
+ "gopkg.in/throttled/throttled.v2/store/storetest"
+)
+
+func TestMemStoreLRU(t *testing.T) {
+ st, err := memstore.New(10)
+ if err != nil {
+ t.Fatal(err)
+ }
+ storetest.TestGCRAStore(t, st)
+}
+
+func TestMemStoreUnlimited(t *testing.T) {
+ st, err := memstore.New(10)
+ if err != nil {
+ t.Fatal(err)
+ }
+ storetest.TestGCRAStore(t, st)
+}
+
+func BenchmarkMemStoreLRU(b *testing.B) {
+ st, err := memstore.New(10)
+ if err != nil {
+ b.Fatal(err)
+ }
+ storetest.BenchmarkGCRAStore(b, st)
+}
+
+func BenchmarkMemStoreUnlimited(b *testing.B) {
+ st, err := memstore.New(0)
+ if err != nil {
+ b.Fatal(err)
+ }
+ storetest.BenchmarkGCRAStore(b, st)
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/store/redigostore/redigostore.go b/vendor/gopkg.in/throttled/throttled.v2/store/redigostore/redigostore.go
new file mode 100644
index 000000000..54208fa6d
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/store/redigostore/redigostore.go
@@ -0,0 +1,156 @@
+// Package redigostore offers Redis-based store implementation for throttled using redigo.
+package redigostore // import "gopkg.in/throttled/throttled.v2/store/redigostore"
+
+import (
+ "strings"
+ "time"
+
+ "github.com/garyburd/redigo/redis"
+)
+
+const (
+ redisCASMissingKey = "key does not exist"
+ redisCASScript = `
+local v = redis.call('get', KEYS[1])
+if v == false then
+ return redis.error_reply("key does not exist")
+end
+if v ~= ARGV[1] then
+ return 0
+end
+if ARGV[3] ~= "0" then
+ redis.call('setex', KEYS[1], ARGV[3], ARGV[2])
+else
+ redis.call('set', KEYS[1], ARGV[2])
+end
+return 1
+`
+)
+
+// RedigoStore implements a Redis-based store using redigo.
+type RedigoStore struct {
+ pool *redis.Pool
+ prefix string
+ db int
+}
+
+// New creates a new Redis-based store, using the provided pool to get
+// its connections. The keys will have the specified keyPrefix, which
+// may be an empty string, and the database index specified by db will
+// be selected to store the keys. Any updating operations will reset
+// the key TTL to the provided value rounded down to the nearest
+// second. Depends on Redis 2.6+ for EVAL support.
+func New(pool *redis.Pool, keyPrefix string, db int) (*RedigoStore, error) {
+ return &RedigoStore{
+ pool: pool,
+ prefix: keyPrefix,
+ db: db,
+ }, nil
+}
+
+// GetWithTime returns the value of the key if it is in the store
+// or -1 if it does not exist. It also returns the current time at
+// the redis server to microsecond precision.
+func (r *RedigoStore) GetWithTime(key string) (int64, time.Time, error) {
+ var now time.Time
+
+ key = r.prefix + key
+
+ conn, err := r.getConn()
+ if err != nil {
+ return 0, now, err
+ }
+ defer conn.Close()
+
+ conn.Send("TIME")
+ conn.Send("GET", key)
+ conn.Flush()
+ timeReply, err := redis.Values(conn.Receive())
+ if err != nil {
+ return 0, now, err
+ }
+
+ var s, us int64
+ if _, err := redis.Scan(timeReply, &s, &us); err != nil {
+ return 0, now, err
+ }
+ now = time.Unix(s, us*int64(time.Microsecond))
+
+ v, err := redis.Int64(conn.Receive())
+ if err == redis.ErrNil {
+ return -1, now, nil
+ } else if err != nil {
+ return 0, now, err
+ }
+
+ return v, now, nil
+}
+
+// SetIfNotExistsWithTTL sets the value of key only if it is not
+// already set in the store it returns whether a new value was set.
+// If a new value was set, the ttl in the key is also set, though this
+// operation is not performed atomically.
+func (r *RedigoStore) SetIfNotExistsWithTTL(key string, value int64, ttl time.Duration) (bool, error) {
+ key = r.prefix + key
+
+ conn, err := r.getConn()
+ if err != nil {
+ return false, err
+ }
+ defer conn.Close()
+
+ v, err := redis.Int64(conn.Do("SETNX", key, value))
+ if err != nil {
+ return false, err
+ }
+
+ updated := v == 1
+
+ if ttl >= time.Second {
+ if _, err := conn.Do("EXPIRE", key, int(ttl.Seconds())); err != nil {
+ return updated, err
+ }
+ }
+
+ return updated, nil
+}
+
+// CompareAndSwapWithTTL atomically compares the value at key to the
+// old value. If it matches, it sets it to the new value and returns
+// true. Otherwise, it returns false. If the key does not exist in the
+// store, it returns false with no error. If the swap succeeds, the
+// ttl for the key is updated atomically.
+func (r *RedigoStore) CompareAndSwapWithTTL(key string, old, new int64, ttl time.Duration) (bool, error) {
+ key = r.prefix + key
+ conn, err := r.getConn()
+ if err != nil {
+ return false, err
+ }
+ defer conn.Close()
+
+ swapped, err := redis.Bool(conn.Do("EVAL", redisCASScript, 1, key, old, new, int(ttl.Seconds())))
+ if err != nil {
+ if strings.Contains(err.Error(), redisCASMissingKey) {
+ return false, nil
+ }
+
+ return false, err
+ }
+
+ return swapped, nil
+}
+
+// Select the specified database index.
+func (r *RedigoStore) getConn() (redis.Conn, error) {
+ conn := r.pool.Get()
+
+ // Select the specified database
+ if r.db > 0 {
+ if _, err := redis.String(conn.Do("SELECT", r.db)); err != nil {
+ conn.Close()
+ return nil, err
+ }
+ }
+
+ return conn, nil
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/store/redigostore/redisstore_test.go b/vendor/gopkg.in/throttled/throttled.v2/store/redigostore/redisstore_test.go
new file mode 100644
index 000000000..d47b635d2
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/store/redigostore/redisstore_test.go
@@ -0,0 +1,85 @@
+package redigostore_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/garyburd/redigo/redis"
+
+ "gopkg.in/throttled/throttled.v2/store/redigostore"
+ "gopkg.in/throttled/throttled.v2/store/storetest"
+)
+
+const (
+ redisTestDB = 1
+ redisTestPrefix = "throttled:"
+)
+
+func getPool() *redis.Pool {
+ pool := &redis.Pool{
+ MaxIdle: 3,
+ IdleTimeout: 30 * time.Second,
+ Dial: func() (redis.Conn, error) {
+ return redis.Dial("tcp", ":6379")
+ },
+ TestOnBorrow: func(c redis.Conn, t time.Time) error {
+ _, err := c.Do("PING")
+ return err
+ },
+ }
+ return pool
+}
+
+func TestRedisStore(t *testing.T) {
+ c, st := setupRedis(t, 0)
+ defer c.Close()
+ defer clearRedis(c)
+
+ clearRedis(c)
+ storetest.TestGCRAStore(t, st)
+ storetest.TestGCRAStoreTTL(t, st)
+}
+
+func BenchmarkRedisStore(b *testing.B) {
+ c, st := setupRedis(b, 0)
+ defer c.Close()
+ defer clearRedis(c)
+
+ storetest.BenchmarkGCRAStore(b, st)
+}
+
+func clearRedis(c redis.Conn) error {
+ keys, err := redis.Values(c.Do("KEYS", redisTestPrefix+"*"))
+ if err != nil {
+ return err
+ }
+
+ if _, err := redis.Int(c.Do("DEL", keys...)); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func setupRedis(tb testing.TB, ttl time.Duration) (redis.Conn, *redigostore.RedigoStore) {
+ pool := getPool()
+ c := pool.Get()
+
+ if _, err := redis.String(c.Do("PING")); err != nil {
+ c.Close()
+ tb.Skip("redis server not available on localhost port 6379")
+ }
+
+ if _, err := redis.String(c.Do("SELECT", redisTestDB)); err != nil {
+ c.Close()
+ tb.Fatal(err)
+ }
+
+ st, err := redigostore.New(pool, redisTestPrefix, redisTestDB)
+ if err != nil {
+ c.Close()
+ tb.Fatal(err)
+ }
+
+ return c, st
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/store/storetest/doc.go b/vendor/gopkg.in/throttled/throttled.v2/store/storetest/doc.go
new file mode 100644
index 000000000..ecfee2638
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/store/storetest/doc.go
@@ -0,0 +1,2 @@
+// Package storetest provides a helper for testing throttled stores.
+package storetest // import "gopkg.in/throttled/throttled.v2/store/storetest"
diff --git a/vendor/gopkg.in/throttled/throttled.v2/store/storetest/storetest.go b/vendor/gopkg.in/throttled/throttled.v2/store/storetest/storetest.go
new file mode 100644
index 000000000..191b40a4f
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/store/storetest/storetest.go
@@ -0,0 +1,176 @@
+// Package storetest provides a helper for testing throttled stores.
+package storetest // import "gopkg.in/throttled/throttled.v2/store/storetest"
+
+import (
+ "math/rand"
+ "strconv"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "gopkg.in/throttled/throttled.v2"
+)
+
+// TestGCRAStore tests the behavior of a GCRAStore implementation for
+// compliance with the throttled API. It does not require support
+// for TTLs.
+func TestGCRAStore(t *testing.T, st throttled.GCRAStore) {
+ // GetWithTime a missing key
+ if have, _, err := st.GetWithTime("foo"); err != nil {
+ t.Fatal(err)
+ } else if have != -1 {
+ t.Errorf("expected GetWithTime to return -1 for a missing key but got %d", have)
+ }
+
+ // SetIfNotExists on a new key
+ want := int64(1)
+
+ if set, err := st.SetIfNotExistsWithTTL("foo", want, 0); err != nil {
+ t.Fatal(err)
+ } else if !set {
+ t.Errorf("expected SetIfNotExists on an empty key to succeed")
+ }
+
+ before := time.Now()
+
+ if have, now, err := st.GetWithTime("foo"); err != nil {
+ t.Fatal(err)
+ } else if have != want {
+ t.Errorf("expected GetWithTime to return %d but got %d", want, have)
+ } else if now.UnixNano() <= 0 {
+ t.Errorf("expected GetWithTime to return a time representable representable as a positive int64 of nanoseconds since the epoch")
+ } else if now.Before(before) || now.After(time.Now()) {
+ // Note that we make the assumption here that the store is running on
+ // the same machine as this test and thus shares a clock. This can be a
+ // little tricky in the case of Redis, which could be running
+ // elsewhere. The test assumes that it's running either locally on on
+ // Travis (where currently the Redis is available on localhost). If new
+ // test environments are procured, this may need to be revisited.
+ t.Errorf("expected GetWithTime to return a time between the time before the call and the time after the call")
+ }
+
+ // SetIfNotExists on an existing key
+ if set, err := st.SetIfNotExistsWithTTL("foo", 123, 0); err != nil {
+ t.Fatal(err)
+ } else if set {
+ t.Errorf("expected SetIfNotExists on an existing key to fail")
+ }
+
+ if have, _, err := st.GetWithTime("foo"); err != nil {
+ t.Fatal(err)
+ } else if have != want {
+ t.Errorf("expected GetWithTime to return %d but got %d", want, have)
+ }
+
+ // SetIfNotExists on a different key
+ if set, err := st.SetIfNotExistsWithTTL("bar", 456, 0); err != nil {
+ t.Fatal(err)
+ } else if !set {
+ t.Errorf("expected SetIfNotExists on an empty key to succeed")
+ }
+
+ // Returns the false on a missing key
+ if swapped, err := st.CompareAndSwapWithTTL("baz", 1, 2, 0); err != nil {
+ t.Fatal(err)
+ } else if swapped {
+ t.Errorf("expected CompareAndSwap to fail on a missing key")
+ }
+
+ // Test a successful CAS
+ want = int64(2)
+
+ if swapped, err := st.CompareAndSwapWithTTL("foo", 1, want, 0); err != nil {
+ t.Fatal(err)
+ } else if !swapped {
+ t.Errorf("expected CompareAndSwap to succeed")
+ }
+
+ if have, _, err := st.GetWithTime("foo"); err != nil {
+ t.Fatal(err)
+ } else if have != want {
+ t.Errorf("expected GetWithTime to return %d but got %d", want, have)
+ }
+
+ // Test an unsuccessful CAS
+ if swapped, err := st.CompareAndSwapWithTTL("foo", 1, 2, 0); err != nil {
+ t.Fatal(err)
+ } else if swapped {
+ t.Errorf("expected CompareAndSwap to fail")
+ }
+
+ if have, _, err := st.GetWithTime("foo"); err != nil {
+ t.Fatal(err)
+ } else if have != want {
+ t.Errorf("expected GetWithTime to return %d but got %d", want, have)
+ }
+}
+
+// TestGCRAStoreTTL tests the behavior of TTLs in a GCRAStore implementation.
+func TestGCRAStoreTTL(t *testing.T, st throttled.GCRAStore) {
+ ttl := time.Second
+ want := int64(1)
+ key := "ttl"
+
+ if _, err := st.SetIfNotExistsWithTTL(key, want, ttl); err != nil {
+ t.Fatal(err)
+ }
+
+ if have, _, err := st.GetWithTime(key); err != nil {
+ t.Fatal(err)
+ } else if have != want {
+ t.Errorf("expected GetWithTime to return %d, got %d", want, have)
+ }
+
+ // I can't think of a generic way to test expiration without a sleep
+ time.Sleep(ttl + time.Millisecond)
+
+ if have, _, err := st.GetWithTime(key); err != nil {
+ t.Fatal(err)
+ } else if have != -1 {
+ t.Errorf("expected GetWithTime to fail on an expired key but got %d", have)
+ }
+}
+
+// BenchmarkGCRAStore runs parallel benchmarks against a GCRAStore implementation.
+// Aside from being useful for performance testing, this is useful for finding
+// race conditions with the Go race detector.
+func BenchmarkGCRAStore(b *testing.B, st throttled.GCRAStore) {
+ seed := int64(42)
+ var attempts, updates int64
+
+ b.RunParallel(func(pb *testing.PB) {
+ // We need atomic behavior around the RNG or go detects a race in the test
+ delta := int64(1)
+ seedValue := atomic.AddInt64(&seed, delta) - delta
+ gen := rand.New(rand.NewSource(seedValue))
+
+ for pb.Next() {
+ key := strconv.FormatInt(gen.Int63n(50), 10)
+
+ var v int64
+ var updated bool
+
+ v, _, err := st.GetWithTime(key)
+ if v == -1 {
+ updated, err = st.SetIfNotExistsWithTTL(key, gen.Int63(), 0)
+ if err != nil {
+ b.Error(err)
+ }
+ } else if err != nil {
+ b.Error(err)
+ } else {
+ updated, err = st.CompareAndSwapWithTTL(key, v, gen.Int63(), 0)
+ if err != nil {
+ b.Error(err)
+ }
+ }
+
+ atomic.AddInt64(&attempts, 1)
+ if updated {
+ atomic.AddInt64(&updates, 1)
+ }
+ }
+ })
+
+ b.Logf("%d/%d update operations succeeed", updates, attempts)
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/varyby.go b/vendor/gopkg.in/throttled/throttled.v2/varyby.go
new file mode 100644
index 000000000..dc6a01718
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/varyby.go
@@ -0,0 +1,78 @@
+package throttled
+
+import (
+ "bytes"
+ "net/http"
+ "strings"
+)
+
+// VaryBy defines the criteria to use to group requests.
+type VaryBy struct {
+ // Vary by the RemoteAddr as specified by the net/http.Request field.
+ RemoteAddr bool
+
+ // Vary by the HTTP Method as specified by the net/http.Request field.
+ Method bool
+
+ // Vary by the URL's Path as specified by the Path field of the net/http.Request
+ // URL field.
+ Path bool
+
+ // Vary by this list of header names, read from the net/http.Request Header field.
+ Headers []string
+
+ // Vary by this list of parameters, read from the net/http.Request FormValue method.
+ Params []string
+
+ // Vary by this list of cookie names, read from the net/http.Request Cookie method.
+ Cookies []string
+
+ // Use this separator string to concatenate the various criteria of the VaryBy struct.
+ // Defaults to a newline character if empty (\n).
+ Separator string
+
+ // DEPRECATED. Custom specifies the custom-generated key to use for this request.
+ // If not nil, the value returned by this function is used instead of any
+ // VaryBy criteria.
+ Custom func(r *http.Request) string
+}
+
+// Key returns the key for this request based on the criteria defined by the VaryBy struct.
+func (vb *VaryBy) Key(r *http.Request) string {
+ var buf bytes.Buffer
+
+ if vb == nil {
+ return "" // Special case for no vary-by option
+ }
+ if vb.Custom != nil {
+ // A custom key generator is specified
+ return vb.Custom(r)
+ }
+ sep := vb.Separator
+ if sep == "" {
+ sep = "\n" // Separator defaults to newline
+ }
+ if vb.RemoteAddr {
+ buf.WriteString(strings.ToLower(r.RemoteAddr) + sep)
+ }
+ if vb.Method {
+ buf.WriteString(strings.ToLower(r.Method) + sep)
+ }
+ for _, h := range vb.Headers {
+ buf.WriteString(strings.ToLower(r.Header.Get(h)) + sep)
+ }
+ if vb.Path {
+ buf.WriteString(r.URL.Path + sep)
+ }
+ for _, p := range vb.Params {
+ buf.WriteString(r.FormValue(p) + sep)
+ }
+ for _, c := range vb.Cookies {
+ ck, err := r.Cookie(c)
+ if err == nil {
+ buf.WriteString(ck.Value)
+ }
+ buf.WriteString(sep) // Write the separator anyway, whether or not the cookie exists
+ }
+ return buf.String()
+}
diff --git a/vendor/gopkg.in/throttled/throttled.v2/varyby_test.go b/vendor/gopkg.in/throttled/throttled.v2/varyby_test.go
new file mode 100644
index 000000000..66a5f4e98
--- /dev/null
+++ b/vendor/gopkg.in/throttled/throttled.v2/varyby_test.go
@@ -0,0 +1,58 @@
+package throttled_test
+
+import (
+ "net/http"
+ "net/url"
+ "testing"
+
+ "gopkg.in/throttled/throttled.v2"
+)
+
+func TestVaryBy(t *testing.T) {
+ u, err := url.Parse("http://localhost/test/path?q=s")
+ if err != nil {
+ panic(err)
+ }
+ ck := &http.Cookie{Name: "ssn", Value: "test"}
+ cases := []struct {
+ vb *throttled.VaryBy
+ r *http.Request
+ k string
+ }{
+ 0: {nil, &http.Request{}, ""},
+ 1: {&throttled.VaryBy{RemoteAddr: true}, &http.Request{RemoteAddr: "::"}, "::\n"},
+ 2: {
+ &throttled.VaryBy{Method: true, Path: true},
+ &http.Request{Method: "POST", URL: u},
+ "post\n/test/path\n",
+ },
+ 3: {
+ &throttled.VaryBy{Headers: []string{"Content-length"}},
+ &http.Request{Header: http.Header{"Content-Type": []string{"text/plain"}, "Content-Length": []string{"123"}}},
+ "123\n",
+ },
+ 4: {
+ &throttled.VaryBy{Separator: ",", Method: true, Headers: []string{"Content-length"}, Params: []string{"q", "user"}},
+ &http.Request{Method: "GET", Header: http.Header{"Content-Type": []string{"text/plain"}, "Content-Length": []string{"123"}}, Form: url.Values{"q": []string{"s"}, "pwd": []string{"secret"}, "user": []string{"test"}}},
+ "get,123,s,test,",
+ },
+ 5: {
+ &throttled.VaryBy{Cookies: []string{"ssn"}},
+ &http.Request{Header: http.Header{"Cookie": []string{ck.String()}}},
+ "test\n",
+ },
+ 6: {
+ &throttled.VaryBy{Cookies: []string{"ssn"}, RemoteAddr: true, Custom: func(r *http.Request) string {
+ return "blah"
+ }},
+ &http.Request{Header: http.Header{"Cookie": []string{ck.String()}}},
+ "blah",
+ },
+ }
+ for i, c := range cases {
+ got := c.vb.Key(c.r)
+ if got != c.k {
+ t.Errorf("%d: expected '%s' (%d), got '%s' (%d)", i, c.k, len(c.k), got, len(got))
+ }
+ }
+}