mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 15:00:48 +01:00
* volume: overlap the local needle write with the replica fan-out ReplicatedWrite wrote the needle to local disk and then fanned it out to the replicas serially, so a replicated write cost local + replicas. The needle is already fully buffered, so the two can run at once. Snapshot the fields the fan-out reads first (the local write stamps the volume TTL and updates needle flags, so the replica goroutine must not touch the shared needle), then run the local write and the replica fan-out concurrently and ack only after both. Where local disk and replica network are separate resources the write latency drops to max(local, replicas) instead of their sum; co-located on one disk the two writes contend, so this helps distributed placements, not single-host tests. * volume: return the local isUnchanged from the write goroutine * volume: trim overlap-write comments * volume-rs: overlap the replica fan-out with the local needle write * volume: test the overlapped local write and replica fan-out * volume: decode replica pairs only when replicas exist A single-copy or incoming replicate write never uses the decoded pair map, so a malformed Pairs blob only burned an error counter and a log line. Gate the decode on replicaCount. * volume: release the stalled replica on test failure paths An early t.Fatal left the httptest handler blocked on releaseReplica, so the deferred Close hung instead of reporting the failure. Release idempotently ahead of the server close. * test: mismatched If-None-Match serves the object over If-Modified-Since The precedence case asserted the old If-Modified-Since-first ordering and expected 304. The volume servers now evaluate If-None-Match first, so a non-matching tag must return 200 with the body even when If-Modified-Since would allow 304 (RFC 9110 13.1.3).
448 lines
14 KiB
Go
448 lines
14 KiB
Go
package topology
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"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/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
// TestDistributedOperationCancelsSiblingsOnFirstError verifies that once one
|
|
// replica fails, an outstanding replica still stalled in a dial timeout is
|
|
// cancelled rather than gating the caller until it times out.
|
|
func TestDistributedOperationCancelsSiblingsOnFirstError(t *testing.T) {
|
|
locations := []operation.Location{{Url: "fast"}, {Url: "slow"}}
|
|
cancelled := make(chan struct{}, 1)
|
|
|
|
start := time.Now()
|
|
err := DistributedOperation(context.Background(), locations, func(ctx context.Context, location operation.Location) error {
|
|
if location.Url == "fast" {
|
|
return errors.New("connection refused")
|
|
}
|
|
// slow: a replica stalled in a dial timeout
|
|
select {
|
|
case <-ctx.Done():
|
|
cancelled <- struct{}{}
|
|
return ctx.Err()
|
|
case <-time.After(10 * time.Second):
|
|
return nil
|
|
}
|
|
})
|
|
|
|
if err == nil {
|
|
t.Fatal("expected an error from the fast-failing replica")
|
|
}
|
|
if elapsed := time.Since(start); elapsed > 2*time.Second {
|
|
t.Fatalf("did not fail fast: took %v", elapsed)
|
|
}
|
|
select {
|
|
case <-cancelled:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("slow replica was not cancelled after the first error")
|
|
}
|
|
}
|
|
|
|
func TestDistributedOperationEmpty(t *testing.T) {
|
|
err := DistributedOperation(context.Background(), nil, func(ctx context.Context, location operation.Location) error {
|
|
t.Fatal("op should not be called when there are no locations")
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("expected nil for no locations, got %v", err)
|
|
}
|
|
}
|
|
|
|
type mockMasterServer struct {
|
|
master_pb.UnimplementedSeaweedServer
|
|
mu sync.Mutex
|
|
calls int
|
|
locations []*master_pb.Location
|
|
}
|
|
|
|
func (m *mockMasterServer) LookupVolume(ctx context.Context, req *master_pb.LookupVolumeRequest) (*master_pb.LookupVolumeResponse, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.calls++
|
|
var vls []*master_pb.LookupVolumeResponse_VolumeIdLocation
|
|
for _, vid := range req.VolumeOrFileIds {
|
|
vl := &master_pb.LookupVolumeResponse_VolumeIdLocation{
|
|
VolumeOrFileId: vid,
|
|
Locations: m.locations,
|
|
}
|
|
if len(m.locations) == 0 {
|
|
vl.Error = fmt.Sprintf("volume id %s not found", vid)
|
|
}
|
|
vls = append(vls, vl)
|
|
}
|
|
return &master_pb.LookupVolumeResponse{VolumeIdLocations: vls}, nil
|
|
}
|
|
|
|
func startMockMasterServer(t *testing.T, master *mockMasterServer) (operation.GetMasterFn, grpc.DialOption) {
|
|
t.Helper()
|
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
grpcServer := grpc.NewServer()
|
|
master_pb.RegisterSeaweedServer(grpcServer, master)
|
|
serveErr := make(chan error, 1)
|
|
go func() { serveErr <- grpcServer.Serve(lis) }()
|
|
t.Cleanup(func() {
|
|
grpcServer.Stop()
|
|
if err := <-serveErr; err != nil && !errors.Is(err, grpc.ErrServerStopped) {
|
|
t.Errorf("mock master serve: %v", err)
|
|
}
|
|
})
|
|
|
|
grpcPort := lis.Addr().(*net.TCPAddr).Port
|
|
return func(_ context.Context) pb.ServerAddress {
|
|
return pb.NewServerAddressWithGrpcPort(fmt.Sprintf("127.0.0.1:%d", grpcPort), grpcPort)
|
|
}, grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
}
|
|
|
|
func TestGetWritableRemoteReplicationsRefreshesReadOnlyReplicas(t *testing.T) {
|
|
master := &mockMasterServer{locations: []*master_pb.Location{
|
|
{Url: "127.0.0.1:8080"},
|
|
{Url: "127.0.0.2:8080"},
|
|
}}
|
|
masterFn, dialOption := startMockMasterServer(t, master)
|
|
store := &storage.Store{Ip: "127.0.0.1", Port: 8080}
|
|
volumeId := needle.VolumeId(1)
|
|
operation.InvalidateVolumeIdLocationCache(volumeId.String())
|
|
|
|
locations, err := GetWritableRemoteReplications(store, dialOption, volumeId, masterFn)
|
|
if err != nil {
|
|
t.Fatalf("first lookup: %v", err)
|
|
}
|
|
if len(locations) != 1 || locations[0].Url != "127.0.0.2:8080" {
|
|
t.Fatalf("first lookup locations = %v", locations)
|
|
}
|
|
|
|
master.mu.Lock()
|
|
master.locations = []*master_pb.Location{
|
|
{Url: "127.0.0.1:8080"},
|
|
{Url: "127.0.0.2:8080", ReadOnly: true, ReadOnlyCanDelete: true},
|
|
}
|
|
master.mu.Unlock()
|
|
|
|
locations, err = GetWritableRemoteReplications(store, dialOption, volumeId, masterFn)
|
|
if err != nil {
|
|
t.Fatalf("second lookup: %v", err)
|
|
}
|
|
if len(locations) != 0 {
|
|
t.Fatalf("read-only replica remained a write target: %v", locations)
|
|
}
|
|
master.mu.Lock()
|
|
calls := master.calls
|
|
master.mu.Unlock()
|
|
if calls != 2 {
|
|
t.Fatalf("master lookup calls = %d, want 2", calls)
|
|
}
|
|
|
|
locations, err = GetRemoteReplications(store, dialOption, volumeId, masterFn)
|
|
if err != nil {
|
|
t.Fatalf("delete lookup: %v", err)
|
|
}
|
|
if len(locations) != 1 || !locations[0].ReadOnly || !locations[0].ReadOnlyCanDelete {
|
|
t.Fatalf("read-only-can-delete target was dropped: %v", locations)
|
|
}
|
|
}
|
|
|
|
// TestReplicatedWriteForwardsFsyncToReplicas verifies that the fsync=true
|
|
// request parameter is forwarded to replica volume servers in the fan-out
|
|
// request, so a durable write means every replica has flushed to disk.
|
|
func TestReplicatedWriteForwardsFsyncToReplicas(t *testing.T) {
|
|
replicaQueries := make(chan url.Values, 4)
|
|
replica := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
replicaQueries <- r.URL.Query()
|
|
w.WriteHeader(http.StatusCreated)
|
|
if _, err := w.Write([]byte(`{"size":1}`)); err != nil {
|
|
t.Errorf("replica write response: %v", err)
|
|
}
|
|
}))
|
|
defer replica.Close()
|
|
replicaHost := strings.TrimPrefix(replica.URL, "http://")
|
|
|
|
masterFn, dialOption := startMockMasterServer(t, &mockMasterServer{
|
|
locations: []*master_pb.Location{{Url: replicaHost}},
|
|
})
|
|
|
|
store := &storage.Store{}
|
|
volumeId := needle.VolumeId(1)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
fsync string
|
|
wantFsync bool
|
|
}{
|
|
{name: "fsync requested", fsync: "true", wantFsync: true},
|
|
{name: "no fsync requested", fsync: "", wantFsync: false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
operation.InvalidateVolumeIdLocationCache(volumeId.String())
|
|
|
|
path := "http://127.0.0.1:8080/1,01637037d6"
|
|
if tc.fsync != "" {
|
|
path += "?fsync=" + tc.fsync
|
|
}
|
|
r := httptest.NewRequest(http.MethodPost, path, nil)
|
|
if err := r.ParseForm(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
n := &needle.Needle{
|
|
Id: 1,
|
|
Data: []byte("test data"),
|
|
Ttl: needle.EMPTY_TTL,
|
|
}
|
|
if _, err := ReplicatedWrite(context.Background(), masterFn, dialOption, store, volumeId, n, r, ""); err != nil {
|
|
t.Fatalf("ReplicatedWrite: %v", err)
|
|
}
|
|
|
|
select {
|
|
case q := <-replicaQueries:
|
|
got := q.Get("fsync")
|
|
if tc.wantFsync && got != "true" {
|
|
t.Errorf("expected fsync=true in replica query, got %q (query: %v)", got, q)
|
|
}
|
|
if !tc.wantFsync && got != "" {
|
|
t.Errorf("expected no fsync in replica query, got %q (query: %v)", got, q)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("replica never received the fan-out request")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// newReplicateTestStore builds a single-disk store in dir, draining the notify
|
|
// channels so volume mounts never block on a heartbeat reader.
|
|
func newReplicateTestStore(t *testing.T, dir string) *storage.Store {
|
|
t.Helper()
|
|
store := storage.NewStore(nil, "127.0.0.1", 8080, 18080, "http://127.0.0.1:8080", "store-id",
|
|
[]string{dir}, []int32{100}, []util.MinFreeSpace{{}}, "",
|
|
storage.NeedleMapInMemory, []types.DiskType{types.HardDriveType}, nil, 3,
|
|
stats.DefaultDiskIOProbeConfig())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-store.NewVolumesChan:
|
|
case <-store.DeletedVolumesChan:
|
|
case <-store.NewEcShardsChan:
|
|
case <-store.DeletedEcShardsChan:
|
|
case <-store.StateUpdateChan:
|
|
case <-done:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
t.Cleanup(func() {
|
|
close(done)
|
|
store.Close()
|
|
})
|
|
return store
|
|
}
|
|
|
|
// TestReplicatedWriteOverlapsLocalAndReplicaWrites runs the write path with a
|
|
// real local volume and a replica that stalls mid-request: the fan-out starts
|
|
// while the local write is still free to run, the local write lands before the
|
|
// replica unblocks, and the acknowledged write waits for both legs.
|
|
func TestReplicatedWriteOverlapsLocalAndReplicaWrites(t *testing.T) {
|
|
replicaStarted := make(chan url.Values, 1)
|
|
releaseReplica := make(chan struct{})
|
|
var releaseOnce sync.Once
|
|
release := func() { releaseOnce.Do(func() { close(releaseReplica) }) }
|
|
replica := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
replicaStarted <- r.URL.Query()
|
|
<-releaseReplica
|
|
w.WriteHeader(http.StatusCreated)
|
|
if _, err := w.Write([]byte(`{"size":21}`)); err != nil {
|
|
t.Errorf("replica write response: %v", err)
|
|
}
|
|
}))
|
|
defer replica.Close()
|
|
defer release()
|
|
replicaHost := strings.TrimPrefix(replica.URL, "http://")
|
|
|
|
masterFn, dialOption := startMockMasterServer(t, &mockMasterServer{
|
|
locations: []*master_pb.Location{{Url: "127.0.0.1:8080"}, {Url: replicaHost}},
|
|
})
|
|
|
|
store := newReplicateTestStore(t, t.TempDir())
|
|
volumeId := needle.VolumeId(7)
|
|
operation.InvalidateVolumeIdLocationCache(volumeId.String())
|
|
if err := store.AddVolume(volumeId, "", storage.NeedleMapInMemory, "001", "15d", 0, needle.GetCurrentVersion(), 0, types.HardDriveType, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data := []byte("overlapped write data")
|
|
n := &needle.Needle{
|
|
Id: 1,
|
|
Data: data,
|
|
Checksum: needle.NewCRC(data),
|
|
Ttl: needle.EMPTY_TTL,
|
|
}
|
|
r := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:8080/7,01637037d6", nil)
|
|
if err := r.ParseForm(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
_, err := ReplicatedWrite(context.Background(), masterFn, dialOption, store, volumeId, n, r, "")
|
|
done <- err
|
|
}()
|
|
|
|
var q url.Values
|
|
select {
|
|
case q = <-replicaStarted:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("replica never received the fan-out request")
|
|
}
|
|
if got := q.Get("type"); got != "replicate" {
|
|
t.Errorf("replica type = %q, want replicate", got)
|
|
}
|
|
if got := q.Get("ttl"); got != "15d" {
|
|
t.Errorf("replica ttl = %q, want the inherited volume ttl 15d", got)
|
|
}
|
|
|
|
select {
|
|
case err := <-done:
|
|
t.Fatalf("write returned before the replica responded: %v", err)
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
readBack := &needle.Needle{Id: n.Id}
|
|
if _, err := store.ReadVolumeNeedle(volumeId, readBack, nil, nil); err == nil && string(readBack.Data) == string(data) {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("local write did not land while the replica was still blocked")
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
|
|
release()
|
|
if err := <-done; err != nil {
|
|
t.Fatalf("ReplicatedWrite: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestReplicatedWriteFailsWhenEitherLegFails checks that a failed local write
|
|
// or a failed replica fan-out fails the whole request even when the other leg
|
|
// succeeded.
|
|
func TestReplicatedWriteFailsWhenEitherLegFails(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
volumeId needle.VolumeId
|
|
readonlyLocal bool
|
|
replicaStatus int
|
|
}{
|
|
{name: "local write fails", volumeId: 8, readonlyLocal: true, replicaStatus: http.StatusCreated},
|
|
{name: "replica write fails", volumeId: 9, replicaStatus: http.StatusInternalServerError},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
replica := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(tc.replicaStatus)
|
|
}))
|
|
defer replica.Close()
|
|
replicaHost := strings.TrimPrefix(replica.URL, "http://")
|
|
|
|
masterFn, dialOption := startMockMasterServer(t, &mockMasterServer{
|
|
locations: []*master_pb.Location{{Url: "127.0.0.1:8080"}, {Url: replicaHost}},
|
|
})
|
|
|
|
store := newReplicateTestStore(t, t.TempDir())
|
|
operation.InvalidateVolumeIdLocationCache(tc.volumeId.String())
|
|
if err := store.AddVolume(tc.volumeId, "", storage.NeedleMapInMemory, "001", "", 0, needle.GetCurrentVersion(), 0, types.HardDriveType, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tc.readonlyLocal {
|
|
if err := store.MarkVolumeReadonly(tc.volumeId, false, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
data := []byte("write data")
|
|
n := &needle.Needle{
|
|
Id: 1,
|
|
Data: data,
|
|
Checksum: needle.NewCRC(data),
|
|
Ttl: needle.EMPTY_TTL,
|
|
}
|
|
r := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://127.0.0.1:8080/%d,01637037d6", tc.volumeId), nil)
|
|
if err := r.ParseForm(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := ReplicatedWrite(context.Background(), masterFn, dialOption, store, tc.volumeId, n, r, ""); err == nil {
|
|
t.Fatal("ReplicatedWrite succeeded despite a failed leg")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestReplicatedWriteRejectsWriteWithNoTarget verifies that a write this server
|
|
// cannot store and has no replica to forward to fails instead of being
|
|
// acknowledged with nothing written.
|
|
func TestReplicatedWriteRejectsWriteWithNoTarget(t *testing.T) {
|
|
master := &mockMasterServer{}
|
|
masterFn, dialOption := startMockMasterServer(t, master)
|
|
store := &storage.Store{Ip: "127.0.0.1", Port: 8080}
|
|
volumeId := needle.VolumeId(31234)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
query string
|
|
locations []*master_pb.Location
|
|
}{
|
|
{name: "volume unknown to the master"},
|
|
{name: "master lists only this server", locations: []*master_pb.Location{{Url: "127.0.0.1:8080"}}},
|
|
{name: "replica write for a volume not on this server", query: "?type=replicate"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
master.mu.Lock()
|
|
master.locations = tc.locations
|
|
master.mu.Unlock()
|
|
|
|
r := httptest.NewRequest(http.MethodPost, "http://127.0.0.1:8080/31234,01637037d6"+tc.query, nil)
|
|
if err := r.ParseForm(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
n := &needle.Needle{
|
|
Id: 1,
|
|
Data: []byte("test data"),
|
|
Ttl: needle.EMPTY_TTL,
|
|
}
|
|
_, err := ReplicatedWrite(context.Background(), masterFn, dialOption, store, volumeId, n, r, "")
|
|
if err == nil {
|
|
t.Fatal("ReplicatedWrite acknowledged a write that no volume stored")
|
|
}
|
|
if !strings.Contains(err.Error(), "not found") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|