From b55a608ae01e1d80f6931ba06c75b1a3851a5e48 Mon Sep 17 00:00:00 2001 From: 7y-9 Date: Tue, 30 Jun 2026 03:43:19 +0800 Subject: [PATCH] feat: add collection pattern to delete empty volumes (#10129) * feat: add collection pattern to delete empty volumes Co-authored-by: Codex * shell: match collection pattern with wildcard matcher Use wildcard.MatchesWildcard in the shared collection-pattern helper, matching command_volume_fix_replication's matchCollectionPattern. The flag only advertises '*' and '?', which is exactly what the matcher supports. --------- Co-authored-by: Codex Co-authored-by: Chris Lu --- weed/shell/command_volume_delete_empty.go | 11 ++++- .../shell/command_volume_delete_empty_test.go | 41 +++++++++++++++++++ weed/shell/command_volume_list.go | 24 ++++++----- 3 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 weed/shell/command_volume_delete_empty_test.go diff --git a/weed/shell/command_volume_delete_empty.go b/weed/shell/command_volume_delete_empty.go index 333aa151c..0c12a033c 100644 --- a/weed/shell/command_volume_delete_empty.go +++ b/weed/shell/command_volume_delete_empty.go @@ -27,6 +27,7 @@ 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 one volume server. @@ -41,6 +42,7 @@ func (c *commandVolumeDeleteEmpty) Do(args []string, commandEnv *CommandEnv, wri 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", "", "match with wildcard characters '*' and '?'") 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)") @@ -67,7 +69,7 @@ func (c *commandVolumeDeleteEmpty) Do(args []string, commandEnv *CommandEnv, wri eachDataNode(topologyInfo, func(dc DataCenterId, rack RackId, dn *master_pb.DataNodeInfo) { for _, diskInfo := range dn.DiskInfos { for _, v := range diskInfo.VolumeInfos { - if v.Size <= super_block.SuperBlockSize && v.ModifiedAtSecond > 0 && v.ModifiedAtSecond+quietSeconds < nowUnixSeconds { + if isEmptyVolumeDeleteCandidate(v, quietSeconds, nowUnixSeconds, *collectionPattern) { if *applyBalancing { log.Printf("deleting empty volume %d from %s", v.Id, dn.Id) if deleteErr := deleteVolume(commandEnv.option.GrpcDialOption, needle.VolumeId(v.Id), @@ -85,3 +87,10 @@ func (c *commandVolumeDeleteEmpty) Do(args []string, commandEnv *CommandEnv, wri return } + +func isEmptyVolumeDeleteCandidate(v *master_pb.VolumeInformationMessage, quietSeconds, nowUnixSeconds int64, collectionPattern string) bool { + return matchesVolumeCollectionPattern(collectionPattern, v.Collection) && + v.Size <= super_block.SuperBlockSize && + v.ModifiedAtSecond > 0 && + v.ModifiedAtSecond+quietSeconds < nowUnixSeconds +} diff --git a/weed/shell/command_volume_delete_empty_test.go b/weed/shell/command_volume_delete_empty_test.go new file mode 100644 index 000000000..7150244e4 --- /dev/null +++ b/weed/shell/command_volume_delete_empty_test.go @@ -0,0 +1,41 @@ +package shell + +import ( + "testing" + + "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" +) + +func TestIsEmptyVolumeDeleteCandidateCollectionPattern(t *testing.T) { + now := int64(1000) + quietSeconds := int64(100) + quietEmptyVolume := &master_pb.VolumeInformationMessage{ + Size: 0, + ModifiedAtSecond: now - quietSeconds - 1, + Collection: "important-logs", + } + + tests := []struct { + name string + pattern string + collection string + want bool + }{ + {name: "empty pattern matches named collection", pattern: "", collection: "important-logs", want: true}, + {name: "wildcard matches collection", pattern: "important*", collection: "important-logs", want: true}, + {name: "wildcard rejects collection", pattern: "important*", collection: "other-logs", want: false}, + {name: "default pattern matches empty collection", pattern: CollectionDefault, collection: "", want: true}, + {name: "default pattern rejects named collection", pattern: CollectionDefault, collection: "important-logs", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := *quietEmptyVolume + v.Collection = tt.collection + if got := isEmptyVolumeDeleteCandidate(&v, quietSeconds, now, tt.pattern); got != tt.want { + t.Fatalf("isEmptyVolumeDeleteCandidate(collection=%q, pattern=%q) = %v, want %v", + tt.collection, tt.pattern, got, tt.want) + } + }) + } +} diff --git a/weed/shell/command_volume_list.go b/weed/shell/command_volume_list.go index 2cf6829c2..a9912356c 100644 --- a/weed/shell/command_volume_list.go +++ b/weed/shell/command_volume_list.go @@ -4,7 +4,6 @@ import ( "bytes" "flag" "fmt" - "path/filepath" "slices" "sort" "strings" @@ -15,6 +14,7 @@ import ( "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding" "github.com/seaweedfs/seaweedfs/weed/storage/types" "github.com/seaweedfs/seaweedfs/weed/util" + "github.com/seaweedfs/seaweedfs/weed/util/wildcard" "io" ) @@ -319,16 +319,8 @@ func (c *commandVolumeList) isNotMatchDiskInfo(readOnly bool, collection string, if *c.writable && (readOnly || volumeSize == -1 || (c.volumeSizeLimitMb > 0 && uint64(volumeSize) >= c.volumeSizeLimitMb*util.MiByte)) { return true } - if *c.collectionPattern != "" { - var matched bool - if *c.collectionPattern == CollectionDefault { - matched = (collection == "") - } else { - matched, _ = filepath.Match(*c.collectionPattern, collection) - } - if !matched { - return true - } + if !matchesVolumeCollectionPattern(*c.collectionPattern, collection) { + return true } if *c.volumeId > 0 && *c.volumeId != uint64(volumeId) { return true @@ -336,6 +328,16 @@ func (c *commandVolumeList) isNotMatchDiskInfo(readOnly bool, collection string, return false } +func matchesVolumeCollectionPattern(pattern, collection string) bool { + if pattern == "" { + return true + } + if pattern == CollectionDefault { + return collection == "" + } + return wildcard.MatchesWildcard(pattern, collection) +} + func (c *commandVolumeList) writeDiskInfo(writer io.Writer, t *master_pb.DiskInfo, verbosityLevel int, outNodeInfo func()) statistics { var s statistics diskType := t.Type