| 
 | 1 | +// Copyright 2025 The Gitea Authors. All rights reserved.  | 
 | 2 | +// SPDX-License-Identifier: MIT  | 
 | 3 | + | 
 | 4 | +package common  | 
 | 5 | + | 
 | 6 | +import (  | 
 | 7 | +	"context"  | 
 | 8 | +	"fmt"  | 
 | 9 | +	"net/http"  | 
 | 10 | +	"strings"  | 
 | 11 | + | 
 | 12 | +	user_model "code.gitea.io/gitea/models/user"  | 
 | 13 | +	"code.gitea.io/gitea/modules/log"  | 
 | 14 | +	"code.gitea.io/gitea/modules/setting"  | 
 | 15 | +	"code.gitea.io/gitea/modules/templates"  | 
 | 16 | +	"code.gitea.io/gitea/modules/web/middleware"  | 
 | 17 | +	giteacontext "code.gitea.io/gitea/services/context"  | 
 | 18 | + | 
 | 19 | +	"github.com/bohde/codel"  | 
 | 20 | +	"github.com/go-chi/chi/v5"  | 
 | 21 | +)  | 
 | 22 | + | 
 | 23 | +const tplStatus503 templates.TplName = "status/503"  | 
 | 24 | + | 
 | 25 | +type Priority int  | 
 | 26 | + | 
 | 27 | +func (p Priority) String() string {  | 
 | 28 | +	switch p {  | 
 | 29 | +	case HighPriority:  | 
 | 30 | +		return "high"  | 
 | 31 | +	case DefaultPriority:  | 
 | 32 | +		return "default"  | 
 | 33 | +	case LowPriority:  | 
 | 34 | +		return "low"  | 
 | 35 | +	default:  | 
 | 36 | +		return fmt.Sprintf("%d", p)  | 
 | 37 | +	}  | 
 | 38 | +}  | 
 | 39 | + | 
 | 40 | +const (  | 
 | 41 | +	LowPriority     = Priority(-10)  | 
 | 42 | +	DefaultPriority = Priority(0)  | 
 | 43 | +	HighPriority    = Priority(10)  | 
 | 44 | +)  | 
 | 45 | + | 
 | 46 | +// QoS implements quality of service for requests, based upon whether  | 
 | 47 | +// or not the user is logged in. All traffic may get dropped, and  | 
 | 48 | +// anonymous users are deprioritized.  | 
 | 49 | +func QoS() func(next http.Handler) http.Handler {  | 
 | 50 | +	if !setting.Service.QoS.Enabled {  | 
 | 51 | +		return nil  | 
 | 52 | +	}  | 
 | 53 | + | 
 | 54 | +	maxOutstanding := setting.Service.QoS.MaxInFlightRequests  | 
 | 55 | +	if maxOutstanding <= 0 {  | 
 | 56 | +		maxOutstanding = 10  | 
 | 57 | +	}  | 
 | 58 | + | 
 | 59 | +	c := codel.NewPriority(codel.Options{  | 
 | 60 | +		// The maximum number of waiting requests.  | 
 | 61 | +		MaxPending: setting.Service.QoS.MaxWaitingRequests,  | 
 | 62 | +		// The maximum number of in-flight requests.  | 
 | 63 | +		MaxOutstanding: maxOutstanding,  | 
 | 64 | +		// The target latency that a blocked request should wait  | 
 | 65 | +		// for. After this, it might be dropped.  | 
 | 66 | +		TargetLatency: setting.Service.QoS.TargetWaitTime,  | 
 | 67 | +	})  | 
 | 68 | + | 
 | 69 | +	return func(next http.Handler) http.Handler {  | 
 | 70 | +		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {  | 
 | 71 | +			ctx := req.Context()  | 
 | 72 | + | 
 | 73 | +			priority := requestPriority(ctx)  | 
 | 74 | + | 
 | 75 | +			// Check if the request can begin processing.  | 
 | 76 | +			err := c.Acquire(ctx, int(priority))  | 
 | 77 | +			if err != nil {  | 
 | 78 | +				log.Error("QoS error, dropping request of priority %s: %v", priority, err)  | 
 | 79 | +				renderServiceUnavailable(w, req)  | 
 | 80 | +				return  | 
 | 81 | +			}  | 
 | 82 | + | 
 | 83 | +			// Release long-polling immediately, so they don't always  | 
 | 84 | +			// take up an in-flight request  | 
 | 85 | +			if strings.Contains(req.URL.Path, "/user/events") {  | 
 | 86 | +				c.Release()  | 
 | 87 | +			} else {  | 
 | 88 | +				defer c.Release()  | 
 | 89 | +			}  | 
 | 90 | + | 
 | 91 | +			next.ServeHTTP(w, req)  | 
 | 92 | +		})  | 
 | 93 | +	}  | 
 | 94 | +}  | 
 | 95 | + | 
 | 96 | +// requestPriority assigns a priority value for a request based upon  | 
 | 97 | +// whether the user is logged in and how expensive the endpoint is  | 
 | 98 | +func requestPriority(ctx context.Context) Priority {  | 
 | 99 | +	// If the user is logged in, assign high priority.  | 
 | 100 | +	data := middleware.GetContextData(ctx)  | 
 | 101 | +	if _, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {  | 
 | 102 | +		return HighPriority  | 
 | 103 | +	}  | 
 | 104 | + | 
 | 105 | +	rctx := chi.RouteContext(ctx)  | 
 | 106 | +	if rctx == nil {  | 
 | 107 | +		return DefaultPriority  | 
 | 108 | +	}  | 
 | 109 | + | 
 | 110 | +	// If we're operating in the context of a repo, assign low priority  | 
 | 111 | +	routePattern := rctx.RoutePattern()  | 
 | 112 | +	if strings.HasPrefix(routePattern, "/{username}/{reponame}/") {  | 
 | 113 | +		return LowPriority  | 
 | 114 | +	}  | 
 | 115 | + | 
 | 116 | +	return DefaultPriority  | 
 | 117 | +}  | 
 | 118 | + | 
 | 119 | +// renderServiceUnavailable will render an HTTP 503 Service  | 
 | 120 | +// Unavailable page, providing HTML if the client accepts it.  | 
 | 121 | +func renderServiceUnavailable(w http.ResponseWriter, req *http.Request) {  | 
 | 122 | +	acceptsHTML := false  | 
 | 123 | +	for _, part := range req.Header["Accept"] {  | 
 | 124 | +		if strings.Contains(part, "text/html") {  | 
 | 125 | +			acceptsHTML = true  | 
 | 126 | +			break  | 
 | 127 | +		}  | 
 | 128 | +	}  | 
 | 129 | + | 
 | 130 | +	// If the client doesn't accept HTML, then render a plain text response  | 
 | 131 | +	if !acceptsHTML {  | 
 | 132 | +		http.Error(w, "503 Service Unavailable", http.StatusServiceUnavailable)  | 
 | 133 | +		return  | 
 | 134 | +	}  | 
 | 135 | + | 
 | 136 | +	tmplCtx := giteacontext.TemplateContext{}  | 
 | 137 | +	tmplCtx["Locale"] = middleware.Locale(w, req)  | 
 | 138 | +	ctxData := middleware.GetContextData(req.Context())  | 
 | 139 | +	err := templates.HTMLRenderer().HTML(w, http.StatusServiceUnavailable, tplStatus503, ctxData, tmplCtx)  | 
 | 140 | +	if err != nil {  | 
 | 141 | +		log.Error("Error occurs again when rendering service unavailable page: %v", err)  | 
 | 142 | +		w.WriteHeader(http.StatusInternalServerError)  | 
 | 143 | +		_, _ = w.Write([]byte("Internal server error, please collect error logs and report to Gitea issue tracker"))  | 
 | 144 | +	}  | 
 | 145 | +}  | 
0 commit comments