|  | @@ -62,6 +62,8 @@ import java.io.File;
 | 
	
		
			
				|  |  |  import java.io.IOException;
 | 
	
		
			
				|  |  |  import java.io.InputStream;
 | 
	
		
			
				|  |  |  import java.io.LineNumberReader;
 | 
	
		
			
				|  |  | +import java.io.PrintWriter;
 | 
	
		
			
				|  |  | +import java.io.StringWriter;
 | 
	
		
			
				|  |  |  import java.io.UncheckedIOException;
 | 
	
		
			
				|  |  |  import java.net.URL;
 | 
	
		
			
				|  |  |  import java.nio.charset.StandardCharsets;
 | 
	
	
		
			
				|  | @@ -489,6 +491,13 @@ public class ElasticsearchNode implements TestClusterConfiguration {
 | 
	
		
			
				|  |  |          configurationFrozen.set(true);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    private static String throwableToString(Throwable t) {
 | 
	
		
			
				|  |  | +        StringWriter sw = new StringWriter();
 | 
	
		
			
				|  |  | +        PrintWriter pw = new PrintWriter(sw);
 | 
	
		
			
				|  |  | +        t.printStackTrace(pw);
 | 
	
		
			
				|  |  | +        return sw.toString();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      @Override
 | 
	
		
			
				|  |  |      public synchronized void start() {
 | 
	
		
			
				|  |  |          LOGGER.info("Starting `{}`", this);
 | 
	
	
		
			
				|  | @@ -505,11 +514,9 @@ public class ElasticsearchNode implements TestClusterConfiguration {
 | 
	
		
			
				|  |  |                  // make sure we always start fresh
 | 
	
		
			
				|  |  |                  if (Files.exists(workingDir)) {
 | 
	
		
			
				|  |  |                      if (preserveDataDir) {
 | 
	
		
			
				|  |  | -                        Files.list(workingDir)
 | 
	
		
			
				|  |  | -                            .filter(path -> path.equals(confPathData) == false)
 | 
	
		
			
				|  |  | -                            .forEach(path -> fileSystemOperations.delete(d -> d.delete(path)));
 | 
	
		
			
				|  |  | +                        Files.list(workingDir).filter(path -> path.equals(confPathData) == false).forEach(this::uncheckedDeleteWithRetry);
 | 
	
		
			
				|  |  |                      } else {
 | 
	
		
			
				|  |  | -                        fileSystemOperations.delete(d -> d.delete(workingDir));
 | 
	
		
			
				|  |  | +                        deleteWithRetry(workingDir);
 | 
	
		
			
				|  |  |                      }
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |                  isWorkingDirConfigured = true;
 | 
	
	
		
			
				|  | @@ -517,7 +524,13 @@ public class ElasticsearchNode implements TestClusterConfiguration {
 | 
	
		
			
				|  |  |              setupNodeDistribution(getExtractedDistributionDir());
 | 
	
		
			
				|  |  |              createWorkingDir();
 | 
	
		
			
				|  |  |          } catch (IOException e) {
 | 
	
		
			
				|  |  | -            throw new UncheckedIOException("Failed to create working directory for " + this, e);
 | 
	
		
			
				|  |  | +            String msg = "Failed to create working directory for " + this + ", with: " + e + throwableToString(e);
 | 
	
		
			
				|  |  | +            logToProcessStdout(msg);
 | 
	
		
			
				|  |  | +            throw new UncheckedIOException(msg, e);
 | 
	
		
			
				|  |  | +        } catch (org.gradle.api.UncheckedIOException e) {
 | 
	
		
			
				|  |  | +            String msg = "Failed to create working directory for " + this + ", with: " + e + throwableToString(e);
 | 
	
		
			
				|  |  | +            logToProcessStdout(msg);
 | 
	
		
			
				|  |  | +            throw e;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          copyExtraJars();
 | 
	
	
		
			
				|  | @@ -1192,9 +1205,75 @@ public class ElasticsearchNode implements TestClusterConfiguration {
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    private static final int RETRY_DELETE_MILLIS = OS.current() == OS.WINDOWS ? 500 : 0;
 | 
	
		
			
				|  |  | +    private static final int MAX_RETRY_DELETE_TIMES = OS.current() == OS.WINDOWS ? 15 : 0;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /**
 | 
	
		
			
				|  |  | +     * Deletes a path, retrying if necessary.
 | 
	
		
			
				|  |  | +     *
 | 
	
		
			
				|  |  | +     * @param path  the path to delete
 | 
	
		
			
				|  |  | +     * @throws IOException
 | 
	
		
			
				|  |  | +     *         if an I/O error occurs
 | 
	
		
			
				|  |  | +     */
 | 
	
		
			
				|  |  | +    void deleteWithRetry(Path path) throws IOException {
 | 
	
		
			
				|  |  | +        try {
 | 
	
		
			
				|  |  | +            deleteWithRetry0(path);
 | 
	
		
			
				|  |  | +        } catch (InterruptedException x) {
 | 
	
		
			
				|  |  | +            throw new IOException("Interrupted while deleting.", x);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    /** Unchecked variant of deleteWithRetry. */
 | 
	
		
			
				|  |  | +    void uncheckedDeleteWithRetry(Path path) {
 | 
	
		
			
				|  |  | +        try {
 | 
	
		
			
				|  |  | +            deleteWithRetry0(path);
 | 
	
		
			
				|  |  | +        } catch (IOException e) {
 | 
	
		
			
				|  |  | +            throw new UncheckedIOException(e);
 | 
	
		
			
				|  |  | +        } catch (InterruptedException x) {
 | 
	
		
			
				|  |  | +            throw new UncheckedIOException("Interrupted while deleting.", new IOException());
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    // The exception handling here is loathsome, but necessary!
 | 
	
		
			
				|  |  | +    private void deleteWithRetry0(Path path) throws IOException, InterruptedException {
 | 
	
		
			
				|  |  | +        int times = 0;
 | 
	
		
			
				|  |  | +        IOException ioe = null;
 | 
	
		
			
				|  |  | +        while (true) {
 | 
	
		
			
				|  |  | +            try {
 | 
	
		
			
				|  |  | +                fileSystemOperations.delete(d -> d.delete(path));
 | 
	
		
			
				|  |  | +                times++;
 | 
	
		
			
				|  |  | +                // Checks for absence of the file. Semantics of Files.exists() is not the same.
 | 
	
		
			
				|  |  | +                while (Files.notExists(path) == false) {
 | 
	
		
			
				|  |  | +                    if (times > MAX_RETRY_DELETE_TIMES) {
 | 
	
		
			
				|  |  | +                        throw new IOException("File still exists after " + times + " waits.");
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    Thread.sleep(RETRY_DELETE_MILLIS);
 | 
	
		
			
				|  |  | +                    // retry
 | 
	
		
			
				|  |  | +                    fileSystemOperations.delete(d -> d.delete(path));
 | 
	
		
			
				|  |  | +                    times++;
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                break;
 | 
	
		
			
				|  |  | +            } catch (NoSuchFileException ignore) {
 | 
	
		
			
				|  |  | +                // already deleted, ignore
 | 
	
		
			
				|  |  | +                break;
 | 
	
		
			
				|  |  | +            } catch (org.gradle.api.UncheckedIOException | IOException x) {
 | 
	
		
			
				|  |  | +                if (x.getCause() instanceof NoSuchFileException) {
 | 
	
		
			
				|  |  | +                    // already deleted, ignore
 | 
	
		
			
				|  |  | +                    break;
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                // Backoff/retry in case another process is accessing the file
 | 
	
		
			
				|  |  | +                times++;
 | 
	
		
			
				|  |  | +                if (ioe == null) ioe = new IOException();
 | 
	
		
			
				|  |  | +                ioe.addSuppressed(x);
 | 
	
		
			
				|  |  | +                if (times > MAX_RETRY_DELETE_TIMES) throw ioe;
 | 
	
		
			
				|  |  | +                Thread.sleep(RETRY_DELETE_MILLIS);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      private void createWorkingDir() throws IOException {
 | 
	
		
			
				|  |  |          // Start configuration from scratch in case of a restart
 | 
	
		
			
				|  |  | -        fileSystemOperations.delete(d -> d.delete(configFile.getParent()));
 | 
	
		
			
				|  |  | +        deleteWithRetry(configFile.getParent());
 | 
	
		
			
				|  |  |          Files.createDirectories(configFile.getParent());
 | 
	
		
			
				|  |  |          Files.createDirectories(confPathRepo);
 | 
	
		
			
				|  |  |          Files.createDirectories(confPathData);
 |