HADOOP-14769. WASB: delete recursive should not fail if a file is deleted.
Contributed by Thomas Marquardt
This commit is contained in:
parent
99e558b13b
commit
c6b4e656b7
|
@ -2459,8 +2459,11 @@ public class AzureNativeFileSystemStore implements NativeFileSystemStore {
|
||||||
try {
|
try {
|
||||||
blob.delete(operationContext, lease);
|
blob.delete(operationContext, lease);
|
||||||
} catch (StorageException e) {
|
} catch (StorageException e) {
|
||||||
LOG.error("Encountered Storage Exception for delete on Blob: {}, Exception Details: {} Error Code: {}",
|
if (!NativeAzureFileSystemHelper.isFileNotFoundException(e)) {
|
||||||
blob.getUri(), e.getMessage(), e.getErrorCode());
|
LOG.error("Encountered Storage Exception for delete on Blob: {}"
|
||||||
|
+ ", Exception Details: {} Error Code: {}",
|
||||||
|
blob.getUri(), e.getMessage(), e.getErrorCode());
|
||||||
|
}
|
||||||
// On exception, check that if:
|
// On exception, check that if:
|
||||||
// 1. It's a BlobNotFound exception AND
|
// 1. It's a BlobNotFound exception AND
|
||||||
// 2. It got there after one-or-more retries THEN
|
// 2. It got there after one-or-more retries THEN
|
||||||
|
@ -2491,17 +2494,17 @@ public class AzureNativeFileSystemStore implements NativeFileSystemStore {
|
||||||
// Container doesn't exist, no need to do anything
|
// Container doesn't exist, no need to do anything
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the blob reference and delete it.
|
// Get the blob reference and delete it.
|
||||||
CloudBlobWrapper blob = getBlobReference(key);
|
CloudBlobWrapper blob = getBlobReference(key);
|
||||||
if (blob.exists(getInstrumentedContext())) {
|
safeDelete(blob, lease);
|
||||||
safeDelete(blob, lease);
|
return true;
|
||||||
return true;
|
} catch (Exception e) {
|
||||||
} else {
|
if (e instanceof StorageException
|
||||||
|
&& NativeAzureFileSystemHelper.isFileNotFoundException(
|
||||||
|
(StorageException) e)) {
|
||||||
|
// the file or directory does not exist
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
// Re-throw as an Azure storage exception.
|
|
||||||
throw new AzureException(e);
|
throw new AzureException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2043,7 +2043,12 @@ public class NativeAzureFileSystem extends FileSystem {
|
||||||
AzureFileSystemThreadTask task = new AzureFileSystemThreadTask() {
|
AzureFileSystemThreadTask task = new AzureFileSystemThreadTask() {
|
||||||
@Override
|
@Override
|
||||||
public boolean execute(FileMetadata file) throws IOException{
|
public boolean execute(FileMetadata file) throws IOException{
|
||||||
return deleteFile(file.getKey(), file.isDir());
|
if (!deleteFile(file.getKey(), file.isDir())) {
|
||||||
|
LOG.warn("Attempt to delete non-existent {} {}",
|
||||||
|
file.isDir() ? "directory" : "file",
|
||||||
|
file.getKey());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2080,30 +2085,28 @@ public class NativeAzureFileSystem extends FileSystem {
|
||||||
return new AzureFileSystemThreadPoolExecutor(threadCount, threadNamePrefix, operation, key, config);
|
return new AzureFileSystemThreadPoolExecutor(threadCount, threadNamePrefix, operation, key, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete single file / directory from key.
|
/**
|
||||||
|
* Delete the specified file or directory and increment metrics.
|
||||||
|
* If the file or directory does not exist, the operation returns false.
|
||||||
|
* @param path the path to a file or directory.
|
||||||
|
* @param isDir true if the path is a directory; otherwise false.
|
||||||
|
* @return true if delete is successful; otherwise false.
|
||||||
|
* @throws IOException if an IO error occurs while attempting to delete the
|
||||||
|
* path.
|
||||||
|
*
|
||||||
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
boolean deleteFile(String key, boolean isDir) throws IOException {
|
boolean deleteFile(String path, boolean isDir) throws IOException {
|
||||||
try {
|
if (!store.delete(path)) {
|
||||||
if (store.delete(key)) {
|
return false;
|
||||||
if (isDir) {
|
|
||||||
instrumentation.directoryDeleted();
|
|
||||||
} else {
|
|
||||||
instrumentation.fileDeleted();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch(IOException e) {
|
|
||||||
Throwable innerException = NativeAzureFileSystemHelper.checkForAzureStorageException(e);
|
|
||||||
|
|
||||||
if (innerException instanceof StorageException
|
|
||||||
&& NativeAzureFileSystemHelper.isFileNotFoundException((StorageException) innerException)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDir) {
|
||||||
|
instrumentation.directoryDeleted();
|
||||||
|
} else {
|
||||||
|
instrumentation.fileDeleted();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -39,6 +39,8 @@ import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.ExpectedException;
|
import org.junit.rules.ExpectedException;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
|
import org.mockito.stubbing.Answer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests the Native Azure file system (WASB) using parallel threads for rename and delete operations.
|
* Tests the Native Azure file system (WASB) using parallel threads for rename and delete operations.
|
||||||
|
@ -529,30 +531,65 @@ public class TestFileSystemOperationsWithThreads extends AbstractWasbTestBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Test case for delete operation with multiple threads and flat listing enabled.
|
* Validate that when a directory is deleted recursively, the operation succeeds
|
||||||
|
* even if a child directory delete fails because the directory does not exist.
|
||||||
|
* This can happen if a child directory is deleted by an external agent while
|
||||||
|
* the parent is in progress of being deleted recursively.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testDeleteSingleDeleteFailure() throws Exception {
|
public void testRecursiveDirectoryDeleteWhenChildDirectoryDeleted()
|
||||||
|
throws Exception {
|
||||||
|
testRecusiveDirectoryDelete(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Validate that when a directory is deleted recursively, the operation succeeds
|
||||||
|
* even if a file delete fails because it does not exist.
|
||||||
|
* This can happen if a file is deleted by an external agent while
|
||||||
|
* the parent directory is in progress of being deleted.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testRecursiveDirectoryDeleteWhenDeletingChildFileReturnsFalse()
|
||||||
|
throws Exception {
|
||||||
|
testRecusiveDirectoryDelete(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testRecusiveDirectoryDelete(boolean useDir) throws Exception {
|
||||||
|
String childPathToBeDeletedByExternalAgent = (useDir)
|
||||||
|
? "root/0"
|
||||||
|
: "root/0/fileToRename";
|
||||||
// Spy azure file system object and return false for deleting one file
|
// Spy azure file system object and return false for deleting one file
|
||||||
LOG.info("testDeleteSingleDeleteFailure");
|
|
||||||
NativeAzureFileSystem mockFs = Mockito.spy((NativeAzureFileSystem) fs);
|
NativeAzureFileSystem mockFs = Mockito.spy((NativeAzureFileSystem) fs);
|
||||||
String path = mockFs.pathToKey(mockFs.makeAbsolute(new Path("root/0")));
|
String path = mockFs.pathToKey(mockFs.makeAbsolute(new Path(
|
||||||
Mockito.when(mockFs.deleteFile(path, true)).thenReturn(false);
|
childPathToBeDeletedByExternalAgent)));
|
||||||
|
|
||||||
|
Answer<Boolean> answer = new Answer<Boolean>() {
|
||||||
|
public Boolean answer(InvocationOnMock invocation) throws Throwable {
|
||||||
|
String path = (String) invocation.getArguments()[0];
|
||||||
|
boolean isDir = (boolean) invocation.getArguments()[1];
|
||||||
|
boolean realResult = fs.deleteFile(path, isDir);
|
||||||
|
assertTrue(realResult);
|
||||||
|
boolean fakeResult = false;
|
||||||
|
return fakeResult;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Mockito.when(mockFs.deleteFile(path, useDir)).thenAnswer(answer);
|
||||||
|
|
||||||
createFolder(mockFs, "root");
|
createFolder(mockFs, "root");
|
||||||
Path sourceFolder = new Path("root");
|
Path sourceFolder = new Path("root");
|
||||||
assertFalse(mockFs.delete(sourceFolder, true));
|
|
||||||
assertTrue(mockFs.exists(sourceFolder));
|
|
||||||
|
|
||||||
// Validate from logs that threads are enabled and delete operation failed.
|
assertTrue(mockFs.delete(sourceFolder, true));
|
||||||
|
assertFalse(mockFs.exists(sourceFolder));
|
||||||
|
|
||||||
|
// Validate from logs that threads are enabled, that a child directory was
|
||||||
|
// deleted by an external caller, and the parent delete operation still
|
||||||
|
// succeeds.
|
||||||
String content = logs.getOutput();
|
String content = logs.getOutput();
|
||||||
assertInLog(content,
|
assertInLog(content,
|
||||||
"Using thread pool for Delete operation with threads");
|
"Using thread pool for Delete operation with threads");
|
||||||
assertInLog(content, "Delete operation failed for file " + path);
|
assertInLog(content, String.format("Attempt to delete non-existent %s %s",
|
||||||
assertInLog(content,
|
useDir ? "directory" : "file", path));
|
||||||
"Terminating execution of Delete operation now as some other thread already got exception or operation failed");
|
|
||||||
assertInLog(content, "Failed to delete files / subfolders in blob");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
Loading…
Reference in New Issue