s3api: fix TestRoutedMultipartFinalize flake from a reused fake filer port (#12571)

* s3api: drop cached gRPC channels when a fake filer takes a port

A test that dials a dead owner leaves its failed channel in the
process-wide gRPC connection cache. When a later test's fake filer is
given the same port, its first RPC fails at once with the cached
connection refused, which is how TestRoutedMultipartFinalize failed on
the 32-bit job. The fake filer helpers now share fakeFilerAddress, which
invalidates any cached channel for the new address.

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

* s3api: test a fake filer on a dead owner's reused port

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

* s3api: drop cached channels for dead test filer addresses too

closedFilerAddress can be handed a port whose cached channel belonged to
a fake filer that just stopped and may still look ready; then the dead
owner's transaction is sent on it instead of failing over. Build its
address with fakeFilerAddress as well.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris LuandClaude Opus 5.5 authored and GitHub committed 2026-11-11 14:50:16 -08:00
1 parent 6c1eec8703
commit a0f95e16bc
3 files changed
+49 -8

No files matched your search

+47 -4
View File
@@ -336,8 +336,18 @@ func startFakeFiler(t *testing.T, impl filer_pb.SeaweedFilerServer) pb.ServerAdd
filer_pb.RegisterSeaweedFilerServer(srv, impl)
go srv.Serve(lis)
t.Cleanup(srv.Stop)
port := lis.Addr().(*net.TCPAddr).Port
return pb.ServerAddress(fmt.Sprintf("127.0.0.1:1.%d", port))
return fakeFilerAddress(lis)
}
// fakeFilerAddress returns the S3-style filer address of lis. The gRPC
// connection cache is process-wide and keyed by address, so it drops whatever
// channel an earlier test left for this port: a failed one would fail the first
// RPC with a stale "connection refused", and one to a stopped fake filer can
// still look ready.
func fakeFilerAddress(lis net.Listener) pb.ServerAddress {
address := pb.ServerAddress(fmt.Sprintf("127.0.0.1:1.%d", lis.Addr().(*net.TCPAddr).Port))
pb.InvalidateGrpcConnection(address.ToGrpcAddress())
return address
}
func TestRoutedDeleteSpecificVersionUsesWormCondition(t *testing.T) {
@@ -480,9 +490,42 @@ func closedFilerAddress(t *testing.T) pb.ServerAddress {
if err != nil {
t.Fatal(err)
}
port := lis.Addr().(*net.TCPAddr).Port
lis.Close()
return pb.ServerAddress(fmt.Sprintf("127.0.0.1:1.%d", port))
return fakeFilerAddress(lis)
}
// A fake filer that gets the port of a dead owner an earlier test dialed must
// not inherit that owner's failed channel.
func TestFakeFilerOnReusedPortIsReachable(t *testing.T) {
live := &fakeTxnFiler{}
liveAddr := startFakeFiler(t, live)
deadOwner := closedFilerAddress(t)
dialOption := grpc.WithTransportCredentials(insecure.NewCredentials())
s3a := &S3ApiServer{
option: &S3ApiServerOption{GrpcDialOption: dialOption},
filerClient: wdclient.NewFilerClient([]pb.ServerAddress{liveAddr}, dialOption, ""),
}
if _, err := s3a.objectTxnOnFiler(deadOwner, &filer_pb.ObjectTransactionRequest{LockKey: "/buckets/b/o", RouteKey: "s3.object.write:/buckets/b/o"}); err != nil {
t.Fatalf("expected failover success, got %v", err)
}
lis, err := net.Listen("tcp", deadOwner.ToGrpcAddress())
if err != nil {
t.Skipf("port of the dead owner was taken: %v", err)
}
reused := &fakeTxnFiler{}
srv := grpc.NewServer()
filer_pb.RegisterSeaweedFilerServer(srv, reused)
go srv.Serve(lis)
t.Cleanup(srv.Stop)
direct := &S3ApiServer{option: &S3ApiServerOption{GrpcDialOption: dialOption}}
if _, err := direct.objectTxnOnFiler(fakeFilerAddress(lis), &filer_pb.ObjectTransactionRequest{LockKey: "/buckets/b/o"}); err != nil {
t.Fatalf("fake filer on a reused port: %v", err)
}
if got := atomic.LoadInt32(&reused.calls); got != 1 {
t.Fatalf("reused-port filer calls = %d, want 1", got)
}
}
// A routed write whose owner address is dead fails over to a live filer (which