Skip to content

Commit f58945d

Browse files
authored
HDFS-16791. Add getEnclosingRoot() API to filesystem interface and implementations (#6198)
The enclosing root path is a common ancestor that should be used for temp and staging dirs as well as within encryption zones and other restricted directories. Contributed by Tom McCormick
1 parent 90e9aa2 commit f58945d

File tree

29 files changed

+878
-32
lines changed

29 files changed

+878
-32
lines changed

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/AbstractFileSystem.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,6 +1638,24 @@ public MultipartUploaderBuilder createMultipartUploader(Path basePath)
16381638
return null;
16391639
}
16401640

1641+
/**
1642+
* Return path of the enclosing root for a given path
1643+
* The enclosing root path is a common ancestor that should be used for temp and staging dirs
1644+
* as well as within encryption zones and other restricted directories.
1645+
*
1646+
* Call makeQualified on the param path to ensure its part of the correct filesystem
1647+
*
1648+
* @param path file path to find the enclosing root path for
1649+
* @return a path to the enclosing root
1650+
* @throws IOException early checks like failure to resolve path cause IO failures
1651+
*/
1652+
@InterfaceAudience.Public
1653+
@InterfaceStability.Unstable
1654+
public Path getEnclosingRoot(Path path) throws IOException {
1655+
makeQualified(path);
1656+
return makeQualified(new Path("/"));
1657+
}
1658+
16411659
/**
16421660
* Helper method that throws an {@link UnsupportedOperationException} for the
16431661
* current {@link FileSystem} method being called.

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FileSystem.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4944,6 +4944,24 @@ public CompletableFuture<FSDataInputStream> build() throws IOException {
49444944

49454945
}
49464946

4947+
/**
4948+
* Return path of the enclosing root for a given path.
4949+
* The enclosing root path is a common ancestor that should be used for temp and staging dirs
4950+
* as well as within encryption zones and other restricted directories.
4951+
*
4952+
* Call makeQualified on the param path to ensure its part of the correct filesystem.
4953+
*
4954+
* @param path file path to find the enclosing root path for
4955+
* @return a path to the enclosing root
4956+
* @throws IOException early checks like failure to resolve path cause IO failures
4957+
*/
4958+
@InterfaceAudience.Public
4959+
@InterfaceStability.Unstable
4960+
public Path getEnclosingRoot(Path path) throws IOException {
4961+
this.makeQualified(path);
4962+
return this.makeQualified(new Path("/"));
4963+
}
4964+
49474965
/**
49484966
* Create a multipart uploader.
49494967
* @param basePath file path under which all files are uploaded

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FilterFileSystem.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,11 @@ protected CompletableFuture<FSDataInputStream> openFileWithOptions(
732732
return fs.openFileWithOptions(pathHandle, parameters);
733733
}
734734

735+
@Override
736+
public Path getEnclosingRoot(Path path) throws IOException {
737+
return fs.getEnclosingRoot(path);
738+
}
739+
735740
@Override
736741
public boolean hasPathCapability(final Path path, final String capability)
737742
throws IOException {

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/FilterFs.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,9 @@ public MultipartUploaderBuilder createMultipartUploader(final Path basePath)
459459
throws IOException {
460460
return myFs.createMultipartUploader(basePath);
461461
}
462+
463+
@Override
464+
public Path getEnclosingRoot(Path path) throws IOException {
465+
return myFs.getEnclosingRoot(path);
466+
}
462467
}

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/viewfs/ViewFileSystem.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,24 @@ public boolean hasPathCapability(Path path, String capability)
13701370
}
13711371
}
13721372

1373+
@Override
1374+
public Path getEnclosingRoot(Path path) throws IOException {
1375+
InodeTree.ResolveResult<FileSystem> res;
1376+
try {
1377+
res = fsState.resolve(getUriPath(path), true);
1378+
} catch (FileNotFoundException ex) {
1379+
NotInMountpointException mountPointEx =
1380+
new NotInMountpointException(path,
1381+
String.format("getEnclosingRoot - %s", ex.getMessage()));
1382+
mountPointEx.initCause(ex);
1383+
throw mountPointEx;
1384+
}
1385+
Path mountPath = new Path(res.resolvedPath);
1386+
Path enclosingPath = res.targetFileSystem.getEnclosingRoot(new Path(getUriPath(path)));
1387+
return fixRelativePart(this.makeQualified(enclosingPath.depth() > mountPath.depth()
1388+
? enclosingPath : mountPath));
1389+
}
1390+
13731391
/**
13741392
* An instance of this class represents an internal dir of the viewFs
13751393
* that is internal dir of the mount table.
@@ -1919,6 +1937,25 @@ public Collection<? extends BlockStoragePolicySpi> getAllStoragePolicies()
19191937
}
19201938
return allPolicies;
19211939
}
1940+
1941+
@Override
1942+
public Path getEnclosingRoot(Path path) throws IOException {
1943+
InodeTree.ResolveResult<FileSystem> res;
1944+
try {
1945+
res = fsState.resolve((path.toString()), true);
1946+
} catch (FileNotFoundException ex) {
1947+
NotInMountpointException mountPointEx =
1948+
new NotInMountpointException(path,
1949+
String.format("getEnclosingRoot - %s", ex.getMessage()));
1950+
mountPointEx.initCause(ex);
1951+
throw mountPointEx;
1952+
}
1953+
Path fullPath = new Path(res.resolvedPath);
1954+
Path enclosingPath = res.targetFileSystem.getEnclosingRoot(path);
1955+
return enclosingPath.depth() > fullPath.depth()
1956+
? enclosingPath
1957+
: fullPath;
1958+
}
19221959
}
19231960

19241961
enum RenameStrategy {

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/viewfs/ViewFs.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,5 +1477,22 @@ public void setStoragePolicy(Path path, String policyName)
14771477
throws IOException {
14781478
throw readOnlyMountTable("setStoragePolicy", path);
14791479
}
1480+
1481+
@Override
1482+
public Path getEnclosingRoot(Path path) throws IOException {
1483+
InodeTree.ResolveResult<AbstractFileSystem> res;
1484+
try {
1485+
res = fsState.resolve((path.toString()), true);
1486+
} catch (FileNotFoundException ex) {
1487+
NotInMountpointException mountPointEx =
1488+
new NotInMountpointException(path,
1489+
String.format("getEnclosingRoot - %s", ex.getMessage()));
1490+
mountPointEx.initCause(ex);
1491+
throw mountPointEx;
1492+
}
1493+
Path fullPath = new Path(res.resolvedPath);
1494+
Path enclosingPath = res.targetFileSystem.getEnclosingRoot(path);
1495+
return enclosingPath.depth() > fullPath.depth() ? enclosingPath : fullPath;
1496+
}
14801497
}
14811498
}

hadoop-common-project/hadoop-common/src/site/markdown/filesystem/filesystem.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,40 @@ on the filesystem.
601601

602602
1. The outcome of this operation MUST be identical to the value of
603603
`getFileStatus(P).getBlockSize()`.
604-
1. By inference, it MUST be > 0 for any file of length > 0.
604+
2. By inference, it MUST be > 0 for any file of length > 0.
605+
606+
### `Path getEnclosingRoot(Path p)`
607+
608+
This method is used to find a root directory for a path given. This is useful for creating
609+
staging and temp directories in the same enclosing root directory. There are constraints around how
610+
renames are allowed to atomically occur (ex. across hdfs volumes or across encryption zones).
611+
612+
For any two paths p1 and p2 that do not have the same enclosing root, `rename(p1, p2)` is expected to fail or will not
613+
be atomic.
614+
615+
For object stores, even with the same enclosing root, there is no guarantee file or directory rename is atomic
616+
617+
The following statement is always true:
618+
`getEnclosingRoot(p) == getEnclosingRoot(getEnclosingRoot(p))`
619+
620+
621+
```python
622+
path in ancestors(FS, p) or path == p:
623+
isDir(FS, p)
624+
```
625+
626+
#### Preconditions
627+
628+
The path does not have to exist, but the path does need to be valid and reconcilable by the filesystem
629+
* if a linkfallback is used all paths are reconcilable
630+
* if a linkfallback is not used there must be a mount point covering the path
631+
632+
633+
#### Postconditions
634+
635+
* The path returned will not be null, if there is no deeper enclosing root, the root path ("/") will be returned.
636+
* The path returned is a directory
637+
605638

606639
## <a name="state_changing_operations"></a> State Changing Operations
607640

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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;
19+
20+
import java.io.IOException;
21+
import java.security.PrivilegedExceptionAction;
22+
import org.apache.hadoop.conf.Configuration;
23+
import org.apache.hadoop.security.UserGroupInformation;
24+
import org.apache.hadoop.test.HadoopTestBase;
25+
import org.junit.Test;
26+
27+
public class TestGetEnclosingRoot extends HadoopTestBase {
28+
@Test
29+
public void testEnclosingRootEquivalence() throws IOException {
30+
FileSystem fs = getFileSystem();
31+
Path root = path("/");
32+
Path foobar = path("/foo/bar");
33+
34+
assertEquals(root, fs.getEnclosingRoot(root));
35+
assertEquals(root, fs.getEnclosingRoot(foobar));
36+
assertEquals(root, fs.getEnclosingRoot(fs.getEnclosingRoot(foobar)));
37+
assertEquals(fs.getEnclosingRoot(root), fs.getEnclosingRoot(foobar));
38+
39+
assertEquals(root, fs.getEnclosingRoot(path(foobar.toString())));
40+
assertEquals(root, fs.getEnclosingRoot(fs.getEnclosingRoot(path(foobar.toString()))));
41+
assertEquals(fs.getEnclosingRoot(root), fs.getEnclosingRoot(path(foobar.toString())));
42+
}
43+
44+
@Test
45+
public void testEnclosingRootPathExists() throws Exception {
46+
FileSystem fs = getFileSystem();
47+
Path root = path("/");
48+
Path foobar = path("/foo/bar");
49+
fs.mkdirs(foobar);
50+
51+
assertEquals(root, fs.getEnclosingRoot(foobar));
52+
assertEquals(root, fs.getEnclosingRoot(path(foobar.toString())));
53+
}
54+
55+
@Test
56+
public void testEnclosingRootPathDNE() throws Exception {
57+
FileSystem fs = getFileSystem();
58+
Path foobar = path("/foo/bar");
59+
Path root = path("/");
60+
61+
assertEquals(root, fs.getEnclosingRoot(foobar));
62+
assertEquals(root, fs.getEnclosingRoot(path(foobar.toString())));
63+
}
64+
65+
@Test
66+
public void testEnclosingRootWrapped() throws Exception {
67+
FileSystem fs = getFileSystem();
68+
Path root = path("/");
69+
70+
assertEquals(root, fs.getEnclosingRoot(new Path("/foo/bar")));
71+
72+
UserGroupInformation ugi = UserGroupInformation.createRemoteUser("foo");
73+
Path p = ugi.doAs((PrivilegedExceptionAction<Path>) () -> {
74+
FileSystem wFs = getFileSystem();
75+
return wFs.getEnclosingRoot(new Path("/foo/bar"));
76+
});
77+
assertEquals(root, p);
78+
}
79+
80+
private FileSystem getFileSystem() throws IOException {
81+
return FileSystem.get(new Configuration());
82+
}
83+
84+
/**
85+
* Create a path under the test path provided by
86+
* the FS contract.
87+
* @param filepath path string in
88+
* @return a path qualified by the test filesystem
89+
* @throws IOException IO problems
90+
*/
91+
private Path path(String filepath) throws IOException {
92+
return getFileSystem().makeQualified(
93+
new Path(filepath));
94+
}}

hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/TestHarFileSystem.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ MultipartUploaderBuilder createMultipartUploader(Path basePath)
255255

256256
FSDataOutputStream append(Path f, int bufferSize,
257257
Progressable progress, boolean appendToNewBlock) throws IOException;
258+
259+
Path getEnclosingRoot(Path path) throws IOException;
258260
}
259261

260262
@Test
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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.contract;
19+
20+
import java.io.IOException;
21+
import java.security.PrivilegedExceptionAction;
22+
import org.apache.hadoop.fs.FileSystem;
23+
import org.apache.hadoop.fs.Path;
24+
import org.apache.hadoop.security.UserGroupInformation;
25+
import org.junit.Test;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
29+
30+
public abstract class AbstractContractGetEnclosingRoot extends AbstractFSContractTestBase {
31+
private static final Logger LOG = LoggerFactory.getLogger(AbstractContractGetEnclosingRoot.class);
32+
33+
@Test
34+
public void testEnclosingRootEquivalence() throws IOException {
35+
FileSystem fs = getFileSystem();
36+
Path root = path("/");
37+
Path foobar = path("/foo/bar");
38+
39+
assertEquals("Ensure getEnclosingRoot on the root directory returns the root directory",
40+
root, fs.getEnclosingRoot(foobar));
41+
assertEquals("Ensure getEnclosingRoot called on itself returns the root directory",
42+
root, fs.getEnclosingRoot(fs.getEnclosingRoot(foobar)));
43+
assertEquals(
44+
"Ensure getEnclosingRoot for different paths in the same enclosing root "
45+
+ "returns the same path",
46+
fs.getEnclosingRoot(root), fs.getEnclosingRoot(foobar));
47+
assertEquals("Ensure getEnclosingRoot on a path returns the root directory",
48+
root, fs.getEnclosingRoot(methodPath()));
49+
assertEquals("Ensure getEnclosingRoot called on itself on a path returns the root directory",
50+
root, fs.getEnclosingRoot(fs.getEnclosingRoot(methodPath())));
51+
assertEquals(
52+
"Ensure getEnclosingRoot for different paths in the same enclosing root "
53+
+ "returns the same path",
54+
fs.getEnclosingRoot(root),
55+
fs.getEnclosingRoot(methodPath()));
56+
}
57+
58+
59+
@Test
60+
public void testEnclosingRootPathExists() throws Exception {
61+
FileSystem fs = getFileSystem();
62+
Path root = path("/");
63+
Path foobar = methodPath();
64+
fs.mkdirs(foobar);
65+
66+
assertEquals(
67+
"Ensure getEnclosingRoot returns the root directory when the root directory exists",
68+
root, fs.getEnclosingRoot(foobar));
69+
assertEquals("Ensure getEnclosingRoot returns the root directory when the directory exists",
70+
root, fs.getEnclosingRoot(foobar));
71+
}
72+
73+
@Test
74+
public void testEnclosingRootPathDNE() throws Exception {
75+
FileSystem fs = getFileSystem();
76+
Path foobar = path("/foo/bar");
77+
Path root = path("/");
78+
79+
// .
80+
assertEquals(
81+
"Ensure getEnclosingRoot returns the root directory even when the path does not exist",
82+
root, fs.getEnclosingRoot(foobar));
83+
assertEquals(
84+
"Ensure getEnclosingRoot returns the root directory even when the path does not exist",
85+
root, fs.getEnclosingRoot(methodPath()));
86+
}
87+
88+
@Test
89+
public void testEnclosingRootWrapped() throws Exception {
90+
FileSystem fs = getFileSystem();
91+
Path root = path("/");
92+
93+
assertEquals("Ensure getEnclosingRoot returns the root directory when the directory exists",
94+
root, fs.getEnclosingRoot(new Path("/foo/bar")));
95+
96+
UserGroupInformation ugi = UserGroupInformation.createRemoteUser("foo");
97+
Path p = ugi.doAs((PrivilegedExceptionAction<Path>) () -> {
98+
FileSystem wFs = getContract().getTestFileSystem();
99+
return wFs.getEnclosingRoot(new Path("/foo/bar"));
100+
});
101+
assertEquals("Ensure getEnclosingRoot works correctly within a wrapped FileSystem", root, p);
102+
}
103+
}

0 commit comments

Comments
 (0)