aboutsummaryrefslogtreecommitdiffstats
path: root/routers/common/qos.go
diff options
context:
space:
mode:
Diffstat (limited to 'routers/common/qos.go')
-rw-r--r--routers/common/qos.go145
1 files changed, 145 insertions, 0 deletions
diff --git a/routers/common/qos.go b/routers/common/qos.go
new file mode 100644
index 0000000000..e50fbe4f69
--- /dev/null
+++ b/routers/common/qos.go
@@ -0,0 +1,145 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package common
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strings"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/web/middleware"
+ giteacontext "code.gitea.io/gitea/services/context"
+
+ "github.com/bohde/codel"
+ "github.com/go-chi/chi/v5"
+)
+
+const tplStatus503 templates.TplName = "status/503"
+
+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 {
+ if !setting.Service.QoS.Enabled {
+ return nil
+ }
+
+ 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 := requestPriority(ctx)
+
+ // Check if the request can begin processing.
+ err := c.Acquire(ctx, int(priority))
+ if err != nil {
+ log.Error("QoS error, dropping request of priority %s: %v", priority, err)
+ renderServiceUnavailable(w, req)
+ 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)
+ })
+ }
+}
+
+// requestPriority assigns a priority value for a request based upon
+// whether the user is logged in and how expensive the endpoint is
+func requestPriority(ctx context.Context) Priority {
+ // If the user is logged in, assign high priority.
+ data := middleware.GetContextData(ctx)
+ if _, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
+ return HighPriority
+ }
+
+ rctx := chi.RouteContext(ctx)
+ if rctx == nil {
+ return DefaultPriority
+ }
+
+ // If we're operating in the context of a repo, assign low priority
+ routePattern := rctx.RoutePattern()
+ if strings.HasPrefix(routePattern, "/{username}/{reponame}/") {
+ return LowPriority
+ }
+
+ return DefaultPriority
+}
+
+// renderServiceUnavailable will render an HTTP 503 Service
+// Unavailable page, providing HTML if the client accepts it.
+func renderServiceUnavailable(w http.ResponseWriter, req *http.Request) {
+ acceptsHTML := false
+ for _, part := range req.Header["Accept"] {
+ if strings.Contains(part, "text/html") {
+ acceptsHTML = true
+ break
+ }
+ }
+
+ // If the client doesn't accept HTML, then render a plain text response
+ if !acceptsHTML {
+ http.Error(w, "503 Service Unavailable", http.StatusServiceUnavailable)
+ return
+ }
+
+ tmplCtx := giteacontext.TemplateContext{}
+ tmplCtx["Locale"] = middleware.Locale(w, req)
+ ctxData := middleware.GetContextData(req.Context())
+ err := templates.HTMLRenderer().HTML(w, http.StatusServiceUnavailable, tplStatus503, ctxData, tmplCtx)
+ if err != nil {
+ log.Error("Error occurs again when rendering service unavailable page: %v", err)
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte("Internal server error, please collect error logs and report to Gitea issue tracker"))
+ }
+}