HADOOP-14769. WASB: delete recursive should not fail if a file is deleted.

Contributed by Thomas Marquardt
This commit is contained in:
Steve Loughran 2017-08-18 14:13:40 +01:00
parent 99e558b13b
commit c6b4e656b7
No known key found for this signature in database
GPG Key ID: 950CC3E032B79CA2
3 changed files with 87 additions and 44 deletions

View File

@ -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);
} }
} }

View File

@ -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

View File

@ -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");
} }
/* /*