mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-05 11:37:44 +01:00
mount/shell: bucket allow-empty-folders toggle, mount keeps explicit false (#11370)
* mount: keep a deliberate bucket allow-empty-folders setting * shell: s3.bucket.allowEmptyFolders toggles the empty folder cleaner * shell: guard allow-empty-folders toggle with expected extended attrs * filer: drop cached empty-folder policy on bucket entry update * mount: guard allow-empty-folders write with expected extended attrs * filer: skip caching a stale cleanup policy read across an update * mount, shell: snapshot the full extended attributes for update preconditions * filer: fail closed and invalidate on all bucket entry events for cleanup policy * filer: key the cleanup policy generation by bucket * filer: requeue cleanup when the bucket policy cannot be loaded * filer: expire idle cleanup policy generations * filer: skip requeueing cleanup after the cleaner stops * filer: bound cleanup retries on repeated policy failures * filer: cover cleanup requeue on repeated policy failures * filer: keep cleanup policy generations while reads are in flight * filer: exercise the cleanup queue lifecycle in the retry-cap test
This commit is contained in:
1 parent
2d2619f0b4
commit
bdc37a1e86
7 files changed
+569
-53
No files matched your search
@@ -2,6 +2,7 @@ package empty_folder_cleanup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -17,12 +18,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMaxCountCheck = 1000
|
||||
DefaultCacheExpiry = 5 * time.Minute
|
||||
DefaultQueueMaxSize = 1000
|
||||
DefaultQueueMaxAge = 2 * time.Minute
|
||||
DefaultProcessorSleep = 30 * time.Second // How often to check queue
|
||||
DefaultMaxDeletedKept = 10000 // Deleted folders remembered for the restore check
|
||||
DefaultMaxCountCheck = 1000
|
||||
DefaultCacheExpiry = 5 * time.Minute
|
||||
DefaultQueueMaxSize = 1000
|
||||
DefaultQueueMaxAge = 2 * time.Minute
|
||||
DefaultProcessorSleep = 30 * time.Second // How often to check queue
|
||||
DefaultMaxDeletedKept = 10000 // Deleted folders remembered for the restore check
|
||||
DefaultMaxPolicyFailures = 3 // Consecutive policy-load failures before a folder is dropped
|
||||
// How long a deleted folder is kept so that a create event arriving for it can
|
||||
// still put it back. It bounds how far behind the event stream may run, not how
|
||||
// long the race window is.
|
||||
@@ -59,10 +61,17 @@ type FilerOperations interface {
|
||||
|
||||
// folderState tracks the state of a folder for empty folder cleanup
|
||||
type folderState struct {
|
||||
roughCount int // Cached rough count (up to maxCountCheck)
|
||||
lastAddTime time.Time // Last time an item was added
|
||||
lastDelTime time.Time // Last time an item was deleted
|
||||
lastCheck time.Time // Last time we checked the actual count
|
||||
roughCount int // Cached rough count (up to maxCountCheck)
|
||||
lastAddTime time.Time // Last time an item was added
|
||||
lastDelTime time.Time // Last time an item was deleted
|
||||
lastCheck time.Time // Last time we checked the actual count
|
||||
policyFailures int // Consecutive bucket policy load failures
|
||||
}
|
||||
|
||||
type bucketPolicyGen struct {
|
||||
gen uint64
|
||||
lastBump time.Time
|
||||
inflight int
|
||||
}
|
||||
|
||||
type bucketCleanupPolicyState struct {
|
||||
@@ -82,6 +91,7 @@ type EmptyFolderCleaner struct {
|
||||
mu sync.RWMutex
|
||||
folderCounts map[string]*folderState // Rough count cache
|
||||
bucketCleanupPolicies map[string]*bucketCleanupPolicyState // bucket path -> cleanup policy cache
|
||||
policyGen map[string]*bucketPolicyGen // bucket path -> generation, bumped on bucket entry updates to invalidate in-flight policy loads
|
||||
|
||||
// Folders deleted recently, kept so that a create event arriving for one of them
|
||||
// can put it back
|
||||
@@ -115,6 +125,7 @@ func NewEmptyFolderCleaner(filer FilerOperations, lockRing *lock_manager.LockRin
|
||||
host: host,
|
||||
folderCounts: make(map[string]*folderState),
|
||||
bucketCleanupPolicies: make(map[string]*bucketCleanupPolicyState),
|
||||
policyGen: make(map[string]*bucketPolicyGen),
|
||||
deleted: make(map[string]*deletedFolder),
|
||||
cleanupQueue: NewCleanupQueue(DefaultQueueMaxSize, cleanupDelay),
|
||||
maxCountCheck: DefaultMaxCountCheck,
|
||||
@@ -251,6 +262,33 @@ func (efc *EmptyFolderCleaner) OnCreateEvent(directory string, entryName string,
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateBucketPolicy drops the cached cleanup policy when a bucket entry changes
|
||||
func (efc *EmptyFolderCleaner) InvalidateBucketPolicy(directory string, entryName string, isDirectory bool) {
|
||||
if !isDirectory || directory != efc.bucketPath {
|
||||
return
|
||||
}
|
||||
|
||||
efc.mu.Lock()
|
||||
defer efc.mu.Unlock()
|
||||
|
||||
if !efc.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if efc.policyGen == nil {
|
||||
efc.policyGen = make(map[string]*bucketPolicyGen)
|
||||
}
|
||||
path := string(util.NewFullPath(directory, entryName))
|
||||
gen := efc.policyGen[path]
|
||||
if gen == nil {
|
||||
gen = &bucketPolicyGen{}
|
||||
efc.policyGen[path] = gen
|
||||
}
|
||||
gen.gen++
|
||||
gen.lastBump = time.Now()
|
||||
delete(efc.bucketCleanupPolicies, path)
|
||||
}
|
||||
|
||||
// cleanupProcessor runs in background and processes the cleanup queue
|
||||
func (efc *EmptyFolderCleaner) cleanupProcessor() {
|
||||
ticker := time.NewTicker(efc.processorSleep)
|
||||
@@ -491,9 +529,28 @@ func (efc *EmptyFolderCleaner) executeCleanup(folder string, triggeredBy string)
|
||||
return
|
||||
}
|
||||
glog.V(2).Infof("EmptyFolderCleaner: failed to load bucket cleanup policy for folder %s (triggered by %s): %v", folder, triggeredBy, err)
|
||||
efc.mu.Lock()
|
||||
if efc.enabled {
|
||||
state, exists := efc.folderCounts[folder]
|
||||
if !exists {
|
||||
state = &folderState{}
|
||||
efc.folderCounts[folder] = state
|
||||
}
|
||||
state.policyFailures++
|
||||
if state.policyFailures <= DefaultMaxPolicyFailures {
|
||||
efc.cleanupQueue.Add(folder, triggeredBy, time.Now())
|
||||
}
|
||||
}
|
||||
efc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
efc.mu.Lock()
|
||||
if state, exists := efc.folderCounts[folder]; exists {
|
||||
state.policyFailures = 0
|
||||
}
|
||||
efc.mu.Unlock()
|
||||
|
||||
if !autoRemove {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: skipping folder %s (triggered by %s), bucket %s auto-remove-empty-folders disabled (source=%s attr=%s)",
|
||||
folder, triggeredBy, bucketPath, source, attrValue)
|
||||
@@ -617,25 +674,47 @@ func (efc *EmptyFolderCleaner) getBucketCleanupPolicy(ctx context.Context, folde
|
||||
}
|
||||
efc.mu.RUnlock()
|
||||
|
||||
attrs, err := efc.filer.GetEntryAttributes(ctx, util.FullPath(bucketPath))
|
||||
if err != nil {
|
||||
return "", true, "", "", err
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
efc.mu.Lock()
|
||||
if efc.policyGen == nil {
|
||||
efc.policyGen = make(map[string]*bucketPolicyGen)
|
||||
}
|
||||
state := efc.policyGen[bucketPath]
|
||||
if state == nil {
|
||||
state = &bucketPolicyGen{}
|
||||
efc.policyGen[bucketPath] = state
|
||||
}
|
||||
state.inflight++
|
||||
gen := state.gen
|
||||
efc.mu.Unlock()
|
||||
|
||||
attrs, err := efc.filer.GetEntryAttributes(ctx, util.FullPath(bucketPath))
|
||||
|
||||
efc.mu.Lock()
|
||||
state.inflight--
|
||||
if err != nil {
|
||||
efc.mu.Unlock()
|
||||
return "", true, "", "", err
|
||||
}
|
||||
autoRemove, attrValue = autoRemoveEmptyFoldersEnabled(attrs)
|
||||
if gen != state.gen {
|
||||
efc.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
if efc.bucketCleanupPolicies == nil {
|
||||
efc.bucketCleanupPolicies = make(map[string]*bucketCleanupPolicyState)
|
||||
}
|
||||
efc.bucketCleanupPolicies[bucketPath] = &bucketCleanupPolicyState{
|
||||
autoRemove: autoRemove,
|
||||
attrValue: attrValue,
|
||||
lastCheck: now,
|
||||
}
|
||||
efc.mu.Unlock()
|
||||
|
||||
return bucketPath, autoRemove, "filer", attrValue, nil
|
||||
}
|
||||
|
||||
autoRemove, attrValue = autoRemoveEmptyFoldersEnabled(attrs)
|
||||
|
||||
efc.mu.Lock()
|
||||
if efc.bucketCleanupPolicies == nil {
|
||||
efc.bucketCleanupPolicies = make(map[string]*bucketCleanupPolicyState)
|
||||
}
|
||||
efc.bucketCleanupPolicies[bucketPath] = &bucketCleanupPolicyState{
|
||||
autoRemove: autoRemove,
|
||||
attrValue: attrValue,
|
||||
lastCheck: now,
|
||||
}
|
||||
efc.mu.Unlock()
|
||||
|
||||
return bucketPath, autoRemove, "filer", attrValue, nil
|
||||
return "", false, "", "", fmt.Errorf("bucket cleanup policy changed during read: %s", bucketPath)
|
||||
}
|
||||
|
||||
// isCatalogEntry reports whether the directory is an s3tables catalog record.
|
||||
@@ -775,6 +854,14 @@ func (efc *EmptyFolderCleaner) evictStaleCacheEntries() {
|
||||
}
|
||||
}
|
||||
|
||||
// A generation stays while a read that captured it can still return; an idle
|
||||
// one drops after the expiry so bucket churn cannot grow the map forever.
|
||||
for bucketPath, gen := range efc.policyGen {
|
||||
if gen.inflight == 0 && now.Sub(gen.lastBump) > efc.cacheExpiry {
|
||||
delete(efc.policyGen, bucketPath)
|
||||
}
|
||||
}
|
||||
|
||||
if expiredCount > 0 {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: evicted %d stale cache entries", expiredCount)
|
||||
}
|
||||
@@ -791,6 +878,7 @@ func (efc *EmptyFolderCleaner) Stop() {
|
||||
efc.cleanupQueue.Clear()
|
||||
efc.folderCounts = make(map[string]*folderState) // Clear cache on stop
|
||||
efc.bucketCleanupPolicies = make(map[string]*bucketCleanupPolicyState)
|
||||
efc.policyGen = make(map[string]*bucketPolicyGen)
|
||||
efc.deleted, efc.deletedDropped = make(map[string]*deletedFolder), 0
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user