Compare commits

...

1 Commits

Author SHA1 Message Date
Feng Ruohang 0f9199e0c3 fix(multipart): remove empty upload parents promptly
Signed-off-by: Feng Ruohang <rh@vonng.com>
2026-10-18 08:21:35 +08:00
2 changed files with 67 additions and 16 deletions
+60 -3
View File
@@ -126,8 +126,8 @@ func (d *multipartCleanupDisk) ReadVersion(context.Context, string, string, stri
return FileInfo{ModTime: d.modTime}, d.readErr return FileInfo{ModTime: d.modTime}, d.readErr
} }
func (d *multipartCleanupDisk) StatVol(context.Context, string) (VolInfo, error) { func (d *multipartCleanupDisk) StatVol(ctx context.Context, volume string) (VolInfo, error) {
return VolInfo{Created: time.Now()}, nil return (&xlStorage{drivePath: d.root}).StatVol(ctx, volume)
} }
func TestCleanupStaleUploadsOnDisk(t *testing.T) { func TestCleanupStaleUploadsOnDisk(t *testing.T) {
@@ -157,7 +157,13 @@ func TestCleanupStaleUploadsOnDisk(t *testing.T) {
} }
disk := &multipartCleanupDisk{root: t.TempDir(), modTime: modTime, readErr: tc.readErr} disk := &multipartCleanupDisk{root: t.TempDir(), modTime: modTime, readErr: tc.readErr}
uploadDir := filepath.Join(disk.root, minioMetaMultipartBucket, "sha", id) uploadDir := filepath.Join(disk.root, minioMetaMultipartBucket, "sha", id)
if err := os.MkdirAll(uploadDir, 0o755); err != nil { for _, dir := range []string{uploadDir, filepath.Join(disk.root, minioMetaTmpDeletedBucket)} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
}
// An old parent must not bypass a skipped upload's age or read error.
if err := os.Chtimes(filepath.Dir(uploadDir), modTime, modTime); err != nil {
t.Fatal(err) t.Fatal(err)
} }
erasureObjects{}.cleanupStaleUploadsOnDisk(t.Context(), disk) erasureObjects{}.cleanupStaleUploadsOnDisk(t.Context(), disk)
@@ -169,9 +175,60 @@ func TestCleanupStaleUploadsOnDisk(t *testing.T) {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
t.Errorf("expired upload must be removed: %v", err) t.Errorf("expired upload must be removed: %v", err)
} }
if _, err := os.Stat(filepath.Dir(uploadDir)); !os.IsNotExist(err) {
t.Errorf("empty hash directory must be removed in the same pass: %v", err)
}
} else if err != nil { } else if err != nil {
t.Errorf("upload must be preserved: %v", err) t.Errorf("upload must be preserved: %v", err)
} }
}) })
} }
} }
func TestCleanupStaleUploadsEmptyHashDirectory(t *testing.T) {
for _, age := range []time.Duration{0, 2 * globalAPIConfig.getStaleUploadsExpiry()} {
t.Run(age.String(), func(t *testing.T) {
disk := &multipartCleanupDisk{root: t.TempDir()}
shaDir := filepath.Join(disk.root, minioMetaMultipartBucket, "sha")
for _, dir := range []string{shaDir, filepath.Join(disk.root, minioMetaTmpDeletedBucket)} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
}
modTime := time.Now().Add(-age)
if err := os.Chtimes(shaDir, modTime, modTime); err != nil {
t.Fatal(err)
}
erasureObjects{}.cleanupStaleUploadsOnDisk(t.Context(), disk)
if _, err := os.Stat(shaDir); !os.IsNotExist(err) {
t.Fatalf("empty hash directory must be removed regardless of age: %v", err)
}
})
}
}
func TestCleanupStaleUploadsSharedHashDirectory(t *testing.T) {
disk := &multipartCleanupDisk{root: t.TempDir(), readErr: errFileNotFound}
shaDir := filepath.Join(disk.root, minioMetaMultipartBucket, "sha")
now := time.Now()
expiredID := fmt.Sprintf("d16ad42a-6fe5-4a3b-8092-69983142238fx%d", now.Add(-2*globalAPIConfig.getStaleUploadsExpiry()).UnixNano())
recentID := fmt.Sprintf("4eacd477-e733-4851-b821-6fd42785ff87x%d", now.UnixNano())
for _, id := range []string{expiredID, recentID} {
if err := os.MkdirAll(filepath.Join(shaDir, id), 0o755); err != nil {
t.Fatal(err)
}
}
partPath := filepath.Join(shaDir, recentID, "part.1")
const part = "recent upload part"
if err := os.WriteFile(partPath, []byte(part), 0o600); err != nil {
t.Fatal(err)
}
erasureObjects{}.cleanupStaleUploadsOnDisk(t.Context(), disk)
if _, err := os.Stat(filepath.Join(shaDir, expiredID)); !os.IsNotExist(err) {
t.Errorf("expired upload must be removed: %v", err)
}
if got, err := os.ReadFile(partPath); err != nil || string(got) != part {
t.Errorf("recent upload must remain intact: got %q, err %v", got, err)
}
}
+7 -13
View File
@@ -197,6 +197,7 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
drivePath := disk.Endpoint().Path drivePath := disk.Endpoint().Path
readDirFn(pathJoin(drivePath, minioMetaMultipartBucket), func(shaDir string, typ os.FileMode) error { readDirFn(pathJoin(drivePath, minioMetaMultipartBucket), func(shaDir string, typ os.FileMode) error {
hasSkippedUploads := false
readDirFn(pathJoin(drivePath, minioMetaMultipartBucket, shaDir), func(uploadIDDir string, typ os.FileMode) error { readDirFn(pathJoin(drivePath, minioMetaMultipartBucket, shaDir), func(uploadIDDir string, typ os.FileMode) error {
uploadIDPath := pathJoin(shaDir, uploadIDDir) uploadIDPath := pathJoin(shaDir, uploadIDDir)
modTime := extractModTimeFromUploadIDDir(uploadIDDir) modTime := extractModTimeFromUploadIDDir(uploadIDDir)
@@ -205,12 +206,14 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
wait := deleteMultipartCleanupSleeper.Timer(ctx) wait := deleteMultipartCleanupSleeper.Timer(ctx)
fi, err := disk.ReadVersion(ctx, "", minioMetaMultipartBucket, uploadIDPath, "", ReadOptions{}) fi, err := disk.ReadVersion(ctx, "", minioMetaMultipartBucket, uploadIDPath, "", ReadOptions{})
if err != nil { if err != nil {
hasSkippedUploads = true
return nil return nil
} }
modTime = fi.ModTime modTime = fi.ModTime
wait() wait()
} }
if time.Since(modTime) < globalAPIConfig.getStaleUploadsExpiry() { if time.Since(modTime) < globalAPIConfig.getStaleUploadsExpiry() {
hasSkippedUploads = true
return nil return nil
} }
w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout()) w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
@@ -223,24 +226,15 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
return nil return nil
}) })
}) })
// Get the modtime of the shaDir. if hasSkippedUploads {
vi, err := disk.StatVol(ctx, pathJoin(minioMetaMultipartBucket, shaDir))
if err != nil {
return nil
}
// Modtime is returned in the Created field. See (*xlStorage).StatVol
if time.Since(vi.Created) < globalAPIConfig.getStaleUploadsExpiry() {
return nil return nil
} }
w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout()) w := xioutil.NewDeadlineWorker(globalDriveConfig.GetMaxTimeout())
return w.Run(func() error { return w.Run(func() error {
wait := deleteMultipartCleanupSleeper.Timer(ctx) wait := deleteMultipartCleanupSleeper.Timer(ctx)
pathUUID := mustGetUUID() // Removing an upload updates the parent's mtime. Remove the parent
targetPath := pathJoin(drivePath, minioMetaTmpDeletedBucket, pathUUID) // immediately if it is empty; other uploads keep it in place.
_ = Remove(pathJoin(drivePath, minioMetaMultipartBucket, shaDir))
// We are not deleting shaDir recursively here, if shaDir is empty
// and its older then we can happily delete it.
Rename(pathJoin(drivePath, minioMetaMultipartBucket, shaDir), targetPath)
wait() wait()
return nil return nil
}) })