Skip to content

Commit 2acd164

Browse files
prdoylen1v0lg
andauthored
Use different file-based settings error message for invalid JSON and NotMasterException (#116359)
* Fixup: remove unused pattern variable from before * Try1 handle XContentParseException * Mocks wrap XContentParseException in ExecutionException like the real code does * onProcessFileChangesException case for XContentParseException * Handle NotMasterException while we're at it. * Cleanup * Use Nikolaj's addFileChangedListener approach to test * Add REPLACE_EXISTING * Remove ATOMIC_MOVE Co-authored-by: Nikolaj Volgushev <[email protected]> * Delete stray generated files * Remove unused method --------- Co-authored-by: Nikolaj Volgushev <[email protected]>
1 parent 6ab260c commit 2acd164

File tree

3 files changed

+92
-10
lines changed

3 files changed

+92
-10
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,12 @@ final WatchKey enableDirectoryWatcher(WatchKey previousKey, Path settingsDir) th
302302
void processSettingsOnServiceStartAndNotifyListeners() throws InterruptedException {
303303
try {
304304
processFileOnServiceStart();
305-
for (var listener : eventListeners) {
306-
listener.watchedFileChanged();
307-
}
308305
} catch (IOException | ExecutionException e) {
309-
logger.error(() -> "Error processing watched file: " + watchedFile(), e);
306+
onProcessFileChangesException(e);
307+
return;
308+
}
309+
for (var listener : eventListeners) {
310+
listener.watchedFileChanged();
310311
}
311312
}
312313

server/src/main/java/org/elasticsearch/reservedstate/service/FileSettingsService.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
import org.elasticsearch.action.support.PlainActionFuture;
1616
import org.elasticsearch.cluster.ClusterState;
1717
import org.elasticsearch.cluster.ClusterStateListener;
18+
import org.elasticsearch.cluster.NotMasterException;
1819
import org.elasticsearch.cluster.coordination.FailedToCommitClusterStateException;
1920
import org.elasticsearch.cluster.metadata.Metadata;
2021
import org.elasticsearch.cluster.metadata.ReservedStateMetadata;
2122
import org.elasticsearch.cluster.service.ClusterService;
2223
import org.elasticsearch.common.file.MasterNodeFileWatchingService;
2324
import org.elasticsearch.env.Environment;
25+
import org.elasticsearch.xcontent.XContentParseException;
2426
import org.elasticsearch.xcontent.XContentParserConfiguration;
2527

2628
import java.io.BufferedInputStream;
@@ -146,11 +148,20 @@ private void processFileChanges(ReservedStateVersionCheck versionCheck) throws I
146148

147149
@Override
148150
protected void onProcessFileChangesException(Exception e) {
149-
if (e instanceof ExecutionException && e.getCause() instanceof FailedToCommitClusterStateException f) {
150-
logger.error("Unable to commit cluster state", e);
151-
} else {
152-
super.onProcessFileChangesException(e);
151+
if (e instanceof ExecutionException) {
152+
var cause = e.getCause();
153+
if (cause instanceof FailedToCommitClusterStateException) {
154+
logger.error("Unable to commit cluster state", e);
155+
return;
156+
} else if (cause instanceof XContentParseException) {
157+
logger.error("Unable to parse settings", e);
158+
return;
159+
} else if (cause instanceof NotMasterException) {
160+
logger.error("Node is no longer master", e);
161+
return;
162+
}
153163
}
164+
super.onProcessFileChangesException(e);
154165
}
155166

156167
@Override

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

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.elasticsearch.test.ESTestCase;
3939
import org.elasticsearch.threadpool.TestThreadPool;
4040
import org.elasticsearch.threadpool.ThreadPool;
41+
import org.elasticsearch.xcontent.XContentParseException;
4142
import org.elasticsearch.xcontent.XContentParser;
4243
import org.junit.After;
4344
import org.junit.Before;
@@ -55,16 +56,22 @@
5556
import java.util.List;
5657
import java.util.Map;
5758
import java.util.Set;
59+
import java.util.concurrent.BrokenBarrierException;
5860
import java.util.concurrent.CountDownLatch;
61+
import java.util.concurrent.CyclicBarrier;
62+
import java.util.concurrent.ExecutionException;
5963
import java.util.concurrent.TimeUnit;
64+
import java.util.concurrent.TimeoutException;
6065
import java.util.concurrent.atomic.AtomicBoolean;
6166
import java.util.function.Consumer;
6267

6368
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
69+
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
6470
import static org.elasticsearch.node.Node.NODE_NAME_SETTING;
6571
import static org.hamcrest.Matchers.anEmptyMap;
6672
import static org.hamcrest.Matchers.hasEntry;
6773
import static org.mockito.ArgumentMatchers.any;
74+
import static org.mockito.ArgumentMatchers.argThat;
6875
import static org.mockito.ArgumentMatchers.eq;
6976
import static org.mockito.Mockito.doAnswer;
7077
import static org.mockito.Mockito.mock;
@@ -262,6 +269,68 @@ public void testProcessFileChanges() throws Exception {
262269
verify(controller, times(1)).process(any(), any(XContentParser.class), eq(ReservedStateVersionCheck.HIGHER_VERSION_ONLY), any());
263270
}
264271

272+
@SuppressWarnings("unchecked")
273+
public void testInvalidJSON() throws Exception {
274+
doAnswer((Answer<Void>) invocation -> {
275+
invocation.getArgument(1, XContentParser.class).map(); // Throw if JSON is invalid
276+
((Consumer<Exception>) invocation.getArgument(3)).accept(null);
277+
return null;
278+
}).when(controller).process(any(), any(XContentParser.class), any(), any());
279+
280+
CyclicBarrier fileChangeBarrier = new CyclicBarrier(2);
281+
fileSettingsService.addFileChangedListener(() -> awaitOrBust(fileChangeBarrier));
282+
283+
Files.createDirectories(fileSettingsService.watchedFileDir());
284+
// contents of the JSON don't matter, we just need a file to exist
285+
writeTestFile(fileSettingsService.watchedFile(), "{}");
286+
287+
doAnswer((Answer<?>) invocation -> {
288+
boolean returnedNormally = false;
289+
try {
290+
var result = invocation.callRealMethod();
291+
returnedNormally = true;
292+
return result;
293+
} catch (XContentParseException e) {
294+
// We're expecting a parse error. processFileChanges specifies that this is supposed to throw ExecutionException.
295+
throw new ExecutionException(e);
296+
} catch (Throwable e) {
297+
throw new AssertionError("Unexpected exception", e);
298+
} finally {
299+
if (returnedNormally == false) {
300+
// Because of the exception, listeners aren't notified, so we need to activate the barrier ourselves
301+
awaitOrBust(fileChangeBarrier);
302+
}
303+
}
304+
}).when(fileSettingsService).processFileChanges();
305+
306+
// Establish the initial valid JSON
307+
fileSettingsService.start();
308+
fileSettingsService.clusterChanged(new ClusterChangedEvent("test", clusterService.state(), ClusterState.EMPTY_STATE));
309+
awaitOrBust(fileChangeBarrier);
310+
311+
// Now break the JSON
312+
writeTestFile(fileSettingsService.watchedFile(), "test_invalid_JSON");
313+
awaitOrBust(fileChangeBarrier);
314+
315+
verify(fileSettingsService, times(1)).processFileOnServiceStart(); // The initial state
316+
verify(fileSettingsService, times(1)).processFileChanges(); // The changed state
317+
verify(fileSettingsService, times(1)).onProcessFileChangesException(
318+
argThat(e -> e instanceof ExecutionException && e.getCause() instanceof XContentParseException)
319+
);
320+
321+
// Note: the name "processFileOnServiceStart" is a bit misleading because it is not
322+
// referring to fileSettingsService.start(). Rather, it is referring to the initialization
323+
// of the watcher thread itself, which occurs asynchronously when clusterChanged is first called.
324+
}
325+
326+
private static void awaitOrBust(CyclicBarrier barrier) {
327+
try {
328+
barrier.await(20, TimeUnit.SECONDS);
329+
} catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
330+
throw new AssertionError("Unexpected exception waiting for barrier", e);
331+
}
332+
}
333+
265334
@SuppressWarnings("unchecked")
266335
public void testStopWorksInMiddleOfProcessing() throws Exception {
267336
CountDownLatch processFileLatch = new CountDownLatch(1);
@@ -356,10 +425,10 @@ private static void writeTestFile(Path path, String contents) throws IOException
356425
Path tempFilePath = createTempFile();
357426
Files.writeString(tempFilePath, contents);
358427
try {
359-
Files.move(tempFilePath, path, ATOMIC_MOVE);
428+
Files.move(tempFilePath, path, REPLACE_EXISTING, ATOMIC_MOVE);
360429
} catch (AtomicMoveNotSupportedException e) {
361430
logger.info("Atomic move not available. Falling back on non-atomic move to write [{}]", path.toAbsolutePath());
362-
Files.move(tempFilePath, path);
431+
Files.move(tempFilePath, path, REPLACE_EXISTING);
363432
}
364433
}
365434

@@ -374,4 +443,5 @@ private static void longAwait(CountDownLatch latch) {
374443
fail(e, "longAwait: interrupted waiting for CountDownLatch to reach zero");
375444
}
376445
}
446+
377447
}

0 commit comments

Comments
 (0)