check / check (push) Waiting to run
Process looked at its context only while waiting for a processing slot, so a transcode kept its slot and worked on after its request's downstream_timeout. acquireSlot now gives the slot back when the context has ended by the time it has one, and encode returns the context's error without beginning. govips gives pixa no way to stop libvips once an encode has begun, so that encode keeps its slot. A request waiting on a shared transcode stopped at the first request's deadline is answered 503, not 500. README.md "Routes" says both. Model: opus-5-5
88 lines
2.8 KiB
Go
88 lines
2.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/pixa/internal/httpfetcher"
|
|
"sneak.berlin/go/pixa/internal/imageprocessor"
|
|
"sneak.berlin/go/pixa/internal/imgcache"
|
|
)
|
|
|
|
// TestServerBusyAnswers503 checks that both image routes answer 503 with
|
|
// a clear error when the image service gives up waiting for a free upstream
|
|
// connection or processing slot, wrapped as the service wraps them.
|
|
func TestServerBusyAnswers503(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h := &Handlers{log: slog.New(slog.DiscardHandler)}
|
|
req := &imgcache.ImageRequest{SourceHost: "img.example.com", SourcePath: "/a.jpg"}
|
|
|
|
for _, err := range []error{
|
|
fmt.Errorf("upstream fetch failed: %w", httpfetcher.ErrTooManyConnections),
|
|
fmt.Errorf("image processing failed: %w", imageprocessor.ErrTooManyImages),
|
|
} {
|
|
plain := httptest.NewRecorder()
|
|
h.respondImageError(plain, req, err)
|
|
|
|
encrypted := httptest.NewRecorder()
|
|
h.handleImageError(encrypted, err)
|
|
|
|
for route, rec := range map[string]*httptest.ResponseRecorder{
|
|
"/v1/image/": plain, "/v1/e/": encrypted,
|
|
} {
|
|
if rec.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("%s for %v: status = %d, want %d",
|
|
route, err, rec.Code, http.StatusServiceUnavailable)
|
|
}
|
|
|
|
if !strings.Contains(rec.Body.String(), "server busy, try again later") {
|
|
t.Errorf("%s for %v: body = %q, want the server busy error",
|
|
route, err, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProcessingTimedOutAnswers503 checks that respondImageError and
|
|
// handleImageError, which answer the errors of the two image routes, answer
|
|
// 503 with the server busy error when the image service reports that
|
|
// processing stopped at the deadline of the request doing it, wrapped as the
|
|
// service wraps it. A request that waited for that processing gets this error
|
|
// while its own deadline may have time left. The request's source only goes
|
|
// into a log line, so it is left empty.
|
|
func TestProcessingTimedOutAnswers503(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
h := &Handlers{log: slog.New(slog.DiscardHandler)}
|
|
err := fmt.Errorf("%w: failed to encode: %w",
|
|
imgcache.ErrProcessingTimedOut, context.DeadlineExceeded)
|
|
|
|
plain := httptest.NewRecorder()
|
|
h.respondImageError(plain, &imgcache.ImageRequest{}, err)
|
|
|
|
encrypted := httptest.NewRecorder()
|
|
h.handleImageError(encrypted, err)
|
|
|
|
for name, rec := range map[string]*httptest.ResponseRecorder{
|
|
"respondImageError": plain, "handleImageError": encrypted,
|
|
} {
|
|
t.Logf("%s: status = %d, body = %q", name, rec.Code, rec.Body.String())
|
|
|
|
if rec.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("%s: status = %d, want %d",
|
|
name, rec.Code, http.StatusServiceUnavailable)
|
|
}
|
|
|
|
if !strings.Contains(rec.Body.String(), "server busy, try again later") {
|
|
t.Errorf("%s: body = %q, want the server busy error",
|
|
name, rec.Body.String())
|
|
}
|
|
}
|
|
}
|