Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retry TemporaryFolder.newFolder's call to mkdir if unsuccessful #1304

Closed
wants to merge 1 commit into from
Closed

Retry TemporaryFolder.newFolder's call to mkdir if unsuccessful #1304

wants to merge 1 commit into from

Conversation

varming
Copy link

@varming varming commented May 22, 2016

If java.io.File.mkdir returns false, then a new directory has not been created, causing problems when running tests in parallel (see issue #1223).

@kcooney
Copy link
Member

kcooney commented May 22, 2016

I don't feel comfortable having an infinite loop. We should limit the attempts.

According to some of the comments at http://stackoverflow.com/a/617438 Java might not delete files right away. One of the commenters there suggested a fix and that fix should limit collisions as well.

@varming
Copy link
Author

varming commented May 22, 2016

Guava does 10,000 attempts and use a new timestamp on every attempt, before throwing an exception. We can do something similar and throw an IOException if all attempts fail.

@kcooney
Copy link
Member

kcooney commented May 22, 2016

I was thinking of something like this:

for (int i = 0; i < TEMP_DIR_ATTEMPTS; ++i) {
    File tmp = File.createTempFile("junit", ".tmp", parentFolder);
    String path = temp.getPath();
    File createdFolder = new File(path().substring(0, path.length() - 4));
    if (createdFolder.mkdir()) {
        tmp.delete()
        return createdFolder;
     }
     tmp.delete();
}
...

This is more secure plus shouldn't require too many iterations. As a bonus, the temporary directory paths would look the same as they do in 4.12

@varming
Copy link
Author

varming commented May 22, 2016

What is benefit of calling createTempFile when we cannot use the resulting file name directly?

@varming
Copy link
Author

varming commented May 22, 2016

The is one thing I really do not like about repeated executions of:

    File tmp = File.createTempFile("junit", ".tmp", parentFolder);
    String path = temp.getPath();
    File createdFolder = new File(path().substring(0, path.length() - 4));
    if (createdFolder.mkdir()) {
        tmp.delete()
        return createdFolder;
    }
    tmp.delete();

It might be that every call to File.createTempFile returns a file with the same name, say "junitFoo.tmp", yet "junitFoo" already exists. Then we do 10,000 rounds for nothing.

@kcooney
Copy link
Member

kcooney commented May 22, 2016

Calling createTempFile gives us a guaranteed unique path that no other thread or process can take, all without doing lots of looping (looping makes the test run time unpredictable so can result in flaky timeouts). Once we have a path we can remove the extension and create the directory. I doubt be would need any looping, but we can try a few times just in case.

@kcooney
Copy link
Member

kcooney commented May 22, 2016

According to the Javadoc File.createTempFile() won't return the same path every time.

@varming
Copy link
Author

varming commented May 22, 2016

Okay, you got me to look at the JDK code. When calling File.createTempFile JDK7 and later picks a random long X and check if junitX.tmp exists. If it exists then it starts over in a loop. Finally it attempts to create the file by calling into the JVM and from there into libc:open with flags O_CREAT | O_RDRW | O_EXCL (see http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/f51368baecd9/src/share/classes/java/io/File.java).

In the NIO2 implementation of Files.createTempDirectory the JDK also picks a random long X and attempts to create a directory junitX. If it already exists then they loop (see http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/f51368baecd9/src/share/classes/java/nio/file/TempFileHelper.java#l80)

I am doing pretty much the same as the NIO2 implementation, except I am using a nano timestamp instead SecureRandom.nextLong, and I bound the number of iterations, similar to what Guava does. I can switch to SecureRandom to be more like NIO2 if you like, but I fail to see any benefit of calling File.createTempFile before attempting to create the directory.

@kcooney
Copy link
Member

kcooney commented May 22, 2016

I really rather reuse the existing JDK implementation so we get the benefit of any bug fixes. So I would prefer either the solution I gave above or your original solution (but with a limitation on the number of iterations).

@varming
Copy link
Author

varming commented May 23, 2016

Looks like Github is being very slow today. I pushed an update to use File.createTempFile as requested, but it will probably take a bit of time before it shows up. The current commit is: https://github.com/varming/junit4/commit/3c03251bbc458ced409018ca9b614f55a6df4af4

// Use createTempFile to get a suitable folder name.
final String suffix = ".tmp";
final File tmpFile = File.createTempFile(TMP_PREFIX, suffix, parentFolder);
tmpFile.delete();
Copy link
Member

@kcooney kcooney May 24, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my proposal, the temporary file was deleted after the temporary directory was created. That way we wouldn't have a race conditio if the user is using an older JDK where File.createTempFile() was sometimes returning paths that were returned from previous calls.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You pointed out that the specification of File.createTempFile says "Neither this method nor any of its variants will return the same abstract pathname again in the current invocation of the virtual machine". I checked JDK6 (it returns random filenames: http://hg.openjdk.java.net/jdk6/jdk6/jdk/file/b72e7b89dda9/src/share/classes/java/io/File.java#l1795), and GNU Classpath (they use a mix of a timestamp and a global counter to prevent returning the same name in the lifetime of the VM). Oracle's JDK5 is not public, but I doubt there were significant changes.

I'll remove the finals.

@kcooney
Copy link
Member

kcooney commented May 24, 2016

Sorry for yet more back and forth, but we keep getting into situations where I suggest something and you do something subtlety different. If you make the change I propose (and only that change) I can merge this tonight.

@kcooney kcooney closed this in 24b8ee0 May 24, 2016
@kcooney
Copy link
Member

kcooney commented May 24, 2016

Merged. Thanks!

@marcphilipp
Copy link
Member

@varming Please add a description of this improvement to the draft of the release notes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants