1
0
Эх сурвалжийг харах

Add write response timeout

DarthSim 11 сар өмнө
parent
commit
e867d0f10a

+ 3 - 0
.golangci.yml

@@ -33,6 +33,9 @@ issues:
     - linters: [bodyclose]
       path: ".*_test.go"
 
+    - linters: [bodyclose]
+      path: "router/timeout_response.go"
+
     # False positives on CGO generated code
     - linters: [staticcheck]
       text: "SA4000:"

+ 2 - 1
CHANGELOG.md

@@ -3,9 +3,10 @@
 ## [Unreleased]
 ### Add
 - Add [IMGPROXY_S3_ASSUME_ROLE_EXTERNAL_ID](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_S3_ASSUME_ROLE_EXTERNAL_ID) config.
+- Add [IMGPROXY_WRITE_RESPONSE_TIMEOUT](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_WRITE_RESPONSE_TIMEOUT) config.
+- Add [IMGPROXY_REPORT_IO_ERRORS](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_REPORT_IO_ERRORS) config.
 - (pro) Add [colorize](https://docs.imgproxy.net/latest/usage/processing#colorize) processing option.
 
-
 ### Changed
 - Automatically add `http://` scheme to the `IMGPROXY_S3_ENDPOINT` value if it has no scheme.
 - Trim redundant slashes in the S3 object key.

+ 6 - 0
config/config.go

@@ -23,6 +23,7 @@ var (
 	Bind                   string
 	ReadTimeout            int
 	WriteTimeout           int
+	WriteResponseTimeout   int
 	KeepAliveTimeout       int
 	ClientKeepAliveTimeout int
 	DownloadTimeout        int
@@ -188,6 +189,7 @@ var (
 	AirbrakeEnv       string
 
 	ReportDownloadingErrors bool
+	ReportIOErrors          bool
 
 	EnableDebugHeaders bool
 
@@ -217,6 +219,7 @@ func Reset() {
 	Bind = ":8080"
 	ReadTimeout = 10
 	WriteTimeout = 10
+	WriteResponseTimeout = 10
 	KeepAliveTimeout = 10
 	ClientKeepAliveTimeout = 90
 	DownloadTimeout = 5
@@ -380,6 +383,7 @@ func Reset() {
 	AirbrakeEnv = "production"
 
 	ReportDownloadingErrors = true
+	ReportIOErrors = false
 
 	EnableDebugHeaders = false
 
@@ -401,6 +405,7 @@ func Configure() error {
 	configurators.String(&Bind, "IMGPROXY_BIND")
 	configurators.Int(&ReadTimeout, "IMGPROXY_READ_TIMEOUT")
 	configurators.Int(&WriteTimeout, "IMGPROXY_WRITE_TIMEOUT")
+	configurators.Int(&WriteResponseTimeout, "IMGPROXY_WRITE_RESPONSE_TIMEOUT")
 	configurators.Int(&KeepAliveTimeout, "IMGPROXY_KEEP_ALIVE_TIMEOUT")
 	configurators.Int(&ClientKeepAliveTimeout, "IMGPROXY_CLIENT_KEEP_ALIVE_TIMEOUT")
 	configurators.Int(&DownloadTimeout, "IMGPROXY_DOWNLOAD_TIMEOUT")
@@ -597,6 +602,7 @@ func Configure() error {
 	configurators.String(&AirbrakeProjecKey, "IMGPROXY_AIRBRAKE_PROJECT_KEY")
 	configurators.String(&AirbrakeEnv, "IMGPROXY_AIRBRAKE_ENVIRONMENT")
 	configurators.Bool(&ReportDownloadingErrors, "IMGPROXY_REPORT_DOWNLOADING_ERRORS")
+	configurators.Bool(&ReportIOErrors, "IMGPROXY_REPORT_IO_ERRORS")
 	configurators.Bool(&EnableDebugHeaders, "IMGPROXY_ENABLE_DEBUG_HEADERS")
 
 	configurators.Int(&FreeMemoryInterval, "IMGPROXY_FREE_MEMORY_INTERVAL")

+ 21 - 10
processing_handler.go

@@ -143,10 +143,21 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
 
 	rw.Header().Set("Content-Length", strconv.Itoa(len(resultData.Data)))
 	rw.WriteHeader(statusCode)
-	rw.Write(resultData.Data)
+	_, err := rw.Write(resultData.Data)
+
+	var ierr *ierrors.Error
+	if err != nil {
+		ierr = ierrors.New(statusCode, fmt.Sprintf("Failed to write response: %s", err), "Failed to write response")
+		ierr.Unexpected = true
+
+		if config.ReportIOErrors {
+			sendErr(r.Context(), "IO", ierr)
+			errorreport.Report(ierr, r)
+		}
+	}
 
 	router.LogResponse(
-		reqID, r, statusCode, nil,
+		reqID, r, statusCode, ierr,
 		log.Fields{
 			"image_url":          originURL,
 			"processing_options": po,
@@ -204,14 +215,6 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 
 	ctx := r.Context()
 
-	if queueSem != nil {
-		acquired := queueSem.TryAcquire(1)
-		if !acquired {
-			panic(ierrors.New(429, "Too many requests", "Too many requests"))
-		}
-		defer queueSem.Release(1)
-	}
-
 	path := r.RequestURI
 	if queryStart := strings.IndexByte(path, '?'); queryStart >= 0 {
 		path = path[:queryStart]
@@ -282,6 +285,14 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	if queueSem != nil {
+		acquired := queueSem.TryAcquire(1)
+		if !acquired {
+			panic(ierrors.New(429, "Too many requests", "Too many requests"))
+		}
+		defer queueSem.Release(1)
+	}
+
 	// The heavy part starts here, so we need to restrict worker number
 	func() {
 		defer metrics.StartQueueSegment(ctx)()

+ 2 - 0
router/router.go

@@ -95,6 +95,8 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 	req, timeoutCancel := startRequestTimer(req)
 	defer timeoutCancel()
 
+	rw = newTimeoutResponse(rw)
+
 	reqID := req.Header.Get(xRequestIDHeader)
 
 	if len(reqID) == 0 || !requestIDRe.MatchString(reqID) {

+ 43 - 0
router/timeout_response.go

@@ -0,0 +1,43 @@
+package router
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/imgproxy/imgproxy/v3/config"
+)
+
+type timeoutResponse struct {
+	http.ResponseWriter
+	controller *http.ResponseController
+}
+
+func newTimeoutResponse(rw http.ResponseWriter) http.ResponseWriter {
+	return &timeoutResponse{
+		ResponseWriter: rw,
+		controller:     http.NewResponseController(rw),
+	}
+}
+
+func (rw *timeoutResponse) WriteHeader(statusCode int) {
+	rw.withWriteDeadline(func() {
+		rw.ResponseWriter.WriteHeader(statusCode)
+	})
+}
+
+func (rw *timeoutResponse) Write(b []byte) (int, error) {
+	var (
+		n   int
+		err error
+	)
+	rw.withWriteDeadline(func() {
+		n, err = rw.ResponseWriter.Write(b)
+	})
+	return n, err
+}
+
+func (rw *timeoutResponse) withWriteDeadline(f func()) {
+	rw.controller.SetWriteDeadline(time.Now().Add(time.Duration(config.WriteResponseTimeout) * time.Second))
+	defer rw.controller.SetWriteDeadline(time.Time{})
+	f()
+}