mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-29 16:30:44 +01:00
* topology: add AvailableSpaceWithinLocation for location-scoped free slots AvailableSpaceFor answers for the whole topology; a location-constrained assign needs to know whether the requested data center, rack, or data node still has room before deciding growth can satisfy it. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: let location-constrained assigns wait for pending growth An assign carrying a data center or rack constraint failed on the first unsatisfied pick, even when the same request had just queued volume growth for that location — so the first write into a fresh collection errored out instead of picking up the volume growth landed a moment later. Keep the fail-fast path only when growth cannot help: growth disabled, no growth pending, or no free slots left inside the constraint. Otherwise the request takes the same wait/shed path as an unconstrained assign. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * topology: sum leaf free slots in AvailableSpaceWithinLocation Rolled-up rack and data center counters keep a maintenance-mode server's capacity, and one overcommitted node can cancel a sibling's slots. Sum the matching data nodes directly so the location check agrees with what growth can actually use. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: let constrained growth initiators reach the shed path When a constrained assign's own growth concluded unfulfilled, the fast-fail check saw only that no grow was pending and returned the plain pick error — a code the assign client does not retry, so the first write died instead of re-triggering growth. Exclude the request's own initiated grow from the no-pending-growth condition so it reaches the existing re-pick and ResourceExhausted shed like an unconstrained assign. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: judge targeted grow requests by the location they name A grow request carrying a data center, rack, or data node constraint was discarded whenever the layout's writables elsewhere were healthy, so a location with free slots but no writable could never receive the volume its assign was waiting for. Add ShouldGrowVolumesWithinLocation and use it for constrained requests; the layout-wide check still governs untargeted ones. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * topology: only positive leaf slots count in AvailableSpaceWithinLocation An over-committed node reports negative free slots; summed plainly it\ncancelled a sibling's room and a constrained assign could fail fast\nwhile the location still had space to grow into. * master: fail constrained assigns fast when growth cannot place A constrained assign with free slots in its location still waited out\ngrowth that could never satisfy the replica placement — a single-rack\ndata center asked for 010 sheds the same doomed grow on every client\nretry. Run the grow-time node search as a dry run and return its\nplacement error instead of a retryable shed. * master: judge constrained growth by layout, not free slots A slot-level feasibility probe races with the grow it judges: a\npending grow consuming the last slot in one rack reports the\nplacement impossible while it mints the request's own volume, and\nthe hard error skips the re-pick that would find it. GrowthBlocker\nnow counts racks and data nodes inside the named location — shape a\ngrow cannot consume — so only genuinely unplaceable layouts fail\nfast. * topology: GrowthBlocker mirrors the grow selector's feasibility The location filter bound every replica tier, so a 010 request naming one rack needed two racks inside it while the selector constrains only the first placement and counts the rest by room. And counting nodes without free slots let a full location queue a grow it could never serve, so the assign shed ResourceExhausted at timeout instead of failing fast. GrowthBlocker now applies the option's location to the primary placement chain only and leaf-sums free slots the way the selector does, so it reports a blocker exactly when the grow can never place. * master: repick once before surfacing a growth blocker An in-flight grow can spend the last free slot minting the request's own volume: the blocker's slot counts predate that registration by a heartbeat, so returning the placement error on the first failed pick could reject a write whose volume lands a moment later. A pending grow now earns one repick grace before the blocker error is returned. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
561 lines
19 KiB
Go
561 lines
19 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/topology"
|
|
|
|
"github.com/seaweedfs/raft"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"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"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
const (
|
|
volumeGrowStepCount = 2
|
|
)
|
|
|
|
func (ms *MasterServer) DoAutomaticVolumeGrow(req *topology.VolumeGrowRequest) {
|
|
if ms.option.VolumeGrowthDisabled {
|
|
glog.V(1).Infof("automatic volume grow disabled")
|
|
return
|
|
}
|
|
glog.V(1).Infoln("starting automatic volume grow")
|
|
start := time.Now()
|
|
newVidLocations, err := ms.vg.AutomaticGrowByType(req.Option, ms.grpcDialOption, ms.Topo, req.Count)
|
|
glog.V(1).Infoln("finished automatic volume grow, cost ", time.Now().Sub(start))
|
|
if err != nil {
|
|
glog.V(1).Infof("automatic volume grow failed: %+v", err)
|
|
return
|
|
}
|
|
ms.broadcastVolumeLocationsToClients(newVidLocations)
|
|
}
|
|
|
|
// periodicGrowCount decides how many volumes the periodic growth scan adds to
|
|
// a layout ahead of its rack-aware pass, from the writableVolumeCount clients
|
|
// last asked for (lastGrowCount) and the layout's writable and crowded counts.
|
|
// mustGrow reports a refill back up to lastGrowCount writable volumes, which
|
|
// the caller only grows when there is room for all of it. A layout that never
|
|
// received a writableVolumeCount (lastGrowCount 0) is left to the rack-aware
|
|
// pass.
|
|
func periodicGrowCount(lastGrowCount uint32, writable, crowded int) (count uint32, mustGrow bool) {
|
|
if n := int(lastGrowCount) - writable; n > 0 {
|
|
return uint32(n), true
|
|
}
|
|
if lastGrowCount > 0 && writable < int(lastGrowCount*2) && float64(crowded+volumeGrowStepCount) > float64(writable)*topology.VolumeGrowStrategy.Threshold {
|
|
return volumeGrowStepCount, false
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func (ms *MasterServer) ProcessGrowRequest() {
|
|
go func() {
|
|
ctx := context.Background()
|
|
firstRun := true
|
|
for {
|
|
if firstRun {
|
|
firstRun = false
|
|
} else {
|
|
time.Sleep(5*time.Minute + time.Duration(30*rand.Float32())*time.Second)
|
|
}
|
|
if !ms.Topo.IsLeader() {
|
|
continue
|
|
}
|
|
dcs := ms.Topo.ListDCAndRacks()
|
|
for _, vlc := range ms.Topo.ListVolumeLayoutCollections() {
|
|
var err error
|
|
vl := vlc.VolumeLayout
|
|
lastGrowCount := vl.GetLastGrowCount()
|
|
if vl.HasGrowRequest() {
|
|
continue
|
|
}
|
|
writable, crowded := vl.GetWritableVolumeCount()
|
|
vgr := vlc.ToVolumeGrowRequest()
|
|
underReplicated := vl.CountUnderReplicatedVolumes()
|
|
stats.MasterVolumeLayoutWritable.WithLabelValues(vlc.Collection, vgr.DiskType, vgr.Replication, vgr.Ttl).Set(float64(writable))
|
|
stats.MasterVolumeLayoutCrowded.WithLabelValues(vlc.Collection, vgr.DiskType, vgr.Replication, vgr.Ttl).Set(float64(crowded))
|
|
stats.MasterUnderReplicatedVolumes.WithLabelValues(vlc.Collection, vgr.DiskType, vgr.Replication, vgr.Ttl).Set(float64(underReplicated))
|
|
|
|
growCount, mustGrow := periodicGrowCount(lastGrowCount, writable, crowded)
|
|
switch {
|
|
case mustGrow:
|
|
if rp, rpErr := super_block.NewReplicaPlacementFromString(vgr.Replication); rpErr != nil {
|
|
glog.V(0).Infof("failed to parse replica placement %s: %v", vgr.Replication, rpErr)
|
|
} else {
|
|
vgr.WritableVolumeCount = growCount
|
|
if ms.Topo.AvailableSpaceFor(&topology.VolumeGrowOption{DiskType: types.ToDiskType(vgr.DiskType)}) >= int64(vgr.WritableVolumeCount*uint32(rp.GetCopyCount())) {
|
|
_, err = ms.VolumeGrow(ctx, vgr)
|
|
}
|
|
}
|
|
case growCount > 0:
|
|
vgr.WritableVolumeCount = growCount
|
|
_, err = ms.VolumeGrow(ctx, vgr)
|
|
}
|
|
if err != nil {
|
|
glog.V(0).Infof("volume grow request failed: %+v", err)
|
|
}
|
|
for _, plan := range vl.PlanRackAwareGrowth(dcs, lastGrowCount, volumeGrowStepCount) {
|
|
vgr.DataCenter = plan.DataCenter
|
|
vgr.Rack = plan.Rack
|
|
vgr.WritableVolumeCount = plan.WritableVolumeCount
|
|
if _, err = ms.VolumeGrow(ctx, vgr); err != nil {
|
|
glog.V(0).Infof("volume grow request for dc:%s rack:%s failed: %+v", plan.DataCenter, plan.Rack, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
go func() {
|
|
filter := sync.Map{}
|
|
for {
|
|
req, ok := <-ms.volumeGrowthRequestChan
|
|
if !ok {
|
|
break
|
|
}
|
|
|
|
option := req.Option
|
|
vl := ms.Topo.GetVolumeLayout(option.Collection, option.ReplicaPlacement, option.Ttl, option.DiskType)
|
|
|
|
if !ms.Topo.IsLeader() {
|
|
//discard buffered requests
|
|
time.Sleep(time.Second * 1)
|
|
vl.DoneGrowRequest()
|
|
continue
|
|
}
|
|
|
|
// filter out identical requests being processed
|
|
found := false
|
|
filter.Range(func(k, v interface{}) bool {
|
|
existingReq := k.(*topology.VolumeGrowRequest)
|
|
if existingReq.Equals(req) {
|
|
found = true
|
|
}
|
|
return !found
|
|
})
|
|
|
|
// A targeted request is judged by the location it names: the
|
|
// layout-wide check would discard it whenever another location's
|
|
// writables are healthy, leaving the assign that asked for it
|
|
// without a volume no matter how often it retries.
|
|
needsGrowth := vl.ShouldGrowVolumes()
|
|
if option.DataCenter != "" || option.Rack != "" || option.DataNode != "" {
|
|
needsGrowth = vl.ShouldGrowVolumesWithinLocation(option)
|
|
}
|
|
// not atomic but it's okay
|
|
if found || (!req.Force && !needsGrowth) {
|
|
glog.V(4).Infoln("discard volume grow request")
|
|
time.Sleep(time.Millisecond * 211)
|
|
vl.DoneGrowRequest()
|
|
continue
|
|
}
|
|
|
|
filter.Store(req, nil)
|
|
// we have lock called inside vg
|
|
glog.V(0).Infof("volume grow %+v", req)
|
|
go func(req *topology.VolumeGrowRequest, vl *topology.VolumeLayout) {
|
|
// defer so a panic can't strand growRequest.
|
|
defer filter.Delete(req)
|
|
defer vl.DoneGrowRequest()
|
|
ms.DoAutomaticVolumeGrow(req)
|
|
}(req, vl)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// LookupVolume resolves one or more volume ids (or "<vid>,<cookie>" file ids)
|
|
// to their current replica locations on the volume servers. Each returned
|
|
// entry carries DataInRemote per replica so the caller can prefer a local
|
|
// replica; entries carrying a file id also receive a freshly generated read
|
|
// jwt the volume server will accept.
|
|
func (ms *MasterServer) LookupVolume(ctx context.Context, req *master_pb.LookupVolumeRequest) (*master_pb.LookupVolumeResponse, error) {
|
|
|
|
resp := &master_pb.LookupVolumeResponse{}
|
|
volumeLocations := ms.lookupVolumeId(req.VolumeOrFileIds, req.Collection)
|
|
|
|
notFoundCount := 0
|
|
for _, volumeOrFileId := range req.VolumeOrFileIds {
|
|
vid := volumeOrFileId
|
|
commaSep := strings.Index(vid, ",")
|
|
if commaSep > 0 {
|
|
vid = vid[0:commaSep]
|
|
}
|
|
if result, found := volumeLocations[vid]; found {
|
|
var locations []*master_pb.Location
|
|
for _, loc := range result.Locations {
|
|
locations = append(locations, &master_pb.Location{
|
|
Url: loc.Url,
|
|
PublicUrl: loc.PublicUrl,
|
|
DataCenter: loc.DataCenter,
|
|
GrpcPort: uint32(loc.GrpcPort),
|
|
DataInRemote: loc.DataInRemote,
|
|
ReadOnly: loc.ReadOnly,
|
|
ReadOnlyCanDelete: loc.ReadOnlyCanDelete,
|
|
})
|
|
}
|
|
var auth string
|
|
if commaSep > 0 { // this is a file id
|
|
auth = string(security.GenJwtForVolumeServer(ms.guard.SigningKey(), ms.guard.ExpiresAfterSec(), result.VolumeOrFileId))
|
|
}
|
|
if result.NotFound {
|
|
notFoundCount++
|
|
}
|
|
resp.VolumeIdLocations = append(resp.VolumeIdLocations, &master_pb.LookupVolumeResponse_VolumeIdLocation{
|
|
VolumeOrFileId: result.VolumeOrFileId,
|
|
Locations: locations,
|
|
Error: result.Error,
|
|
Auth: auth,
|
|
})
|
|
}
|
|
}
|
|
|
|
// While warming up, a not-found may only mean the volume server has not
|
|
// reported yet, so no part of the answer can be treated as authoritative.
|
|
// Callers retry Unavailable; a partial answer would let them take a
|
|
// missing volume as gone.
|
|
if notFoundCount > 0 && ms.Topo.IsLeader() && ms.Topo.IsWarmingUp() {
|
|
glog.V(0).Infof("lookup volume warming up: topology is still loading (%d of %d not found)", notFoundCount, len(req.VolumeOrFileIds))
|
|
return nil, status.Errorf(codes.Unavailable, "master is warming up, topology is still loading")
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// CollectionStatistics summarises every collection, so callers tracking usage
|
|
// do not pull the whole volume list to add it up themselves.
|
|
func (ms *MasterServer) CollectionStatistics(ctx context.Context, req *master_pb.CollectionStatisticsRequest) (*master_pb.CollectionStatisticsResponse, error) {
|
|
if !ms.Topo.IsLeader() {
|
|
return nil, raft.NotLeaderError
|
|
}
|
|
|
|
stats := ms.Topo.CollectionStatistics()
|
|
resp := &master_pb.CollectionStatisticsResponse{
|
|
Collections: make([]*master_pb.CollectionStatistics, 0, len(stats)),
|
|
}
|
|
for _, s := range stats {
|
|
resp.Collections = append(resp.Collections, &master_pb.CollectionStatistics{
|
|
Collection: s.Collection,
|
|
FileCount: s.FileCount,
|
|
DeleteCount: s.DeleteCount,
|
|
DeletedByteCount: s.DeletedByteCount,
|
|
Size: s.Size,
|
|
PhysicalSize: s.PhysicalSize,
|
|
VolumeCount: s.VolumeCount,
|
|
})
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (ms *MasterServer) Statistics(ctx context.Context, req *master_pb.StatisticsRequest) (*master_pb.StatisticsResponse, error) {
|
|
|
|
if !ms.Topo.IsLeader() {
|
|
return nil, raft.NotLeaderError
|
|
}
|
|
|
|
// an empty collection means all collections, and a named collection covers
|
|
// all its layouts and EC volumes
|
|
stats := ms.Topo.CollectionVolumeStats(req.Collection)
|
|
totalSize := uint64(ms.Topo.GetDiskUsages().GetMaxVolumeCount() * int64(ms.option.VolumeSizeLimitMB) * 1024 * 1024)
|
|
// capacity is cluster-wide, so what is left over is what every collection
|
|
// has not taken, not just the one asked about
|
|
clusterUsedSize := stats.UsedSize
|
|
if req.Collection != "" {
|
|
clusterUsedSize = ms.Topo.CollectionVolumeStats("").UsedSize
|
|
}
|
|
// volume slots are provisioning, not capacity: a cluster configured with
|
|
// more of them than its disks hold would report space it can never take.
|
|
// What the disks still have free, on top of what the cluster already wrote,
|
|
// is the real ceiling.
|
|
if freeBytes, reported := ms.Topo.FreeBytes(); reported {
|
|
totalSize = min(totalSize, clusterUsedSize+freeBytes)
|
|
}
|
|
// and the free space holds that many copies fewer of whatever the caller writes
|
|
var freeSize uint64
|
|
if totalSize > clusterUsedSize {
|
|
freeSize = (totalSize - clusterUsedSize) / uint64(ms.replicaCopyCount(req.Replication))
|
|
}
|
|
resp := &master_pb.StatisticsResponse{
|
|
TotalSize: totalSize,
|
|
UsedSize: stats.UsedSize,
|
|
FileCount: stats.FileCount,
|
|
LogicalTotalSize: stats.LogicalUsedSize + freeSize,
|
|
LogicalUsedSize: stats.LogicalUsedSize,
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// replicaCopyCount returns how many copies the given replication makes, falling
|
|
// back to the master default and then to a single copy. Unparsable input is
|
|
// reported rather than failing the call: statistics are informational.
|
|
func (ms *MasterServer) replicaCopyCount(replication string) int {
|
|
for _, s := range []string{replication, ms.option.DefaultReplicaPlacement} {
|
|
if s == "" {
|
|
continue
|
|
}
|
|
rp, err := super_block.NewReplicaPlacementFromString(s)
|
|
if err != nil {
|
|
glog.V(1).Infof("statistics replication %q: %v", s, err)
|
|
continue
|
|
}
|
|
return rp.GetCopyCount()
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func (ms *MasterServer) VolumeList(ctx context.Context, req *master_pb.VolumeListRequest) (*master_pb.VolumeListResponse, error) {
|
|
|
|
if !ms.Topo.IsLeader() {
|
|
return nil, raft.NotLeaderError
|
|
}
|
|
|
|
filter, err := topology.NewVolumeFilter(req)
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
|
|
resp := &master_pb.VolumeListResponse{
|
|
TopologyInfo: ms.Topo.ToTopologyInfo(filter),
|
|
VolumeSizeLimitMb: uint64(ms.option.VolumeSizeLimitMB),
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// VolumeListStream answers VolumeList without building the whole reply first.
|
|
// The topology goes out on its own, then the volumes in batches, so the master
|
|
// holds one batch rather than every volume in the cluster.
|
|
func (ms *MasterServer) VolumeListStream(req *master_pb.VolumeListRequest, stream master_pb.Seaweed_VolumeListStreamServer) error {
|
|
|
|
if !ms.Topo.IsLeader() {
|
|
return raft.NotLeaderError
|
|
}
|
|
|
|
filter, err := topology.NewVolumeFilter(req)
|
|
if err != nil {
|
|
return status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
|
|
listed := ms.Topo.ToTopologyInfo(topology.NoVolumes())
|
|
err = stream.Send(&master_pb.VolumeListStreamResponse{
|
|
Header: &master_pb.VolumeListResponse{
|
|
TopologyInfo: listed,
|
|
VolumeSizeLimitMb: uint64(ms.option.VolumeSizeLimitMB),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ms.Topo.StreamVolumes(listed, filter, 0, stream.Send)
|
|
}
|
|
|
|
func (ms *MasterServer) LookupEcVolume(ctx context.Context, req *master_pb.LookupEcVolumeRequest) (*master_pb.LookupEcVolumeResponse, error) {
|
|
|
|
if !ms.Topo.IsLeader() {
|
|
return nil, raft.NotLeaderError
|
|
}
|
|
|
|
resp := &master_pb.LookupEcVolumeResponse{}
|
|
|
|
ecLocations, found := ms.Topo.LookupEcShards(needle.VolumeId(req.VolumeId))
|
|
|
|
if !found {
|
|
return resp, fmt.Errorf("ec volume %d not found", req.VolumeId)
|
|
}
|
|
|
|
resp.VolumeId = req.VolumeId
|
|
|
|
for shardId, shardLocations := range ecLocations.Locations {
|
|
var locations []*master_pb.Location
|
|
for _, dn := range shardLocations {
|
|
locations = append(locations, &master_pb.Location{
|
|
Url: dn.Url(),
|
|
PublicUrl: dn.PublicUrl,
|
|
DataCenter: dn.GetDataCenterId(),
|
|
// without this, clients derive grpc as httpPort+10000
|
|
GrpcPort: uint32(dn.GrpcPort),
|
|
})
|
|
}
|
|
resp.ShardIdLocations = append(resp.ShardIdLocations, &master_pb.LookupEcVolumeResponse_EcShardIdLocation{
|
|
ShardId: uint32(shardId),
|
|
Locations: locations,
|
|
})
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (ms *MasterServer) VacuumVolume(ctx context.Context, req *master_pb.VacuumVolumeRequest) (*master_pb.VacuumVolumeResponse, error) {
|
|
|
|
if !ms.Topo.IsLeader() {
|
|
return nil, raft.NotLeaderError
|
|
}
|
|
|
|
resp := &master_pb.VacuumVolumeResponse{}
|
|
|
|
ms.Topo.Vacuum(ms.grpcDialOption, float64(req.GarbageThreshold), ms.option.MaxParallelVacuumPerServer, ms.option.MaxParallelVacuumPerRack, req.VolumeId, req.Collection, ms.preallocateSize, false, 0)
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (ms *MasterServer) DisableVacuum(ctx context.Context, req *master_pb.DisableVacuumRequest) (*master_pb.DisableVacuumResponse, error) {
|
|
// The caller explicitly indicates whether this disable request comes
|
|
// from the vacuum plugin monitor. Track ownership so the safety net
|
|
// in the vacuum loop won't override an operator's intentional disable.
|
|
if req.GetByPlugin() {
|
|
ms.Topo.DisableVacuumByPlugin()
|
|
} else {
|
|
ms.Topo.DisableVacuum()
|
|
}
|
|
resp := &master_pb.DisableVacuumResponse{}
|
|
return resp, nil
|
|
}
|
|
|
|
func (ms *MasterServer) EnableVacuum(ctx context.Context, req *master_pb.EnableVacuumRequest) (*master_pb.EnableVacuumResponse, error) {
|
|
if req.GetByPlugin() {
|
|
ms.Topo.EnableVacuumByPlugin()
|
|
} else {
|
|
ms.Topo.EnableVacuum()
|
|
}
|
|
resp := &master_pb.EnableVacuumResponse{}
|
|
return resp, nil
|
|
}
|
|
|
|
func (ms *MasterServer) VolumeMarkReadonly(ctx context.Context, req *master_pb.VolumeMarkReadonlyRequest) (*master_pb.VolumeMarkReadonlyResponse, error) {
|
|
|
|
if !ms.Topo.IsLeader() {
|
|
return nil, raft.NotLeaderError
|
|
}
|
|
|
|
resp := &master_pb.VolumeMarkReadonlyResponse{}
|
|
|
|
replicaPlacement, _ := super_block.NewReplicaPlacementFromByte(byte(req.ReplicaPlacement))
|
|
vl := ms.Topo.GetVolumeLayout(req.Collection, replicaPlacement, needle.LoadTTLFromUint32(req.Ttl), types.ToDiskType(req.DiskType))
|
|
vid := needle.VolumeId(req.VolumeId)
|
|
dataNodes := ms.Topo.Lookup(req.Collection, vid)
|
|
|
|
found := false
|
|
for _, dn := range dataNodes {
|
|
if dn.Ip == req.Ip && dn.Port == int(req.Port) {
|
|
found = true
|
|
if req.IsReadonly {
|
|
vl.SetVolumeReadOnly(dn, vid)
|
|
if pending := vl.GetPendingSize(vid); pending > 0 {
|
|
glog.V(0).Infof("volume %d marked readonly with %d pending bytes", vid, pending)
|
|
}
|
|
} else {
|
|
vl.SetVolumeWritable(dn, vid)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A vacuum worker marks the volume writable after a successful commit. If the
|
|
// index lost it meanwhile (e.g. a disconnect race during the vacuum), the loop
|
|
// found nothing to update; re-register it from the node that still holds it so
|
|
// LookupVolume stops returning "not found".
|
|
if !found && !req.IsReadonly {
|
|
if dn := ms.Topo.LookupDataNodeByAddress(pb.NewServerAddress(req.Ip, int(req.Port), 0)); dn != nil {
|
|
if vi, err := dn.GetVolumesById(vid); err == nil {
|
|
ms.Topo.RegisterVolumeLayout(vi, dn)
|
|
glog.V(0).Infof("volume %d re-registered from %s:%d, was missing from the lookup index at mark-writable", vid, req.Ip, req.Port)
|
|
}
|
|
}
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (ms *MasterServer) ResumeVolumeGrowth(ctx context.Context, req *master_pb.ResumeVolumeGrowthRequest) (*master_pb.ResumeVolumeGrowthResponse, error) {
|
|
if !ms.Topo.IsLeader() {
|
|
return nil, raft.NotLeaderError
|
|
}
|
|
return &master_pb.ResumeVolumeGrowthResponse{WasFrozen: ms.Topo.ResumeVolumeGrowth()}, nil
|
|
}
|
|
|
|
func (ms *MasterServer) VolumeGrow(ctx context.Context, req *master_pb.VolumeGrowRequest) (*master_pb.VolumeGrowResponse, error) {
|
|
if !ms.Topo.IsLeader() {
|
|
return nil, raft.NotLeaderError
|
|
}
|
|
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 req.DataCenter != "" && !ms.Topo.DataCenterExists(req.DataCenter) {
|
|
return nil, fmt.Errorf("data center not exists")
|
|
}
|
|
|
|
ver := needle.GetCurrentVersion()
|
|
volumeGrowOption := topology.VolumeGrowOption{
|
|
Collection: req.Collection,
|
|
ReplicaPlacement: replicaPlacement,
|
|
Ttl: ttl,
|
|
DiskType: types.ToDiskType(req.DiskType),
|
|
Preallocate: ms.preallocateSize,
|
|
DataCenter: req.DataCenter,
|
|
Rack: req.Rack,
|
|
DataNode: req.DataNode,
|
|
MemoryMapMaxSizeMb: req.MemoryMapMaxSizeMb,
|
|
Version: uint32(ver),
|
|
}
|
|
volumeGrowRequest := topology.VolumeGrowRequest{
|
|
Option: &volumeGrowOption,
|
|
Count: req.WritableVolumeCount,
|
|
Force: true,
|
|
Reason: "grpc volume grow",
|
|
}
|
|
|
|
// Report the layout's fill observed at request time whether or not
|
|
// growth was triggered, so callers can tell an empty layout from a
|
|
// skipped one. A missing collection/layout reports zeros.
|
|
var avgFill float64
|
|
var writableCount int
|
|
if vl := ms.Topo.FindVolumeLayout(req.Collection, replicaPlacement, ttl, types.ToDiskType(req.DiskType)); vl != nil {
|
|
avgFill, writableCount = vl.WritableAvgFill(&volumeGrowOption)
|
|
}
|
|
|
|
// Conditional grow: with a threshold set, grow only when the layout's
|
|
// writable volumes have reached this average fill. A layout with no
|
|
// writable volumes (none yet, or all just filled) skips the check — that
|
|
// is the case growth exists to fix.
|
|
if req.GrowAvgFillThreshold > 0 && writableCount > 0 && avgFill < float64(req.GrowAvgFillThreshold) {
|
|
return &master_pb.VolumeGrowResponse{
|
|
AvgFill: float32(avgFill),
|
|
WritableCount: uint32(writableCount),
|
|
}, nil
|
|
}
|
|
|
|
replicaCount := int64(req.WritableVolumeCount * uint32(replicaPlacement.GetCopyCount()))
|
|
|
|
if ms.Topo.AvailableSpaceFor(&volumeGrowOption) < replicaCount {
|
|
return nil, fmt.Errorf("only %d volumes left, not enough for %d", ms.Topo.AvailableSpaceFor(&volumeGrowOption), replicaCount)
|
|
}
|
|
|
|
ms.DoAutomaticVolumeGrow(&volumeGrowRequest)
|
|
|
|
return &master_pb.VolumeGrowResponse{
|
|
AvgFill: float32(avgFill),
|
|
WritableCount: uint32(writableCount),
|
|
}, nil
|
|
}
|