8000 add middleware for request prioritization by bohde · Pull Request #33951 · go-gitea/gitea · GitHub
[go: up one dir, main page]

Skip to content

add middleware for request prioritization #33951

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
add middleware for request prioritization
This adds a middleware for overload protection, that is intended to help protect against malicious scrapers. It does this by via  [`codel`](https://github.com/bohde/codel), which will perform the following:

1. Limit the number of in-flight requests to some user defined max
2. When in-flight requests have reached their max, begin queuing requests, with logged in requests having priority above logged out requests
3. Once a request has been queued for too long, it has a percentage chance to be rejected based upon how overloaded the entire system is.

When a server experiences more traffic than it  can handle, this has the effect of keeping latency low for logged in users, while rejecting just enough requests from logged out users to keep the service from being overloaded.

Below are benchmarks showing a system at 100% capacity and 200% capacity in a few different configurations. The 200% capacity is shown to highlight an extreme case. I used [hey](https://github.com/rakyll/hey) to simulate the bot traffic:

```
hey -z 1m -c 10 "http://localhost:3000/rowan/demo/issues?state=open&type=all&labels=&milestone=0&project=0&assignee=0&poster=0&q=fix"
```

The concurrency of 10 was chosen from experiments where my local server began to experience higher latency.

Results

| Method | Concurrency |  p95 latency | Successful RPS | Requests Dropped |
|--------|--------|--------|--------|--------|
| QoS Off | 10 | 0.2960s | 44 rps | 0% |
| QoS Off | 20 | 0.5667s | 44 rps | 0%|
| QoS On | 20 |  0.4409s | 48 rps | 10% |
| QoS On 50% Logged In* | 10 | 0.3891s | 33 rps | 7% |
| QoS On 50% Logged Out* | 10  | 2.0388s | 13 rps | 6% |

Logged in users were given the additional parameter ` -H "Cookie: i_like_gitea=<session>`.

Tests with `*` were run at the same time, representing a workload with mixed logged in & logged out users. Results are separated to show prioritization, and how logged in users experience a 100ms latency increase under load, compared to the 1.6 seconds logged out users see.
  • Loading branch information
bohde committed Apr 7, 2025
commit a38d71b4cbf795b56fb706470e6e73f7e27a9bf0
5 changes: 5 additions & 0 deletions assets/go-licenses.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,16 @@ LEVEL = Info
;USER_DELETE_WITH_COMMENTS_MAX_TIME = 0
;; Valid site url schemes for user profiles
;VALID_SITE_URL_SCHEMES=http,https
;;
;; Enable request quality of service and load shedding.
; QOS_ENABLED = false
;; The number of requests that are in flight to service before queuing
;; begins. Default is 4 * number of CPUs
; QOS_MAX_INFLIGHT =
;; The maximum number of requests that can be enqueued before they will be dropped.
; QOS_MAX_WAITING = 100
;; The target time for a request to be enqueued before it might be dropped.
; QOS_TARGET_WAIT_TIME = 50ms

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/codecommit v1.28.1
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.4.2
github.com/bohde/codel v0.2.0
github.com/buildkite/terminal-to-html/v3 v3.16.8
github.com/caddyserver/certmagic v0.22.0
github.com/charmbracelet/git-lfs-transfer v0.2.0
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ github.com/blevesearch/zapx/v16 v16.1.5 h1:b0sMcarqNFxuXvjoXsF8WtwVahnxyhEvBSRJi
github.com/blevesearch/zapx/v16 v16.1.5/go.mod h1:J4mSF39w1QELc11EWRSBFkPeZuO7r/NPKkHzDCoiaI8=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
github.com/bohde/codel v0.2.0 h1:fzF7ibgKmCfQbOzQCblmQcwzDRmV7WO7VMLm/hDvD3E=
github.com/bohde/codel v0.2.0/go.mod h1:Idb1IRvTdwkRjIjguLIo+FXhIBhcpGl94o7xra6ggWk=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
Expand Down Expand Up @@ -881,6 +884,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
Expand Down Expand Up @@ -1025,6 +1029,8 @@ modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0=
pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU=
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs=
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
Expand Down
13 changes: 13 additions & 0 deletions modules/setting/service.go
ED48
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package setting

import (
"regexp"
"runtime"
"strings"
"time"

Expand Down Expand Up @@ -98,6 +99,13 @@ var Service = struct {
DisableOrganizationsPage bool `ini:"DISABLE_ORGANIZATIONS_PAGE"`
DisableCodePage bool `ini:"DISABLE_CODE_PAGE"`
} `ini:"service.explore"`

QoS struct {
Enabled bool
MaxInFlightRequests int
MaxWaitingRequests int
TargetWaitTime time.Duration
}
}{
AllowedUserVisibilityModesSlice: []bool{true, true, true},
}
Expand Down Expand Up @@ -254,6 +262,11 @@ func loadServiceFrom(rootCfg ConfigProvider) {

mustMapSetting(rootCfg, "service.explore", &Service.Explore)

Service.QoS.Enabled = sec.Key("QOS_ENABLED").MustBool(false)
Service.QoS.MaxInFlightRequests = sec.Key("QOS_MAX_INFLIGHT").MustInt(4 * runtime.NumCPU())
Service.QoS.MaxWaitingRequests = sec.Key("QOS_MAX_WAITING").MustInt(100)
Service.QoS.TargetWaitTime = sec.Key("QOS_TARGET_WAIT_TIME").MustDuration(50 * time.Millisecond)

loadOpenIDSetting(rootCfg)
}

Expand Down
121 changes: 121 additions & 0 deletions routers/common/qos.go
1E80
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package common

import (
"fmt"
"net/http"
"strings"

user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"

"github.com/bohde/codel"
)

type Priority int

func (p Priority) String() string {
switch p {
case HighPriority:
return "high"
case DefaultPriority:
return "default"
case LowPriority:
return "low"
default:
return fmt.Sprintf("%d", p)
}
}

const (
LowPriority = Priority(-10)
DefaultPriority = Priority(0)
HighPriority = Priority(10)
)

// QoS implements quality of service for requests, based upon whether
// or not the user is logged in. All traffic may get dropped, and
// anonymous users are deprioritized.
func QoS() func(next http.Handler) http.Handler {
maxOutstanding := setting.Service.QoS.MaxInFlightRequests
if maxOutstanding <= 0 {
maxOutstanding = 10
}

c := codel.NewPriority(codel.Options{
// The maximum number of waiting requests.
MaxPending: setting.Service.QoS.MaxWaitingRequests,
// The maximum number of in-flight requests.
MaxOutstanding: maxOutstanding,
// The target latency that a blocked request should wait
// for. After this, it might be dropped.
TargetLatency: setting.Service.QoS.TargetWaitTime,
})

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()

priority := DefaultPriority

// If the user is logged in, assign high priority.
data := middleware.GetContextData(req.Context())
if _, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
priority = HighPriority
} else if IsGitContents(req.URL.Path) {
// Otherwise, if the path would is accessing git contents directly, mark as low priority
priority = LowPriority
}

// Check if the request can begin processing.
err := c.Acquire(ctx, int(priority))
if err != nil {
// If it failed, the service is over capacity and should error
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
return
}

// Release long-polling immediately, so they don't always
// take up an in-flight request
if strings.Contains(req.URL.Path, "/user/events") {
c.Release()
} else {
defer c.Release()
}

next.ServeHTTP(w, req)
})
}
}

func IsGitContents(path string) bool {
parts := []string{
"refs",
"archive",
"commit",
"graph",
"blame",
"branches",
"tags",
"labels",
"stars",
"search",
"activity",
"wiki",
"watchers",
"compare",
"raw",
"src",
"commits",
}

for _, p := range parts {
if strings.Contains(path, p) {
return true
}
}
return false
}
4 changes: 4 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ func Routes() *web.Router {
// Get user from session if logged in.
mid = append(mid, webAuth(buildAuthGroup()))

if setting.Service.QoS.Enabled {
mid = append(mid, common.QoS())
}

// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
mid = append(mid, chi_middleware.GetHead)

Expand Down
0