Compare commits

...
Author SHA1 Message Date
chrislu 04b1961a2b more comments 2025-11-05 23:05:55 -08:00
chrislu 0a3bc313b3 context 2025-11-05 22:53:26 -08:00
chrislu de24b2e42c simplify 2025-11-05 22:40:24 -08:00
chrislu 1cd8a591b9 determine stopAtPath 2025-11-05 22:35:41 -08:00
chrislu 36ddaaaf91 not return entry if failed to delete 2025-11-05 22:26:30 -08:00
chrislu 4b12262431 Avoids unnecessary directory emptiness checks and potential race conditions when the entry was never deleted in the first place. 2025-11-05 22:19:52 -08:00
chrislu eec179ef02 fmt 2025-11-05 22:14:31 -08:00
chrislu 50d81319a9 Merge branch 'master' into also-delete-parent-directory-if-empty 2025-11-05 22:13:01 -08:00
chrislu 282e0dcbc8 only check empty folder once when LC 2025-11-05 22:04:16 -08:00
chrislu a8a4bc06e8 optionally delete empty parent directories 2025-11-05 21:29:06 -08:00
chrislu 9612457a32 Safety check 2025-11-05 20:44:22 -08:00
chrislu 9866287d8d s3 TTL time 2025-11-05 16:31:17 -08:00
chrislu 06e9ca70a6 refactoring 2025-11-05 16:03:07 -08:00
chrislu c125060b51 more logging 2025-11-05 15:59:59 -08:00
chrislu 3e440d2145 reuse code 2025-11-05 15:34:14 -08:00
chrislu 7484fcb137 constant 2025-11-05 14:43:18 -08:00
chrislu dc69c875a1 prevent deleting bucket 2025-11-05 14:42:27 -08:00
chrislu 3cff8846c2 batched operation, refactoring 2025-11-05 14:32:20 -08:00
chrislu ec8ca216a5 add context, sort directories by depth (deepest first) to avoid redundant checks 2025-11-05 14:17:38 -08:00
chrislu 55ee4513e7 cleaner 2025-11-05 14:12:20 -08:00
chrislu d8bef68752 join path 2025-11-05 13:50:20 -08:00
chrislu c087a47d38 errors join 2025-11-05 13:50:08 -08:00
chrislu 835e5696d9 still issue UpdateEntry when the flag must be added 2025-11-05 13:49:48 -08:00
chrislu 92525d78ce stop a gRPC stream from the client-side callback is to return a specific error, e.g., io.EOF 2025-11-05 13:38:40 -08:00
chrislu 629b520edf use iterative approach with a queue to avoid recursive WithFilerClient calls 2025-11-05 13:37:40 -08:00
chrislu 0cad84ee36 reuse code to delete empty folders 2025-11-05 13:27:47 -08:00
chrislu 45e0a661da strut copying 2025-11-05 13:11:16 -08:00
chrislu 636540aba2 handle listing errors 2025-11-05 13:10:58 -08:00
chrislu 671de48369 clearer handling on recursive empty directory deletion 2025-11-05 13:10:40 -08:00
chrislu 672488c828 Revert "fix sqlite not support concurrent writes/reads"
This reverts commit 5d5da14e0e.
2025-11-05 12:44:07 -08:00
chrislu 543e70c511 move deletion out of listing transaction; delete entries and empty folders 2025-11-05 12:42:30 -08:00
Konstantin Lebedev 5d5da14e0e fix sqlite not support concurrent writes/reads 2025-11-06 00:14:37 +05:00
Konstantin Lebedev 455dec12f4 fix delete chunks 2025-11-05 23:04:00 +05:00
Konstantin Lebedev 454964353a fix delete on FindEntry 2025-11-05 22:56:49 +05:00
Konstantin Lebedev 50e1cf568e fix S3Versioning 2025-11-05 22:50:09 +05:00
Konstantin Lebedev 53e9d408ab fix delete version object 2025-11-05 20:06:50 +05:00
Konstantin Lebedev cf75abb408 resolv comment 2025-11-05 18:39:21 +05:00
Konstantin Lebedev 360c2387db rm log 2025-11-05 18:16:04 +05:00
Konstantin Lebedev 20fb1ead77 fix updateTTL 2025-11-05 18:14:55 +05:00
Konstantin Lebedev a88eab0b97 revert expiration tests 2025-11-05 15:05:34 +05:00
Konstantin Lebedev c18004f9f6 rm dublicate SeaweedFSExpiresS3 2025-11-05 14:16:07 +05:00
Konstantin Lebedev 20254cd2db fix pipline tests 2025-11-05 14:10:29 +05:00
Konstantin Lebedev 558f4be73b allowDeleteObjectsByTTL by default 2025-11-05 14:06:02 +05:00
Konstantin LebedevandGitHub 7c41795078 Merge branch 'master' into allow_delete_objects_by_TTL 2025-11-05 13:44:27 +05:00
Konstantin Lebedev bbd7546cea test s3 put multipart 2025-11-04 20:26:07 +05:00
Konstantin Lebedev 1abea0d9b5 test s3 put 2025-11-04 20:03:36 +05:00
Konstantin Lebedev e8e080b4fa del unusing func removeExpiredObject 2025-11-04 15:29:42 +05:00
Konstantin Lebedev 6a0d1e0b6f filer delete meta and data 2025-11-04 14:54:49 +05:00
Konstantin Lebedev 8d768885c5 move s3 delete expired entry to filer 2025-11-04 13:17:36 +05:00
Konstantin LebedevandGitHub d1b5d95d84 Merge branch 'master' into allow_delete_objects_by_TTL 2025-11-04 12:19:34 +05:00
Konstantin Lebedev 082c1d6431 Merge remote-tracking branch 'fork/allow_delete_objects_by_TTL' into allow_delete_objects_by_TTL 2025-11-04 12:17:15 +05:00
Konstantin Lebedev a4638d4e1d clear TtlSeconds for volume 2025-11-04 12:17:09 +05:00
chrislu e0a4af1342 go mod 2025-11-03 22:46:24 -08:00
Konstantin Lebedev 981bc96082 GetS3ExpireTime on filer 2025-11-04 10:29:16 +05:00
Konstantin Lebedev f8b874d752 resolv coderabbitai 2025-11-04 01:35:07 +05:00
Konstantin Lebedev 0e6f40e903 fix s3tests 2025-11-04 00:43:30 +05:00
Konstantin Lebedev bff703e126 fix locationPrefix for updateEntriesTTL 2025-11-03 18:21:53 +05:00
Konstantin Lebedev e2b43c0b5e fix IsExpired 2025-11-03 18:07:24 +05:00
Konstantin Lebedev 47c7d5fc8f fix test lifecycle expiration 2025-11-03 17:59:44 +05:00
Konstantin Lebedev aea7327089 fix opt allowDeleteObjectsByTTL for server 2025-11-03 15:51:09 +05:00
Konstantin Lebedev 0bcc2b1156 add lifecycle expiration s3 tests 2025-11-03 15:39:21 +05:00
Konstantin Lebedev 8efd47bf8f delete on get and head 2025-11-03 15:11:28 +05:00
Konstantin Lebedev 391f261ba5 pass opt allowDeleteObjectsByTTL to all servers 2025-11-03 14:16:44 +05:00
Konstantin Lebedev e086793cb3 disable delete expires s3 entry in filer 2025-11-03 14:09:02 +05:00
Konstantin Lebedev dcc84f9f34 do delete expired entries on s3 list request
https://github.com/seaweedfs/seaweedfs/issues/6837
2025-11-03 12:46:22 +05:00
9 changed files with 356 additions and 286 deletions
+37 -8
View File
@@ -355,11 +355,17 @@ func (f *Filer) FindEntry(ctx context.Context, p util.FullPath) (entry *Entry, e
if entry.GetS3ExpireTime().Before(time.Now()) && !entry.IsS3Versioning() {
if delErr := f.doDeleteEntryMetaAndData(ctx, entry, true, false, nil); delErr != nil {
glog.ErrorfCtx(ctx, "FindEntry doDeleteEntryMetaAndData %s failed: %v", entry.FullPath, delErr)
// Return error to prevent serving expired content (safer than returning the entry)
return nil, fmt.Errorf("failed to delete expired entry %s: %w", entry.FullPath, delErr)
}
return nil, filer_pb.ErrNotFound
}
} else if entry.Crtime.Add(time.Duration(entry.TtlSec) * time.Second).Before(time.Now()) {
f.Store.DeleteOneEntry(ctx, entry)
if delErr := f.Store.DeleteOneEntry(ctx, entry); delErr != nil {
glog.ErrorfCtx(ctx, "FindEntry DeleteOneEntry %s failed: %v", entry.FullPath, delErr)
// Return error to prevent serving expired content (safer than returning the entry)
return nil, fmt.Errorf("failed to delete expired entry %s: %w", entry.FullPath, delErr)
}
return nil, filer_pb.ErrNotFound
}
}
@@ -403,23 +409,46 @@ func (f *Filer) doListDirectoryEntries(ctx context.Context, p util.FullPath, sta
}
// Delete expired entries after iteration completes to avoid DB connection deadlock
// Use context.WithoutCancel to ensure cleanup completes even if request is cancelled
if len(s3ExpiredEntries) > 0 || len(expiredEntries) > 0 {
opCtx := context.WithoutCancel(ctx)
// Delete all expired entries first
deletedCount := 0
for _, entry := range s3ExpiredEntries {
if delErr := f.doDeleteEntryMetaAndData(ctx, entry, true, false, nil); delErr != nil {
if delErr := f.doDeleteEntryMetaAndData(opCtx, entry, true, false, nil); delErr != nil {
glog.ErrorfCtx(ctx, "doListDirectoryEntries doDeleteEntryMetaAndData %s failed: %v", entry.FullPath, delErr)
} else {
deletedCount++
}
}
for _, entry := range expiredEntries {
if delErr := f.Store.DeleteOneEntry(ctx, entry); delErr != nil {
if delErr := f.Store.DeleteOneEntry(opCtx, entry); delErr != nil {
glog.ErrorfCtx(ctx, "doListDirectoryEntries DeleteOneEntry %s failed: %v", entry.FullPath, delErr)
} else {
deletedCount++
}
}
// After expiring entries, the directory might be empty.
// Attempt to clean it up and any empty parent directories.
if !hasValidEntries && p != "/" && startFileName == "" {
stopAtPath := util.FullPath(f.DirBucketsPath)
f.DeleteEmptyParentDirectories(ctx, p, stopAtPath)
// After successfully expiring entries, check if directory is now empty and cleanup
// Only do this on first page (startFileName == "") to avoid partial directory states
// DeleteEmptyParentDirectories has built-in protection against deleting bucket directories
if deletedCount > 0 && !hasValidEntries && p != "/" && startFileName == "" {
glog.V(2).InfofCtx(ctx, "doListDirectoryEntries: deleted %d expired entries from %s, checking for empty directory cleanup", deletedCount, p)
// Determine appropriate stop path based on whether this is an S3 path
var stopAtPath util.FullPath
if strings.HasPrefix(string(p), f.DirBucketsPath+"/") {
// S3 path: stop at the bucket root (e.g., /buckets/mybucket)
pathAfterBuckets := strings.TrimPrefix(string(p), f.DirBucketsPath+"/")
bucketName, _, _ := strings.Cut(pathAfterBuckets, "/")
stopAtPath = util.NewFullPath(f.DirBucketsPath, bucketName)
} else {
// Non-S3 path: allow cleanup up to root
stopAtPath = "/"
}
f.DeleteEmptyParentDirectories(opCtx, p, stopAtPath)
}
}
+1 -1
View File
@@ -176,6 +176,6 @@ func (f *FilerConsumerGroupOffsetStorage) DeleteConsumerGroupOffset(t topic.Topi
offsetFileName := fmt.Sprintf("%s.offset", consumerGroup)
return f.filerClientAccessor.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
return filer_pb.DoRemove(context.Background(), client, consumersDir, offsetFileName, false, false, false, false, nil)
return filer_pb.DoRemove(context.Background(), client, consumersDir, offsetFileName, false, false, false, false, nil, false, "")
})
}
+2
View File
@@ -232,6 +232,8 @@ message DeleteEntryRequest {
bool is_from_other_cluster = 7;
repeated int32 signatures = 8;
int64 if_not_modified_after = 9;
bool delete_empty_parent_directories = 10; // If true, recursively delete empty parent directories
string delete_empty_parent_directories_stop_path = 11; // Stop empty directory cleanup at this path (e.g., "/buckets/mybucket")
}
message DeleteEntryResponse {
File diff suppressed because it is too large Load Diff
+12 -10
View File
@@ -278,19 +278,21 @@ func MkFile(ctx context.Context, filerClient FilerClient, parentDirectoryPath st
func Remove(ctx context.Context, filerClient FilerClient, parentDirectoryPath, name string, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster bool, signatures []int32) error {
return filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
return DoRemove(ctx, client, parentDirectoryPath, name, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster, signatures)
return DoRemove(ctx, client, parentDirectoryPath, name, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster, signatures, false, "")
})
}
func DoRemove(ctx context.Context, client SeaweedFilerClient, parentDirectoryPath string, name string, isDeleteData bool, isRecursive bool, ignoreRecursiveErr bool, isFromOtherCluster bool, signatures []int32) error {
func DoRemove(ctx context.Context, client SeaweedFilerClient, parentDirectoryPath string, name string, isDeleteData bool, isRecursive bool, ignoreRecursiveErr bool, isFromOtherCluster bool, signatures []int32, deleteEmptyParentDirectories bool, stopPath string) error {
deleteEntryRequest := &DeleteEntryRequest{
Directory: parentDirectoryPath,
Name: name,
IsDeleteData: isDeleteData,
IsRecursive: isRecursive,
IgnoreRecursiveError: ignoreRecursiveErr,
IsFromOtherCluster: isFromOtherCluster,
Signatures: signatures,
Directory: parentDirectoryPath,
Name: name,
IsDeleteData: isDeleteData,
IsRecursive: isRecursive,
IgnoreRecursiveError: ignoreRecursiveErr,
IsFromOtherCluster: isFromOtherCluster,
Signatures: signatures,
DeleteEmptyParentDirectories: deleteEmptyParentDirectories,
DeleteEmptyParentDirectoriesStopPath: stopPath,
}
if resp, err := client.DeleteEntry(ctx, deleteEntryRequest); err != nil {
if strings.Contains(err.Error(), ErrNotFound.Error()) {
@@ -356,7 +358,7 @@ func DoDeleteEmptyParentDirectories(ctx context.Context, client SeaweedFilerClie
glog.V(2).InfofCtx(ctx, "DoDeleteEmptyParentDirectories: deleting empty directory %s", dirPath)
parentDir, dirName := dirPath.DirAndName()
if err := DoRemove(ctx, client, parentDir, dirName, false, false, false, false, nil); err == nil {
if err := DoRemove(ctx, client, parentDir, dirName, false, false, false, false, nil, false, ""); err == nil {
// Successfully deleted, continue checking upwards
DoDeleteEmptyParentDirectories(ctx, client, util.FullPath(parentDir), stopAtPath, checked)
} else {
+2 -2
View File
@@ -2,7 +2,7 @@
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// source: filer.proto
// source: weed/pb/filer.proto
package filer_pb
@@ -1047,5 +1047,5 @@ var SeaweedFiler_ServiceDesc = grpc.ServiceDesc{
ServerStreams: true,
},
},
Metadata: "filer.proto",
Metadata: "weed/pb/filer.proto",
}
+18 -20
View File
@@ -131,18 +131,11 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
// This ensures deletion completes atomically to avoid inconsistent state
opCtx := context.WithoutCancel(r.Context())
if err := doDeleteEntry(client, dir, name, true, false); err != nil {
return err
}
// Delete entry with optional empty parent directory cleanup
bucketPath := fmt.Sprintf("%s/%s", s3a.option.BucketsPath, bucket)
deleteEmptyDirs := !s3a.option.AllowEmptyFolder && strings.LastIndex(object, "/") > 0
// Cleanup empty directories
if !s3a.option.AllowEmptyFolder && strings.LastIndex(object, "/") > 0 {
bucketPath := fmt.Sprintf("%s/%s", s3a.option.BucketsPath, bucket)
// Recursively delete empty parent directories, stop at bucket path
filer_pb.DoDeleteEmptyParentDirectories(opCtx, client, util.FullPath(dir), util.FullPath(bucketPath), nil)
}
return nil
return filer_pb.DoRemove(opCtx, client, dir, name, true, false, true, false, nil, deleteEmptyDirs, bucketPath)
})
if err != nil {
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
@@ -222,6 +215,7 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
var deleteErrors []DeleteError
var auditLog *s3err.AccessLog
// Track directories with deletions for batch cleanup optimization
directoriesWithDeletion := make(map[string]bool)
if s3err.Logger != nil {
@@ -346,7 +340,7 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
continue
}
} else {
// Handle non-versioned delete (original logic)
// Handle non-versioned delete (defer cleanup for batch optimization)
lastSeparator := strings.LastIndex(object.Key, "/")
parentDirectoryPath, entryName, isDeleteData, isRecursive := "", object.Key, true, false
if lastSeparator > 0 && lastSeparator+1 < len(object.Key) {
@@ -355,10 +349,11 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
}
parentDirectoryPath = fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, parentDirectoryPath)
err := doDeleteEntry(client, parentDirectoryPath, entryName, isDeleteData, isRecursive)
// Delete file without cleanup (batch cleanup at the end for efficiency)
err := filer_pb.DoRemove(opCtx, client, parentDirectoryPath, entryName, isDeleteData, isRecursive, true, false, nil, false, "")
if err == nil {
// Track directory for empty directory cleanup
if !s3a.option.AllowEmptyFolder {
// Track directory for batch cleanup
if !s3a.option.AllowEmptyFolder && lastSeparator > 0 {
directoriesWithDeletion[parentDirectoryPath] = true
}
deletedObjects = append(deletedObjects, object)
@@ -380,26 +375,29 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
}
}
// Cleanup empty directories - optimize by processing deepest first
// Batch cleanup: Process empty directories after all deletions
// This is much more efficient than checking after each deletion
if !s3a.option.AllowEmptyFolder && len(directoriesWithDeletion) > 0 {
bucketPath := fmt.Sprintf("%s/%s", s3a.option.BucketsPath, bucket)
// Collect and sort directories by depth (deepest first) to avoid redundant checks
// Sort directories by depth (deepest first) to avoid redundant checks
// Deeper directories are more likely to be empty and cleaning them first
// may make their parents empty, reducing total checks needed
var allDirs []string
for dirPath := range directoriesWithDeletion {
allDirs = append(allDirs, dirPath)
}
// Sort by depth (deeper directories first)
slices.SortFunc(allDirs, func(a, b string) int {
return strings.Count(b, "/") - strings.Count(a, "/")
})
// Track already-checked directories to avoid redundant work
// When we check a directory and recursively clean parents,
// mark them all as checked so we skip them in subsequent iterations
checked := make(map[string]bool)
for _, dirPath := range allDirs {
if !checked[dirPath] {
// Recursively delete empty parent directories, stop at bucket path
// Mark this directory and all its parents as checked during recursion
// Use server-side cleanup for consistency
filer_pb.DoDeleteEmptyParentDirectories(opCtx, client, util.FullPath(dirPath), util.FullPath(bucketPath), checked)
}
}
+22 -2
View File
@@ -293,9 +293,29 @@ func (fs *FilerServer) DeleteEntry(ctx context.Context, req *filer_pb.DeleteEntr
err = fs.filer.DeleteEntryMetaAndData(ctx, util.JoinPath(req.Directory, req.Name), req.IsRecursive, req.IgnoreRecursiveError, req.IsDeleteData, req.IsFromOtherCluster, req.Signatures, req.IfNotModifiedAfter)
resp = &filer_pb.DeleteEntryResponse{}
if err != nil && err != filer_pb.ErrNotFound {
resp.Error = err.Error()
if err != nil {
if err != filer_pb.ErrNotFound {
resp.Error = err.Error()
}
// Return early: either a real error or entry not found (nothing deleted, so no cleanup needed)
return resp, nil
}
// Optional cleanup of empty parent directories (only if deletion succeeded)
if req.DeleteEmptyParentDirectories {
stopAtPath := util.FullPath(req.DeleteEmptyParentDirectoriesStopPath)
if stopAtPath == "" {
// Default to root to allow cleanup for non-S3 paths
// S3 API clients provide a specific bucket stop path
stopAtPath = "/"
}
// Use non-cancellable context to ensure cleanup completes atomically
// even if the client cancels the request after deletion succeeds
opCtx := context.WithoutCancel(ctx)
fs.filer.DeleteEmptyParentDirectories(opCtx, util.FullPath(req.Directory), stopAtPath)
}
return resp, nil
}
+1 -1
View File
@@ -100,7 +100,7 @@ func (c *commandRemoteUnmount) purgeMountedData(commandEnv *CommandEnv, dir stri
oldEntry := lookupResp.Entry
deleteError := filer_pb.DoRemove(ctx, client, parent, name, true, true, true, false, nil)
deleteError := filer_pb.DoRemove(ctx, client, parent, name, true, true, true, false, nil, false, "")
if deleteError != nil {
return fmt.Errorf("delete %s: %v", dir, deleteError)
}