Skip to content

Commit 1b9135e

Browse files
huaxiangsunsteveloughran
authored andcommitted
HADOOP-18340. deleteOnExit does not work with S3AFileSystem (#4608)
Contributed by Huaxiang Sun
1 parent a0e2ab2 commit 1b9135e

File tree

3 files changed

+284
-0
lines changed

3 files changed

+284
-0
lines changed

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@
3232
import java.util.Collections;
3333
import java.util.Date;
3434
import java.util.EnumSet;
35+
import java.util.Iterator;
3536
import java.util.List;
3637
import java.util.Locale;
3738
import java.util.Map;
3839
import java.util.Optional;
3940
import java.util.Set;
4041
import java.util.Objects;
42+
import java.util.TreeSet;
4143
import java.util.concurrent.CompletableFuture;
4244
import java.util.concurrent.ExecutorService;
4345
import java.util.concurrent.LinkedBlockingQueue;
@@ -386,6 +388,12 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities,
386388
*/
387389
private ArnResource accessPoint;
388390

391+
/**
392+
* A cache of files that should be deleted when the FileSystem is closed
393+
* or the JVM is exited.
394+
*/
395+
private final Set<Path> deleteOnExit = new TreeSet<>();
396+
389397
/** Add any deprecated keys. */
390398
@SuppressWarnings("deprecation")
391399
private static void addDeprecatedKeys() {
@@ -3063,6 +3071,24 @@ public void removeKeys(
30633071
@AuditEntryPoint
30643072
public boolean delete(Path f, boolean recursive) throws IOException {
30653073
checkNotClosed();
3074+
return deleteWithoutCloseCheck(f, recursive);
3075+
}
3076+
3077+
/**
3078+
* Same as delete(), except that it does not check if fs is closed.
3079+
*
3080+
* @param f the path to delete.
3081+
* @param recursive if path is a directory and set to
3082+
* true, the directory is deleted else throws an exception. In
3083+
* case of a file the recursive can be set to either true or false.
3084+
* @return true if the path existed and then was deleted; false if there
3085+
* was no path in the first place, or the corner cases of root path deletion
3086+
* have surfaced.
3087+
* @throws IOException due to inability to delete a directory or file.
3088+
*/
3089+
3090+
@VisibleForTesting
3091+
protected boolean deleteWithoutCloseCheck(Path f, boolean recursive) throws IOException {
30663092
final Path path = qualify(f);
30673093
// span covers delete, getFileStatus, fake directory operations.
30683094
try (AuditSpan span = createSpan(INVOCATION_DELETE.getSymbol(),
@@ -3804,6 +3830,61 @@ UploadResult waitForUploadCompletion(String key, UploadInfo uploadInfo)
38043830
}
38053831
}
38063832

3833+
/**
3834+
* This override bypasses checking for existence.
3835+
*
3836+
* @param f the path to delete; this may be unqualified.
3837+
* @return true, always. * @param f the path to delete.
3838+
* @return true if deleteOnExit is successful, otherwise false.
3839+
* @throws IOException IO failure
3840+
*/
3841+
@Override
3842+
public boolean deleteOnExit(Path f) throws IOException {
3843+
Path qualifedPath = makeQualified(f);
3844+
synchronized (deleteOnExit) {
3845+
deleteOnExit.add(qualifedPath);
3846+
}
3847+
return true;
3848+
}
3849+
3850+
/**
3851+
* Cancel the scheduled deletion of the path when the FileSystem is closed.
3852+
* @param f the path to cancel deletion
3853+
* @return true if the path was found in the delete-on-exit list.
3854+
*/
3855+
@Override
3856+
public boolean cancelDeleteOnExit(Path f) {
3857+
Path qualifedPath = makeQualified(f);
3858+
synchronized (deleteOnExit) {
3859+
return deleteOnExit.remove(qualifedPath);
3860+
}
3861+
}
3862+
3863+
/**
3864+
* Delete all paths that were marked as delete-on-exit. This recursively
3865+
* deletes all files and directories in the specified paths. It does not
3866+
* check if file exists and filesystem is closed.
3867+
*
3868+
* The time to process this operation is {@code O(paths)}, with the actual
3869+
* time dependent on the time for existence and deletion operations to
3870+
* complete, successfully or not.
3871+
*/
3872+
@Override
3873+
protected void processDeleteOnExit() {
3874+
synchronized (deleteOnExit) {
3875+
for (Iterator<Path> iter = deleteOnExit.iterator(); iter.hasNext();) {
3876+
Path path = iter.next();
3877+
try {
3878+
deleteWithoutCloseCheck(path, true);
3879+
} catch (IOException e) {
3880+
LOG.info("Ignoring failure to deleteOnExit for path {}", path);
3881+
LOG.debug("The exception for deleteOnExit is {}", e);
3882+
}
3883+
iter.remove();
3884+
}
3885+
}
3886+
}
3887+
38073888
/**
38083889
* Close the filesystem. This shuts down all transfers.
38093890
* @throws IOException IO problem
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.hadoop.fs.s3a;
19+
20+
import org.junit.Test;
21+
22+
import org.apache.hadoop.fs.FileSystem;
23+
import org.apache.hadoop.fs.Path;
24+
25+
import static org.apache.hadoop.fs.contract.ContractTestUtils.createFile;
26+
import static org.apache.hadoop.fs.contract.ContractTestUtils.dataset;
27+
28+
/**
29+
* Test deleteOnExit for S3A.
30+
* The following cases for deleteOnExit are tested:
31+
* 1. A nonexist file, which is added to deleteOnExit set.
32+
* 2. An existing file
33+
* 3. A file is added to deleteOnExist set first, then created.
34+
* 4. A directory with some files under it.
35+
*/
36+
public class ITestS3ADeleteOnExit extends AbstractS3ATestBase {
37+
38+
private static final String PARENT_DIR_PATH_STR = "testDeleteOnExitDir";
39+
private static final String NON_EXIST_FILE_PATH_STR =
40+
PARENT_DIR_PATH_STR + "/nonExistFile";
41+
private static final String INORDER_FILE_PATH_STR =
42+
PARENT_DIR_PATH_STR + "/inOrderFile";
43+
private static final String OUT_OF_ORDER_FILE_PATH_STR =
44+
PARENT_DIR_PATH_STR + "/outOfOrderFile";
45+
private static final String SUBDIR_PATH_STR =
46+
PARENT_DIR_PATH_STR + "/subDir";
47+
private static final String FILE_UNDER_SUBDIR_PATH_STR =
48+
SUBDIR_PATH_STR + "/subDirFile";
49+
50+
@Test
51+
public void testDeleteOnExit() throws Exception {
52+
FileSystem fs = getFileSystem();
53+
54+
// Get a new filesystem object which is same as fs.
55+
FileSystem s3aFs = new S3AFileSystem();
56+
s3aFs.initialize(fs.getUri(), fs.getConf());
57+
Path nonExistFilePath = path(NON_EXIST_FILE_PATH_STR);
58+
Path inOrderFilePath = path(INORDER_FILE_PATH_STR);
59+
Path outOfOrderFilePath = path(OUT_OF_ORDER_FILE_PATH_STR);
60+
Path subDirPath = path(SUBDIR_PATH_STR);
61+
Path fileUnderSubDirPath = path(FILE_UNDER_SUBDIR_PATH_STR);
62+
63+
// 1. set up the test directory.
64+
Path dir = path("testDeleteOnExitDir");
65+
s3aFs.mkdirs(dir);
66+
67+
// 2. Add a nonexisting file to DeleteOnExit set.
68+
s3aFs.deleteOnExit(nonExistFilePath);
69+
assertPathDoesNotExist("File " + NON_EXIST_FILE_PATH_STR + " should not exist",
70+
nonExistFilePath);
71+
72+
// 3. create a file and then add it to DeleteOnExit set.
73+
byte[] data = dataset(16, 'a', 26);
74+
createFile(s3aFs, inOrderFilePath, true, data);
75+
assertPathExists("File " + INORDER_FILE_PATH_STR + " should exist", inOrderFilePath);
76+
s3aFs.deleteOnExit(inOrderFilePath);
77+
78+
// 4. add a path to DeleteOnExit set first, then create it.
79+
s3aFs.deleteOnExit(outOfOrderFilePath);
80+
createFile(s3aFs, outOfOrderFilePath, true, data);
81+
assertPathExists("File " + OUT_OF_ORDER_FILE_PATH_STR + " should exist",
82+
outOfOrderFilePath);
83+
84+
// 5. create a subdirectory, a file under it, and add subdirectory DeleteOnExit set.
85+
s3aFs.mkdirs(subDirPath);
86+
s3aFs.deleteOnExit(subDirPath);
87+
createFile(s3aFs, fileUnderSubDirPath, true, data);
88+
assertPathExists("Directory " + SUBDIR_PATH_STR + " should exist", subDirPath);
89+
assertPathExists("File " + FILE_UNDER_SUBDIR_PATH_STR + " should exist",
90+
fileUnderSubDirPath);
91+
92+
s3aFs.close();
93+
94+
// After s3aFs is closed, make sure that all files/directories in deleteOnExit
95+
// set are deleted.
96+
assertPathDoesNotExist("File " + NON_EXIST_FILE_PATH_STR + " should not exist",
97+
nonExistFilePath);
98+
assertPathDoesNotExist("File " + INORDER_FILE_PATH_STR + " should not exist",
99+
inOrderFilePath);
100+
assertPathDoesNotExist("File " + OUT_OF_ORDER_FILE_PATH_STR + " should not exist",
101+
outOfOrderFilePath);
102+
assertPathDoesNotExist("Directory " + SUBDIR_PATH_STR + " should not exist",
103+
subDirPath);
104+
}
105+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.hadoop.fs.s3a;
20+
21+
import static org.apache.hadoop.fs.s3a.Constants.FS_S3A;
22+
import static org.junit.Assert.assertEquals;
23+
import static org.mockito.ArgumentMatchers.argThat;
24+
import static org.mockito.Mockito.when;
25+
26+
import java.io.IOException;
27+
import java.net.URI;
28+
import java.util.Date;
29+
30+
import com.amazonaws.services.s3.AmazonS3;
31+
import com.amazonaws.services.s3.model.GetObjectMetadataRequest;
32+
import com.amazonaws.services.s3.model.ObjectMetadata;
33+
34+
import org.apache.hadoop.conf.Configuration;
35+
import org.apache.hadoop.fs.Path;
36+
37+
import org.junit.Test;
38+
import org.mockito.ArgumentMatcher;
39+
40+
/**
41+
* deleteOnExit test for S3A.
42+
*/
43+
public class TestS3ADeleteOnExit extends AbstractS3AMockTest {
44+
45+
static class TestS3AFileSystem extends S3AFileSystem {
46+
private int deleteOnDnExitCount;
47+
48+
public int getDeleteOnDnExitCount() {
49+
return deleteOnDnExitCount;
50+
}
51+
52+
@Override
53+
public boolean deleteOnExit(Path f) throws IOException {
54+
deleteOnDnExitCount++;
55+
return super.deleteOnExit(f);
56+
}
57+
58+
// This is specifically designed for deleteOnExit processing.
59+
// In this specific case, deleteWithoutCloseCheck() will only be called in the path of
60+
// processDeleteOnExit.
61+
@Override
62+
protected boolean deleteWithoutCloseCheck(Path f, boolean recursive) throws IOException {
63+
boolean result = super.deleteWithoutCloseCheck(f, recursive);
64+
deleteOnDnExitCount--;
65+
return result;
66+
}
67+
}
68+
69+
@Test
70+
public void testDeleteOnExit() throws Exception {
71+
Configuration conf = createConfiguration();
72+
TestS3AFileSystem testFs = new TestS3AFileSystem();
73+
URI uri = URI.create(FS_S3A + "://" + BUCKET);
74+
// unset S3CSE property from config to avoid pathIOE.
75+
conf.unset(Constants.S3_ENCRYPTION_ALGORITHM);
76+
testFs.initialize(uri, conf);
77+
AmazonS3 testS3 = testFs.getAmazonS3ClientForTesting("mocking");
78+
79+
Path path = new Path("/file");
80+
String key = path.toUri().getPath().substring(1);
81+
ObjectMetadata meta = new ObjectMetadata();
82+
meta.setContentLength(1L);
83+
meta.setLastModified(new Date(2L));
84+
when(testS3.getObjectMetadata(argThat(correctGetMetadataRequest(BUCKET, key))))
85+
.thenReturn(meta);
86+
87+
testFs.deleteOnExit(path);
88+
testFs.close();
89+
assertEquals(0, testFs.getDeleteOnDnExitCount());
90+
}
91+
92+
private ArgumentMatcher<GetObjectMetadataRequest> correctGetMetadataRequest(
93+
String bucket, String key) {
94+
return request -> request != null
95+
&& request.getBucketName().equals(bucket)
96+
&& request.getKey().equals(key);
97+
}
98+
}

0 commit comments

Comments
 (0)