Skip to content

Commit 12b70b0

Browse files
steveloughrandeepakdamri
authored andcommitted
HADOOP-14630 Contract Tests to verify create, mkdirs and rename under a file is forbidden
Contributed by Steve Loughran. Not all stores do complete validation here; in particular the S3A Connector does not: checking up the entire directory tree to see if a path matches is a file significantly slows things down. This check does take place in S3A mkdirs(), which walks backwards up the list of parent paths until it finds a directory (success) or a file (failure). In practice production applications invariably create destination directories before writing 1+ file into them -restricting check purely to the mkdirs() call deliver significant speed up while implicitly including the checks. Change-Id: I2c9df748e92b5655232e7d888d896f1868806eb0
1 parent 1930e47 commit 12b70b0

File tree

11 files changed

+300
-46
lines changed

11 files changed

+300
-46
lines changed

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -507,11 +507,11 @@ running out of memory as it calculates the partitions.
507507

508508
Any FileSystem that does not actually break files into blocks SHOULD
509509
return a number for this that results in efficient processing.
510-
A FileSystem MAY make this user-configurable (the S3 and Swift filesystem clients do this).
510+
A FileSystem MAY make this user-configurable (the object store connectors usually do this).
511511

512512
### `long getDefaultBlockSize(Path p)`
513513

514-
Get the "default" block size for a path that is, the block size to be used
514+
Get the "default" block size for a path --that is, the block size to be used
515515
when writing objects to a path in the filesystem.
516516

517517
#### Preconditions
@@ -560,14 +560,21 @@ on the filesystem.
560560

561561
### `boolean mkdirs(Path p, FsPermission permission)`
562562

563-
Create a directory and all its parents
563+
Create a directory and all its parents.
564564

565565
#### Preconditions
566566

567567

568+
The path must either be a directory or not exist
569+
568570
if exists(FS, p) and not isDir(FS, p) :
569571
raise [ParentNotDirectoryException, FileAlreadyExistsException, IOException]
570572

573+
No ancestor may be a file
574+
575+
forall d = ancestors(FS, p) :
576+
if exists(FS, d) and not isDir(FS, d) :
577+
raise [ParentNotDirectoryException, FileAlreadyExistsException, IOException]
571578

572579
#### Postconditions
573580

@@ -607,14 +614,20 @@ Writing to or overwriting a directory must fail.
607614

608615
if isDir(FS, p) : raise {FileAlreadyExistsException, FileNotFoundException, IOException}
609616

617+
No ancestor may be a file
618+
619+
forall d = ancestors(FS, p) :
620+
if exists(FS, d) and not isDir(FS, d) :
621+
raise [ParentNotDirectoryException, FileAlreadyExistsException, IOException]
610622

611623
FileSystems may reject the request for other
612624
reasons, such as the FS being read-only (HDFS),
613625
the block size being below the minimum permitted (HDFS),
614626
the replication count being out of range (HDFS),
615627
quotas on namespace or filesystem being exceeded, reserved
616628
names, etc. All rejections SHOULD be `IOException` or a subclass thereof
617-
and MAY be a `RuntimeException` or subclass. For instance, HDFS may raise a `InvalidPathException`.
629+
and MAY be a `RuntimeException` or subclass.
630+
For instance, HDFS may raise an `InvalidPathException`.
618631

619632
#### Postconditions
620633

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

Lines changed: 116 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
import org.apache.hadoop.fs.FileAlreadyExistsException;
2323
import org.apache.hadoop.fs.FileStatus;
2424
import org.apache.hadoop.fs.FileSystem;
25+
import org.apache.hadoop.fs.ParentNotDirectoryException;
2526
import org.apache.hadoop.fs.Path;
2627
import org.junit.Test;
27-
import org.junit.internal.AssumptionViolatedException;
28+
import org.junit.AssumptionViolatedException;
2829

29-
import java.io.FileNotFoundException;
3030
import java.io.IOException;
3131

