Files
seaweedfs/weed/shell/command_volume_delete_empty.go
T
Chris LuandClaude Opus 5.5 1dcf721ce3 shell: name the copies a partial empty-volume delete leaves behind
Once some copies of a volume are deleted, a rerun of volume.deleteEmpty
skips the rest as under-replicated, and the shell keeps no state to
finish them. Log each copy whose delete failed with the volume.delete
command that removes it once it is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 12:09:47 -08:00

142 lines
5.0 KiB
Go

package shell
import (
"context"
"flag"
"io"
"log"
"maps"
"slices"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
)
func init() {
Commands = append(Commands, &commandVolumeDeleteEmpty{})
}
type commandVolumeDeleteEmpty struct {
}
func (c *commandVolumeDeleteEmpty) Name() string {
return "volume.deleteEmpty"
}
func (c *commandVolumeDeleteEmpty) Help() string {
return `delete empty volumes from all volume servers
volume.deleteEmpty -quietFor=24h -apply
volume.deleteEmpty -collectionPattern=important* -quietFor=24h -apply
This command deletes all empty volumes from all volume servers.
A volume with no live needles left is empty too, even when its
.dat file is still large: compacting it first would only rewrite
bytes that are all deleted already.
A replicated volume is deleted only when all of its copies are
listed and every copy is empty.
`
}
func (c *commandVolumeDeleteEmpty) HasTag(CommandTag) bool {
return false
}
func (c *commandVolumeDeleteEmpty) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
volDeleteCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
quietPeriod := volDeleteCommand.Duration("quietFor", 24*time.Hour, "select empty volumes with no recent writes, avoid newly created ones")
collectionPattern := volDeleteCommand.String("collectionPattern", "", "comma-separated collection names, with '*' and '?' wildcards; empty matches all")
applyBalancing := volDeleteCommand.Bool("apply", false, "apply to delete empty volumes")
// TODO: remove this alias
applyBalancingAlias := volDeleteCommand.Bool("force", false, "apply to delete empty volumes (alias for -apply)")
if err = volDeleteCommand.Parse(args); err != nil {
return nil
}
handleDeprecatedForceFlag(writer, volDeleteCommand, applyBalancingAlias, applyBalancing)
infoAboutSimulationMode(writer, *applyBalancing, "-apply")
if err = commandEnv.confirmIsLocked(args); err != nil {
return
}
// collect topology information
topologyInfo, _, err := collectTopologyInfo(commandEnv, 0)
if err != nil {
return err
}
collectionMatcher, err := wildcard.CompileCollectionMatcher(*collectionPattern)
if err != nil {
return err
}
quietSeconds := int64(*quietPeriod / time.Second)
nowUnixSeconds := time.Now().Unix()
volumeReplicas, _ := collectVolumeReplicaLocations(topologyInfo)
for _, vid := range slices.Sorted(maps.Keys(volumeReplicas)) {
replicas := volumeReplicas[vid]
if !isEmptyVolumeCopiesDeleteCandidate(replicas, quietSeconds, nowUnixSeconds, collectionMatcher) {
continue
}
var kept []pb.ServerAddress
for _, r := range replicas {
dn := r.location.dataNode
if !*applyBalancing {
log.Printf("empty volume %d from %s", vid, dn.Id)
continue
}
log.Printf("deleting empty volume %d from %s", vid, dn.Id)
onlyGarbage := r.info.FileCount > 0 && r.info.FileCount <= r.info.DeleteCount
// onlyEmpty stays set so a pre-upgrade server checks
// emptiness and refuses instead of deleting unseen.
server := pb.NewServerAddressFromDataNode(dn)
if deleteErr := deleteVolume(context.Background(), commandEnv.option.GrpcDialOption, needle.VolumeId(vid),
server, true, onlyGarbage, false); deleteErr != nil {
err = deleteErr
kept = append(kept, server)
}
}
// a rerun skips the survivors as under-replicated
if len(kept) < len(replicas) {
for _, server := range kept {
log.Printf("volume %d stays on %s after its other copies were deleted; once it is empty, remove it with volume.delete -node %s -volumeId %d", vid, server, server, vid)
}
}
}
return
}
// isEmptyVolumeCopiesDeleteCandidate requires every copy to be listed and
// empty: deleting only the empty ones would leave a copy that holds data, or
// one that is offline, as the volume's only copy.
func isEmptyVolumeCopiesDeleteCandidate(replicas []*VolumeReplica, quietSeconds, nowUnixSeconds int64, collectionMatcher *wildcard.CollectionMatcher) bool {
for _, r := range replicas {
rp, err := super_block.NewReplicaPlacementFromByte(byte(r.info.ReplicaPlacement))
if err != nil || len(replicas) < rp.GetCopyCount() ||
!isEmptyVolumeDeleteCandidate(r.info, quietSeconds, nowUnixSeconds, collectionMatcher) {
return false
}
}
return len(replicas) > 0
}
func isEmptyVolumeDeleteCandidate(v *master_pb.VolumeInformationMessage, quietSeconds, nowUnixSeconds int64, collectionMatcher *wildcard.CollectionMatcher) bool {
return collectionMatcher.Matches(v.Collection) &&
// A remote-backed replica shares its cloud object with the others, so
// deleting one cannot drop it without hurting the survivors.
v.RemoteStorageName == "" &&
(!v.ReadOnly || v.ReadOnlyCanDelete) &&
(v.Size <= super_block.SuperBlockSize || v.FileCount > 0 && v.FileCount <= v.DeleteCount) &&
v.ModifiedAtSecond > 0 &&
v.ModifiedAtSecond+quietSeconds < nowUnixSeconds
}