Skip to content

Commit 6ce3e71

Browse files
authored
Fix race conditions in file settings service tests (#116309)
This test resolves two race conditions in `FileSettingsServiceTests#testProcessFileChanges`: 1. The test used `writeTestFile` to update the settings.json file. It did so in multiple steps: first it created a temp file in the operator directory, then moved that file to replace the existing settings.json file. The first step (creating the temp file) triggered the watcher thread in the file settings service to access the settings.json file to check for changes. When this access happened concurrently with the `move` call inside `writeTestFile` the test would throw on a Windows file-system (mocked or real), since you can't move a file while it's open. To fix this, the PR changes `writeTestFile` to creating a temp file elsewhere and simplifies the method. Instead of relying on this method (and multiple file operations) to update the file, the PR instead simply "touches" the settings file with a timestamp update to trigger file processing (more details also in this [comment](#115280 (comment))). 2. The test awaited latches that would count down when `ReservedClusterStateService#process` was invoked. However, at this point in the file settings processing flow, the settings.json file is still open and would therefore likewise block subsequent writes that fall into the small window of the file still being open. This PR instead adds latches based on file-changed listeners which are reliably invoked _after_ the file is closed. Resolves: #115280
1 parent 908d34d commit 6ce3e71

File tree

2 files changed

+27
-61
lines changed

2 files changed

+27
-61
lines changed

muted-tests.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,6 @@ tests:
246246
- class: org.elasticsearch.search.basic.SearchWhileRelocatingIT
247247
method: testSearchAndRelocateConcurrentlyRandomReplicas
248248
issue: https://github.com/elastic/elasticsearch/issues/116145
249-
- class: org.elasticsearch.reservedstate.service.FileSettingsServiceTests
250-
method: testProcessFileChanges
251-
issue: https://github.com/elastic/elasticsearch/issues/115280
252249
- class: org.elasticsearch.xpack.test.rest.XPackRestIT
253250
method: test {p0=ml/filter_crud/Test update filter}
254251
issue: https://github.com/elastic/elasticsearch/issues/116271

server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java

Lines changed: 27 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
import org.elasticsearch.common.logging.Loggers;
3030
import org.elasticsearch.common.settings.ClusterSettings;
3131
import org.elasticsearch.common.settings.Settings;
32-
import org.elasticsearch.core.IOUtils;
33-
import org.elasticsearch.core.Strings;
3432
import org.elasticsearch.core.TimeValue;
3533
import org.elasticsearch.env.BuildVersion;
3634
import org.elasticsearch.env.Environment;
@@ -46,10 +44,14 @@
4644
import org.mockito.stubbing.Answer;
4745

4846
import java.io.IOException;
49-
import java.io.UncheckedIOException;
5047
import java.nio.file.AtomicMoveNotSupportedException;
5148
import java.nio.file.Files;
5249
import java.nio.file.Path;
50+
import java.nio.file.attribute.FileTime;
51+
import java.time.Instant;
52+
import java.time.LocalDateTime;
53+
import java.time.ZoneId;
54+
import java.time.ZoneOffset;
5355
import java.util.List;
5456
import java.util.Map;
5557
import java.util.Set;
@@ -59,7 +61,6 @@
5961
import java.util.function.Consumer;
6062

6163
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
62-
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
6364
import static org.elasticsearch.node.Node.NODE_NAME_SETTING;
6465
import static org.hamcrest.Matchers.anEmptyMap;
6566
import static org.hamcrest.Matchers.hasEntry;
@@ -210,24 +211,17 @@ public void testInitialFileWorks() throws Exception {
210211
return null;
211212
}).when(controller).process(any(), any(XContentParser.class), any(), any());
212213

213-
CountDownLatch fileProcessingLatch = new CountDownLatch(1);
214+
CountDownLatch processFileLatch = new CountDownLatch(1);
215+
fileSettingsService.addFileChangedListener(processFileLatch::countDown);
214216

215217
Files.createDirectories(fileSettingsService.watchedFileDir());
216218
// contents of the JSON don't matter, we just need a file to exist
217219
writeTestFile(fileSettingsService.watchedFile(), "{}");
218220

219-
doAnswer((Answer<?>) invocation -> {
220-
try {
221-
return invocation.callRealMethod();
222-
} finally {
223-
fileProcessingLatch.countDown();
224-
}
225-
}).when(fileSettingsService).processFileOnServiceStart();
226-
227221
fileSettingsService.start();
228222
fileSettingsService.clusterChanged(new ClusterChangedEvent("test", clusterService.state(), ClusterState.EMPTY_STATE));
229223

230-
longAwait(fileProcessingLatch);
224+
longAwait(processFileLatch);
231225

232226
verify(fileSettingsService, times(1)).processFileOnServiceStart();
233227
verify(controller, times(1)).process(any(), any(XContentParser.class), eq(ReservedStateVersionCheck.HIGHER_OR_SAME_VERSION), any());
@@ -240,23 +234,8 @@ public void testProcessFileChanges() throws Exception {
240234
return null;
241235
}).when(controller).process(any(), any(XContentParser.class), any(), any());
242236

243-
CountDownLatch changesOnStartLatch = new CountDownLatch(1);
244-
doAnswer((Answer<?>) invocation -> {
245-
try {
246-
return invocation.callRealMethod();
247-
} finally {
248-
changesOnStartLatch.countDown();
249-
}
250-
}).when(fileSettingsService).processFileOnServiceStart();
251-
252-
CountDownLatch changesLatch = new CountDownLatch(1);
253-
doAnswer((Answer<?>) invocation -> {
254-
try {
255-
return invocation.callRealMethod();
256-
} finally {
257-
changesLatch.countDown();
258-
}
259-
}).when(fileSettingsService).processFileChanges();
237+
CountDownLatch processFileCreationLatch = new CountDownLatch(1);
238+
fileSettingsService.addFileChangedListener(processFileCreationLatch::countDown);
260239

261240
Files.createDirectories(fileSettingsService.watchedFileDir());
262241
// contents of the JSON don't matter, we just need a file to exist
@@ -265,14 +244,19 @@ public void testProcessFileChanges() throws Exception {
265244
fileSettingsService.start();
266245
fileSettingsService.clusterChanged(new ClusterChangedEvent("test", clusterService.state(), ClusterState.EMPTY_STATE));
267246

268-
longAwait(changesOnStartLatch);
247+
longAwait(processFileCreationLatch);
248+
249+
CountDownLatch processFileChangeLatch = new CountDownLatch(1);
250+
fileSettingsService.addFileChangedListener(processFileChangeLatch::countDown);
269251

270252
verify(fileSettingsService, times(1)).processFileOnServiceStart();
271253
verify(controller, times(1)).process(any(), any(XContentParser.class), eq(ReservedStateVersionCheck.HIGHER_OR_SAME_VERSION), any());
272254

273-
// second file change; contents still don't matter
274-
writeTestFile(fileSettingsService.watchedFile(), "[]");
275-
longAwait(changesLatch);
255+
// Touch the file to get an update
256+
Instant now = LocalDateTime.now(ZoneId.systemDefault()).toInstant(ZoneOffset.ofHours(0));
257+
Files.setLastModifiedTime(fileSettingsService.watchedFile(), FileTime.from(now));
258+
259+
longAwait(processFileChangeLatch);
276260

277261
verify(fileSettingsService, times(1)).processFileChanges();
278262
verify(controller, times(1)).process(any(), any(XContentParser.class), eq(ReservedStateVersionCheck.HIGHER_VERSION_ONLY), any());
@@ -367,30 +351,15 @@ public void testHandleSnapshotRestoreResetsMetadata() throws Exception {
367351
}
368352

369353
// helpers
370-
private static void writeTestFile(Path path, String contents) {
371-
Path tempFile = null;
354+
private static void writeTestFile(Path path, String contents) throws IOException {
355+
logger.info("Writing settings file under [{}]", path.toAbsolutePath());
356+
Path tempFilePath = createTempFile();
357+
Files.writeString(tempFilePath, contents);
372358
try {
373-
logger.info("Creating settings temp file under [{}]", path.toAbsolutePath());
374-
tempFile = Files.createTempFile(path.getParent(), path.getFileName().toString(), "tmp");
375-
logger.info("Created settings temp file [{}]", tempFile.getFileName());
376-
Files.writeString(tempFile, contents);
377-
378-
try {
379-
logger.info("Moving settings temp file to replace [{}]", tempFile.getFileName());
380-
Files.move(tempFile, path, REPLACE_EXISTING, ATOMIC_MOVE);
381-
} catch (AtomicMoveNotSupportedException e) {
382-
logger.info(
383-
"Atomic move was not available. Falling back on non-atomic move for settings temp file to replace [{}]",
384-
tempFile.getFileName()
385-
);
386-
Files.move(tempFile, path, REPLACE_EXISTING);
387-
}
388-
} catch (final IOException e) {
389-
throw new UncheckedIOException(Strings.format("could not write file [%s]", path.toAbsolutePath()), e);
390-
} finally {
391-
logger.info("Deleting settings temp file [{}]", tempFile != null ? tempFile.getFileName() : null);
392-
// we are ignoring exceptions here, so we do not need handle whether or not tempFile was initialized nor if the file exists
393-
IOUtils.deleteFilesIgnoringExceptions(tempFile);
359+
Files.move(tempFilePath, path, ATOMIC_MOVE);
360+
} catch (AtomicMoveNotSupportedException e) {
361+
logger.info("Atomic move not available. Falling back on non-atomic move to write [{}]", path.toAbsolutePath());
362+
Files.move(tempFilePath, path);
394363
}
395364
}
396365

0 commit comments

Comments
 (0)