Files
upstream-seaweedfs/weed/mount/peer_announcer_test.go
upstream-mirror 89f5be005d
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

376 lines
12 KiB
Go

package mount
import (
"context"
"sort"
"sync"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/mount_peer_pb"
"google.golang.org/grpc"
)
// fakeMountPeerClient records each ChunkAnnounce call; returns rejected
// fids from a configured set.
type fakeMountPeerClient struct {
mu sync.Mutex
mount_peer_pb.MountPeerClient
announcedBy map[string][]filerAnnouncement
rejectedByClient func(fid string) bool
}
type filerAnnouncement struct {
fids []string
peerAddr string
}
func (f *fakeMountPeerClient) ChunkAnnounce(ctx context.Context, req *mount_peer_pb.ChunkAnnounceRequest, opts ...grpc.CallOption) (*mount_peer_pb.ChunkAnnounceResponse, error) {
f.mu.Lock()
defer f.mu.Unlock()
// No owner key in the request; capture keyed by peer_addr (the announcer).
f.announcedBy[req.PeerAddr] = append(f.announcedBy[req.PeerAddr], filerAnnouncement{
fids: append([]string(nil), req.FileIds...),
peerAddr: req.PeerAddr,
})
resp := &mount_peer_pb.ChunkAnnounceResponse{}
if f.rejectedByClient != nil {
for _, fid := range req.FileIds {
if f.rejectedByClient(fid) {
resp.RejectedFileIds = append(resp.RejectedFileIds, fid)
}
}
}
return resp, nil
}
// fakeDialer returns the same fakeMountPeerClient for every peer addr,
// recording which owner the announcer dialed. Close is a no-op. The
// dialed slice is protected by its own mutex because the announcer now
// fans RPCs out across goroutines — concurrent appends would race.
type dialRecorder struct {
mu sync.Mutex
dialled []string
}
func (d *dialRecorder) record(addr string) {
d.mu.Lock()
d.dialled = append(d.dialled, addr)
d.mu.Unlock()
}
func (d *dialRecorder) snapshot() []string {
d.mu.Lock()
defer d.mu.Unlock()
out := make([]string, len(d.dialled))
copy(out, d.dialled)
return out
}
func fakeDialer(fc *fakeMountPeerClient, rec *dialRecorder) MountPeerDialer {
return func(ctx context.Context, peerAddr string) (mount_peer_pb.MountPeerClient, func(), error) {
rec.record(peerAddr)
return fc, func() {}, nil
}
}
func TestPeerAnnouncer_FlushGroupsByOwner(t *testing.T) {
fc := &fakeMountPeerClient{
announcedBy: map[string][]filerAnnouncement{},
}
rec := &dialRecorder{}
// Owner function: static assignment for test.
ownerFor := func(fid string) string {
if fid == "3,a" || fid == "3,c" {
return "owner-1:18081"
}
return "owner-2:18081"
}
a := NewPeerAnnouncer("self:18080", "", "", ownerFor, fakeDialer(fc, rec), nil)
a.EnqueueAnnounce("3,a")
a.EnqueueAnnounce("3,b")
a.EnqueueAnnounce("3,c")
a.FlushForTest(context.Background())
dialed := rec.snapshot()
sort.Strings(dialed)
if len(dialed) != 2 || dialed[0] != "owner-1:18081" || dialed[1] != "owner-2:18081" {
t.Errorf("expected both owners dialed once, got %v", dialed)
}
_, sent, _, _ := a.Stats()
if sent != 3 {
t.Errorf("expected 3 sent fids, got %d", sent)
}
}
func TestPeerAnnouncer_SkipOwnerEqualsSelf(t *testing.T) {
fc := &fakeMountPeerClient{announcedBy: map[string][]filerAnnouncement{}}
rec := &dialRecorder{}
a := NewPeerAnnouncer("self:18080", "", "", func(fid string) string {
return "self:18080"
}, fakeDialer(fc, rec), nil)
a.EnqueueAnnounce("3,a")
a.FlushForTest(context.Background())
if d := rec.snapshot(); len(d) != 0 {
t.Errorf("expected no dial when owner == self, dialed: %v", d)
}
// Self-owned fids stay in pending so a subsequent flush re-checks
// them against a possibly-refreshed seed view. Without this retry a
// mount whose initial MountList hadn't yet observed its peers would
// silently drop every write-path announce on the floor.
a.mu.Lock()
_, stillPending := a.pending["3,a"]
a.mu.Unlock()
if !stillPending {
t.Errorf("self-owned fid should stay pending for retry")
}
}
func TestPeerAnnouncer_RequeueOnRejection(t *testing.T) {
fc := &fakeMountPeerClient{
announcedBy: map[string][]filerAnnouncement{},
rejectedByClient: func(fid string) bool { return fid == "3,x" },
}
rec := &dialRecorder{}
a := NewPeerAnnouncer("self:18080", "", "", func(fid string) string {
return "owner:18081"
}, fakeDialer(fc, rec), nil)
a.EnqueueAnnounce("3,x")
a.EnqueueAnnounce("3,y")
a.FlushForTest(context.Background())
_, sent, rejected, _ := a.Stats()
if sent != 1 {
t.Errorf("expected 1 sent, got %d", sent)
}
if rejected != 1 {
t.Errorf("expected 1 rejected, got %d", rejected)
}
// The rejected fid should be back in pending.
a.mu.Lock()
_, stillPending := a.pending["3,x"]
a.mu.Unlock()
if !stillPending {
t.Errorf("3,x should have been requeued after rejection")
}
}
func TestPeerAnnouncer_TTLRenewal(t *testing.T) {
fc := &fakeMountPeerClient{announcedBy: map[string][]filerAnnouncement{}}
rec := &dialRecorder{}
now := time.Unix(1000, 0)
a := NewPeerAnnouncer("self:18080", "", "", func(fid string) string {
return "owner:18081"
}, fakeDialer(fc, rec), nil)
a.clock = func() time.Time { return now }
a.announceInterval = 10 * time.Second
a.announceTTL = 60 * time.Second
// Announce once.
a.EnqueueAnnounce("3,a")
a.FlushForTest(context.Background())
if _, sent, _, _ := a.Stats(); sent != 1 {
t.Fatalf("want 1 sent on initial announce, got %d", sent)
}
// Time passes to within the renewal threshold.
now = now.Add(45 * time.Second)
// No new enqueue, but renewal should fire.
a.FlushForTest(context.Background())
if _, sent, _, _ := a.Stats(); sent != 2 {
t.Errorf("want 2 sent after renewal, got %d", sent)
}
}
// TestPeerAnnouncer_ReAnnouncesOnOwnerChange guards the fix for a
// startup race where the writer's seed view reaches self-knowledge of
// its peers in two stages: first just {self,A}, then {self,A,B}. HRW
// may pick different owners across those views, so the announce from
// stage 1 is sent to a mount that will NOT be the HRW owner by the
// time a reader's seed view is the {self,A,B} version. Without
// re-announcing on owner change, the fid silently ages out on the
// wrong directory and readers get "no peer holder."
func TestPeerAnnouncer_ReAnnouncesOnOwnerChange(t *testing.T) {
fc := &fakeMountPeerClient{announcedBy: map[string][]filerAnnouncement{}}
rec := &dialRecorder{}
ownerNow := "owner-1:18081"
a := NewPeerAnnouncer("self:18080", "", "", func(fid string) string {
return ownerNow
}, fakeDialer(fc, rec), nil)
a.EnqueueAnnounce("3,x")
a.FlushForTest(context.Background())
if _, sent, _, _ := a.Stats(); sent != 1 {
t.Fatalf("initial announce: want 1 sent, got %d", sent)
}
// Seed view shifts; HRW now picks a different owner for the same
// fid. A fresh-timestamped entry in announcedAt should be treated
// as stale in owner terms and re-announced even though its age is
// well within the TTL renewal window.
ownerNow = "owner-2:18081"
a.FlushForTest(context.Background())
if _, sent, _, _ := a.Stats(); sent != 2 {
t.Errorf("after owner change: want 2 sent, got %d", sent)
}
dialed := rec.snapshot()
if len(dialed) != 2 || dialed[0] == dialed[1] {
t.Errorf("want dial to both owners, got %v", dialed)
}
}
// TestPeerAnnouncer_DropsEvictedFids guards the write→announce race:
// a chunk can be LRU-evicted between SetChunk and the next flush tick.
// Advertising an evicted fid just hands remote fetchers a NOT_FOUND
// from our FetchChunk server. With SetCachePresence wired, the
// announcer must drop evicted fids before dispatching the RPC and
// also clear them from announcedAt so they stop getting renewed.
func TestPeerAnnouncer_DropsEvictedFids(t *testing.T) {
fc := &fakeMountPeerClient{announcedBy: map[string][]filerAnnouncement{}}
rec := &dialRecorder{}
present := map[string]bool{"3,here": true, "3,gone": false}
a := NewPeerAnnouncer("self:18080", "", "", func(fid string) string {
return "owner:18081"
}, fakeDialer(fc, rec), nil)
a.SetCachePresence(func(fid string) bool { return present[fid] })
a.EnqueueAnnounce("3,here")
a.EnqueueAnnounce("3,gone")
a.FlushForTest(context.Background())
if _, sent, _, _ := a.Stats(); sent != 1 {
t.Errorf("only the still-cached fid should be announced, got sent=%d", sent)
}
fc.mu.Lock()
defer fc.mu.Unlock()
for _, an := range fc.announcedBy["self:18080"] {
for _, fid := range an.fids {
if fid == "3,gone" {
t.Errorf("evicted fid should not have been announced, saw %q", fid)
}
}
}
}
// TestPeerAnnouncer_StopWaitsForFlush guards the shutdown race: Stop
// must not return while a tick-triggered flushOnce is still dispatching
// RPCs, otherwise the owning wfs can tear down the conn pool out from
// under the in-flight sendTo goroutines.
func TestPeerAnnouncer_StopWaitsForFlush(t *testing.T) {
started := make(chan struct{})
unblock := make(chan struct{})
slowDialer := func(ctx context.Context, peerAddr string) (mount_peer_pb.MountPeerClient, func(), error) {
close(started)
select {
case <-unblock:
case <-ctx.Done():
}
return nil, func() {}, context.DeadlineExceeded
}
a := NewPeerAnnouncer("self:18080", "", "", func(string) string {
return "owner:18081"
}, slowDialer, nil)
// Shortcut straight into a flush on a dedicated goroutine so we
// can assert Stop() blocks on it.
a.EnqueueAnnounce("3,slow")
flushDone := make(chan struct{})
go func() {
a.FlushForTest(context.Background())
close(flushDone)
}()
<-started
// Prove Stop() returns only after the flush finishes by racing
// them: Stop() normally can't cancel a FlushForTest because
// we're not going through run(); but unblocking after stop's
// goroutine starts lets us confirm flushDone happens before
// stopDone never does under the old code.
stopDone := make(chan struct{})
go func() {
a.Stop()
close(stopDone)
}()
close(unblock)
select {
case <-flushDone:
case <-time.After(3 * time.Second):
t.Fatalf("flush did not complete within deadline")
}
select {
case <-stopDone:
case <-time.After(3 * time.Second):
t.Fatalf("Stop() did not return after flush completed")
}
}
// TestPeerAnnouncer_SelfOwnedWritesToLocalDir guards the shortcut path
// for fids whose HRW owner resolves to self: no RPC should go out, but
// the local PeerDirectory must end up with a holder entry pointing at
// self — otherwise remote ChunkLookup callers would get an empty
// holder list for fids this mount owns directly.
func TestPeerAnnouncer_SelfOwnedWritesToLocalDir(t *testing.T) {
fc := &fakeMountPeerClient{announcedBy: map[string][]filerAnnouncement{}}
rec := &dialRecorder{}
dir := NewPeerDirectory()
a := NewPeerAnnouncer("self:18080", "dc1", "rackA", func(fid string) string {
return "self:18080"
}, fakeDialer(fc, rec), dir)
a.EnqueueAnnounce("3,selfowned")
a.FlushForTest(context.Background())
if d := rec.snapshot(); len(d) != 0 {
t.Errorf("self-owned fid must not trigger any dial, got %v", d)
}
holders := dir.Lookup([]string{"3,selfowned"}, nil).PeersByFid["3,selfowned"]
if len(holders) != 1 {
t.Fatalf("local dir should have 1 holder after self-owned flush, got %d", len(holders))
}
if h := holders[0]; h.PeerAddr != "self:18080" || h.DataCenter != "dc1" || h.Rack != "rackA" {
t.Errorf("local dir entry wrong: %+v", h)
}
if _, sent, _, _ := a.Stats(); sent != 1 {
t.Errorf("sent counter should increment for self-owned too; got %d", sent)
}
}
func TestPeerAnnouncer_DialerErrorRequeues(t *testing.T) {
errDialer := func(ctx context.Context, peerAddr string) (mount_peer_pb.MountPeerClient, func(), error) {
return nil, func() {}, context.DeadlineExceeded
}
a := NewPeerAnnouncer("self:18080", "", "", func(fid string) string {
return "owner:18081"
}, errDialer, nil)
a.EnqueueAnnounce("3,a")
a.FlushForTest(context.Background())
_, _, _, errs := a.Stats()
if errs != 1 {
t.Errorf("expected 1 flush error, got %d", errs)
}
a.mu.Lock()
_, pending := a.pending["3,a"]
a.mu.Unlock()
if !pending {
t.Errorf("fid should be requeued after dial error")
}
}