Skip to content

Commit c349505

Browse files
cuonglmianlancetaylor
authored andcommitted
os: fix RemoveAll hangs on large directory
golang.org/cl/121255 added close and re-open the directory when looping, prevent us from missing some if previous iteration deleted files. The CL introdued a bug. If we can not delete all entries in one request, the looping never exits, causing RemoveAll hangs. To fix that, simply discard the entries if we can not delete all of them in one iteration, then continue reading entries and delete them. Also make sure removeall_at return first error it encounters. Fixes #29921 Change-Id: I8ec3a4c822d8d2d95d9f1ab71547879da395bc4a Reviewed-on: https://go-review.googlesource.com/c/go/+/171099 Run-TryBot: Ian Lance Taylor <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent a6af104 commit c349505

File tree

5 files changed

+110
-28
lines changed

5 files changed

+110
-28
lines changed

src/os/export_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ package os
99
var Atime = atime
1010
var LstatP = &lstat
1111
var ErrWriteAtInAppendMode = errWriteAtInAppendMode
12+
var RemoveAllTestHook = &removeAllTestHook

src/os/path.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ func MkdirAll(path string, perm FileMode) error {
5858
return nil
5959
}
6060

61+
// removeAllTestHook is a hook for testing.
62+
var removeAllTestHook = func(err error) error { return err }
63+
6164
// RemoveAll removes path and any children it contains.
6265
// It removes everything it can but returns the first error
6366
// it encounters. If the path does not exist, RemoveAll

