feat: add collection pattern to delete empty volumes (#10129)

* feat: add collection pattern to delete empty volumes

Co-authored-by: Codex <noreply@openai.com>

* 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 <noreply@openai.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-06-29 12:43:19 -07:00
1 parent cb95b45282
commit b55a608ae0
3 files changed
+64 -12

No files matched your search

+10 -1
View File
@@ -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
}
@@ -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)
}
})
}
}
+13 -11
View File
@@ -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