Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
3.7 KiB
132 lines
3.7 KiB
package middleware |
|
|
|
import ( |
|
"net/http" |
|
"strconv" |
|
"time" |
|
) |
|
|
|
const ( |
|
errCapacityExceeded = "Server capacity exceeded." |
|
errTimedOut = "Timed out while waiting for a pending request to complete." |
|
errContextCanceled = "Context was canceled." |
|
) |
|
|
|
var ( |
|
defaultBacklogTimeout = time.Second * 60 |
|
) |
|
|
|
// ThrottleOpts represents a set of throttling options. |
|
type ThrottleOpts struct { |
|
Limit int |
|
BacklogLimit int |
|
BacklogTimeout time.Duration |
|
RetryAfterFn func(ctxDone bool) time.Duration |
|
} |
|
|
|
// Throttle is a middleware that limits number of currently processed requests |
|
// at a time across all users. Note: Throttle is not a rate-limiter per user, |
|
// instead it just puts a ceiling on the number of currentl in-flight requests |
|
// being processed from the point from where the Throttle middleware is mounted. |
|
func Throttle(limit int) func(http.Handler) http.Handler { |
|
return ThrottleWithOpts(ThrottleOpts{Limit: limit, BacklogTimeout: defaultBacklogTimeout}) |
|
} |
|
|
|
// ThrottleBacklog is a middleware that limits number of currently processed |
|
// requests at a time and provides a backlog for holding a finite number of |
|
// pending requests. |
|
func ThrottleBacklog(limit, backlogLimit int, backlogTimeout time.Duration) func(http.Handler) http.Handler { |
|
return ThrottleWithOpts(ThrottleOpts{Limit: limit, BacklogLimit: backlogLimit, BacklogTimeout: backlogTimeout}) |
|
} |
|
|
|
// ThrottleWithOpts is a middleware that limits number of currently processed requests using passed ThrottleOpts. |
|
func ThrottleWithOpts(opts ThrottleOpts) func(http.Handler) http.Handler { |
|
if opts.Limit < 1 { |
|
panic("chi/middleware: Throttle expects limit > 0") |
|
} |
|
|
|
if opts.BacklogLimit < 0 { |
|
panic("chi/middleware: Throttle expects backlogLimit to be positive") |
|
} |
|
|
|
t := throttler{ |
|
tokens: make(chan token, opts.Limit), |
|
backlogTokens: make(chan token, opts.Limit+opts.BacklogLimit), |
|
backlogTimeout: opts.BacklogTimeout, |
|
retryAfterFn: opts.RetryAfterFn, |
|
} |
|
|
|
// Filling tokens. |
|
for i := 0; i < opts.Limit+opts.BacklogLimit; i++ { |
|
if i < opts.Limit { |
|
t.tokens <- token{} |
|
} |
|
t.backlogTokens <- token{} |
|
} |
|
|
|
return func(next http.Handler) http.Handler { |
|
fn := func(w http.ResponseWriter, r *http.Request) { |
|
ctx := r.Context() |
|
|
|
select { |
|
|
|
case <-ctx.Done(): |
|
t.setRetryAfterHeaderIfNeeded(w, true) |
|
http.Error(w, errContextCanceled, http.StatusTooManyRequests) |
|
return |
|
|
|
case btok := <-t.backlogTokens: |
|
timer := time.NewTimer(t.backlogTimeout) |
|
|
|
defer func() { |
|
t.backlogTokens <- btok |
|
}() |
|
|
|
select { |
|
case <-timer.C: |
|
t.setRetryAfterHeaderIfNeeded(w, false) |
|
http.Error(w, errTimedOut, http.StatusTooManyRequests) |
|
return |
|
case <-ctx.Done(): |
|
timer.Stop() |
|
t.setRetryAfterHeaderIfNeeded(w, true) |
|
http.Error(w, errContextCanceled, http.StatusTooManyRequests) |
|
return |
|
case tok := <-t.tokens: |
|
defer func() { |
|
timer.Stop() |
|
t.tokens <- tok |
|
}() |
|
next.ServeHTTP(w, r) |
|
} |
|
return |
|
|
|
default: |
|
t.setRetryAfterHeaderIfNeeded(w, false) |
|
http.Error(w, errCapacityExceeded, http.StatusTooManyRequests) |
|
return |
|
} |
|
} |
|
|
|
return http.HandlerFunc(fn) |
|
} |
|
} |
|
|
|
// token represents a request that is being processed. |
|
type token struct{} |
|
|
|
// throttler limits number of currently processed requests at a time. |
|
type throttler struct { |
|
tokens chan token |
|
backlogTokens chan token |
|
backlogTimeout time.Duration |
|
retryAfterFn func(ctxDone bool) time.Duration |
|
} |
|
|
|
// setRetryAfterHeaderIfNeeded sets Retry-After HTTP header if corresponding retryAfterFn option of throttler is initialized. |
|
func (t throttler) setRetryAfterHeaderIfNeeded(w http.ResponseWriter, ctxDone bool) { |
|
if t.retryAfterFn == nil { |
|
return |
|
} |
|
w.Header().Set("Retry-After", strconv.Itoa(int(t.retryAfterFn(ctxDone).Seconds()))) |
|
}
|
|
|