src/os/removeall_at.go

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ func removeAllFrom(parent *File, base string) error {
9191
// Remove the directory's entries.
9292
var recurseErr error
9393
for {
94-
const request = 1024
94+
const reqSize = 1024
95+
var respSize int
9596

9697
// Open the directory to recurse into
9798
file, err := openFdAt(parentFd, base)
@@ -103,23 +104,37 @@ func removeAllFrom(parent *File, base string) error {
103104
break
104105
}
105106

106-
names, readErr := file.Readdirnames(request)
107-
// Errors other than EOF should stop us from continuing.
108-
if readErr != nil && readErr != io.EOF {
109-
file.Close()
110-
if IsNotExist(readErr) {
111-
return nil
107+
for {
108+
numErr := 0
109+
110+
names, readErr := file.Readdirnames(reqSize)
111+
// Errors other than EOF should stop us from continuing.
112+
if readErr != nil && readErr != io.EOF {
113+
file.Close()
114+
if IsNotExist(readErr) {
115+
return nil
116+
}
117+
return &PathError{"readdirnames", base, readErr}
112118
}
113-
return &PathError{"readdirnames", base, readErr}
114-
}
115119

116-
for _, name := range names {
117-
err := removeAllFrom(file, name)
118-
if err != nil {
119-
if pathErr, ok := err.(*PathError); ok {
120-
pathErr.Path = base + string(PathSeparator) + pathErr.Path
120+
respSize = len(names)
121+
for _, name := range names {
122+
err := removeAllFrom(file, name)
123+
if err != nil {
124+
if pathErr, ok := err.(*PathError); ok {
125+
pathErr.Path = base + string(PathSeparator) + pathErr.Path
126+
}
127+
numErr++
128+
if recurseErr == nil {
129+
recurseErr = err
130+
}
121131
}
122-
recurseErr = err
132+
}
133+
134+
// If we can delete any entry, break to start new iteration.
135+
// Otherwise, we discard current names, get next entries and try deleting them.
136+
if numErr != reqSize {
137+
break
123138
}
124139
}
125140

@@ -131,13 +146,14 @@ func removeAllFrom(parent *File, base string) error {
131146
file.Close()
132147

133148
// Finish when the end of the directory is reached
134-
if len(names) < request {
149+
if respSize < reqSize {
135150
break
136151
}
137152
}
138153

139154
// Remove the directory itself.
140155
unlinkError := unix.Unlinkat(parentFd, base, unix.AT_REMOVEDIR)
156+
unlinkError = removeAllTestHook(unlinkError)
141157
if unlinkError == nil || IsNotExist(unlinkError) {
142158
return nil
143159
}

src/os/removeall_noat.go

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,30 @@ func removeAll(path string) error {
5656
return err
5757
}
5858

59-
const request = 1024
60-
names, err1 := fd.Readdirnames(request)
59+
const reqSize = 1024
60+
var names []string
61+
var readErr error
62+
63+
for {
64+
numErr := 0
65+
names, readErr = fd.Readdirnames(reqSize)
66+
67+
for _, name := range names {
68+
err1 := RemoveAll(path + string(PathSeparator) + name)
69+
if err == nil {
70+
err = err1
71+
}
72+
if err1 != nil {
73+
numErr++
74+
}
75+
}
76+
77+
// If we can delete any entry, break to start new iteration.
78+
// Otherwise, we discard current names, get next entries and try deleting them.
79+
if numErr != reqSize {
80+
break
81+
}
82+
}
6183

6284
// Removing files from the directory may have caused
6385
// the OS to reshuffle it. Simply calling Readdirnames
@@ -66,19 +88,12 @@ func removeAll(path string) error {
6688
// directory. See issue 20841.
6789
fd.Close()
6890

69-
for _, name := range names {
70-
err1 := RemoveAll(path + string(PathSeparator) + name)
71-
if err == nil {
72-
err = err1
73-
}
74-
}
75-
76-
if err1 == io.EOF {
91+
if readErr == io.EOF {
7792
break
7893
}
7994
// If Readdirnames returned an error, use it.
8095
if err == nil {
81-
err = err1
96+
err = readErr
8297
}
8398
if len(names) == 0 {
8499
break
@@ -88,7 +103,7 @@ func removeAll(path string) error {
88103
// got fewer than request names from Readdirnames, try
89104
// simply removing the directory now. If that
90105
// succeeds, we are done.
91-
if len(names) < request {
106+
if len(names) < reqSize {
92107
err1 := Remove(path)
93108
if err1 == nil || IsNotExist(err1) {
94109
return nil
@@ -109,6 +124,7 @@ func removeAll(path string) error {
109124

110125
// Remove directory.
111126
err1 := Remove(path)
127+
err1 = removeAllTestHook(err1)
112128
if err1 == nil || IsNotExist(err1) {
113129
return nil
114130
}

src/os/removeall_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package os_test
66

77
import (
8+
"errors"
89
"fmt"
910
"io/ioutil"
1011
. "os"
@@ -405,3 +406,48 @@ func TestRemoveUnreadableDir(t *testing.T) {
405406
t.Fatal(err)
406407
}
407408
}
409+
410+
// Issue 29921
411+
func TestRemoveAllWithMoreErrorThanReqSize(t *testing.T) {
412+
if testing.Short() {
413+
t.Skip("skipping in short mode")
414+
}
415+
oldRemoveAllTestHook := RemoveAllTestHook
416+
*RemoveAllTestHook = func(err error) error {
417+
return errors.New("error from RemoveAllTestHook")
418+
}
419+
defer func() {
420+
*RemoveAllTestHook = *oldRemoveAllTestHook
421+
}()
422+
423+
tmpDir, err := ioutil.TempDir("", "TestRemoveAll-")
424+
if err != nil {
425+
t.Fatal(err)
426+
}
427+
defer RemoveAll(tmpDir)
428+
429+
path := filepath.Join(tmpDir, "_TestRemoveAllWithMoreErrorThanReqSize_")
430+
431+
// Make directory with 1025 files and remove.
432+
if err := MkdirAll(path, 0777); err != nil {
433+
t.Fatalf("MkdirAll %q: %s", path, err)
434+
}
435+
for i := 0; i < 1025; i++ {
436+
fpath := filepath.Join(path, fmt.Sprintf("file%d", i))
437+
fd, err := Create(fpath)
438+
if err != nil {
439+
t.Fatalf("create %q: %s", fpath, err)
440+
}
441+
fd.Close()
442+
}
443+
444+
// This call should not hang
445+
if err := RemoveAll(path); err == nil {
446+
t.Fatal("Want error from RemoveAllTestHook, got nil")
447+
}
448+
449+
// We hook to inject error, but the actual files must be deleted
450+
if _, err := Lstat(path); err == nil {
451+
t.Fatal("directory must be deleted even with removeAllTetHook run")
452+
}
453+
}

0 commit comments

Comments
 (0)