Skip to content

Commit a67920e

Browse files
authored
Add file delete retry to testcluster ElasticsearchNode (#89095)
Add retry logic for cleanup / deletion in testcluster's ElasticsearchNode, to tolerate the asynchronous nature of deletions on the Windows file-system.
1 parent 2c79925 commit a67920e

File tree

1 file changed

+85
-6
lines changed

1 file changed

+85
-6
lines changed

build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
import java.io.IOException;
6363
import java.io.InputStream;
6464
import java.io.LineNumberReader;
65+
import java.io.PrintWriter;
66+
import java.io.StringWriter;
6567
import java.io.UncheckedIOException;
6668
import java.net.URL;
6769
import java.nio.charset.StandardCharsets;
@@ -489,6 +491,13 @@ public void freeze() {
489491
configurationFrozen.set(true);
490492
}
491493

494+
private static String throwableToString(Throwable t) {
495+
StringWriter sw = new StringWriter();
496+
PrintWriter pw = new PrintWriter(sw);
497+
t.printStackTrace(pw);
498+
return sw.toString();
499+
}
500+
492501
@Override
493502
public synchronized void start() {
494503
LOGGER.info("Starting `{}`", this);
@@ -505,19 +514,23 @@ public synchronized void start() {
505514
// make sure we always start fresh
506515
if (Files.exists(workingDir)) {
507516
if (preserveDataDir) {
508-
Files.list(workingDir)
509-
.filter(path -> path.equals(confPathData) == false)
510-
.forEach(path -> fileSystemOperations.delete(d -> d.delete(path)));
517+
Files.list(workingDir).filter(path -> path.equals(confPathData) == false).forEach(this::uncheckedDeleteWithRetry);
511518
} else {
512-
fileSystemOperations.delete(d -> d.delete(workingDir));
519+
deleteWithRetry(workingDir);
513520
}
514521
}
515522
isWorkingDirConfigured = true;
516523
}
517524
setupNodeDistribution(getExtractedDistributionDir());
518525
createWorkingDir();
519526
} catch (IOException e) {
520-
throw new UncheckedIOException("Failed to create working directory for " + this, e);
527+
String msg = "Failed to create working directory for " + this + ", with: " + e + throwableToString(e);
528+
logToProcessStdout(msg);
529+
throw new UncheckedIOException(msg, e);
530+
} catch (org.gradle.api.UncheckedIOException e) {
531+
String msg = "Failed to create working directory for " + this + ", with: " + e + throwableToString(e);
532+
logToProcessStdout(msg);
533+
throw e;
521534
}
522535

523536
copyExtraJars();
@@ -1192,9 +1205,75 @@ private void waitForProcessToExit(ProcessHandle processHandle) {
11921205
}
11931206
}
11941207

1208+
private static final int RETRY_DELETE_MILLIS = OS.current() == OS.WINDOWS ? 500 : 0;
1209+
private static final int MAX_RETRY_DELETE_TIMES = OS.current() == OS.WINDOWS ? 15 : 0;
1210+
1211+
/**
1212+
* Deletes a path, retrying if necessary.
1213+
*
1214+
* @param path the path to delete
1215+
* @throws IOException
1216+
* if an I/O error occurs
1217+
*/
1218+
void deleteWithRetry(Path path) throws IOException {
1219+
try {
1220+
deleteWithRetry0(path);
1221+
} catch (InterruptedException x) {
1222+
throw new IOException("Interrupted while deleting.", x);
1223+
}
1224+
}
1225+
1226+
/** Unchecked variant of deleteWithRetry. */
1227+
void uncheckedDeleteWithRetry(Path path) {
1228+
try {
1229+
deleteWithRetry0(path);
1230+
} catch (IOException e) {
1231+
throw new UncheckedIOException(e);
1232+
} catch (InterruptedException x) {
1233+
throw new UncheckedIOException("Interrupted while deleting.", new IOException());
1234+
}
1235+
}
1236+
1237+
// The exception handling here is loathsome, but necessary!
1238+
private void deleteWithRetry0(Path path) throws IOException, InterruptedException {
1239+
int times = 0;
1240+
IOException ioe = null;
1241+
while (true) {
1242+
try {
1243+
fileSystemOperations.delete(d -> d.delete(path));
1244+
times++;
1245+
// Checks for absence of the file. Semantics of Files.exists() is not the same.
1246+
while (Files.notExists(path) == false) {
1247+
if (times > MAX_RETRY_DELETE_TIMES) {
1248+
throw new IOException("File still exists after " + times + " waits.");
1249+
}
1250+
Thread.sleep(RETRY_DELETE_MILLIS);
1251+
// retry
1252+
fileSystemOperations.delete(d -> d.delete(path));
1253+
times++;
1254+
}
1255+
break;
1256+
} catch (NoSuchFileException ignore) {
1257+
// already deleted, ignore
1258+
break;
1259+
} catch (org.gradle.api.UncheckedIOException | IOException x) {
1260+
if (x.getCause() instanceof NoSuchFileException) {
1261+
// already deleted, ignore
1262+
break;
1263+
}
1264+
// Backoff/retry in case another process is accessing the file
1265+
times++;
1266+
if (ioe == null) ioe = new IOException();
1267+
ioe.addSuppressed(x);
1268+
if (times > MAX_RETRY_DELETE_TIMES) throw ioe;
1269+
Thread.sleep(RETRY_DELETE_MILLIS);
1270+
}
1271+
}
1272+
}
1273+
11951274
private void createWorkingDir() throws IOException {
11961275
// Start configuration from scratch in case of a restart
1197-
fileSystemOperations.delete(d -> d.delete(configFile.getParent()));
1276+
deleteWithRetry(configFile.getParent());
11981277
Files.createDirectories(configFile.getParent());
11991278
Files.createDirectories(confPathRepo);
12001279
Files.createDirectories(confPathData);

0 commit comments

Comments
 (0)