Skip to content

Commit 604b1c5

Browse files
jfredenjoshua-adams-1
authored andcommitted
Add retry for AccessDeniedException in AbstractFileWatchingService (elastic#128653)
* Unmute testSymlinkUpdateTriggerReload * Add retry for AccessDeniedException in AbstractFileWatchingService * Update docs/changelog/128653.yaml
1 parent 356f67c commit 604b1c5

File tree

4 files changed

+47
-12
lines changed

4 files changed

+47
-12
lines changed

docs/changelog/128653.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 128653
2+
summary: Add retry for `AccessDeniedException` in `AbstractFileWatchingService`
3+
area: Infra/Settings
4+
type: bug
5+
issues: []

muted-tests.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -486,9 +486,6 @@ tests:
486486
- class: org.elasticsearch.packaging.test.DockerTests
487487
method: test124CanRestartContainerWithStackLoggingConfig
488488
issue: https://github.com/elastic/elasticsearch/issues/128121
489-
- class: org.elasticsearch.reservedstate.service.FileSettingsServiceIT
490-
method: testSymlinkUpdateTriggerReload
491-
issue: https://github.com/elastic/elasticsearch/issues/128619
492489
- class: org.elasticsearch.packaging.test.DockerTests
493490
method: test085EnvironmentVariablesAreRespectedUnderDockerExec
494491
issue: https://github.com/elastic/elasticsearch/issues/128115

server/src/main/java/org/elasticsearch/common/file/AbstractFileWatchingService.java

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.io.IOException;
1919
import java.io.InputStream;
20+
import java.nio.file.AccessDeniedException;
2021
import java.nio.file.ClosedWatchServiceException;
2122
import java.nio.file.NoSuchFileException;
2223
import java.nio.file.Path;
@@ -56,6 +57,7 @@ public abstract class AbstractFileWatchingService extends AbstractLifecycleCompo
5657

5758
private static final Logger logger = LogManager.getLogger(AbstractFileWatchingService.class);
5859
private static final int REGISTER_RETRY_COUNT = 5;
60+
private static final int ACCESS_DENIED_RETRY_COUNT = 5;
5961
private final Path settingsDir;
6062
private final Map<Path, FileUpdateState> fileUpdateState = new HashMap<>();
6163
private WatchService watchService; // null;
@@ -115,20 +117,33 @@ public final boolean watching() {
115117
return watcherThread != null;
116118
}
117119

118-
private FileUpdateState readFileUpdateState(Path path) throws IOException {
119-
try {
120-
BasicFileAttributes attr = filesReadAttributes(path, BasicFileAttributes.class);
121-
return new FileUpdateState(attr.lastModifiedTime().toMillis(), path.toRealPath().toString(), attr.fileKey());
122-
} catch (NoSuchFileException e) {
123-
// file doesn't exist anymore
124-
return null;
125-
}
120+
// package private for testing
121+
FileUpdateState readFileUpdateState(Path path) throws IOException, InterruptedException {
122+
int retryCount = 0;
123+
do {
124+
try {
125+
BasicFileAttributes attr = filesReadAttributes(path, BasicFileAttributes.class);
126+
return new FileUpdateState(attr.lastModifiedTime().toMillis(), path.toRealPath().toString(), attr.fileKey());
127+
} catch (NoSuchFileException e) {
128+
// file doesn't exist anymore
129+
return null;
130+
} catch (AccessDeniedException e) {
131+
// This can happen on Windows when a symlink is deleted for a path while path.toRealPath() is called. In most cases the
132+
// symlink is recreated, so retry
133+
if (retryCount == ACCESS_DENIED_RETRY_COUNT - 1) {
134+
throw e;
135+
}
136+
logger.debug("Could not read file state [{}] attempt [{}]", path, retryCount);
137+
Thread.sleep(retryDelayMillis(retryCount));
138+
retryCount++;
139+
}
140+
} while (true);
126141
}
127142

128143
// platform independent way to tell if a file changed
129144
// we compare the file modified timestamp, the absolute path (symlinks), and file id on the system
130145
@FixForMultiProject // what do we do when a file is removed?
131-
final boolean fileChanged(Path path) throws IOException {
146+
final boolean fileChanged(Path path) throws IOException, InterruptedException {
132147
FileUpdateState newFileState = readFileUpdateState(path);
133148
if (newFileState == null) {
134149
fileUpdateState.remove(path);

server/src/test/java/org/elasticsearch/common/file/AbstractFileWatchingServiceTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import java.io.IOException;
2727
import java.io.InputStream;
28+
import java.nio.file.AccessDeniedException;
2829
import java.nio.file.Files;
2930
import java.nio.file.Path;
3031
import java.nio.file.StandardCopyOption;
@@ -238,6 +239,23 @@ public void testRegisterWatchKeyRetry() throws IOException, InterruptedException
238239
verify(service, times(2)).retryDelayMillis(anyInt());
239240
}
240241

242+
public void testAccessDeniedRetry() throws IOException, InterruptedException {
243+
var service = spy(fileWatchingService);
244+
doAnswer(i -> 0L).when(service).retryDelayMillis(anyInt());
245+
246+
Files.createDirectories(service.watchedFileDir());
247+
var mockedPath = spy(service.watchedFileDir());
248+
249+
doThrow(new AccessDeniedException("can't read state")).doThrow(new AccessDeniedException("can't read state - attempt 2"))
250+
.doAnswer(i -> mockedPath)
251+
.when(mockedPath)
252+
.toRealPath();
253+
254+
var result = service.readFileUpdateState(mockedPath);
255+
assertNotNull(result);
256+
verify(service, times(2)).retryDelayMillis(anyInt());
257+
}
258+
241259
// helpers
242260
private static void writeTestFile(Path path, String contents) throws IOException {
243261
Path tempFilePath = createTempFile();

0 commit comments

Comments
 (0)