diff options
Diffstat (limited to 'routers/common/qos.go')
-rw-r--r-- | routers/common/qos.go | 145 |
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")) + } +} |