Ver Fonte

Fair timeout; Better errors handling

DarthSim há 7 anos atrás
pai
commit
55b972106c
5 ficheiros alterados com 123 adições e 35 exclusões
  1. 44 0
      errors.go
  2. 0 1
      main.go
  3. 7 1
      process.go
  4. 40 33
      server.go
  5. 32 0
      timer.go

+ 44 - 0
errors.go

@@ -0,0 +1,44 @@
+package main
+
+import (
+	"fmt"
+	"runtime"
+	"strings"
+)
+
+type imgproxyError struct {
+	StatusCode    int
+	Message       string
+	PublicMessage string
+}
+
+func (e imgproxyError) Error() string {
+	return e.Message
+}
+
+func newError(status int, msg string, pub string) imgproxyError {
+	return imgproxyError{status, msg, pub}
+}
+
+func newUnexpectedError(err error, skip int) imgproxyError {
+	msg := fmt.Sprintf("Unexpected error: %s\n%s", err, stacktrace(skip+1))
+	return imgproxyError{500, msg, "Internal error"}
+}
+
+var (
+	invalidSecretErr = newError(403, "Invalid secret", "Forbidden")
+)
+
+func stacktrace(skip int) string {
+	callers := make([]uintptr, 10)
+	n := runtime.Callers(skip+1, callers)
+
+	lines := make([]string, n)
+	for i, pc := range callers[:n] {
+		f := runtime.FuncForPC(pc)
+		file, line := f.FileLine(pc)
+		lines[i] = fmt.Sprintf("%s:%d %s", file, line, f.Name())
+	}
+
+	return strings.Join(lines, "\n")
+}

+ 0 - 1
main.go