3232
import static org.apache.hadoop.fs.contract.ContractTestUtils.dataset;
@@ -40,7 +40,7 @@
4040
* Test creating files, overwrite options etc.
4141
*/
4242
public abstract class AbstractContractCreateTest extends
43-
AbstractFSContractTestBase {
43+
AbstractFSContractTestBase {
4444

4545
/**
4646
* How long to wait for a path to become visible.
@@ -113,7 +113,6 @@ private void testOverwriteExistingFile(boolean useBuilder) throws Throwable {
113113
* This test catches some eventual consistency problems that blobstores exhibit,
114114
* as we are implicitly verifying that updates are consistent. This
115115
* is why different file lengths and datasets are used
116-
* @throws Throwable
117116
*/
118117
@Test
119118
public void testOverwriteExistingFile() throws Throwable {
@@ -137,10 +136,6 @@ private void testOverwriteEmptyDirectory(boolean useBuilder)
137136
} catch (FileAlreadyExistsException expected) {
138137
//expected
139138
handleExpectedException(expected);
140-
} catch (FileNotFoundException e) {
141-
handleRelaxedException("overwriting a dir with a file ",
142-
"FileAlreadyExistsException",
143-
e);
144139
} catch (IOException e) {
145140
handleRelaxedException("overwriting a dir with a file ",
146141
"FileAlreadyExistsException",
@@ -189,10 +184,6 @@ private void testOverwriteNonEmptyDirectory(boolean useBuilder)
189184
} catch (FileAlreadyExistsException expected) {
190185
//expected
191186
handleExpectedException(expected);
192-
} catch (FileNotFoundException e) {
193-
handleRelaxedException("overwriting a dir with a file ",
194-
"FileAlreadyExistsException",
195-
e);
196187
} catch (IOException e) {
197188
handleRelaxedException("overwriting a dir with a file ",
198189
"FileAlreadyExistsException",
@@ -332,4 +323,117 @@ public void testCreateMakesParentDirs() throws Throwable {
332323
assertTrue("Grandparent directory does not appear to be a directory",
333324
fs.getFileStatus(grandparent).isDirectory());
334325
}
326+
327+
@Test
328+
public void testCreateFileUnderFile() throws Throwable {
329+
describe("Verify that it is forbidden to create file/file");
330+
if (isSupported(CREATE_FILE_UNDER_FILE_ALLOWED)) {
331+
// object store or some file systems: downgrade to a skip so that the
332+
// failure is visible in test results
333+
skip("This filesystem supports creating files under files");
334+
}
335+
Path grandparent = methodPath();
336+
Path parent = new Path(grandparent, "parent");
337+
expectCreateUnderFileFails(
338+
"creating a file under a file",
339+
grandparent,
340+
parent);
341+
}
342+
343+
@Test
344+
public void testCreateUnderFileSubdir() throws Throwable {
345+
describe("Verify that it is forbidden to create file/dir/file");
346+
if (isSupported(CREATE_FILE_UNDER_FILE_ALLOWED)) {
347+
// object store or some file systems: downgrade to a skip so that the
348+
// failure is visible in test results
349+
skip("This filesystem supports creating files under files");
350+
}
351+
Path grandparent = methodPath();
352+
Path parent = new Path(grandparent, "parent");
353+
Path child = new Path(parent, "child");
354+
expectCreateUnderFileFails(
355+
"creating a file under a subdirectory of a file",
356+
grandparent,
357+
child);
358+
}
359+
360+
361+
@Test
362+
public void testMkdirUnderFile() throws Throwable {
363+
describe("Verify that it is forbidden to create file/dir");
364+
Path grandparent = methodPath();
365+
Path parent = new Path(grandparent, "parent");
366+
expectMkdirsUnderFileFails("mkdirs() under a file",
367+
grandparent, parent);
368+
}
369+
370+
@Test
371+
public void testMkdirUnderFileSubdir() throws Throwable {
372+
describe("Verify that it is forbidden to create file/dir/dir");
373+
Path grandparent = methodPath();
374+
Path parent = new Path(grandparent, "parent");
375+
Path child = new Path(parent, "child");
376+
expectMkdirsUnderFileFails("mkdirs() file/dir",
377+
grandparent, child);
378+
379+
try {
380+
// create the child
381+
mkdirs(child);
382+
} catch (FileAlreadyExistsException | ParentNotDirectoryException ex) {
383+
// either of these may be raised.
384+
handleExpectedException(ex);
385+
} catch (IOException e) {
386+
handleRelaxedException("creating a file under a subdirectory of a file ",
387+
"FileAlreadyExistsException",
388+
e);
389+
}
390+
}
391+
392+
/**
393+
* Expect that touch() will fail because the parent is a file.
394+
* @param action action for message
395+
* @param file filename to create
396+
* @param descendant path under file
397+
* @throws Exception failure
398+
*/
399+
protected void expectCreateUnderFileFails(String action,
400+
Path file, Path descendant)
401+
throws Exception {
402+
createFile(file);
403+
try {
404+
// create the child
405+
createFile(descendant);
406+
} catch (FileAlreadyExistsException | ParentNotDirectoryException ex) {
407+
//expected
408+
handleExpectedException(ex);
409+
} catch (IOException e) {
410+
handleRelaxedException(action,
411+
"ParentNotDirectoryException",
412+
e);
413+
}
414+
}
415+
416+
protected void expectMkdirsUnderFileFails(String action,
417+
Path file, Path descendant)
418+
throws Exception {
419+
createFile(file);
420+
try {
421+
// now mkdirs
422+
mkdirs(descendant);
423+
} catch (FileAlreadyExistsException | ParentNotDirectoryException ex) {
424+
//expected
425+
handleExpectedException(ex);
426+
} catch (IOException e) {
427+
handleRelaxedException(action,
428+
"ParentNotDirectoryException",
429+
e);
430+
}
431+
}
432+
433+
private void createFile(Path path) throws IOException {
434+
byte[] data = dataset(256, 'a', 'z');
435+
FileSystem fs = getFileSystem();
436+
writeDataset(fs, path, data, data.length, 1024 * 1024,
437+
true);
438+
}
335439
}

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

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
import static org.apache.hadoop.fs.contract.ContractTestUtils.*;
3030

3131
/**
32-
* Test creating files, overwrite options &c
32+
* Test renaming files.
3333
*/
3434
public abstract class AbstractContractRenameTest extends
35-
AbstractFSContractTestBase {
35+
AbstractFSContractTestBase {
3636

3737
@Test
3838
public void testRenameNewFileSameDir() throws Throwable {
@@ -83,7 +83,8 @@ public void testRenameNonexistentFile() throws Throwable {
8383
"FileNotFoundException",
8484
e);
8585
}
86-
assertPathDoesNotExist("rename nonexistent file created a destination file", target);
86+
assertPathDoesNotExist("rename nonexistent file created a destination file",
87+
target);
8788
}
8889

8990
/**
@@ -112,7 +113,7 @@ public void testRenameFileOverExistingFile() throws Throwable {
112113
// the filesystem supports rename(file, file2) by overwriting file2
113114

114115
assertTrue("Rename returned false", renamed);
115-
destUnchanged = false;
116+
destUnchanged = false;
116117
} else {
117118
// rename is rejected by returning 'false' or throwing an exception
118119
if (renamed && !renameReturnsFalseOnRenameDestExists) {
@@ -129,12 +130,13 @@ public void testRenameFileOverExistingFile() throws Throwable {
129130
// verify that the destination file is as expected based on the expected
130131
// outcome
131132
verifyFileContents(getFileSystem(), destFile,
132-
destUnchanged? destData: srcData);
133+
destUnchanged ? destData: srcData);
133134
}
134135

135136
@Test
136137
public void testRenameDirIntoExistingDir() throws Throwable {
137-
describe("Verify renaming a dir into an existing dir puts it underneath"
138+
describe("Verify renaming a dir into an existing dir puts it"
139+
+ " underneath"
138140
+" and leaves existing files alone");
139141
FileSystem fs = getFileSystem();
140142
String sourceSubdir = "source";
@@ -145,15 +147,15 @@ public void testRenameDirIntoExistingDir() throws Throwable {
145147
Path destDir = path("dest");
146148

147149
Path destFilePath = new Path(destDir, "dest-512.txt");
148-
byte[] destDateset = dataset(512, 'A', 'Z');
149-
writeDataset(fs, destFilePath, destDateset, destDateset.length, 1024, false);
150+
byte[] destData = dataset(512, 'A', 'Z');
151+
writeDataset(fs, destFilePath, destData, destData.length, 1024, false);
150152
assertIsFile(destFilePath);
151153

152154
boolean rename = rename(srcDir, destDir);
153155
Path renamedSrc = new Path(destDir, sourceSubdir);
154156
assertIsFile(destFilePath);
155157
assertIsDirectory(renamedSrc);
156-
verifyFileContents(fs, destFilePath, destDateset);
158+
verifyFileContents(fs, destFilePath, destData);
157159
assertTrue("rename returned false though the contents were copied", rename);
158160
}
159161

@@ -285,4 +287,54 @@ private void validateAncestorsMoved(Path src, Path dst, String nestedPath)
285287
}
286288
}
287289

290+
@Test
291+
public void testRenameFileUnderFile() throws Exception {
292+
String action = "rename directly under file";
293+
describe(action);
294+
Path base = methodPath();
295+
Path grandparent = new Path(base, "file");
296+
expectRenameUnderFileFails(action,
297+
grandparent,
298+
new Path(base, "testRenameSrc"),
299+
new Path(grandparent, "testRenameTarget"));
300+
}
301+
302+
@Test
303+
public void testRenameFileUnderFileSubdir() throws Exception {
304+
String action = "rename directly under file/subdir";
305+
describe(action);
306+
Path base = methodPath();
307+
Path grandparent = new Path(base, "file");
308+
Path parent = new Path(grandparent, "parent");
309+
expectRenameUnderFileFails(action,
310+
grandparent,
311+
new Path(base, "testRenameSrc"),
312+
new Path(parent, "testRenameTarget"));
313+
}
314+
315+
protected void expectRenameUnderFileFails(String action,
316+
Path file, Path renameSrc, Path renameTarget)
317+
throws Exception {
318+
byte[] data = dataset(256, 'a', 'z');
319+
FileSystem fs = getFileSystem();
320+
writeDataset(fs, file, data, data.length, 1024 * 1024,
321+
true);
322+
writeDataset(fs, renameSrc, data, data.length, 1024 * 1024,
323+
true);
324+
String outcome;
325+
boolean renamed;
326+
try {
327+
renamed = rename(renameSrc, renameTarget);
328+
outcome = action + ": rename (" + renameSrc + ", " + renameTarget
329+
+ ")= " + renamed;
330+
} catch (IOException e) {
331+
// raw local raises an exception here
332+
renamed = false;
333+
outcome = "rename raised an exception: " + e;
334+
}
335+
assertPathDoesNotExist("after " + outcome, renameTarget);
336+
assertFalse(outcome, renamed);
337+
assertPathExists(action, renameSrc);
338+
}
339+
288340
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,15 @@ protected Path path(String filepath) throws IOException {
225225
new Path(getContract().getTestPath(), filepath));
226226
}
227227

228+
/**
229+
* Get a path whose name ends with the name of this method.
230+
* @return a path implicitly unique amongst all methods in this class
231+
* @throws IOException IO problems
232+
*/
233+
protected Path methodPath() throws IOException {
234+
return path(methodName.getMethodName());
235+
}
236+
228237
/**
229238
* Take a simple path like "/something" and turn it into
230239
* a qualified path against the test FS.

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ public interface ContractOptions {
5151
*/
5252
String CREATE_VISIBILITY_DELAYED = "create-visibility-delayed";
5353

54+
/**
55+
* Flag to indicate that it is possible to create a file under a file.
56+
* This is a complete violation of the filesystem rules, but it is one
57+
* which object stores have been known to do for performance
58+
* <i>and because nobody has ever noticed.</i>
59+
* {@value}
60+
*/
61+
String CREATE_FILE_UNDER_FILE_ALLOWED = "create-file-under-file-allowed";
62+
5463
/**
5564
* Is a filesystem case sensitive.
5665
* Some of the filesystems that say "no" here may mean

hadoop-hdfs-project/hadoop-hdfs-client/src/main/java/org/apache/hadoop/hdfs/DFSClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1538,7 +1538,8 @@ public boolean rename(String src, String dst) throws IOException {
15381538
DSQuotaExceededException.class,
15391539
QuotaByStorageTypeExceededException.class,
15401540
UnresolvedPathException.class,
1541-
SnapshotAccessControlException.class);
1541+
SnapshotAccessControlException.class,
1542+
ParentNotDirectoryException.class);
15421543
}
15431544
}
15441545

0 commit comments

Comments
 (0)