summaryrefslogtreecommitdiff
path: root/internal/middleware/throttling.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/middleware/throttling.go')
-rw-r--r--internal/middleware/throttling.go38
1 files changed, 10 insertions, 28 deletions
diff --git a/internal/middleware/throttling.go b/internal/middleware/throttling.go
index e7ad321f4..cc837f25e 100644
--- a/internal/middleware/throttling.go
+++ b/internal/middleware/throttling.go
@@ -29,17 +29,12 @@ package middleware
import (
"net/http"
"runtime"
+ "strconv"
"time"
"github.com/gin-gonic/gin"
)
-const (
- errCapacityExceeded = "server capacity exceeded"
- errTimedOut = "timed out while waiting for a pending request to complete"
- errContextCanceled = "context canceled"
-)
-
// token represents a request that is being processed.
type token struct{}
@@ -73,11 +68,13 @@ type token struct{}
//
// If the multiplier is <= 0, a noop middleware will be returned instead.
//
+// RetryAfter determines the Retry-After header value to be sent to throttled requests.
+//
// Useful links:
//
// - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
// - https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503
-func Throttle(cpuMultiplier int) gin.HandlerFunc {
+func Throttle(cpuMultiplier int, retryAfter time.Duration) gin.HandlerFunc {
if cpuMultiplier <= 0 {
// throttling is disabled, return a noop middleware
return func(c *gin.Context) {}
@@ -89,36 +86,24 @@ func Throttle(cpuMultiplier int) gin.HandlerFunc {
backlogChannelSize = limit + backlogLimit
tokens = make(chan token, limit)
backlogTokens = make(chan token, backlogChannelSize)
- retryAfter = "30" // seconds
- backlogDuration = 30 * time.Second
+ retryAfterStr = strconv.FormatUint(uint64(retryAfter/time.Second), 10)
)
// prefill token channels
for i := 0; i < limit; i++ {
tokens <- token{}
}
-
for i := 0; i < backlogChannelSize; i++ {
backlogTokens <- token{}
}
- // bail instructs the requester to return after retryAfter seconds, returns a 503,
- // and writes the given message into the "error" field of a returned json object
- bail := func(c *gin.Context, msg string) {
- c.Header("Retry-After", retryAfter)
- c.JSON(http.StatusServiceUnavailable, gin.H{"error": msg})
- c.Abort()
- }
-
return func(c *gin.Context) {
// inside this select, the caller tries to get a backlog token
select {
case <-c.Request.Context().Done():
// request context has been canceled already
- bail(c, errContextCanceled)
+ return
case btok := <-backlogTokens:
- // take a backlog token and wait
- timer := time.NewTimer(backlogDuration)
defer func() {
// when we're finished, return the backlog token to the bucket
backlogTokens <- btok
@@ -127,16 +112,11 @@ func Throttle(cpuMultiplier int) gin.HandlerFunc {
// inside *this* select, the caller has a backlog token,
// and they're waiting for their turn to be processed
select {
- case <-timer.C:
- // waiting too long in the backlog
- bail(c, errTimedOut)
case <-c.Request.Context().Done():
// the request context has been canceled already
- timer.Stop()
- bail(c, errContextCanceled)
+ return
case tok := <-tokens:
// the caller gets a token, so their request can now be processed
- timer.Stop()
defer func() {
// whatever happens to the request, put the
// token back in the bucket when we're finished
@@ -147,7 +127,9 @@ func Throttle(cpuMultiplier int) gin.HandlerFunc {
default:
// we don't have space in the backlog queue
- bail(c, errCapacityExceeded)
+ c.Header("Retry-After", retryAfterStr)
+ c.JSON(http.StatusTooManyRequests, gin.H{"error": "server capacity exceeded"})
+ c.Abort()
}
}
}