snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
810
weed/filer/empty_folder_cleanup/empty_folder_cleaner.go
Normal file
810
weed/filer/empty_folder_cleanup/empty_folder_cleaner.go
Normal file
@@ -0,0 +1,810 @@
|
||||
package empty_folder_cleanup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/cluster/lock_manager"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
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
|
||||
// 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.
|
||||
DefaultObservationWindow = 2 * time.Minute
|
||||
)
|
||||
|
||||
// DirectoryAttributes is what a restored directory needs to come back as it was.
|
||||
type DirectoryAttributes struct {
|
||||
Mode os.FileMode
|
||||
Uid uint32
|
||||
Gid uint32
|
||||
UserName string
|
||||
GroupNames []string
|
||||
}
|
||||
|
||||
// deletedFolder is a folder under observation for entries that landed while it was
|
||||
// being deleted. writtenTo is set by the create event for such an entry.
|
||||
type deletedFolder struct {
|
||||
path string
|
||||
attrs DirectoryAttributes
|
||||
deletedAt time.Time
|
||||
writtenTo bool
|
||||
}
|
||||
|
||||
// FilerOperations defines the filer operations needed by EmptyFolderCleaner
|
||||
type FilerOperations interface {
|
||||
CountDirectoryEntries(ctx context.Context, dirPath util.FullPath, limit int) (count int, err error)
|
||||
DeleteEntryMetaAndData(ctx context.Context, p util.FullPath, isRecursive, ignoreRecursiveError, shouldDeleteChunks, isFromOtherCluster bool, signatures []int32, ifNotModifiedAfter int64) error
|
||||
GetEntryAttributes(ctx context.Context, p util.FullPath) (attributes map[string][]byte, err error)
|
||||
IsDirectoryKeyObject(ctx context.Context, p util.FullPath) (bool, error)
|
||||
DirectoryAttributes(ctx context.Context, p util.FullPath) (DirectoryAttributes, error)
|
||||
EnsureDirectoryEntry(ctx context.Context, p util.FullPath, attrs DirectoryAttributes) error
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type bucketCleanupPolicyState struct {
|
||||
autoRemove bool
|
||||
attrValue string
|
||||
lastCheck time.Time
|
||||
}
|
||||
|
||||
// EmptyFolderCleaner handles asynchronous cleanup of empty folders
|
||||
// Each filer owns specific folders via consistent hashing based on the peer filer list
|
||||
type EmptyFolderCleaner struct {
|
||||
filer FilerOperations
|
||||
lockRing *lock_manager.LockRing
|
||||
host pb.ServerAddress
|
||||
|
||||
// Folder state tracking
|
||||
mu sync.RWMutex
|
||||
folderCounts map[string]*folderState // Rough count cache
|
||||
bucketCleanupPolicies map[string]*bucketCleanupPolicyState // bucket path -> cleanup policy cache
|
||||
|
||||
// Folders deleted recently, kept so that a create event arriving for one of them
|
||||
// can put it back
|
||||
deleted map[string]*deletedFolder
|
||||
deletedDropped int
|
||||
|
||||
// Cleanup queue (thread-safe, has its own lock)
|
||||
cleanupQueue *CleanupQueue
|
||||
|
||||
// Configuration
|
||||
maxCountCheck int // Max items to count (1000)
|
||||
cacheExpiry time.Duration // How long to keep cache entries
|
||||
processorSleep time.Duration // How often processor checks queue
|
||||
bucketPath string // e.g., "/buckets"
|
||||
|
||||
// Control
|
||||
enabled bool
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// NewEmptyFolderCleaner creates a new EmptyFolderCleaner.
|
||||
// cleanupDelay controls how long an empty folder must remain in the queue before deletion.
|
||||
// If zero, DefaultQueueMaxAge is used.
|
||||
func NewEmptyFolderCleaner(filer FilerOperations, lockRing *lock_manager.LockRing, host pb.ServerAddress, bucketPath string, cleanupDelay time.Duration) *EmptyFolderCleaner {
|
||||
if cleanupDelay <= 0 {
|
||||
cleanupDelay = DefaultQueueMaxAge
|
||||
}
|
||||
efc := &EmptyFolderCleaner{
|
||||
filer: filer,
|
||||
lockRing: lockRing,
|
||||
host: host,
|
||||
folderCounts: make(map[string]*folderState),
|
||||
bucketCleanupPolicies: make(map[string]*bucketCleanupPolicyState),
|
||||
deleted: make(map[string]*deletedFolder),
|
||||
cleanupQueue: NewCleanupQueue(DefaultQueueMaxSize, cleanupDelay),
|
||||
maxCountCheck: DefaultMaxCountCheck,
|
||||
cacheExpiry: DefaultCacheExpiry,
|
||||
processorSleep: DefaultProcessorSleep,
|
||||
bucketPath: bucketPath,
|
||||
enabled: true,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
go efc.cacheEvictionLoop()
|
||||
go efc.cleanupProcessor()
|
||||
return efc
|
||||
}
|
||||
|
||||
// SetEnabled enables or disables the cleaner
|
||||
func (efc *EmptyFolderCleaner) SetEnabled(enabled bool) {
|
||||
efc.mu.Lock()
|
||||
defer efc.mu.Unlock()
|
||||
efc.enabled = enabled
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the cleaner is enabled
|
||||
func (efc *EmptyFolderCleaner) IsEnabled() bool {
|
||||
efc.mu.RLock()
|
||||
defer efc.mu.RUnlock()
|
||||
return efc.enabled
|
||||
}
|
||||
|
||||
// ownsFolder checks if this filer owns the folder via consistent hashing
|
||||
func (efc *EmptyFolderCleaner) ownsFolder(folder string) bool {
|
||||
primary := efc.lockRing.GetPrimary(folder)
|
||||
if primary == "" {
|
||||
return true // Single filer case or no servers
|
||||
}
|
||||
return primary == efc.host
|
||||
}
|
||||
|
||||
// OnDeleteEvent is called when a file or directory is deleted
|
||||
// Both file and directory deletions count towards making the parent folder empty
|
||||
// eventTime is the time when the delete event occurred (for proper ordering)
|
||||
func (efc *EmptyFolderCleaner) OnDeleteEvent(directory string, entryName string, isDirectory bool, eventTime time.Time) {
|
||||
// Skip if not under bucket path (must be at least /buckets/<bucket>/...)
|
||||
if efc.bucketPath != "" && !isUnderBucketPath(directory, efc.bucketPath) {
|
||||
return
|
||||
}
|
||||
|
||||
// Never queue the S3 multipart staging area; the upload lifecycle owns it.
|
||||
if isMultipartUploadsPath(efc.bucketPath, directory) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we own this folder
|
||||
if !efc.ownsFolder(directory) {
|
||||
glog.V(4).Infof("EmptyFolderCleaner: not owner of %s, skipping", directory)
|
||||
return
|
||||
}
|
||||
|
||||
efc.mu.Lock()
|
||||
defer efc.mu.Unlock()
|
||||
|
||||
// Check enabled inside lock to avoid race with Stop()
|
||||
if !efc.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(3).Infof("EmptyFolderCleaner: delete event in %s/%s (isDir=%v)", directory, entryName, isDirectory)
|
||||
|
||||
// Update cached count (create entry if needed)
|
||||
state, exists := efc.folderCounts[directory]
|
||||
if !exists {
|
||||
state = &folderState{}
|
||||
efc.folderCounts[directory] = state
|
||||
}
|
||||
if state.roughCount > 0 {
|
||||
state.roughCount--
|
||||
}
|
||||
state.lastDelTime = eventTime
|
||||
|
||||
// Only add to cleanup queue if roughCount suggests folder might be empty
|
||||
if state.roughCount > 0 {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: skipping queue for %s, roughCount=%d", directory, state.roughCount)
|
||||
return
|
||||
}
|
||||
|
||||
// Add to cleanup queue with event time (handles out-of-order events)
|
||||
if efc.cleanupQueue.Add(directory, entryName, eventTime) {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: queued %s for cleanup (triggered by %s)", directory, entryName)
|
||||
}
|
||||
}
|
||||
|
||||
// OnCreateEvent is called when a file or directory is created
|
||||
// Both file and directory creations cancel pending cleanup for the parent folder
|
||||
func (efc *EmptyFolderCleaner) OnCreateEvent(directory string, entryName string, isDirectory bool) {
|
||||
// Skip if not under bucket path (must be at least /buckets/<bucket>/...)
|
||||
if efc.bucketPath != "" && !isUnderBucketPath(directory, efc.bucketPath) {
|
||||
return
|
||||
}
|
||||
|
||||
efc.mu.Lock()
|
||||
defer efc.mu.Unlock()
|
||||
|
||||
// Check enabled inside lock to avoid race with Stop()
|
||||
if !efc.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
// Update cached count only if already tracked (no need to track new folders)
|
||||
if state, exists := efc.folderCounts[directory]; exists {
|
||||
state.roughCount++
|
||||
state.lastAddTime = time.Now()
|
||||
}
|
||||
|
||||
// An entry landing in a folder we just deleted is the race this cleaner cannot
|
||||
// exclude: the folder was empty when checked and is gone now, so the entry has
|
||||
// nothing holding it. The event says so outright, which beats going back to look.
|
||||
if folder, found := efc.deleted[directory]; found {
|
||||
folder.writtenTo = true
|
||||
glog.V(2).Infof("EmptyFolderCleaner: %s was written to while being deleted, restoring it", directory)
|
||||
}
|
||||
|
||||
// Remove from cleanup queue (cancel pending cleanup)
|
||||
if efc.cleanupQueue.Remove(directory) {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: cancelled cleanup for %s due to new entry", directory)
|
||||
}
|
||||
|
||||
// A directory that has just been created is a new incarnation, so a cleanup
|
||||
// queued against the one it replaces would delete it rather than the folder
|
||||
// that was found empty.
|
||||
if isDirectory {
|
||||
recreated := string(util.NewFullPath(directory, entryName))
|
||||
if efc.cleanupQueue.Remove(recreated) {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: cancelled cleanup for %s, recreated", recreated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupProcessor runs in background and processes the cleanup queue
|
||||
func (efc *EmptyFolderCleaner) cleanupProcessor() {
|
||||
ticker := time.NewTicker(efc.processorSleep)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-efc.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
efc.processCleanupQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processCleanupQueue processes items from the cleanup queue
|
||||
func (efc *EmptyFolderCleaner) processCleanupQueue() {
|
||||
efc.restoreFoldersWrittenDuringDelete()
|
||||
|
||||
if efc.cleanupQueue.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(3).Infof("EmptyFolderCleaner: processing cleanup queue (len=%d, oldest_age=%v)",
|
||||
efc.cleanupQueue.Len(), efc.cleanupQueue.OldestAge())
|
||||
|
||||
// Only process items that have been queued longer than maxAge
|
||||
for {
|
||||
// Check if still enabled
|
||||
if !efc.IsEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
// Only pop items old enough — newer items stay in the queue
|
||||
folder, triggeredBy, ok := efc.cleanupQueue.PopOlderThan(efc.cleanupQueue.maxAge)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
// Execute cleanup for this folder
|
||||
efc.executeCleanup(folder, triggeredBy)
|
||||
}
|
||||
}
|
||||
|
||||
// restoreFoldersWrittenDuringDelete puts back folders that received an entry between
|
||||
// the emptiness check and the delete, which leaves that entry with no directory
|
||||
// holding it: reachable by its own path, but absent from any listing.
|
||||
//
|
||||
// A folder stays under observation for DefaultRestoreCheckWindow rather than being
|
||||
// checked once. A writer looks up the parent before inserting the child, so a check
|
||||
// can land in that gap and see nothing; ticks also coalesce when a pass runs long, so
|
||||
// "next pass" is not a delay at all. Re-checking for a bounded wall-clock window
|
||||
// covers both. This narrows the exposure rather than closing it - only making the
|
||||
// emptiness check and the delete atomic would do that.
|
||||
func (efc *EmptyFolderCleaner) restoreFoldersWrittenDuringDelete() {
|
||||
efc.mu.Lock()
|
||||
dropped := efc.deletedDropped
|
||||
efc.deletedDropped = 0
|
||||
var restore []*deletedFolder
|
||||
for path, folder := range efc.deleted {
|
||||
// The window applies whatever the folder's state is. Checking writtenTo first
|
||||
// would keep a folder whose restore keeps failing forever, re-counting it on
|
||||
// every pass.
|
||||
if time.Since(folder.deletedAt) >= DefaultObservationWindow {
|
||||
delete(efc.deleted, path)
|
||||
continue
|
||||
}
|
||||
if folder.writtenTo {
|
||||
restore = append(restore, folder)
|
||||
delete(efc.deleted, path)
|
||||
}
|
||||
}
|
||||
// A cascade takes ancestors along with the folder. Rebuild those from what they
|
||||
// were too: leaving them to the descendant's restore would mint them from the
|
||||
// descendant's own attributes, handing back access the ancestor did not grant.
|
||||
for i := 0; i < len(restore); i++ {
|
||||
ancestor, _ := util.FullPath(restore[i].path).DirAndName()
|
||||
for ancestor != "" && ancestor != "/" {
|
||||
if folder, found := efc.deleted[ancestor]; found {
|
||||
restore = append(restore, folder)
|
||||
delete(efc.deleted, ancestor)
|
||||
}
|
||||
ancestor, _ = util.FullPath(ancestor).DirAndName()
|
||||
}
|
||||
}
|
||||
efc.mu.Unlock()
|
||||
|
||||
if dropped > 0 {
|
||||
glog.V(1).Infof("EmptyFolderCleaner: %d deleted folders left unobserved, past the %d kept", dropped, DefaultMaxDeletedKept)
|
||||
}
|
||||
if len(restore) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Restore shallowest first, so a folder taken by the parent cascade is rebuilt
|
||||
// with its own attributes before anything below it needs it as a parent.
|
||||
sort.Slice(restore, func(i, j int) bool {
|
||||
return strings.Count(restore[i].path, "/") < strings.Count(restore[j].path, "/")
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
var retry []*deletedFolder
|
||||
for i, folder := range restore {
|
||||
if !efc.IsEnabled() {
|
||||
retry = append(retry, restore[i:]...)
|
||||
break
|
||||
}
|
||||
// An event named this folder, but the entry may have been removed again since,
|
||||
// in which case there is nothing to hold and it can stay gone. Ancestors pulled
|
||||
// in above carry no event and are rebuilt regardless, since the folder below
|
||||
// them needs them.
|
||||
if folder.writtenTo {
|
||||
count, err := efc.countItems(ctx, folder.path)
|
||||
if err != nil {
|
||||
glog.V(2).Infof("EmptyFolderCleaner: cannot count %s before restoring it: %v", folder.path, err)
|
||||
retry = append(retry, folder)
|
||||
continue
|
||||
}
|
||||
if count == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
glog.V(1).Infof("EmptyFolderCleaner: restoring %s, written to while it was being deleted", folder.path)
|
||||
if err := efc.filer.EnsureDirectoryEntry(ctx, util.FullPath(folder.path), folder.attrs); err != nil {
|
||||
glog.V(2).Infof("EmptyFolderCleaner: failed to restore %s: %v", folder.path, err)
|
||||
retry = append(retry, folder)
|
||||
}
|
||||
}
|
||||
|
||||
if len(retry) == 0 {
|
||||
return
|
||||
}
|
||||
efc.mu.Lock()
|
||||
for _, folder := range retry {
|
||||
if _, found := efc.deleted[folder.path]; !found {
|
||||
efc.makeRoomForDeletedLocked()
|
||||
}
|
||||
efc.deleted[folder.path] = folder
|
||||
}
|
||||
efc.mu.Unlock()
|
||||
}
|
||||
|
||||
// restoreIfWrittenTo puts folder back when an entry landed in it while it was being
|
||||
// deleted. True means the parent is no longer empty, so the caller must not cascade.
|
||||
func (efc *EmptyFolderCleaner) restoreIfWrittenTo(ctx context.Context, folder string, attrs DirectoryAttributes) bool {
|
||||
count, err := efc.countItems(ctx, folder)
|
||||
if err != nil || count > 0 {
|
||||
// Ask for the retry here rather than leaving it to a create event that may
|
||||
// already have gone by: this pass can be the only sight of the entry.
|
||||
efc.mu.Lock()
|
||||
if observed, found := efc.deleted[folder]; found {
|
||||
observed.writtenTo = true
|
||||
}
|
||||
efc.mu.Unlock()
|
||||
}
|
||||
if err != nil {
|
||||
glog.V(2).Infof("EmptyFolderCleaner: cannot re-check %s after deleting it: %v", folder, err)
|
||||
return false
|
||||
}
|
||||
if count == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
glog.V(1).Infof("EmptyFolderCleaner: restoring %s, written to while it was being deleted", folder)
|
||||
if err := efc.filer.EnsureDirectoryEntry(ctx, util.FullPath(folder), attrs); err != nil {
|
||||
glog.V(2).Infof("EmptyFolderCleaner: failed to restore %s: %v", folder, err)
|
||||
return false
|
||||
}
|
||||
|
||||
efc.mu.Lock()
|
||||
delete(efc.deleted, folder)
|
||||
efc.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
// makeRoomForDeletedLocked drops the oldest of a small sample when the set is full.
|
||||
// The newest folders are the ones whose race is still live, so they must not be the
|
||||
// ones given up; sampling keeps this cheap under heavy deletion rates.
|
||||
func (efc *EmptyFolderCleaner) makeRoomForDeletedLocked() {
|
||||
if len(efc.deleted) < DefaultMaxDeletedKept {
|
||||
return
|
||||
}
|
||||
const sampleSize = 32
|
||||
oldestPath, seen := "", 0
|
||||
for path, folder := range efc.deleted {
|
||||
if oldestPath == "" || folder.deletedAt.Before(efc.deleted[oldestPath].deletedAt) {
|
||||
oldestPath = path
|
||||
}
|
||||
if seen++; seen >= sampleSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
if oldestPath != "" {
|
||||
delete(efc.deleted, oldestPath)
|
||||
efc.deletedDropped++
|
||||
}
|
||||
}
|
||||
|
||||
// executeCleanup performs the actual cleanup of an empty folder
|
||||
func (efc *EmptyFolderCleaner) executeCleanup(folder string, triggeredBy string) {
|
||||
// The bucket-shared .uploads staging tree holds in-progress multipart uploads.
|
||||
// Deleting <bucket>/.uploads (reached here directly or via the parent cascade
|
||||
// below) races a concurrent CreateMultipartUpload: the new upload's marker row
|
||||
// is inserted between the emptiness check and the folder delete, then wiped,
|
||||
// so the upload silently vanishes. Leave this tree to the upload lifecycle.
|
||||
if isMultipartUploadsPath(efc.bucketPath, folder) {
|
||||
return
|
||||
}
|
||||
|
||||
efc.mu.Lock()
|
||||
|
||||
// Quick check: if we have cached count and it's > 0, skip
|
||||
if state, exists := efc.folderCounts[folder]; exists {
|
||||
if state.roughCount > 0 {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: skipping %s (triggered by %s), cached count=%d", folder, triggeredBy, state.roughCount)
|
||||
efc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
// If there was an add after our delete, skip
|
||||
if !state.lastAddTime.IsZero() && state.lastAddTime.After(state.lastDelTime) {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: skipping %s (triggered by %s), add happened after delete", folder, triggeredBy)
|
||||
efc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
efc.mu.Unlock()
|
||||
|
||||
// Re-check ownership (topology might have changed)
|
||||
if !efc.ownsFolder(folder) {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: no longer owner of %s (triggered by %s), skipping", folder, triggeredBy)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
bucketPath, autoRemove, source, attrValue, err := efc.getBucketCleanupPolicy(ctx, folder)
|
||||
if err != nil {
|
||||
if err == filer_pb.ErrNotFound {
|
||||
return
|
||||
}
|
||||
glog.V(2).Infof("EmptyFolderCleaner: failed to load bucket cleanup policy for folder %s (triggered by %s): %v", folder, triggeredBy, err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if folder is actually empty (count up to maxCountCheck)
|
||||
count, err := efc.countItems(ctx, folder)
|
||||
if err != nil {
|
||||
glog.V(2).Infof("EmptyFolderCleaner: error counting items in %s: %v", folder, err)
|
||||
return
|
||||
}
|
||||
|
||||
efc.mu.Lock()
|
||||
// Update cache
|
||||
if _, exists := efc.folderCounts[folder]; !exists {
|
||||
efc.folderCounts[folder] = &folderState{}
|
||||
}
|
||||
efc.folderCounts[folder].roughCount = count
|
||||
efc.folderCounts[folder].lastCheck = time.Now()
|
||||
efc.mu.Unlock()
|
||||
|
||||
if count > 0 {
|
||||
glog.V(4).Infof("EmptyFolderCleaner: folder %s (triggered by %s) has %d items, not empty", folder, triggeredBy, count)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip explicitly created directory markers (e.g., PUT /bucket/folder/)
|
||||
// These have a MIME type set and should be preserved even when empty
|
||||
if isKeyObj, err := efc.filer.IsDirectoryKeyObject(ctx, util.FullPath(folder)); err != nil {
|
||||
glog.V(2).Infof("EmptyFolderCleaner: error checking directory key object %s: %v", folder, err)
|
||||
return
|
||||
} else if isKeyObj {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: skipping %s (triggered by %s), explicit directory marker", folder, triggeredBy)
|
||||
return
|
||||
}
|
||||
|
||||
// Read what it would take to put this folder back before removing it; without
|
||||
// that a restore would have to invent attributes for it.
|
||||
attrs, err := efc.filer.DirectoryAttributes(ctx, util.FullPath(folder))
|
||||
if err != nil {
|
||||
glog.V(2).Infof("EmptyFolderCleaner: cannot read %s before deleting it: %v", folder, err)
|
||||
return
|
||||
}
|
||||
|
||||
// An S3 Tables catalog entry - a namespace, a table whose files a rename left
|
||||
// behind at the old path, a view that never had any - is the directory itself,
|
||||
// so its being empty says nothing about whether the catalog still names it.
|
||||
if extended, err := efc.filer.GetEntryAttributes(ctx, util.FullPath(folder)); err != nil {
|
||||
glog.V(2).Infof("EmptyFolderCleaner: error reading attributes of %s: %v", folder, err)
|
||||
return
|
||||
} else if isCatalogEntry(extended) {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: skipping %s (triggered by %s), s3tables catalog entry", folder, triggeredBy)
|
||||
return
|
||||
}
|
||||
|
||||
// Observe it before the delete rather than after. A delete can fail partway and
|
||||
// still leave the folder gone - the redis stores remove the folder before their
|
||||
// parent-list member - so a failure return is not proof that it is still there.
|
||||
efc.mu.Lock()
|
||||
if efc.deleted == nil {
|
||||
efc.deleted = make(map[string]*deletedFolder)
|
||||
}
|
||||
efc.makeRoomForDeletedLocked()
|
||||
efc.deleted[folder] = &deletedFolder{path: folder, attrs: attrs, deletedAt: time.Now()}
|
||||
efc.mu.Unlock()
|
||||
|
||||
glog.Infof("EmptyFolderCleaner: deleting empty folder %s (triggered by %s)", folder, triggeredBy)
|
||||
if err := efc.deleteFolder(ctx, folder); err != nil {
|
||||
glog.V(2).Infof("EmptyFolderCleaner: failed to delete empty folder %s (triggered by %s): %v", folder, triggeredBy, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up cache entry
|
||||
efc.mu.Lock()
|
||||
delete(efc.folderCounts, folder)
|
||||
efc.mu.Unlock()
|
||||
|
||||
// The delete's own emptiness check and the entry removal are not atomic either.
|
||||
// Paired with parents being created after the insert, whoever acts second sees it.
|
||||
if efc.restoreIfWrittenTo(ctx, folder, attrs) {
|
||||
return
|
||||
}
|
||||
|
||||
// After deleting this folder, immediately try to clean the parent.
|
||||
// Relying solely on cascading metadata events would re-enter the full
|
||||
// delay queue for each ancestor level, causing multi-minute cascading
|
||||
// waits (e.g. 3 levels × 2m = 6m+). Instead, walk up eagerly.
|
||||
parentDir, _ := util.FullPath(folder).DirAndName()
|
||||
if parentDir != "" && parentDir != folder &&
|
||||
efc.bucketPath != "" && isUnderBucketPath(parentDir, efc.bucketPath) {
|
||||
// Remove any pending queue entry for the parent so we don't
|
||||
// double-process it later from a stale event.
|
||||
efc.cleanupQueue.Remove(parentDir)
|
||||
efc.executeCleanup(parentDir, triggeredBy)
|
||||
}
|
||||
}
|
||||
|
||||
// countItems counts items in a folder (up to maxCountCheck)
|
||||
func (efc *EmptyFolderCleaner) countItems(ctx context.Context, folder string) (int, error) {
|
||||
return efc.filer.CountDirectoryEntries(ctx, util.FullPath(folder), efc.maxCountCheck)
|
||||
}
|
||||
|
||||
// deleteFolder deletes an empty folder
|
||||
func (efc *EmptyFolderCleaner) deleteFolder(ctx context.Context, folder string) error {
|
||||
return efc.filer.DeleteEntryMetaAndData(ctx, util.FullPath(folder), false, false, false, false, nil, 0)
|
||||
}
|
||||
|
||||
func (efc *EmptyFolderCleaner) getBucketCleanupPolicy(ctx context.Context, folder string) (bucketPath string, autoRemove bool, source string, attrValue string, err error) {
|
||||
bucketPath, ok := util.ExtractBucketPath(efc.bucketPath, folder, true)
|
||||
if !ok {
|
||||
return "", true, "default", "<not_bucket_path>", nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
efc.mu.RLock()
|
||||
if state, found := efc.bucketCleanupPolicies[bucketPath]; found && now.Sub(state.lastCheck) <= efc.cacheExpiry {
|
||||
efc.mu.RUnlock()
|
||||
return bucketPath, state.autoRemove, "cache", state.attrValue, nil
|
||||
}
|
||||
efc.mu.RUnlock()
|
||||
|
||||
attrs, err := efc.filer.GetEntryAttributes(ctx, util.FullPath(bucketPath))
|
||||
if err != nil {
|
||||
return "", true, "", "", err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// isCatalogEntry reports whether the directory is an s3tables catalog record.
|
||||
func isCatalogEntry(attrs map[string][]byte) bool {
|
||||
for key := range attrs {
|
||||
if strings.HasPrefix(key, s3_constants.ExtS3TablesPrefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func autoRemoveEmptyFoldersEnabled(attrs map[string][]byte) (bool, string) {
|
||||
if attrs == nil {
|
||||
return true, "<no_attrs>"
|
||||
}
|
||||
|
||||
value, found := attrs[s3_constants.ExtAllowEmptyFolders]
|
||||
if !found {
|
||||
return true, "<missing>"
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(string(value))
|
||||
if text == "" {
|
||||
return true, "<empty>"
|
||||
}
|
||||
|
||||
return !strings.EqualFold(text, "true"), text
|
||||
}
|
||||
|
||||
// isUnderPath checks if child is under parent path
|
||||
func isUnderPath(child, parent string) bool {
|
||||
if parent == "" || parent == "/" {
|
||||
return true
|
||||
}
|
||||
// Ensure parent ends without slash for proper prefix matching
|
||||
if len(parent) > 0 && parent[len(parent)-1] == '/' {
|
||||
parent = parent[:len(parent)-1]
|
||||
}
|
||||
// Child must start with parent and then have a / or be exactly parent
|
||||
if len(child) < len(parent) {
|
||||
return false
|
||||
}
|
||||
if child[:len(parent)] != parent {
|
||||
return false
|
||||
}
|
||||
if len(child) == len(parent) {
|
||||
return true
|
||||
}
|
||||
return child[len(parent)] == '/'
|
||||
}
|
||||
|
||||
// isMultipartUploadsPath reports whether directory is the S3 multipart staging
|
||||
// root <bucket>/.uploads or anything beneath it.
|
||||
func isMultipartUploadsPath(bucketPath, directory string) bool {
|
||||
if bucketPath == "" {
|
||||
return false
|
||||
}
|
||||
dir, ok := util.ExtractBucketPath(bucketPath, directory, true)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// requireChild guarantees directory starts with dir + "/", so slice past it.
|
||||
first, _, _ := strings.Cut(directory[len(dir)+1:], "/")
|
||||
return first == s3_constants.MultipartUploadsFolder
|
||||
}
|
||||
|
||||
// isUnderBucketPath checks if directory is inside a bucket (under /buckets/<bucket>/...)
|
||||
// This ensures we only clean up folders inside buckets, not the buckets themselves
|
||||
func isUnderBucketPath(directory, bucketPath string) bool {
|
||||
if bucketPath == "" {
|
||||
return true
|
||||
}
|
||||
// Ensure bucketPath ends without slash
|
||||
if len(bucketPath) > 0 && bucketPath[len(bucketPath)-1] == '/' {
|
||||
bucketPath = bucketPath[:len(bucketPath)-1]
|
||||
}
|
||||
// Directory must be under bucketPath
|
||||
if !isUnderPath(directory, bucketPath) {
|
||||
return false
|
||||
}
|
||||
// Directory must be at least /buckets/<bucket>/<something>
|
||||
// i.e., depth must be at least bucketPath depth + 2
|
||||
// For /buckets (depth 1), we need at least /buckets/mybucket/folder (depth 3)
|
||||
bucketPathDepth := strings.Count(bucketPath, "/")
|
||||
directoryDepth := strings.Count(directory, "/")
|
||||
return directoryDepth >= bucketPathDepth+2
|
||||
}
|
||||
|
||||
// cacheEvictionLoop periodically removes stale entries from folderCounts
|
||||
func (efc *EmptyFolderCleaner) cacheEvictionLoop() {
|
||||
ticker := time.NewTicker(efc.cacheExpiry)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-efc.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
efc.evictStaleCacheEntries()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// evictStaleCacheEntries removes cache entries that haven't been accessed recently
|
||||
func (efc *EmptyFolderCleaner) evictStaleCacheEntries() {
|
||||
efc.mu.Lock()
|
||||
defer efc.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
expiredCount := 0
|
||||
for folder, state := range efc.folderCounts {
|
||||
// Skip if folder is in cleanup queue
|
||||
if efc.cleanupQueue.Contains(folder) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the most recent activity time for this folder
|
||||
lastActivity := state.lastCheck
|
||||
if state.lastAddTime.After(lastActivity) {
|
||||
lastActivity = state.lastAddTime
|
||||
}
|
||||
if state.lastDelTime.After(lastActivity) {
|
||||
lastActivity = state.lastDelTime
|
||||
}
|
||||
|
||||
// Evict if no activity within cache expiry period
|
||||
if now.Sub(lastActivity) > efc.cacheExpiry {
|
||||
delete(efc.folderCounts, folder)
|
||||
expiredCount++
|
||||
}
|
||||
}
|
||||
|
||||
for bucketPath, state := range efc.bucketCleanupPolicies {
|
||||
if now.Sub(state.lastCheck) > efc.cacheExpiry {
|
||||
delete(efc.bucketCleanupPolicies, bucketPath)
|
||||
}
|
||||
}
|
||||
|
||||
if expiredCount > 0 {
|
||||
glog.V(3).Infof("EmptyFolderCleaner: evicted %d stale cache entries", expiredCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the cleaner and cancels all pending tasks
|
||||
func (efc *EmptyFolderCleaner) Stop() {
|
||||
close(efc.stopCh)
|
||||
|
||||
efc.mu.Lock()
|
||||
defer efc.mu.Unlock()
|
||||
|
||||
efc.enabled = false
|
||||
efc.cleanupQueue.Clear()
|
||||
efc.folderCounts = make(map[string]*folderState) // Clear cache on stop
|
||||
efc.bucketCleanupPolicies = make(map[string]*bucketCleanupPolicyState)
|
||||
efc.deleted, efc.deletedDropped = make(map[string]*deletedFolder), 0
|
||||
}
|
||||
|
||||
// GetPendingCleanupCount returns the number of pending cleanup tasks (for testing)
|
||||
func (efc *EmptyFolderCleaner) GetPendingCleanupCount() int {
|
||||
return efc.cleanupQueue.Len()
|
||||
}
|
||||
|
||||
// GetCachedFolderCount returns the cached count for a folder (for testing)
|
||||
func (efc *EmptyFolderCleaner) GetCachedFolderCount(folder string) (int, bool) {
|
||||
efc.mu.RLock()
|
||||
defer efc.mu.RUnlock()
|
||||
if state, exists := efc.folderCounts[folder]; exists {
|
||||
return state.roughCount, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
Reference in New Issue
Block a user