mirror of https://github.com/go-gitea/gitea.git
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.
145 lines
3.9 KiB
145 lines
3.9 KiB
// 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")) |
|
} |
|
}
|
|
|