summaryrefslogtreecommitdiff
path: root/internal/middleware/throttling.go
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-01-04 11:57:59 +0100
committerLibravatar GitHub <noreply@github.com>2023-01-04 11:57:59 +0100
commit90a14abb0c693287d10c5b2b8a6e5515f3ed4c37 (patch)
treeb6cac62664d5bb0eed762e4bb9b8dab33c0da516 /internal/middleware/throttling.go
parent[docs] Fix documentation edit link (#1298) (diff)
downloadgotosocial-90a14abb0c693287d10c5b2b8a6e5515f3ed4c37.tar.xz
[feature] HTTP request throttling middleware (#1297)
* [feature] Add throttling middleware to AP endpoints * refactor a lil bit * use config setting, start updating docs * doc updates * use relative links in faq doc * small docs fixes * return code 503 instead of 429 when throttled * throttle other endpoints too * simplify token channel prefills
Diffstat (limited to 'internal/middleware/throttling.go')
-rw-r--r--internal/middleware/throttling.go153
1 files changed, 153 insertions, 0 deletions
diff --git a/internal/middleware/throttling.go b/internal/middleware/throttling.go
new file mode 100644
index 000000000..0f98387dd
--- /dev/null
+++ b/internal/middleware/throttling.go
@@ -0,0 +1,153 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*
+ The code in this file is adapted from MIT-licensed code in github.com/go-chi/chi. Thanks chi (thi)!
+
+ See: https://github.com/go-chi/chi/blob/e6baba61759b26ddf7b14d1e02d1da81a4d76c08/middleware/throttle.go
+
+ And: https://github.com/sponsors/pkieltyka
+*/
+
+package middleware
+
+import (
+ "net/http"
+ "runtime"
+ "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{}
+
+// Throttle returns a gin middleware that performs throttling of incoming requests,
+// ensuring that only a certain number of requests are handled concurrently, to reduce
+// congestion of the server.
+//
+// Limits are configured using available CPUs and the given cpuMultiplier value.
+// Open request limit is available CPUs * multiplier; backlog limit is limit * multiplier.
+//
+// Example values for multiplier 8:
+//
+// 1 cpu = 08 open, 064 backlog
+// 2 cpu = 16 open, 128 backlog
+// 4 cpu = 32 open, 256 backlog
+//
+// Example values for multiplier 4:
+//
+// 1 cpu = 04 open, 016 backlog
+// 2 cpu = 08 open, 032 backlog
+// 4 cpu = 16 open, 064 backlog
+//
+// Callers will first attempt to get a backlog token. Once they have that, they will
+// wait in the backlog queue until they can get a token to allow their request to be
+// processed.
+//
+// If the backlog queue is full, the request context is closed, or the caller has been
+// waiting in the backlog for too long, this function will abort the request chain,
+// write a JSON error into the response, set an appropriate Retry-After value, and set
+// the HTTP response code to 503: Service Unavailable.
+//
+// If the multiplier is <= 0, a noop middleware will be returned instead.
+//
+// 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 {
+ if cpuMultiplier <= 0 {
+ // throttling is disabled, return a noop middleware
+ return func(c *gin.Context) {}
+ }
+
+ var (
+ limit = runtime.GOMAXPROCS(0) * cpuMultiplier
+ backlogLimit = limit * cpuMultiplier
+ backlogChannelSize = limit + backlogLimit
+ tokens = make(chan token, limit)
+ backlogTokens = make(chan token, backlogChannelSize)
+ retryAfter = "30" // seconds
+ backlogDuration = 30 * time.Second
+ )
+
+ // 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)
+ 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
+ }()
+
+ // 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)
+ 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
+ tokens <- tok
+ }()
+ c.Next() // <- finally process the caller's request
+ }
+
+ default:
+ // we don't have space in the backlog queue
+ bail(c, errCapacityExceeded)
+ }
+ }
+}