Files
seaweedfs/weed/server/master_grpc_server_assign_test.go
T
Chris Lu f643893891 fix(master): shed assign load when volume growth is already in flight (#10121)
Under a herd of concurrent assigns with no writable volume, Assign spun
PickForWrite for the full 10s timeout, pinning a goroutine per request and
starving the master of the cycles it needs to process growth and answer
heartbeats. When growth is the relevant remedy and already in flight, stop
spinning: if free space exists, shed with a fast retryable error so clients
back off and retry once growth lands; if the cluster is out of space, fail fast
with the real out-of-space error instead of masking it as retryable.

The gRPC shed uses ResourceExhausted, not Unavailable: operation.Assign retries
it, but the client connection layer doesn't treat it as a dead channel, so a
per-request shed across a herd doesn't tear down the shared master connection
and cancel every other in-flight assign. The HTTP dirAssignHandler sheds with
503 + Retry-After.
2026-06-26 14:23:40 -07:00

98 lines
3.4 KiB
Go

package weed_server
import (
"context"
"testing"
"time"
"github.com/seaweedfs/raft"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/sequence"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/seaweedfs/seaweedfs/weed/topology"
)
// leaderRaftServer answers only State(); the embedded nil interface panics if any
// other method is called.
type leaderRaftServer struct {
raft.Server
}
func (leaderRaftServer) State() string { return raft.Leader }
func newLeaderMaster() *MasterServer {
topo := topology.NewTopology("test", sequence.NewMemorySequencer(), 1<<20, 5, false)
topo.RaftServer = leaderRaftServer{}
return &MasterServer{
Topo: topo,
option: &MasterOption{},
volumeGrowthRequestChan: make(chan *topology.VolumeGrowRequest, 1<<6),
}
}
// markGrowthInFlight flags growth on the same VolumeLayout the handler resolves,
// by mirroring its exact lookup.
func markGrowthInFlight(t *testing.T, topo *topology.Topology, req *master_pb.AssignRequest) {
t.Helper()
rp, err := super_block.NewReplicaPlacementFromString(req.Replication)
require.NoError(t, err)
ttl, err := needle.ReadTTL(req.Ttl)
require.NoError(t, err)
topo.GetVolumeLayout(req.Collection, rp, ttl, types.ToDiskType(req.DiskType)).AddGrowRequest()
}
// With free space but no writable volume and growth already in flight, Assign
// sheds with a retryable ResourceExhausted immediately instead of spinning to
// timeout. ResourceExhausted (not Unavailable) so the shed isn't mistaken for a
// dead channel and doesn't tear down the shared master connection.
func TestAssignShedsLoadWhenGrowthInFlight(t *testing.T) {
ms := newLeaderMaster()
// Free volume slots but no writable volume yet, so growth is the remedy.
ms.Topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("127.0.0.1", 8080, 18080, "127.0.0.1", "dn1", map[string]uint32{"": 100})
req := &master_pb.AssignRequest{Count: 1, Replication: "000"}
markGrowthInFlight(t, ms.Topo, req)
start := time.Now()
resp, err := ms.Assign(context.Background(), req)
elapsed := time.Since(start)
require.Error(t, err)
require.Nil(t, resp)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.ResourceExhausted, st.Code())
// Shed immediately rather than spinning out the 10s retry budget.
assert.Less(t, elapsed, 2*time.Second)
// Growth was already pending, so we must not enqueue another grow request.
assert.Len(t, ms.volumeGrowthRequestChan, 0)
}
// Out of space, Assign fails fast with the real error rather than masking it as
// a retryable "growth in progress".
func TestAssignFailsFastWhenOutOfSpace(t *testing.T) {
ms := newLeaderMaster() // no data nodes -> no free space
req := &master_pb.AssignRequest{Count: 1, Replication: "000"}
start := time.Now()
resp, err := ms.Assign(context.Background(), req)
elapsed := time.Since(start)
require.Error(t, err)
require.Nil(t, resp)
if st, ok := status.FromError(err); ok {
assert.NotEqual(t, codes.Unavailable, st.Code())
}
assert.Contains(t, err.Error(), "no free volumes left")
assert.Less(t, elapsed, 2*time.Second)
}