@@ -26,7 +26,6 @@ func main() {
 	s := &http.Server{
 		Handler:        newHTTPHandler(),
 		ReadTimeout:    time.Duration(conf.ReadTimeout) * time.Second,
-		WriteTimeout:   time.Duration(conf.WriteTimeout) * time.Second,
 		MaxHeaderBytes: 1 << 20,
 	}
 

+ 7 - 1
process.go

@@ -176,7 +176,7 @@ func calcCrop(width, height int, po processingOptions) (left, top int) {
 	return
 }
 
-func processImage(data []byte, imgtype imageType, po processingOptions) ([]byte, error) {
+func processImage(data []byte, imgtype imageType, po processingOptions, t *timer) ([]byte, error) {
 	defer keepAlive(data)
 
 	if po.gravity == SMART && !vipsSupportSmartcrop {
@@ -205,6 +205,8 @@ func processImage(data []byte, imgtype imageType, po processingOptions) ([]byte,
 		return nil, vipsError()
 	}
 
+	t.Check()
+
 	imgWidth := int(img.Xsize)
 	imgHeight := int(img.Ysize)
 
@@ -251,6 +253,8 @@ func processImage(data []byte, imgtype imageType, po processingOptions) ([]byte,
 		}
 	}
 
+	t.Check()
+
 	// Finally, save
 	var ptr unsafe.Pointer
 	defer C.g_free(C.gpointer(ptr))
@@ -269,6 +273,8 @@ func processImage(data []byte, imgtype imageType, po processingOptions) ([]byte,
 		return nil, vipsError()
 	}
 
+	t.Check()
+
 	buf := C.GoBytes(ptr, C.int(imgsize))
 
 	return buf, nil

+ 40 - 33
server.go

@@ -92,9 +92,9 @@ func parsePath(r *http.Request) (string, processingOptions, error) {
 func logResponse(status int, msg string) {
 	var color int
 
-	if status > 500 {
+	if status >= 500 {
 		color = 31
-	} else if status > 400 {
+	} else if status >= 400 {
 		color = 33
 	} else {
 		color = 32
@@ -103,7 +103,7 @@ func logResponse(status int, msg string) {
 	log.Printf("|\033[7;%dm %d \033[0m| %s\n", color, status, msg)
 }
 
-func respondWithImage(r *http.Request, rw http.ResponseWriter, data []byte, imgURL string, po processingOptions, startTime time.Time) {
+func respondWithImage(r *http.Request, rw http.ResponseWriter, data []byte, imgURL string, po processingOptions, duration time.Duration) {
 	gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && conf.GZipCompression > 0
 
 	rw.Header().Set("Expires", time.Now().Add(time.Second*time.Duration(conf.TTL)).Format(http.TimeFormat))
@@ -123,21 +123,14 @@ func respondWithImage(r *http.Request, rw http.ResponseWriter, data []byte, imgU
 		rw.Write(data)
 	}
 
-	logResponse(200, fmt.Sprintf("Processed in %s: %s; %+v", time.Since(startTime), imgURL, po))
+	logResponse(200, fmt.Sprintf("Processed in %s: %s; %+v", duration, imgURL, po))
 }
 
-func respondWithError(rw http.ResponseWriter, status int, err error, msg string) {
-	logResponse(status, err.Error())
+func respondWithError(rw http.ResponseWriter, err imgproxyError) {
+	logResponse(err.StatusCode, err.Message)
 
-	rw.WriteHeader(status)
-	rw.Write([]byte(msg))
-}
-
-func repondWithForbidden(rw http.ResponseWriter) {
-	logResponse(403, "Invalid secret")
-
-	rw.WriteHeader(403)
-	rw.Write([]byte("Forbidden"))
+	rw.WriteHeader(err.StatusCode)
+	rw.Write([]byte(err.PublicMessage))
 }
 
 func checkSecret(s string) bool {
@@ -147,49 +140,63 @@ func checkSecret(s string) bool {
 	return strings.HasPrefix(s, "Bearer ") && subtle.ConstantTimeCompare([]byte(strings.TrimPrefix(s, "Bearer ")), []byte(conf.Secret)) == 1
 }
 
-func (h *httpHandler) lock() {
-	h.sem <- struct{}{}
+func (h *httpHandler) lock(t *timer) {
+	select {
+	case h.sem <- struct{}{}:
+		// Go ahead
+	case <-t.Timer:
+		panic(t.TimeoutErr())
+	}
 }
 
 func (h *httpHandler) unlock() {
-	defer func() { <-h.sem }()
+	<-h.sem
 }
 
 func (h httpHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
 	log.Printf("GET: %s\n", r.URL.RequestURI())
 
-	h.lock()
-	defer h.unlock()
+	defer func() {
+		if r := recover(); r != nil {
+			if err, ok := r.(imgproxyError); ok {
+				respondWithError(rw, err)
+			} else {
+				respondWithError(rw, newUnexpectedError(r.(error), 4))
+			}
+		}
+	}()
 
-	t := time.Now()
+	t := startTimer(time.Duration(conf.WriteTimeout) * time.Second)
+
+	h.lock(t)
+	defer h.unlock()
 
 	if !checkSecret(r.Header.Get("Authorization")) {
-		repondWithForbidden(rw)
-		return
+		panic(invalidSecretErr)
 	}
 
 	imgURL, procOpt, err := parsePath(r)
 	if err != nil {
-		respondWithError(rw, 404, err, "Invalid image url")
-		return
+		panic(newError(404, err.Error(), "Invalid image url"))
 	}
 
 	if _, err = url.ParseRequestURI(imgURL); err != nil {
-		respondWithError(rw, 404, err, "Invalid image url")
-		return
+		panic(newError(404, err.Error(), "Invalid image url"))
 	}
 
 	b, imgtype, err := downloadImage(imgURL)
 	if err != nil {
-		respondWithError(rw, 404, err, "Image is unreachable")
-		return
+		panic(newError(404, err.Error(), "Image is unreachable"))
 	}
 
-	b, err = processImage(b, imgtype, procOpt)
+	t.Check()
+
+	b, err = processImage(b, imgtype, procOpt, t)
 	if err != nil {
-		respondWithError(rw, 500, err, "Error occurred while processing image")
-		return
+		panic(newError(500, err.Error(), "Error occurred while processing image"))
 	}
 
-	respondWithImage(r, rw, b, imgURL, procOpt, t)
+	t.Check()
+
+	respondWithImage(r, rw, b, imgURL, procOpt, t.Since())
 }

+ 32 - 0
timer.go

@@ -0,0 +1,32 @@
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+type timer struct {
+	StartTime time.Time
+	Timer     <-chan time.Time
+}
+
+func startTimer(dt time.Duration) *timer {
+	return &timer{time.Now(), time.After(dt)}
+}
+
+func (t *timer) Check() {
+	select {
+	case <-t.Timer:
+		panic(t.TimeoutErr())
+	default:
+		// Go ahead
+	}
+}
+
+func (t *timer) TimeoutErr() imgproxyError {
+	return newError(503, fmt.Sprintf("Timeout after %v", t.Since()), "Timeout")
+}
+
+func (t *timer) Since() time.Duration {
+	return time.Since(t.StartTime)
+}