Files
seaweedfs/weed/wdclient/filer_client_test.go
T
Chris LuandClaude Opus 5.5 3a8d756bdd s3: drop retired volume servers from the gateway location cache (#12281)
* wdclient: publish applied volume-location deletes to registered listeners

The master already tells each connected client which volumes a server
stopped hosting; only the MasterClient's own vidMap applied it. Expose
the same filtered deletions (moves between a server's disks excluded)
through OnVolumeLocationDelete so clients keeping a second location
cache can drop the retired server too.

* wdclient: drop retired volume servers from the FilerClient location cache

The S3 gateway's FilerClient keeps its own vidMap, filled by filer
lookups, and only forgot a retired volume server after a read against
it failed (a ~15s header-timeout stall when the host still resolves).
When built with a MasterClient, subscribe to its applied deletions so
the cache forgets the server as soon as the master reports it gone.

* wdclient: drop retired EC servers from the regular location map too

Filer lookups cache EC locations alongside regular ones in vid2Locations,
so an EC removal has to clear the retired server there as well as in the
EC map for the delete to reach what reads actually use.

* wdclient: tombstone pushed deletions against in-flight provider fills

A filer lookup already running when a delete lands can return the retired
location afterwards and re-cache it. Keep a short tombstone on each
deleted (vid, url) pair so fills neither cache nor return it while the
lookup source is still settling.

* wdclient: clear the FilerClient location cache on master reconnect

Removals the master pushed while the gateway link was down never arrive
as deletes, so on reconnect the lookup-fed cache is dropped outright
rather than kept generation-wise like the stream-fed one, and relearned
through filer lookups as before.

* wdclient: invoke push hooks outside their registration locks

A callback that registers or replaces a hook would wait on the read lock
the invocation already holds and stall the master stream. Copy the hook
under the lock and call it after the lock is released.

* wdclient: report pushed adds so confirmed re-adds clear the suppression

A server coming back at the same URL inside the tombstone window stayed
hidden from provider fills. updateVidMap now carries the additions it
applied through the same hook (renamed OnVolumeLocationChange), and the
registered cache untombstones them without eagerly caching them.

* wdclient: amortize tombstone expiry over a delete batch

A server leaving takes every volume it hosted with it, and scanning all
recentDeletes per removed vid squared the batch. Prune lazily on reads
and at most once per half TTL on writes.

* wdclient: let every location cache subscribe to pushed volume changes

The setters were single-slot, so a second FilerClient built on the same
MasterClient would have silently replaced the first one's subscription.
Registration now returns an unhook func, delivered to all listeners, and
FilerClient.Close releases its own.

* wdclient: hold the deletion fence until the master re-confirms a location

* wdclient: check the deletion fence and the cache add under one lock

* wdclient: drop provider answers that crossed a cache reset

* wdclient: bound the deletion fence by evicting its oldest mark

* wdclient: refresh a repeated removal instead of evicting another mark

* wdclient: evict the oldest deletion-fence mark in constant time

* wdclient: fence a server only once it serves the volume in no form

ec.encode registers the shards before the regular copy is deleted from the
same server, and ec.decode mounts the regular copy before the shards come
off it. Both arrive add-first in separate heartbeats, so the later removal
tombstoned a (vid, url) pair the server still serves and dropped it from
the FilerClient map; the filer's answer naming that server was then fenced
out and the volume read as not found until the gateway reconnected.

Tombstone and report a removal only when neither map still names the url
for that volume.

* wdclient: serve a provider answer that crossed a cache reset

The answer is as fresh as the provider's view; only the cache it would
land in has been re-told. Dropping it failed every read whose lookup was
in flight during a master reconnect with 'volume not found'. Return it
and leave the new view to be filled by the next lookup.

* wdclient: test the moved volume whose old server keeps proxying

* wdclient: stop caching inside the master lookup provider

The provider wrote every answered location through addLocation, which
clears the deletion fence: a lookup still naming a server a pushed delete
had just retired untombstoned and re-cached it. Fallback lookups already
gate caching through fillLocation, and authoritative lookups bypass the
cache by contract, so the provider no longer writes the map at all.

* wdclient: count only master-pushed locations when suppressing a removal

hasLocation could not tell a confirmed regular copy from a lookup fill:
the master answers an EC volume with its shard servers, so the regular map
holds shard locations that arrived by lookup, and entries an earlier
generation learned linger past a reconnect. An EC removal whose url still
showed either was suppressed, leaving the retired server served.

The vid map now records the generation each regular location was pushed
at, drops an unconfirmed regular entry alongside the shard that fed it,
and answers hasServingLocation from pushed marks and current-generation
entries only.

* wdclient: test removals against lookup-cached and stale locations

* wdclient: fence removals only in the gateway cache

The fence and the pushed-location marks ran inside every MasterClient,
including the filer's, whose map is also filled by lookups and cleared
by InvalidateCache. A removal reported while the server still served the
volume (e.g. a decoded copy whose mark a failed read had wiped) left the
pair fenced until the master pushed it again, with reads failing as
volume not found meanwhile.

Only the S3 gateway's FilerClient now tombstones, from the removals its
push-only MasterClient reports. That client decides a removal from the
current generation of both its maps after the whole message is applied,
so pushedLocations goes away, and the master lookup provider caches as
before. The hooks follow the existing single-slot SetOn...Fn pattern and
are wired next to SetOnPeerUpdateFn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* wdclient: let a removal fence expire after a minute

The master drops a broadcast when a client's channel is full, so the
re-add that would lift a fence can be lost while the gateway stays
connected. A mark held until that re-add then hid a live server, and a
volume whose only copy it held read as not found until the next master
reconnect. Bounding marks by a minute still outlasts a lookup in flight
when the removal landed; expiring early only falls back to learning by a
failed read, as before this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:05:00 -07:00

564 lines
19 KiB
Go

package wdclient
import (
"context"
"errors"
"fmt"
"net"
"sync/atomic"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/cluster"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func newTestFilerClient(addrs ...pb.ServerAddress) *FilerClient {
health := make([]*filerHealth, len(addrs))
for i := range health {
health[i] = &filerHealth{}
}
return &FilerClient{
filerAddresses: addrs,
filerHealth: health,
vidMapClient: newVidMapClient(nil, "", 0),
}
}
func filerAddressList(fc *FilerClient) []pb.ServerAddress {
out := make([]pb.ServerAddress, len(fc.filerAddresses))
copy(out, fc.filerAddresses)
return out
}
func TestApplyDiscoveredFilersPrunesStaleAddress(t *testing.T) {
a := pb.ServerAddress("10.0.0.1:18888")
b := pb.ServerAddress("10.0.0.2:18888") // gets replaced by c
c := pb.ServerAddress("10.0.0.3:18888")
fc := newTestFilerClient(a, b)
// Give b a non-trivial failure count so we can confirm it leaves with its health.
atomic.StoreInt32(&fc.filerHealth[1].failureCount, 7)
// Give a a known failure count so we can confirm survivor health is preserved.
atomic.StoreInt32(&fc.filerHealth[0].failureCount, 2)
atomic.StoreInt32(&fc.filerIndex, 1) // active filer is b
fc.applyDiscoveredFilers(map[pb.ServerAddress]struct{}{
a: {},
c: {},
})
got := filerAddressList(fc)
if len(got) != 2 || got[0] != a || got[1] != c {
t.Fatalf("expected [%s %s], got %v", a, c, got)
}
if len(fc.filerHealth) != 2 {
t.Fatalf("expected 2 health entries, got %d", len(fc.filerHealth))
}
if got := atomic.LoadInt32(&fc.filerHealth[0].failureCount); got != 2 {
t.Errorf("survivor health was reset: want 2, got %d", got)
}
if got := atomic.LoadInt32(&fc.filerHealth[1].failureCount); got != 0 {
t.Errorf("newly added filer should start with fresh health, got failureCount=%d", got)
}
// Active filer (b) disappeared; index must reset rather than dangle.
if idx := atomic.LoadInt32(&fc.filerIndex); idx != 0 {
t.Errorf("expected filerIndex reset to 0 after active filer removed, got %d", idx)
}
}
func TestApplyDiscoveredFilersKeepsIndexOnSurvivor(t *testing.T) {
a := pb.ServerAddress("10.0.0.1:18888") // gets replaced
b := pb.ServerAddress("10.0.0.2:18888") // active, survives
c := pb.ServerAddress("10.0.0.3:18888") // new
fc := newTestFilerClient(a, b)
atomic.StoreInt32(&fc.filerIndex, 1) // active filer is b
fc.applyDiscoveredFilers(map[pb.ServerAddress]struct{}{
b: {},
c: {},
})
got := filerAddressList(fc)
if len(got) != 2 || got[0] != b || got[1] != c {
t.Fatalf("expected [%s %s], got %v", b, c, got)
}
// b moved to index 0 after a was pruned; index should follow.
if idx := atomic.LoadInt32(&fc.filerIndex); idx != 0 {
t.Errorf("expected filerIndex to follow surviving active filer to position 0, got %d", idx)
}
}
func TestApplyDiscoveredFilersNoChangeIsNoop(t *testing.T) {
a := pb.ServerAddress("10.0.0.1:18888")
b := pb.ServerAddress("10.0.0.2:18888")
fc := newTestFilerClient(a, b)
originalHealthA := fc.filerHealth[0]
originalHealthB := fc.filerHealth[1]
fc.applyDiscoveredFilers(map[pb.ServerAddress]struct{}{
a: {},
b: {},
})
if fc.filerHealth[0] != originalHealthA || fc.filerHealth[1] != originalHealthB {
t.Errorf("no-op refresh should not reallocate health entries")
}
}
func filerUpdate(addr pb.ServerAddress, isAdd bool) *master_pb.ClusterNodeUpdate {
return &master_pb.ClusterNodeUpdate{NodeType: cluster.FilerType, Address: string(addr), IsAdd: isAdd}
}
// A rolling restart replaces every filer within one discovery interval; the
// pushed updates must keep the list live without waiting for the next poll.
func TestOnPeerUpdateTracksRollingReplacement(t *testing.T) {
old1 := pb.ServerAddress("10.0.0.1:8888")
old2 := pb.ServerAddress("10.0.0.2:8888")
new1 := pb.ServerAddress("10.0.1.1:8888")
new2 := pb.ServerAddress("10.0.1.2:8888")
fc := newTestFilerClient(old1, old2)
fc.OnPeerUpdate(filerUpdate(old1, false), time.Now())
fc.OnPeerUpdate(filerUpdate(new1, true), time.Now())
fc.OnPeerUpdate(filerUpdate(old2, false), time.Now())
fc.OnPeerUpdate(filerUpdate(new2, true), time.Now())
got := filerAddressList(fc)
if len(got) != 2 || got[0] != new1 || got[1] != new2 {
t.Fatalf("expected [%s %s], got %v", new1, new2, got)
}
}
func TestOnPeerUpdateKeepsLastFiler(t *testing.T) {
only := pb.ServerAddress("10.0.0.1:8888")
fc := newTestFilerClient(only)
fc.OnPeerUpdate(filerUpdate(only, false), time.Now())
if got := filerAddressList(fc); len(got) != 1 || got[0] != only {
t.Fatalf("removing the last filer must keep it, got %v", got)
}
}
func TestOnPeerUpdateIgnoresOtherNodeTypes(t *testing.T) {
a := pb.ServerAddress("10.0.0.1:8888")
fc := newTestFilerClient(a)
fc.OnPeerUpdate(&master_pb.ClusterNodeUpdate{NodeType: cluster.S3Type, Address: "10.0.0.9:18333", IsAdd: true}, time.Now())
if got := filerAddressList(fc); len(got) != 1 || got[0] != a {
t.Fatalf("non-filer update changed the list: %v", got)
}
}
func TestOnPeerUpdateDuplicateAddKeepsHealth(t *testing.T) {
a := pb.ServerAddress("10.0.0.1:8888")
fc := newTestFilerClient(a)
atomic.StoreInt32(&fc.filerHealth[0].failureCount, 2)
fc.OnPeerUpdate(filerUpdate(a, true), time.Now())
if len(fc.filerHealth) != 1 || atomic.LoadInt32(&fc.filerHealth[0].failureCount) != 2 {
t.Fatalf("re-announced filer lost its health state")
}
}
func TestDiscoverySnapshotTakenBeforePushIsDiscarded(t *testing.T) {
old := pb.ServerAddress("10.0.0.1:8888")
joined := pb.ServerAddress("10.0.1.1:8888")
fc := newTestFilerClient(old)
generation := fc.peerUpdateGeneration()
fc.OnPeerUpdate(filerUpdate(joined, true), time.Now())
fc.OnPeerUpdate(filerUpdate(old, false), time.Now())
fc.applyDiscoverySnapshot(map[pb.ServerAddress]struct{}{old: {}}, generation)
if got := filerAddressList(fc); len(got) != 1 || got[0] != joined {
t.Fatalf("stale snapshot overwrote pushed membership: %v", got)
}
}
func TestDiscoverySnapshotWithoutInterveningPushIsApplied(t *testing.T) {
old := pb.ServerAddress("10.0.0.1:8888")
replacement := pb.ServerAddress("10.0.1.1:8888")
fc := newTestFilerClient(old)
fc.applyDiscoverySnapshot(map[pb.ServerAddress]struct{}{replacement: {}}, fc.peerUpdateGeneration())
if got := filerAddressList(fc); len(got) != 1 || got[0] != replacement {
t.Fatalf("expected snapshot to replace list, got %v", got)
}
}
// A leave suppressed to keep the last filer is deferred, then honored as soon
// as a replacement joins, so the departed address stops being a candidate.
func TestOnPeerUpdateDeferredLeaveFlushesOnJoin(t *testing.T) {
old := pb.ServerAddress("10.0.0.1:8888")
joined := pb.ServerAddress("10.0.1.1:8888")
fc := newTestFilerClient(old)
fc.OnPeerUpdate(filerUpdate(old, false), time.Now())
if got := filerAddressList(fc); len(got) != 1 || got[0] != old {
t.Fatalf("leave of the last filer must be deferred, got %v", got)
}
if len(fc.deferredLeaves) != 1 {
t.Fatalf("expected the leave to be deferred, got %v", fc.deferredLeaves)
}
fc.OnPeerUpdate(filerUpdate(joined, true), time.Now())
if got := filerAddressList(fc); len(got) != 1 || got[0] != joined {
t.Fatalf("deferred leave should flush on join, got %v", got)
}
if len(fc.deferredLeaves) != 0 {
t.Fatalf("deferred leaves should be empty after flush, got %v", fc.deferredLeaves)
}
}
// A pushed add for an already-known filer changes nothing and must not bump
// the generation that guards an in-flight discovery snapshot.
func TestOnPeerUpdateNoopDoesNotDiscardSnapshot(t *testing.T) {
old := pb.ServerAddress("10.0.0.1:8888")
replacement := pb.ServerAddress("10.0.1.1:8888")
fc := newTestFilerClient(old)
generation := fc.peerUpdateGeneration()
fc.OnPeerUpdate(filerUpdate(old, true), time.Now())
fc.applyDiscoverySnapshot(map[pb.ServerAddress]struct{}{replacement: {}}, generation)
if got := filerAddressList(fc); len(got) != 1 || got[0] != replacement {
t.Fatalf("no-op push should not discard the snapshot, got %v", got)
}
}
func TestOnPeerUpdateRejoinCancelsDeferredLeave(t *testing.T) {
restarted := pb.ServerAddress("10.0.0.1:8888")
joined := pb.ServerAddress("10.0.1.1:8888")
fc := newTestFilerClient(restarted)
fc.OnPeerUpdate(filerUpdate(restarted, false), time.Now())
fc.OnPeerUpdate(filerUpdate(restarted, true), time.Now())
fc.OnPeerUpdate(filerUpdate(joined, true), time.Now())
if got := filerAddressList(fc); len(got) != 2 || got[0] != restarted || got[1] != joined {
t.Fatalf("rejoined filer was dropped by its stale deferred leave: %v", got)
}
}
func TestDiscoverySnapshotClearsDeferredLeaves(t *testing.T) {
departed := pb.ServerAddress("10.0.0.1:8888")
replacement := pb.ServerAddress("10.0.1.1:8888")
fc := newTestFilerClient(departed)
fc.OnPeerUpdate(filerUpdate(departed, false), time.Now())
fc.applyDiscoverySnapshot(map[pb.ServerAddress]struct{}{replacement: {}}, fc.peerUpdateGeneration())
fc.OnPeerUpdate(filerUpdate(departed, true), time.Now())
if got := filerAddressList(fc); len(got) != 2 || got[0] != replacement || got[1] != departed {
t.Fatalf("rejoined filer was dropped by a deferred leave the poll should have cleared: %v", got)
}
}
// A poll that started after a deferred leave but before the filer rejoined can
// return a snapshot lacking the rejoined filer; the rejoin must bump the
// generation so that snapshot is discarded.
func TestOnPeerUpdateRejoinBumpsGeneration(t *testing.T) {
old := pb.ServerAddress("10.0.0.1:8888")
other := pb.ServerAddress("10.0.1.1:8888")
fc := newTestFilerClient(old)
fc.OnPeerUpdate(filerUpdate(old, false), time.Now())
generation := fc.peerUpdateGeneration()
fc.OnPeerUpdate(filerUpdate(old, true), time.Now())
fc.applyDiscoverySnapshot(map[pb.ServerAddress]struct{}{other: {}}, generation)
if got := filerAddressList(fc); len(got) != 1 || got[0] != old {
t.Fatalf("rejoined filer was pruned by an in-flight snapshot: %v", got)
}
}
// stallingFiler answers LookupVolume only when the call is abandoned.
type stallingFiler struct {
filer_pb.UnimplementedSeaweedFilerServer
}
func (s *stallingFiler) LookupVolume(ctx context.Context, _ *filer_pb.LookupVolumeRequest) (*filer_pb.LookupVolumeResponse, error) {
<-ctx.Done()
return nil, ctx.Err()
}
func startStallingFiler(t *testing.T) pb.ServerAddress {
t.Helper()
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
grpcServer := grpc.NewServer()
filer_pb.RegisterSeaweedFilerServer(grpcServer, &stallingFiler{})
go func() { _ = grpcServer.Serve(lis) }()
t.Cleanup(grpcServer.Stop)
_, port, _ := net.SplitHostPort(lis.Addr().String())
return pb.ServerAddress(fmt.Sprintf("127.0.0.1:0.%s", port))
}
// lapsedDeadlineCtx mimics the moment after a caller's deadline has passed but
// before the runtime marks the context done.
type lapsedDeadlineCtx struct{ context.Context }
func (lapsedDeadlineCtx) Deadline() (time.Time, bool) { return time.Now().Add(-time.Second), true }
func (lapsedDeadlineCtx) Done() <-chan struct{} { return nil }
func (lapsedDeadlineCtx) Err() error { return nil }
// A caller giving up must not count as a filer failure, the filer's own timeout must.
func TestLookupVolumeIdsCountsOnlyFilerFailures(t *testing.T) {
tests := []struct {
name string
callerCtx func() (context.Context, context.CancelFunc)
grpcTimeout time.Duration
wantErr error
wantFailures int32
}{
{
name: "caller cancelled",
callerCtx: func() (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
time.AfterFunc(50*time.Millisecond, cancel)
return ctx, cancel
},
grpcTimeout: 5 * time.Second,
wantErr: context.Canceled,
wantFailures: 0,
},
{
name: "caller deadline",
callerCtx: func() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 50*time.Millisecond)
},
grpcTimeout: 5 * time.Second,
wantErr: context.DeadlineExceeded,
wantFailures: 0,
},
{
name: "caller deadline already elapsed",
callerCtx: func() (context.Context, context.CancelFunc) {
return lapsedDeadlineCtx{context.Background()}, func() {}
},
grpcTimeout: 5 * time.Second,
wantErr: context.DeadlineExceeded,
wantFailures: 0,
},
{
name: "filer timeout",
callerCtx: func() (context.Context, context.CancelFunc) {
return context.WithCancel(context.Background())
},
grpcTimeout: 50 * time.Millisecond,
wantFailures: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fc := newTestFilerClient(startStallingFiler(t))
fc.grpcDialOption = grpc.WithTransportCredentials(insecure.NewCredentials())
fc.grpcTimeout = tt.grpcTimeout
fc.maxRetries = 1
fc.failureThreshold = 3
fc.resetTimeout = time.Minute
ctx, cancel := tt.callerCtx()
defer cancel()
_, err := (&filerVolumeProvider{filerClient: fc}).LookupVolumeIds(ctx, []string{"4"})
if err == nil {
t.Fatal("lookup against a stalling filer succeeded")
}
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
t.Errorf("err = %v, want %v", err, tt.wantErr)
}
if got := atomic.LoadInt32(&fc.filerHealth[0].failureCount); got != tt.wantFailures {
t.Errorf("failureCount = %d, want %d", got, tt.wantFailures)
}
})
}
}
func TestOnVolumeLocationChangeDropsRetiredServer(t *testing.T) {
fc := newTestFilerClient()
retired := Location{Url: "retired:8080"}
live := Location{Url: "live:8080"}
fc.addLocation(7, retired)
fc.addLocation(7, live)
fc.addLocation(8, retired)
fc.OnVolumeLocationChange(retired, nil, []uint32{7, 8, 9})
locations, found := fc.GetLocations(7)
if !found || len(locations) != 1 || locations[0].Url != live.Url {
t.Errorf("vid 7 kept only the surviving location: found=%v %v", found, locations)
}
if locations, found := fc.GetLocations(8); found && len(locations) > 0 {
t.Errorf("vid 8 lost its last location but stayed cached: %v", locations)
}
}
func TestOnVidMapResetClearsCachedLocations(t *testing.T) {
fc := newTestFilerClient()
fc.addLocation(7, Location{Url: "live:8080"})
fc.OnVidMapReset()
if locations, found := fc.GetLocations(7); found && len(locations) > 0 {
t.Errorf("vid 7 survived the reset: %v", locations)
}
}
func TestOnVolumeLocationChangeReaddClearsTombstone(t *testing.T) {
retired := Location{Url: "retired:8080"}
fc := newTestFilerClient()
fc.vidMapClient = newVidMapClient(&testLocationProvider{
locations: map[string][]Location{"5": {retired}},
}, "", 0)
ctx := context.Background()
fc.OnVolumeLocationChange(retired, nil, []uint32{5})
if locations, _ := fc.LookupVolumeIdsWithFallback(ctx, []string{"5"}); len(locations["5"]) > 0 {
t.Fatalf("removed location still served: %v", locations)
}
fc.OnVolumeLocationChange(retired, []uint32{5}, nil)
locations, err := fc.LookupVolumeIdsWithFallback(ctx, []string{"5"})
if err != nil || len(locations["5"]) != 1 || locations["5"][0].Url != retired.Url {
t.Fatalf("re-added location not served again: %v, err %v", locations, err)
}
}
func hookedFilerClient(mc *MasterClient, provider VolumeLocationProvider) *FilerClient {
fc := newTestFilerClient()
fc.vidMapClient = newVidMapClient(provider, "", 0)
mc.SetOnVolumeLocationChangeFn(fc.OnVolumeLocationChange)
mc.SetOnVidMapResetFn(fc.OnVidMapReset)
return fc
}
func expectServed(t *testing.T, fc *FilerClient, vid string, want ...Location) {
t.Helper()
locations, err := fc.LookupVolumeIdsWithFallback(context.Background(), []string{vid})
got := locations[vid]
if len(want) == 0 {
if len(got) != 0 {
t.Fatalf("volume %s still served from %v", vid, got)
}
return
}
if err != nil || len(got) != len(want) {
t.Fatalf("volume %s served from %v, err %v, want %v", vid, got, err, want)
}
for i := range want {
if got[i].Url != want[i].Url {
t.Fatalf("volume %s served from %v, want %v", vid, got, want)
}
}
}
// ec.decode mounts the regular copy on a shard server before the shards come
// off it, so the later shard removal must leave that copy servable.
func TestDecodedCopyOutlivesShardRemoval(t *testing.T) {
a := Location{Url: "a:8080"}
b := Location{Url: "b:8080"}
provider := &testLocationProvider{locations: map[string][]Location{"5": {a, b}}}
mc := moveClient()
fc := hookedFilerClient(mc, provider)
pushVolumeLocation(mc, a.Url, nil, nil, []uint32{5}, nil)
pushVolumeLocation(mc, b.Url, nil, nil, []uint32{5}, nil)
expectServed(t, fc, "5", a, b)
pushVolumeLocation(mc, a.Url, []uint32{5}, nil, nil, nil)
pushVolumeLocation(mc, b.Url, nil, nil, nil, []uint32{5})
pushVolumeLocation(mc, a.Url, nil, nil, nil, []uint32{5})
provider.locations["5"] = []Location{a, b}
expectServed(t, fc, "5", a)
}
// ec.encode registers the shards before the regular copy is deleted from the
// same server, so the server stays a read candidate for the ec volume.
func TestShardServerOutlivesRegularRemoval(t *testing.T) {
a := Location{Url: "a:8080"}
provider := &testLocationProvider{locations: map[string][]Location{"5": {a}}}
mc := moveClient()
fc := hookedFilerClient(mc, provider)
pushVolumeLocation(mc, a.Url, []uint32{5}, nil, nil, nil)
expectServed(t, fc, "5", a)
pushVolumeLocation(mc, a.Url, nil, nil, []uint32{5}, nil)
pushVolumeLocation(mc, a.Url, nil, []uint32{5}, nil, nil)
expectServed(t, fc, "5", a)
}
// The filer caches shard servers like regular copies, so the last shard
// leaving a server retires it from the cache too.
func TestRetiredShardServerLeavesCache(t *testing.T) {
a := Location{Url: "a:8080"}
b := Location{Url: "b:8080"}
provider := &testLocationProvider{locations: map[string][]Location{"5": {a, b}}}
mc := moveClient()
fc := hookedFilerClient(mc, provider)
pushVolumeLocation(mc, a.Url, nil, nil, []uint32{5}, nil)
pushVolumeLocation(mc, b.Url, nil, nil, []uint32{5}, nil)
expectServed(t, fc, "5", a, b)
pushVolumeLocation(mc, a.Url, nil, nil, nil, []uint32{5})
if cached, _ := fc.GetLocations(5); len(cached) != 1 || cached[0].Url != b.Url {
t.Fatalf("cache holds %v, want only %v", cached, b)
}
fc.InvalidateCache("5,01")
expectServed(t, fc, "5", b)
}
// A volume moved to another server while the old one stays up: the old
// server would proxy the read and succeed, so no failed read would ever
// evict it. The pushed removal has to.
func TestMovedVolumeLeavesOldServer(t *testing.T) {
old := Location{Url: "old:8080"}
moved := Location{Url: "new:8080"}
provider := &testLocationProvider{locations: map[string][]Location{"5": {old}}}
mc := moveClient()
fc := hookedFilerClient(mc, provider)
pushVolumeLocation(mc, old.Url, []uint32{5}, nil, nil, nil)
expectServed(t, fc, "5", old)
pushVolumeLocation(mc, moved.Url, []uint32{5}, nil, nil, nil)
pushVolumeLocation(mc, old.Url, nil, []uint32{5}, nil, nil)
provider.locations["5"] = []Location{old, moved}
expectServed(t, fc, "5", moved)
}
// A server back at the same URL after a master reconnect is re-told as an
// add, which lifts the fence its earlier removal left.
func TestReconnectReplayLiftsFence(t *testing.T) {
a := Location{Url: "a:8080"}
provider := &testLocationProvider{locations: map[string][]Location{"5": {a}}}
mc := moveClient()
fc := hookedFilerClient(mc, provider)
pushVolumeLocation(mc, a.Url, []uint32{5}, nil, nil, nil)
pushVolumeLocation(mc, a.Url, nil, []uint32{5}, nil, nil)
expectServed(t, fc, "5")
mc.resetVidMap()
pushVolumeLocation(mc, a.Url, []uint32{5}, nil, nil, nil)
expectServed(t, fc, "5", a)
}