mirror of
https://github.com/pgsty/minio.git
synced 2026-11-09 17:21:51 +03:00
73025824a3
The xl.meta rename at the end of RenameData creates the object directory the same way the data rename does, and its failure branch left that directory behind for writes without a data directory, such as inline and zero-byte objects. Remove it and its empty parents there too, with the same trailing-slash call that PR #286 adds to the data branch for issue #285. Making the cleanup safe against concurrent writes needs two more changes: - deleteFile now walks up with a trailing slash, so each parent step removes only an empty directory. Between two steps of a walk, another walk can remove the rest of the chain and a writer can commit an object whose xl.meta sits at the path the first walk removes next, for example the object "parent" under the key "parent/xl.meta/child". The walk used to delete that file. Ordinary deletes could already do this; the new cleanup made it reachable with failed writes. - The two renames of RenameData retry when the directory creation or the rename reports "not found" while the source is still in place, up to once per path level. A late, failed RenameData can now remove the directory another write of the same key has just created, and that write used to fail on the drive although its data was intact. The retry decides on the error of the attempt itself, because another write can recreate the directories in between. renameAll's error mapping moves unchanged into renameAllErr so that the retry can see the raw error. The stagedDataGoneDisks test helper now swaps the drives behind the set's disk list under the lock that the list takes, instead of replacing the getDisks callback that background tasks read unlocked. Tests: - TestXLStorageRenameDataCommitMetaMissingRemovesEmptyDirs: zero-byte and inline commit failures leave no empty directories, a sibling and a moved data directory stay. - TestXLStorageWalkUpKeepsCommittedObject: a committed object at a parent path survives the walk, through DeleteVersion and both cleanup branches. - TestRenameDataSurvivesConcurrentEmptyDirCleanup: PutObject (large and inline) and CompleteMultipartUpload succeed when a late failed RenameData of the same key removes their fresh directory on all 16 drives. - TestXLStorageRenameDataRetriesWhileCleanupWalksUp: a write survives a cleanup that removes each level of its path in turn, also when a nested-key write recreates the directories in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Feng Ruohang <rh@vonng.com>
199 lines
6.0 KiB
Go
199 lines
6.0 KiB
Go
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
)
|
|
|
|
// Wrapper functions to os.RemoveAll, which calls reliableRemoveAll
|
|
// this is to ensure that if there is a racy parent directory
|
|
// create in between we can simply retry the operation.
|
|
func removeAll(dirPath string) (err error) {
|
|
if dirPath == "" {
|
|
return errInvalidArgument
|
|
}
|
|
|
|
if err = checkPathLength(dirPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = reliableRemoveAll(dirPath); err != nil {
|
|
switch {
|
|
case isSysErrNotDir(err):
|
|
// File path cannot be verified since one of
|
|
// the parents is a file.
|
|
return errFileAccessDenied
|
|
case isSysErrPathNotFound(err):
|
|
// This is a special case should be handled only for
|
|
// windows, because windows API does not return "not a
|
|
// directory" error message. Handle this specifically
|
|
// here.
|
|
return errFileAccessDenied
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Reliably retries os.RemoveAll if for some reason os.RemoveAll returns
|
|
// syscall.ENOTEMPTY (children has files).
|
|
func reliableRemoveAll(dirPath string) (err error) {
|
|
i := 0
|
|
for {
|
|
// Removes all the directories and files.
|
|
if err = RemoveAll(dirPath); err != nil {
|
|
// Retry only for the first retryable error.
|
|
if isSysErrNotEmpty(err) && i == 0 {
|
|
i++
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Wrapper functions to os.MkdirAll, which calls reliableMkdirAll
|
|
// this is to ensure that if there is a racy parent directory
|
|
// delete in between we can simply retry the operation.
|
|
func mkdirAll(dirPath string, mode os.FileMode, baseDir string) (err error) {
|
|
if dirPath == "" {
|
|
return errInvalidArgument
|
|
}
|
|
|
|
if err = checkPathLength(dirPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = reliableMkdirAll(dirPath, mode, baseDir); err != nil {
|
|
// File path cannot be verified since one of the parents is a file.
|
|
if isSysErrNotDir(err) {
|
|
return errFileAccessDenied
|
|
} else if isSysErrPathNotFound(err) {
|
|
// This is a special case should be handled only for
|
|
// windows, because windows API does not return "not a
|
|
// directory" error message. Handle this specifically here.
|
|
return errFileAccessDenied
|
|
}
|
|
return osErrToFileErr(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Reliably retries os.MkdirAll if for some reason os.MkdirAll returns
|
|
// syscall.ENOENT (parent does not exist).
|
|
func reliableMkdirAll(dirPath string, mode os.FileMode, baseDir string) (err error) {
|
|
i := 0
|
|
for {
|
|
// Creates all the parent directories, with mode 0777 mkdir honors system umask.
|
|
if err = osMkdirAll(dirPath, mode, baseDir); err != nil {
|
|
// Retry only for the first retryable error.
|
|
if osIsNotExist(err) && i == 0 {
|
|
i++
|
|
// Determine if os.NotExist error is because of
|
|
// baseDir's parent being present, retry it once such
|
|
// that the MkdirAll is retried once for the parent
|
|
// of dirPath.
|
|
// Because it is worth a retry to skip a different
|
|
// baseDir which is slightly higher up the depth.
|
|
nbaseDir := path.Dir(baseDir)
|
|
if baseDir != "" && nbaseDir != "" && nbaseDir != SlashSeparator {
|
|
baseDir = nbaseDir
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Wrapper function to os.Rename, which calls reliableMkdirAll
|
|
// and reliableRenameAll. This is to ensure that if there is a
|
|
// racy parent directory delete in between we can simply retry
|
|
// the operation.
|
|
func renameAll(srcFilePath, dstFilePath, baseDir string) (err error) {
|
|
if srcFilePath == "" || dstFilePath == "" {
|
|
return errInvalidArgument
|
|
}
|
|
|
|
if err = checkPathLength(srcFilePath); err != nil {
|
|
return err
|
|
}
|
|
if err = checkPathLength(dstFilePath); err != nil {
|
|
return err
|
|
}
|
|
|
|
return renameAllErr(reliableRename(srcFilePath, dstFilePath, baseDir), srcFilePath, dstFilePath)
|
|
}
|
|
|
|
// renameAllErr converts an error of reliableRename into the error that
|
|
// renameAll returns.
|
|
func renameAllErr(err error, srcFilePath, dstFilePath string) error {
|
|
switch {
|
|
case err == nil:
|
|
return nil
|
|
case isSysErrNotDir(err) && !osIsNotExist(err):
|
|
// Windows can have both isSysErrNotDir(err) and osIsNotExist(err) returning
|
|
// true if the source file path contains an non-existent directory. In that case,
|
|
// we want to return errFileNotFound instead, which will honored in subsequent
|
|
// switch cases
|
|
return errFileAccessDenied
|
|
case isSysErrPathNotFound(err):
|
|
// This is a special case should be handled only for
|
|
// windows, because windows API does not return "not a
|
|
// directory" error message. Handle this specifically here.
|
|
return errFileAccessDenied
|
|
case isSysErrCrossDevice(err):
|
|
return fmt.Errorf("%w (%s)->(%s)", errCrossDeviceLink, srcFilePath, dstFilePath)
|
|
case osIsNotExist(err):
|
|
return errFileNotFound
|
|
case osIsExist(err):
|
|
// This is returned only when destination is a directory and we
|
|
// are attempting a rename from file to directory.
|
|
return errIsNotRegular
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Reliably retries os.RenameAll if for some reason os.RenameAll returns
|
|
// syscall.ENOENT (parent does not exist).
|
|
func reliableRename(srcFilePath, dstFilePath, baseDir string) (err error) {
|
|
if err = reliableMkdirAll(path.Dir(dstFilePath), 0o777, baseDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
i := 0
|
|
for {
|
|
// After a successful parent directory create attempt a renameAll.
|
|
if err = Rename(srcFilePath, dstFilePath); err != nil {
|
|
// Retry only for the first retryable error.
|
|
if osIsNotExist(err) && i == 0 {
|
|
i++
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
return err
|
|
}
|