operations: fix hang when deleting files and a fatal error occurs

When a fatal error such as --max-delete being reached stopped the
deletions, the deleters returned without receiving the rest of the
objects, so whatever was sending them blocked forever once the
channel filled up.

For example "rclone delete --max-delete 1" on a directory with more
files than --checkers hung instead of returning the error.

The deleters now keep receiving objects without deleting them after a
fatal error.
This commit is contained in:
Nick Craig-Wood committed 2026-09-20 11:14:24 +01:00
1 parent db5beead16
commit 2db37b14c2
2 files changed
+32 -3

No files matched your search

+6 -3
View File
@@ -608,6 +608,10 @@ func DeleteFilesWithBackupDir(ctx context.Context, toBeDeleted fs.ObjectsChan, b
go func() {
defer wg.Done()
for dst := range toBeDeleted {
// Empty the channel on fatal error
if fatalErrorCount.Load() != 0 {
continue
}
err := DeleteFileWithBackupDir(ctx, dst, backupDir)
if err != nil {
errorCount.Add(1)
@@ -616,7 +620,6 @@ func DeleteFilesWithBackupDir(ctx context.Context, toBeDeleted fs.ObjectsChan, b
if fserrors.IsFatalError(err) {
fs.Errorf(dst, "Got fatal error on delete: %s", err)
fatalErrorCount.Add(1)
return
}
}
}
@@ -1593,7 +1596,7 @@ func Rmdirs(ctx context.Context, f fs.Fs, dir string, leaveRoot bool) error {
}
fs.Debugf(nil, "removing %d level %d directories", len(dirs), level)
sort.Strings(dirs)
g, gCtx := errgroup.WithContext(ctx)
g, gCtx := errgroup.WithContext(context.Background())
g.SetLimit(ci.Checkers)
for _, dir := range dirs {
// End early if error
@@ -2494,7 +2497,7 @@ func DirMove(ctx context.Context, f fs.Fs, srcRemote, dstRemote string) (err err
newPath string
}
renames := make(chan rename, ci.Checkers)
g, gCtx := errgroup.WithContext(context.Background())
g, gCtx := errgroup.WithContext(ctx)
for range ci.Checkers {
g.Go(func() error {
for job := range renames {
+26
View File
@@ -427,6 +427,32 @@ func TestDelete(t *testing.T) {
r.CheckRemoteItems(t, file3)
}
// Check Delete doesn't hang when a fatal error stops the deletions
// before all the objects have been sent to the deleters
func TestDeleteFatalError(t *testing.T) {
ctx := context.Background()
ctx, ci := fs.AddConfig(ctx)
ci.Checkers = 2
ci.MaxDelete = 1
r := fstest.NewRun(t)
// More files than the deleters' channel can hold
for i := range 20 {
r.WriteObject(ctx, fmt.Sprintf("file%d", i), "x", t1)
}
done := make(chan error, 1)
go func() {
done <- operations.Delete(ctx, r.Fremote)
}()
select {
case err := <-done:
require.Error(t, err)
assert.True(t, fserrors.IsFatalError(err), err)
case <-time.After(30 * time.Second):
t.Fatal("Delete didn't return - deadlocked sending to the deleters")
}
}
func isChunker(f fs.Fs) bool {
return strings.HasPrefix(f.Name(), "TestChunker")
}