mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-03 02:32:35 +01:00
* volume: serve a ranged GET without reading the whole needle A ranged GET read every needle up to 1MB in full, checksum and all, then copied out the requested bytes. Read the metadata and stream only the range instead, as the Go server already does above 1MB and the Rust server does at any size. A small range of a 790KB needle goes from ~340us to ~160us. The metadata read stays non-terminal: the range read that follows reports the io outcome, so a failing disk still builds its error streak. HEAD, compressed needles and chunk manifests keep their current reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * volume: read the whole needle for a range that may need an image transform Without a URL extension, the stored name can still make a ranged GET with width, height or crop parameters an image transform, which needs the payload. Keep the full read there instead of streaming the range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * volume: stream a range from the record its metadata came from The streamed read looked the needle up again, so a replacement written after the metadata read sent new bytes under the old length, checksum and headers. Read the record the metadata read resolved, as the Rust server does, and after a compaction refuse a record whose size changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * rust volume: read the whole needle for a range that may need an image transform A ranged GET with no URL extension but width, height or crop parameters took the metadata-first path, so a stored image name never got its transform. Read a small needle whole there, as the Go server does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * volume: keep later parts of a multi-range reply on a moved record When a compaction moved the record before one part of a multi-range reply, that part re-resolved it, but the next part went back to the offset the metadata read saw. Remember the re-resolved offset too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * volume: check a compaction-moved record is the one the metadata came from After a compaction, a range re-resolved the needle and accepted any record of the same size, so a replacement written in between could send its bytes under the old headers. Compare the moved record's checksum and append time with the metadata already read, and fail otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * volume: accept a re-encoded checksum and count read errors on a moved record Compaction rewrites a pre-3.09 checksum in the current encoding, so the moved-record check now accepts either. A failed read of the moved record's metadata now feeds the io-error streak and surfaces as that error instead of not-found. The pinned record is a fresh value rather than a write through the index's pointer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * volume: keep a moved record's wrapped offset when a range follows it With 4-byte offsets, a record past the 32GB wrap is read at offset plus MaxPossibleVolumeSize. The compaction refresh dropped that, so it checked and streamed the wrong location. Retry the wrapped offset in the moved record check, as the metadata read does, and apply it to the stream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
447 lines
17 KiB
Go
447 lines
17 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func TestInternalErrorWritesTheReadError(t *testing.T) {
|
|
readErr := errors.New("invalid CRC for needle 2a (got 0a1b2c3d, want 4e5f6071), data on disk corrupted: needle data corrupted")
|
|
w := httptest.NewRecorder()
|
|
InternalError(w, httptest.NewRequest(http.MethodGet, "/1,abc", nil), readErr)
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Fatalf("status %d, want 500", w.Code)
|
|
}
|
|
if body := strings.TrimSpace(w.Body.String()); body != `{"error":"`+readErr.Error()+`"}` {
|
|
t.Fatalf("body %q", body)
|
|
}
|
|
|
|
w = httptest.NewRecorder()
|
|
InternalError(w, httptest.NewRequest(http.MethodGet, "/1,abc", nil), nil)
|
|
if w.Code != http.StatusInternalServerError || w.Body.Len() != 0 {
|
|
t.Fatalf("without an error: status %d body %q, want a bare 500", w.Code, w.Body.String())
|
|
}
|
|
|
|
w = httptest.NewRecorder()
|
|
InternalError(w, httptest.NewRequest(http.MethodHead, "/1,abc", nil), readErr)
|
|
if w.Code != http.StatusInternalServerError || w.Body.Len() != 0 {
|
|
t.Fatalf("on a HEAD request: status %d body %q, want a bare 500", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// A HEAD only attempts a metadata-only read, like a GET: a needle up to
|
|
// PagedReadLimit is read whole. Demanding metadata only would turn that one
|
|
// read into a header read followed by a trailer read. A ranged GET skips the
|
|
// payload at any size, since it streams only the range.
|
|
func TestShouldAttemptStreamWrite(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
method string
|
|
target string
|
|
ext string
|
|
rangeHeader string
|
|
hasVolume bool
|
|
wantAttempt bool
|
|
wantMustMeta bool
|
|
wantRange bool
|
|
}{
|
|
{"HEAD", http.MethodHead, "/1,abc.jpg", ".jpg", "", true, true, false, false},
|
|
{"GET", http.MethodGet, "/1,abc.jpg", ".jpg", "", true, true, false, false},
|
|
{"ranged GET", http.MethodGet, "/1,abc", "", "bytes=0-99", true, true, false, true},
|
|
{"ranged HEAD", http.MethodHead, "/1,abc", "", "bytes=0-99", true, true, false, false},
|
|
{"ranged GET of a resized image", http.MethodGet, "/1,abc.jpg?width=100", ".jpg", "bytes=0-99", true, false, false, false},
|
|
{"ranged GET to resize, no extension", http.MethodGet, "/1,abc?width=100", "", "bytes=0-99", true, true, false, false},
|
|
{"ranged GET to crop, no extension", http.MethodGet, "/1,abc?crop_x1=0&crop_y1=0&crop_x2=10&crop_y2=10", "", "bytes=0-99", true, true, false, false},
|
|
{"ranged GET with a width on a non-image", http.MethodGet, "/1,abc.bin?width=100", ".bin", "bytes=0-99", true, true, false, true},
|
|
{"ranged GET without a local volume", http.MethodGet, "/1,abc", "", "bytes=0-99", false, false, false, false},
|
|
{"GET of a resized image", http.MethodGet, "/1,abc.jpg?width=100", ".jpg", "", true, false, false, false},
|
|
{"HEAD of a resized image", http.MethodHead, "/1,abc.jpg?width=100", ".jpg", "", true, false, false, false},
|
|
{"HEAD of a cropped image", http.MethodHead, "/1,abc.jpg?crop_x1=0&crop_y1=0&crop_x2=100&crop_y2=100", ".jpg", "", true, false, false, false},
|
|
{"HEAD without a local volume", http.MethodHead, "/1,abc.jpg", ".jpg", "", false, false, false, false},
|
|
} {
|
|
r := httptest.NewRequest(tc.method, tc.target, nil)
|
|
if tc.rangeHeader != "" {
|
|
r.Header.Set("Range", tc.rangeHeader)
|
|
}
|
|
attempt, mustMeta, hasRange := shouldAttemptStreamWrite(tc.hasVolume, tc.ext, r)
|
|
if attempt != tc.wantAttempt || mustMeta != tc.wantMustMeta || hasRange != tc.wantRange {
|
|
t.Errorf("%s: attempt %v, must be metadata only %v, range %v; want %v, %v, %v",
|
|
tc.name, attempt, mustMeta, hasRange, tc.wantAttempt, tc.wantMustMeta, tc.wantRange)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNotModifiedChecksIfNoneMatchFirst(t *testing.T) {
|
|
const etag = "353dd8be"
|
|
const lastModified = 1700000000
|
|
before := time.Unix(lastModified-60, 0).UTC().Format(http.TimeFormat)
|
|
after := time.Unix(lastModified+60, 0).UTC().Format(http.TimeFormat)
|
|
|
|
tests := []struct {
|
|
name string
|
|
headers [][2]string
|
|
want bool
|
|
}{
|
|
{"matching etag", [][2]string{{"If-None-Match", `"353dd8be"`}}, true},
|
|
{"weak etag", [][2]string{{"If-None-Match", `W/"353dd8be"`}}, true},
|
|
{"etag in a list", [][2]string{{"If-None-Match", `"x", "353dd8be"`}}, true},
|
|
{"star", [][2]string{{"If-None-Match", `*`}}, true},
|
|
{"other etag", [][2]string{{"If-None-Match", `"x"`}}, false},
|
|
{"etag across repeated lines", [][2]string{{"If-None-Match", `"x"`}, {"If-None-Match", `"353dd8be"`}}, true},
|
|
{"comma inside quotes", [][2]string{{"If-None-Match", `"353dd8be,old"`}}, false},
|
|
{"other etag ignores If-Modified-Since", [][2]string{{"If-None-Match", `"x"`}, {"If-Modified-Since", after}}, false},
|
|
{"empty If-None-Match ignores If-Modified-Since", [][2]string{{"If-None-Match", ``}, {"If-Modified-Since", after}}, false},
|
|
{"not modified since", [][2]string{{"If-Modified-Since", after}}, true},
|
|
{"modified since", [][2]string{{"If-Modified-Since", before}}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := httptest.NewRequest(http.MethodGet, "/3,01637037d6", nil)
|
|
for _, kv := range tt.headers {
|
|
r.Header.Add(kv[0], kv[1])
|
|
}
|
|
if got := notModified(r, etag, lastModified); got != tt.want {
|
|
t.Errorf("notModified = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type proxyTestMaster struct {
|
|
master_pb.UnimplementedSeaweedServer
|
|
lookup func(context.Context, *master_pb.LookupVolumeRequest) (*master_pb.LookupVolumeResponse, error)
|
|
}
|
|
|
|
func (m *proxyTestMaster) LookupVolume(ctx context.Context, req *master_pb.LookupVolumeRequest) (*master_pb.LookupVolumeResponse, error) {
|
|
return m.lookup(ctx, req)
|
|
}
|
|
|
|
func newProxyTestHTTPServer(t *testing.T, handler http.Handler) *httptest.Server {
|
|
t.Helper()
|
|
listener, err := net.Listen("tcp4", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
server := &httptest.Server{Listener: listener, Config: &http.Server{Handler: handler}}
|
|
server.Start()
|
|
t.Cleanup(server.Close)
|
|
return server
|
|
}
|
|
|
|
type proxyTestResponseWriter struct {
|
|
http.ResponseWriter
|
|
statusWritten chan struct{}
|
|
}
|
|
|
|
func (w proxyTestResponseWriter) WriteHeader(status int) {
|
|
w.ResponseWriter.WriteHeader(status)
|
|
w.statusWritten <- struct{}{}
|
|
}
|
|
|
|
func newProxyTestVolumeServer(t *testing.T, volumeID string, locations []*master_pb.Location, lookupErr error) *VolumeServer {
|
|
t.Helper()
|
|
operation.InvalidateVolumeIdLocationCache(volumeID)
|
|
|
|
listener, err := net.Listen("tcp4", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
masterAddress := pb.NewServerAddress("127.0.0.1", 9333, listener.Addr().(*net.TCPAddr).Port)
|
|
masterServer := grpc.NewServer()
|
|
master_pb.RegisterSeaweedServer(masterServer, &proxyTestMaster{lookup: func(_ context.Context, req *master_pb.LookupVolumeRequest) (*master_pb.LookupVolumeResponse, error) {
|
|
if lookupErr != nil {
|
|
return nil, lookupErr
|
|
}
|
|
if len(req.VolumeOrFileIds) == 0 {
|
|
return nil, errors.New("lookup request contains no volume IDs")
|
|
}
|
|
return &master_pb.LookupVolumeResponse{VolumeIdLocations: []*master_pb.LookupVolumeResponse_VolumeIdLocation{{
|
|
VolumeOrFileId: req.VolumeOrFileIds[0],
|
|
Locations: locations,
|
|
}}}, nil
|
|
}})
|
|
go func() {
|
|
_ = masterServer.Serve(listener)
|
|
}()
|
|
|
|
vs := &VolumeServer{
|
|
store: &storage.Store{Ip: "192.0.2.10", Port: 8080},
|
|
ReadMode: "proxy",
|
|
grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
}
|
|
vs.setCurrentMaster(masterAddress)
|
|
t.Cleanup(func() {
|
|
operation.InvalidateVolumeIdLocationCache(volumeID)
|
|
pb.InvalidateGrpcConnection(masterAddress.ToGrpcAddress())
|
|
masterServer.Stop()
|
|
})
|
|
return vs
|
|
}
|
|
|
|
func TestProxyReqToTargetServerPreservesMethodAndResponse(t *testing.T) {
|
|
const volumeID = "849300101"
|
|
type observedRequest struct {
|
|
method string
|
|
rangeHeader string
|
|
ifNoneMatch string
|
|
ifModifiedAt string
|
|
header http.Header
|
|
}
|
|
observed := make(chan observedRequest, 2)
|
|
peer := newProxyTestHTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
observed <- observedRequest{
|
|
method: r.Method,
|
|
rangeHeader: r.Header.Get("Range"),
|
|
ifNoneMatch: r.Header.Get("If-None-Match"),
|
|
ifModifiedAt: r.Header.Get("If-Modified-Since"),
|
|
header: r.Header.Clone(),
|
|
}
|
|
w.Header().Set("ETag", `"peer-etag"`)
|
|
w.Header().Set("Content-Range", "bytes 0-3/10")
|
|
w.Header().Set("X-Peer-Header", "preserved")
|
|
w.Header().Set("Keep-Alive", "timeout=5")
|
|
w.Header().Set("X-Resp-Marker", "private")
|
|
w.Header()["Connection"] = []string{"keep-alive, X-Resp-Marker"}
|
|
w.WriteHeader(http.StatusPartialContent)
|
|
_, _ = io.WriteString(w, "data")
|
|
}))
|
|
|
|
remoteAddress := strings.TrimPrefix(peer.URL, "http://")
|
|
vs := newProxyTestVolumeServer(t, volumeID, []*master_pb.Location{{Url: remoteAddress}}, nil)
|
|
|
|
for _, method := range []string{http.MethodGet, http.MethodHead} {
|
|
t.Run(method, func(t *testing.T) {
|
|
r := httptest.NewRequest(method, "/"+volumeID+",abcdef", nil)
|
|
r.Header.Set("Range", "bytes=0-3")
|
|
r.Header.Set("If-None-Match", `"client-etag"`)
|
|
r.Header.Set("If-Modified-Since", "Wed, 21 Oct 2015 07:28:00 GMT")
|
|
r.Header.Set("Connection", "keep-alive, X-Hop-Marker")
|
|
r.Header.Set("Keep-Alive", "timeout=5")
|
|
r.Header.Set("Upgrade", "h2c")
|
|
r.Header.Set("Te", "trailers")
|
|
r.Header.Set("X-Hop-Marker", "private")
|
|
r.Header.Set("Proxy-Connection", "keep-alive")
|
|
r.Header.Set("Proxy-Authorization", "Basic cHJveHk6c2VjcmV0")
|
|
w := httptest.NewRecorder()
|
|
vs.proxyReqToTargetServer(w, r)
|
|
|
|
var got observedRequest
|
|
select {
|
|
case got = <-observed:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("peer did not receive the proxied request; proxy status %d", w.Code)
|
|
}
|
|
if got.method != method {
|
|
t.Errorf("peer received method %q, want %q", got.method, method)
|
|
}
|
|
if got.rangeHeader != "bytes=0-3" || got.ifNoneMatch != `"client-etag"` || got.ifModifiedAt != "Wed, 21 Oct 2015 07:28:00 GMT" {
|
|
t.Errorf("peer received conditional/range headers %+v", got)
|
|
}
|
|
for _, dropped := range []string{"Connection", "Keep-Alive", "Upgrade", "Te", "X-Hop-Marker", "Proxy-Connection", "Proxy-Authorization"} {
|
|
if got.header.Get(dropped) != "" {
|
|
t.Errorf("hop-by-hop header %s reached the replica: %v", dropped, got.header)
|
|
}
|
|
}
|
|
if w.Code != http.StatusPartialContent {
|
|
t.Errorf("status %d, want %d", w.Code, http.StatusPartialContent)
|
|
}
|
|
if w.Header().Get("ETag") != `"peer-etag"` || w.Header().Get("Content-Range") != "bytes 0-3/10" || w.Header().Get("X-Peer-Header") != "preserved" {
|
|
t.Errorf("peer response headers were not preserved: %v", w.Header())
|
|
}
|
|
for _, dropped := range []string{"Connection", "Keep-Alive", "X-Resp-Marker"} {
|
|
if w.Header().Get(dropped) != "" {
|
|
t.Errorf("hop-by-hop response header %s reached the client: %v", dropped, w.Header())
|
|
}
|
|
}
|
|
wantBody := "data"
|
|
if method == http.MethodHead {
|
|
wantBody = ""
|
|
}
|
|
if gotBody := w.Body.String(); gotBody != wantBody {
|
|
t.Errorf("response body %q, want %q", gotBody, wantBody)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProxyReqToTargetServerCancelsUpstreamWithInboundRequest(t *testing.T) {
|
|
const volumeID = "849300102"
|
|
peerStarted := make(chan struct{})
|
|
peerCanceled := make(chan struct{})
|
|
releasePeer := make(chan struct{})
|
|
var releasePeerOnce sync.Once
|
|
peer := newProxyTestHTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
close(peerStarted)
|
|
select {
|
|
case <-r.Context().Done():
|
|
close(peerCanceled)
|
|
case <-releasePeer:
|
|
}
|
|
}))
|
|
defer releasePeerOnce.Do(func() { close(releasePeer) })
|
|
|
|
remoteAddress := strings.TrimPrefix(peer.URL, "http://")
|
|
vs := newProxyTestVolumeServer(t, volumeID, []*master_pb.Location{{Url: remoteAddress}}, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
r := httptest.NewRequest(http.MethodGet, "/"+volumeID+",abcdef", nil).WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
proxyDone := make(chan struct{})
|
|
go func() {
|
|
vs.proxyReqToTargetServer(w, r)
|
|
close(proxyDone)
|
|
}()
|
|
|
|
select {
|
|
case <-peerStarted:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("peer did not receive the proxied request")
|
|
}
|
|
cancel()
|
|
select {
|
|
case <-peerCanceled:
|
|
case <-time.After(time.Second):
|
|
t.Error("canceling the inbound request did not cancel the peer request")
|
|
}
|
|
releasePeerOnce.Do(func() { close(releasePeer) })
|
|
select {
|
|
case <-proxyDone:
|
|
case <-time.After(2 * time.Second):
|
|
t.Error("proxy handler did not finish after inbound cancellation")
|
|
}
|
|
}
|
|
|
|
func TestProxyReqToTargetServerCancelsUpstreamBodyReadWithInboundRequest(t *testing.T) {
|
|
const volumeID = "849300106"
|
|
peerCanceled := make(chan struct{})
|
|
releasePeer := make(chan struct{})
|
|
var releasePeerOnce sync.Once
|
|
peer := newProxyTestHTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.(http.Flusher).Flush()
|
|
select {
|
|
case <-r.Context().Done():
|
|
close(peerCanceled)
|
|
case <-releasePeer:
|
|
}
|
|
}))
|
|
defer releasePeerOnce.Do(func() { close(releasePeer) })
|
|
|
|
remoteAddress := strings.TrimPrefix(peer.URL, "http://")
|
|
vs := newProxyTestVolumeServer(t, volumeID, []*master_pb.Location{{Url: remoteAddress}}, nil)
|
|
r := httptest.NewRequest(http.MethodGet, "/"+volumeID+",abcdef", nil)
|
|
ctx, cancel := context.WithCancel(r.Context())
|
|
r = r.WithContext(ctx)
|
|
recorder := httptest.NewRecorder()
|
|
statusWritten := make(chan struct{}, 1)
|
|
w := proxyTestResponseWriter{ResponseWriter: recorder, statusWritten: statusWritten}
|
|
proxyDone := make(chan struct{})
|
|
go func() {
|
|
vs.proxyReqToTargetServer(w, r)
|
|
close(proxyDone)
|
|
}()
|
|
|
|
select {
|
|
case <-statusWritten:
|
|
case <-time.After(2 * time.Second):
|
|
cancel()
|
|
t.Fatal("proxy did not receive the peer response headers")
|
|
}
|
|
cancel()
|
|
select {
|
|
case <-peerCanceled:
|
|
case <-time.After(time.Second):
|
|
t.Error("canceling the inbound request did not cancel the peer response body")
|
|
}
|
|
releasePeerOnce.Do(func() { close(releasePeer) })
|
|
select {
|
|
case <-proxyDone:
|
|
case <-time.After(2 * time.Second):
|
|
t.Error("proxy handler did not finish reading after inbound cancellation")
|
|
}
|
|
}
|
|
|
|
func TestProxyReqToTargetServerDistinguishesLookupFailureFromNoLocation(t *testing.T) {
|
|
t.Run("cancelled request skips the lookup error response", func(t *testing.T) {
|
|
const volumeID = "849300107"
|
|
vs := newProxyTestVolumeServer(t, volumeID, nil, nil)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
w := httptest.NewRecorder()
|
|
vs.proxyReqToTargetServer(w, httptest.NewRequest(http.MethodGet, "/"+volumeID+",abcdef", nil).WithContext(ctx))
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status %d, want no response written for a cancelled request", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("master lookup failure is internal error", func(t *testing.T) {
|
|
const volumeID = "849300103"
|
|
vs := newProxyTestVolumeServer(t, volumeID, nil, status.Error(codes.Unavailable, "master unavailable"))
|
|
w := httptest.NewRecorder()
|
|
vs.proxyReqToTargetServer(w, httptest.NewRequest(http.MethodGet, "/"+volumeID+",abcdef", nil))
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("status %d, want %d", w.Code, http.StatusInternalServerError)
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
locations []*master_pb.Location
|
|
}{
|
|
{name: "lookup returned no replica locations"},
|
|
{name: "only local replica is not a usable proxy target", locations: []*master_pb.Location{{Url: "192.0.2.10:8080"}}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
volumeID := "84930010"
|
|
if tc.name == "lookup returned no replica locations" {
|
|
volumeID += "4"
|
|
} else {
|
|
volumeID += "5"
|
|
}
|
|
vs := newProxyTestVolumeServer(t, volumeID, tc.locations, nil)
|
|
w := httptest.NewRecorder()
|
|
vs.proxyReqToTargetServer(w, httptest.NewRequest(http.MethodGet, "/"+volumeID+",abcdef", nil))
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("status %d, want %d", w.Code, http.StatusNotFound)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A request that already carries proxied=true must not be proxied again: a
|
|
// stale lookup pointing at servers that already answered would otherwise
|
|
// bounce the read between them until the client gives up.
|
|
func TestGetOrHeadHandlerDoesNotReproxyAProxiedRequest(t *testing.T) {
|
|
const volumeID = "849300106"
|
|
peer := newProxyTestHTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Error("a proxied request must not be forwarded again")
|
|
}))
|
|
remoteAddress := strings.TrimPrefix(peer.URL, "http://")
|
|
vs := newProxyTestVolumeServer(t, volumeID, []*master_pb.Location{{Url: remoteAddress}}, nil)
|
|
vs.guard = security.NewGuard(nil, "", 0, "", 0)
|
|
|
|
w := httptest.NewRecorder()
|
|
vs.GetOrHeadHandler(w, httptest.NewRequest(http.MethodGet, "/"+volumeID+",01637037d6?proxied=true", nil))
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("status %d, want %d", w.Code, http.StatusNotFound)
|
|
}
|
|
}
|