From 0a4657770002d1d106c3eda818f7bbc58c691ffa Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 16 Jan 2026 12:31:48 -0800 Subject: [PATCH] Fix #8040: Support '_default' keyword in collectionPattern to match default collection (#8046) * Fix #8040: Support 'default' keyword in collectionPattern to match default collection The default collection in SeaweedFS is represented as an empty string internally. Previously, it was impossible to specifically target only the default collection because: - Empty collectionPattern matched ALL collections (filter was skipped) - Using collectionPattern="default" tried to match the literal string "default" This commit adds special handling for the keyword "default" in collectionPattern across multiple shell commands: - volume.tier.move - volume.list - volume.fix.replication - volume.configure.replication Now users can use -collectionPattern="default" to specifically target volumes in the default collection (empty collection name), while maintaining backward compatibility where empty pattern matches all collections. Updated help text to document this feature. * Update compileCollectionPattern to support 'default' keyword This extends the fix to all commands that use regex-based collection pattern matching: - ec.encode - ec.decode - volume.tier.download - volume.balance The compileCollectionPattern function now treats "default" as a special keyword that compiles to the regex "^$" (matching empty strings), making it consistent with the other commands that use filepath.Match. * Use CollectionDefault constant instead of hardcoded "default" string Refactored the collection pattern matching logic to use a central constant CollectionDefault defined in weed/shell/common.go. This improves maintainability and ensures consistency across all shell commands. * Address PR review feedback: simplify logic and use '_default' keyword Changes: 1. Changed CollectionDefault from "default" to "_default" to avoid collision with literal collection names 2. Simplified pattern matching logic to reduce code duplication across all affected commands 3. Fixed error handling in command_volume_tier_move.go to properly propagate filepath.Match errors instead of swallowing them 4. Updated documentation to clarify how to match a literal "default" collection using regex patterns like "^default$" This addresses all feedback from PR review comments. * Remove unnecessary documentation about matching literal 'default' Since we changed the keyword to '_default', users can now simply use 'default' to match a literal collection named "default". The previous documentation about using regex patterns was confusing and no longer needed. * Fix error propagation and empty pattern handling 1. command_volume_tier_move.go: Added early termination check after eachDataNode callback to stop processing remaining nodes if a pattern matching error occurred, improving efficiency 2. command_volume_configure_replication.go: Fixed empty pattern handling to match all collections (collectionMatched = true when pattern is empty), mirroring the behavior in other commands These changes address the remaining PR review feedback. --- weed/shell/command_ec_common.go | 5 ++++ .../command_volume_configure_replication.go | 17 +++++++++---- weed/shell/command_volume_fix_replication.go | 24 ++++++++++++++----- weed/shell/command_volume_list.go | 8 ++++++- weed/shell/command_volume_tier_move.go | 22 ++++++++++++++--- weed/shell/common.go | 3 +++ 6 files changed, 65 insertions(+), 14 deletions(-) diff --git a/weed/shell/command_ec_common.go b/weed/shell/command_ec_common.go index a6f714a0d..4c3b7459c 100644 --- a/weed/shell/command_ec_common.go +++ b/weed/shell/command_ec_common.go @@ -1628,11 +1628,16 @@ func EcBalance(commandEnv *CommandEnv, collections []string, dc string, ecReplic // compileCollectionPattern compiles a regex pattern for collection matching. // Empty patterns match empty collections only. +// The special keyword CollectionDefault ("_default") matches empty collections. func compileCollectionPattern(pattern string) (*regexp.Regexp, error) { if pattern == "" { // empty pattern matches empty collection return regexp.Compile("^$") } + if pattern == CollectionDefault { + // CollectionDefault keyword matches empty collection + return regexp.Compile("^$") + } return regexp.Compile(pattern) } diff --git a/weed/shell/command_volume_configure_replication.go b/weed/shell/command_volume_configure_replication.go index c54efd05c..74d20eb93 100644 --- a/weed/shell/command_volume_configure_replication.go +++ b/weed/shell/command_volume_configure_replication.go @@ -116,10 +116,19 @@ func getVolumeFilter(replicaPlacement *super_block.ReplicaPlacement, volumeId ui } } return func(v *master_pb.VolumeInformationMessage) bool { - matched, err := filepath.Match(collectionPattern, v.Collection) - if err != nil { - return false + var collectionMatched bool + if collectionPattern == "" { + // Empty pattern matches all collections + collectionMatched = true + } else if collectionPattern == CollectionDefault { + collectionMatched = v.Collection == "" + } else { + m, err := filepath.Match(collectionPattern, v.Collection) + if err != nil { + return false + } + collectionMatched = m } - return matched + return collectionMatched && v.ReplicaPlacement != replicaPlacementInt32 } } diff --git a/weed/shell/command_volume_fix_replication.go b/weed/shell/command_volume_fix_replication.go index 9c760ef9d..574579c34 100644 --- a/weed/shell/command_volume_fix_replication.go +++ b/weed/shell/command_volume_fix_replication.go @@ -268,9 +268,15 @@ func (c *commandVolumeFixReplication) deleteOneVolume(commandEnv *CommandEnv, wr // check collection name pattern if *c.collectionPattern != "" { - matched, err := filepath.Match(*c.collectionPattern, replica.info.Collection) - if err != nil { - return fmt.Errorf("match pattern %s with collection %s: %v", *c.collectionPattern, replica.info.Collection, err) + var matched bool + if *c.collectionPattern == CollectionDefault { + matched = replica.info.Collection == "" + } else { + var err error + matched, err = filepath.Match(*c.collectionPattern, replica.info.Collection) + if err != nil { + return fmt.Errorf("match pattern %s with collection %s: %v", *c.collectionPattern, replica.info.Collection, err) + } } if !matched { continue @@ -357,9 +363,15 @@ func (c *commandVolumeFixReplication) fixOneUnderReplicatedVolume(commandEnv *Co if fn(dst.dataNode) > 0 && satisfyReplicaPlacement(replicaPlacement, replicas, dst) { // check collection name pattern if *c.collectionPattern != "" { - matched, err := filepath.Match(*c.collectionPattern, replica.info.Collection) - if err != nil { - return fmt.Errorf("match pattern %s with collection %s: %v", *c.collectionPattern, replica.info.Collection, err) + var matched bool + if *c.collectionPattern == CollectionDefault { + matched = replica.info.Collection == "" + } else { + var err error + matched, err = filepath.Match(*c.collectionPattern, replica.info.Collection) + if err != nil { + return fmt.Errorf("match pattern %s with collection %s: %v", *c.collectionPattern, replica.info.Collection, err) + } } if !matched { hasSkippedCollection = true diff --git a/weed/shell/command_volume_list.go b/weed/shell/command_volume_list.go index 04573d402..956d372bb 100644 --- a/weed/shell/command_volume_list.go +++ b/weed/shell/command_volume_list.go @@ -202,7 +202,13 @@ func (c *commandVolumeList) isNotMatchDiskInfo(readOnly bool, collection string, return true } if *c.collectionPattern != "" { - if matched, _ := filepath.Match(*c.collectionPattern, collection); !matched { + var matched bool + if *c.collectionPattern == CollectionDefault { + matched = (collection == "") + } else { + matched, _ = filepath.Match(*c.collectionPattern, collection) + } + if !matched { return true } } diff --git a/weed/shell/command_volume_tier_move.go b/weed/shell/command_volume_tier_move.go index cfa2eaef3..b0f9cba12 100644 --- a/weed/shell/command_volume_tier_move.go +++ b/weed/shell/command_volume_tier_move.go @@ -50,6 +50,10 @@ func (c *commandVolumeTierMove) Help() string { Even if the volume is replicated, only one replica will be changed and the rest replicas will be dropped. So "volume.fix.replication" and "volume.balance" should be followed. + Note: + Use -collectionPattern="_default" to match only the default collection (volumes with no collection name). + Empty collectionPattern matches all collections. + ` } @@ -310,9 +314,16 @@ func collectVolumeIdsForTierChange(topologyInfo *master_pb.TopologyInfo, volumeS for _, v := range diskInfo.VolumeInfos { // check collection name pattern if collectionPattern != "" { - matched, err := filepath.Match(collectionPattern, v.Collection) - if err != nil { - return + var matched bool + if collectionPattern == CollectionDefault { + matched = v.Collection == "" + } else { + var matchErr error + matched, matchErr = filepath.Match(collectionPattern, v.Collection) + if matchErr != nil { + err = fmt.Errorf("collection pattern %q failed to match: %w", collectionPattern, matchErr) + return + } } if !matched { continue @@ -328,6 +339,11 @@ func collectVolumeIdsForTierChange(topologyInfo *master_pb.TopologyInfo, volumeS } }) + // Check if an error occurred during iteration and return early + if err != nil { + return + } + for vid := range vidMap { vids = append(vids, needle.VolumeId(vid)) } diff --git a/weed/shell/common.go b/weed/shell/common.go index cb2df5828..922cd5316 100644 --- a/weed/shell/common.go +++ b/weed/shell/common.go @@ -9,6 +9,9 @@ import ( var ( // Default maximum parallelization/concurrency for commands supporting it. DefaultMaxParallelization = 10 + // CollectionDefault is the special keyword to match empty collection names. + // Use "_default" to avoid collision with a literal collection named "default". + CollectionDefault = "_default" ) // ErrorWaitGroup implements a goroutine wait group which aggregates errors, if any.