mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-13 15:29:12 +01:00
403 lines
14 KiB
Go
403 lines
14 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/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.
|
|
func TestShouldAttemptStreamWrite(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
method string
|
|
target string
|
|
ext string
|
|
hasVolume bool
|
|
wantAttempt bool
|
|
wantMustMeta bool
|
|
}{
|
|
{"HEAD", http.MethodHead, "/1,abc.jpg", ".jpg", true, true, false},
|
|
{"GET", http.MethodGet, "/1,abc.jpg", ".jpg", true, true, false},
|
|
{"GET of a resized image", http.MethodGet, "/1,abc.jpg?width=100", ".jpg", true, false, false},
|
|
{"HEAD of a resized image", http.MethodHead, "/1,abc.jpg?width=100", ".jpg", true, 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},
|
|
{"HEAD without a local volume", http.MethodHead, "/1,abc.jpg", ".jpg", false, false, false},
|
|
} {
|
|
r := httptest.NewRequest(tc.method, tc.target, nil)
|
|
attempt, mustMeta := shouldAttemptStreamWrite(tc.hasVolume, tc.ext, r)
|
|
if attempt != tc.wantAttempt || mustMeta != tc.wantMustMeta {
|
|
t.Errorf("%s: attempt %v, must be metadata only %v; want %v, %v", tc.name, attempt, mustMeta, tc.wantAttempt, tc.wantMustMeta)
|
|
}
|
|
}
|
|
}
|
|
|
|
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.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")
|
|
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"} {
|
|
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())
|
|
}
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|