Skip to content

Commit d4d4c37

Browse files
committed
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 18050bc commit d4d4c37

File tree

11 files changed

+295
-46
lines changed

11 files changed

+295
-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
@@ -486,11 +486,11 @@ running out of memory as it calculates the partitions.
486486

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

491491
### `long getDefaultBlockSize(Path p)`
492492

493-
Get the "default" block size for a path that is, the block size to be used
493+
Get the "default" block size for a path --that is, the block size to be used
494494
when writing objects to a path in the filesystem.
495495

496496
#### Preconditions
@@ -539,14 +539,21 @@ on the filesystem.
539539

540540
### `boolean mkdirs(Path p, FsPermission permission)`
541541

542-
Create a directory and all its parents
542+
Create a directory and all its parents.
543543

544544
#### Preconditions
545545

546546

547+
The path must either be a directory or not exist
548+
547549
if exists(FS, p) and not isDir(FS, p) :
548550
raise [ParentNotDirectoryException, FileAlreadyExistsException, IOException]
549551

552+
No ancestor may be a file
553+
554+
forall d = ancestors(FS, p) :
555+
if exists(FS, d) and not isDir(FS, d) :
556+
raise [ParentNotDirectoryException, FileAlreadyExistsException, IOException]
550557

551558
#### Postconditions
552559

@@ -586,14 +593,20 @@ Writing to or overwriting a directory must fail.
586593

587594
if isDir(FS, p) : raise {FileAlreadyExistsException, FileNotFoundException, IOException}
588595

596+
No ancestor may be a file
597+
598+
forall d = ancestors(FS, p) :
599+
if exists(FS, d) and not isDir(FS, d) :
600+
raise [ParentNotDirectoryException, FileAlreadyExistsException, IOException]
589601

590602
FileSystems may reject the request for other
591603
reasons, such as the FS being read-only (HDFS),
592604
the block size being below the minimum permitted (HDFS),
593605
the replication count being out of range (HDFS),
594606
quotas on namespace or filesystem being exceeded, reserved
595607
names, etc. All rejections SHOULD be `IOException` or a subclass thereof
596-
and MAY be a `RuntimeException` or subclass. For instance, HDFS may raise a `InvalidPathException`.
608+
and MAY be a `RuntimeException` or subclass.
609+
For instance, HDFS may raise an `InvalidPathException`.
597610

598611
#### Postconditions
599612

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

@@ -286,4 +288,54 @@ protected void validateAncestorsMoved(Path src, Path dst, String nestedPath)
286288
}
287289
}
288290

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

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
@@ -236,6 +236,15 @@ protected Path path(String filepath) throws IOException {
236236
new Path(getContract().getTestPath(), filepath));
237237
}
238238

239+
/**
240+
* Get a path whose name ends with the name of this method.
241+
* @return a path implicitly unique amongst all methods in this class
242+
* @throws IOException IO problems
243+
*/
244+
protected Path methodPath() throws IOException {
245+
return path(methodName.getMethodName());
246+
}
247+
239248
/**
240249
* Take a simple path like "/something" and turn it into
241250
* 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
@@ -1571,7 +1571,8 @@ public boolean rename(String src, String dst) throws IOException {
15711571
DSQuotaExceededException.class,
15721572
QuotaByStorageTypeExceededException.class,
15731573
UnresolvedPathException.class,
1574-
SnapshotAccessControlException.class);
1574+
SnapshotAccessControlException.class,
1575+
ParentNotDirectoryException.class);
15751576
}
15761577
}
15771578

0 commit comments

Comments
 (0)