diff --git a/routers/common/qos.go b/routers/common/qos.go index d47b5cd4dac2d..e686ad03242e3 100644 --- a/routers/common/qos.go +++ b/routers/common/qos.go @@ -4,42 +4,27 @@ package common import ( - "fmt" "net/http" + "slices" "strings" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" "github.com/bohde/codel" + "github.com/go-chi/chi/v5" ) -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) +// QoS implements quality of service for requests, based upon +// whether 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 } -} -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 @@ -57,30 +42,18 @@ func QoS() func(next http.Handler) http.Handler { 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 - } + priority, longPolling := determineRequestPriority(reqctx.FromContext(req.Context())) // Check if the request can begin processing. - err := c.Acquire(ctx, int(priority)) + err := c.Acquire(req.Context(), priority) if err != nil { // If it failed, the service is over capacity and should error - http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) + http.Error(w, "Service Unavailable (QoS)", 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") { + if longPolling { + // Release long-polling immediately, so they don't always take up an in-flight request c.Release() } else { defer c.Release() @@ -91,31 +64,52 @@ func QoS() func(next http.Handler) http.Handler { } } -func IsGitContents(path string) bool { +func isRoutePathLowPriority(routePattern string) bool { + subPath, ok := strings.CutPrefix(routePattern, "/{username}/{reponame}/") + if !ok { + return false + } + subPath, _, _ = strings.Cut(subPath, "/") parts := []string{ - "refs", + "activity", "archive", - "commit", - "graph", "blame", "branches", - "tags", - "labels", - "stars", - "search", - "activity", - "wiki", - "watchers", + "commit", + "commits", "compare", + "graph", + "labels", + "media", "raw", + "search", "src", - "commits", + "stars", + "tags", + "watchers", + "wiki", } + return slices.Contains(parts, subPath) +} + +func isRoutePathForLongPolling(routePattern string) bool { + return routePattern == "/user/events" +} + +// TODO: add some tests - for _, p := range parts { - if strings.Contains(path, p) { - return true - } +func determineRequestPriority(reqCtx reqctx.RequestContext) (priority int, longPolling bool) { + const priorityLow = -10 + const priorityHigh = 10 + + chiRoutePath := chi.RouteContext(reqCtx).RoutePattern() + if _, ok := reqCtx.GetData()[middleware.ContextDataKeySignedUser].(*user_model.User); ok { + // If the user is logged in, assign high priority. + priority = priorityHigh + } else if isRoutePathLowPriority(chiRoutePath) { + // Otherwise, if the path would is accessing git contents directly, mark as low priority + priority = priorityLow } - return false + + return priority, isRoutePathForLongPolling(chiRoutePath) } diff --git a/routers/web/web.go b/routers/web/web.go index 9865447ce86cb..58e6dfcbc76d7 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -272,10 +272,6 @@ 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) @@ -289,17 +285,17 @@ func Routes() *web.Router { mid = append(mid, repo.GetActiveStopwatch) mid = append(mid, goGet) - others := web.NewRouter() - others.Use(mid...) - registerRoutes(others) - routes.Mount("", others) + webRoutes := web.NewRouter() + webRoutes.Use(mid...) + webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.QoS()) + + routes.Mount("", webRoutes) return routes } var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true}) -// registerRoutes register routes -func registerRoutes(m *web.Router) { +func registerWebRoutes(m *web.Router) { // required to be signed in or signed out reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true}) reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})