Files
seaweedfs/weed/server/master_grpc_server_assign.go
T
Chris Lu 0c59c0fb05 master: scope the startup capacity shed to a truly empty topology (#11058)
* master: scope the startup capacity shed to a truly empty topology

The retryable "no volume server capacity registered yet" shed checked
capacity for the requested disk type, so a cluster serving only other
media -- where that capacity will never register -- shed every assign
until the client's deadline instead of failing fast. An unsteered write
to such a cluster hung for its full HTTP deadline and surfaced "context
deadline exceeded" in place of "No writable volumes". Shed only while
no disk type has any registered capacity, and name the unserved medium
in the fast failure.

Claude-Session: https://claude.ai/code/session_01TF7FQghfDkpdoZgakTMX4R

* master: name the unserved medium for every fail-fast caller

The diagnostic sat in the growth-initiator block, so a follower joining
an in-flight growth and a growth-disabled master failed the same way
with only the generic pick error. Wrap at the fail-fast break instead,
which every caller reaches, and cover all three paths in the test.

Claude-Session: https://claude.ai/code/session_01TF7FQghfDkpdoZgakTMX4R
2026-08-31 21:29:27 -07:00

221 lines
7.7 KiB
Go

package weed_server
import (
"context"
"fmt"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/raft"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/security"
"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"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (ms *MasterServer) StreamAssign(server master_pb.Seaweed_StreamAssignServer) error {
for {
req, err := server.Recv()
if err != nil {
glog.Errorf("StreamAssign failed to receive: %v", err)
return err
}
resp, err := ms.Assign(server.Context(), req)
if err != nil {
// Return transient errors (warmup, growth-in-progress shed) as in-band
// error responses instead of killing the stream, so pooled connections
// survive.
if st, ok := status.FromError(err); ok && (st.Code() == codes.Unavailable || st.Code() == codes.ResourceExhausted) {
glog.V(1).Infof("StreamAssign transient error: %v", err)
resp = &master_pb.AssignResponse{Error: st.Message()}
} else {
glog.Errorf("StreamAssign failed to assign: %v", err)
return err
}
}
if err = server.Send(resp); err != nil {
glog.Errorf("StreamAssign failed to send: %v", err)
return err
}
}
}
func (ms *MasterServer) Assign(ctx context.Context, req *master_pb.AssignRequest) (*master_pb.AssignResponse, error) {
if !ms.Topo.IsLeader() {
return nil, raft.NotLeaderError
}
if req.Count == 0 {
req.Count = 1
}
if req.Replication == "" {
req.Replication = ms.option.DefaultReplicaPlacement
}
replicaPlacement, err := super_block.NewReplicaPlacementFromString(req.Replication)
if err != nil {
return nil, err
}
ttl, err := needle.ReadTTL(req.Ttl)
if err != nil {
return nil, err
}
if ms.Topo.IsWarmingUp() {
return nil, status.Errorf(codes.Unavailable, "master is warming up, topology is still loading")
}
diskType := types.ToDiskType(req.DiskType)
ver := needle.GetCurrentVersion()
option := &topology.VolumeGrowOption{
Collection: req.Collection,
ReplicaPlacement: replicaPlacement,
Ttl: ttl,
DiskType: diskType,
Preallocate: ms.preallocateSize,
DataCenter: req.DataCenter,
Rack: req.Rack,
DataNode: req.DataNode,
MemoryMapMaxSizeMb: req.MemoryMapMaxSizeMb,
Version: uint32(ver),
}
if !ms.Topo.DataCenterExists(option.DataCenter) {
return nil, fmt.Errorf("data center %v not found in topology", option.DataCenter)
}
vl := ms.Topo.GetVolumeLayout(option.Collection, option.ReplicaPlacement, option.Ttl, option.DiskType)
if req.DiskType == "" {
if writable, _ := vl.GetWritableVolumeCount(); writable == 0 {
if hddVl := ms.Topo.GetVolumeLayout(option.Collection, option.ReplicaPlacement, option.Ttl, types.ToDiskType(types.HddType)); hddVl != nil {
if writable, _ := hddVl.GetWritableVolumeCount(); writable > 0 {
option.DiskType = types.ToDiskType(types.HddType)
vl = hddVl
}
}
}
}
vl.SetLastGrowCount(req.WritableVolumeCount)
var (
lastErr error
maxTimeout = time.Second * 10
startTime = time.Now()
initiatedGrow bool
repickedAfterGrow bool
)
for time.Now().Sub(startTime) < maxTimeout {
fid, count, dnList, shouldGrow, err := ms.Topo.PickForWrite(req.Count, option, vl, req.ExpectedDataSize)
if shouldGrow && !initiatedGrow && !ms.option.VolumeGrowthDisabled && vl.AddGrowRequestIfAbsent() {
initiatedGrow = true
if err != nil && ms.Topo.AvailableSpaceFor(option) <= 0 && ms.Topo.CapacityFor(option) > 0 {
err = fmt.Errorf("%s and no free volumes left for %s", err.Error(), option.String())
}
ms.volumeGrowthRequestChan <- &topology.VolumeGrowRequest{
Option: option,
Count: req.WritableVolumeCount,
Reason: "grpc assign",
}
}
if err != nil {
glog.V(1).Infof("assign %v %v: %v", req, option.String(), err)
stats.MasterPickForWriteErrorCounter.Inc()
lastErr = err
if (req.DataCenter != "" || req.Rack != "") && strings.Contains(err.Error(), topology.NoWritableVolumes) {
glog.V(0).Infof("assign %v %v: %v", req, option.String(), err)
return nil, err
}
if shouldGrow {
if ms.Topo.AvailableSpaceFor(option) <= 0 {
// Fail fast whenever any capacity is registered: full for
// this medium, or a medium no volume server serves — a
// state a heartbeat won't change, so a retryable shed
// would loop until the client's deadline. Shed retryably
// only while nothing at all has registered, a just-started
// cluster whose volume servers have not heartbeated, so
// the first write rides out the startup window instead of
// failing outright.
if ms.Topo.CapacityForAnyDisk() > 0 {
if ms.Topo.CapacityFor(option) <= 0 {
// Wrapped here, not beside the "no free volumes left"
// wrap above, so followers and growth-disabled
// masters name the unserved medium too — the
// initiator block is skipped for both.
lastErr = fmt.Errorf("%s and no volume server carries disk type %q for %s", err.Error(), option.DiskType.ReadableString(), option.String())
}
break // surface the real error, not a retryable shed
}
return nil, status.Errorf(codes.ResourceExhausted, "no volume server capacity registered yet for %s", option.String())
}
// Only the initiator waits, and only while the growth it triggered
// is still pending: followers shed fast so a herd doesn't pin a
// goroutine each, and an initiator whose growth concluded without
// yielding a writable volume sheds so client retries re-trigger
// growth instead of looping it here. ResourceExhausted, not
// Unavailable: clients retry it (assign_file_id.go) without
// invalidating the shared master connection.
if initiatedGrow != vl.HasGrowRequest() {
// The failed pick above may predate the growth concluding, so
// re-pick once before shedding: growth registers its volumes
// before clearing the flag, and shedding here would bounce the
// client off a volume that just landed.
if initiatedGrow && !repickedAfterGrow {
repickedAfterGrow = true
continue
}
return nil, status.Errorf(codes.ResourceExhausted, "no writable volumes for %s, volume growth in progress", option.String())
}
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(200 * time.Millisecond):
}
continue
}
dn := dnList.Head()
if dn == nil {
continue
}
var replicas []*master_pb.Location
for _, r := range dnList.Rest() {
replicas = append(replicas, &master_pb.Location{
Url: r.Url(),
PublicUrl: r.PublicUrl,
GrpcPort: uint32(r.GrpcPort),
DataCenter: r.GetDataCenterId(),
})
}
return &master_pb.AssignResponse{
Fid: fid,
Location: &master_pb.Location{
Url: dn.Url(),
PublicUrl: dn.PublicUrl,
GrpcPort: uint32(dn.GrpcPort),
DataCenter: dn.GetDataCenterId(),
},
Count: count,
Auth: string(security.GenJwtForVolumeServer(ms.guard.SigningKey(), ms.guard.ExpiresAfterSec(), fid)),
Replicas: replicas,
}, nil
}
// The initiator timed out with its growth still pending: shed retryably
// rather than failing the write that growth is about to satisfy.
if initiatedGrow && vl.HasGrowRequest() && ms.Topo.AvailableSpaceFor(option) > 0 {
return nil, status.Errorf(codes.ResourceExhausted, "no writable volumes for %s, volume growth in progress", option.String())
}
if lastErr != nil {
glog.V(0).Infof("assign %v %v: %v", req, option.String(), lastErr)
}
return nil, lastErr
}