This is an automated email from the ASF dual-hosted git repository.
rpuch pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new d4dc59000b6 [IGNITE-23326] Validate configuration for duplicated keys
(#5166)
d4dc59000b6 is described below
commit d4dc59000b6a6a4962d501480f656b76879d0fd5
Author: Phillippko <[email protected]>
AuthorDate: Thu Feb 6 18:53:59 2025 +0700
[IGNITE-23326] Validate configuration for duplicated keys (#5166)
---
.../cluster/management/ClusterInitializer.java | 23 +-
.../ConfigurationValidationException.java | 13 +-
.../configuration/validation/ValidationIssue.java | 21 ++
.../ConfigurationDuplicatesValidator.java | 329 +++++++++++++++++++++
.../ConfigurationDuplicatesValidatorTest.java | 109 +++++++
.../configuration/ItClusterConfigurationTest.java | 65 ++++
.../ItNodeBootstrapConfigurationTest.java | 17 ++
.../storage/LocalFileConfigurationStorage.java | 11 +-
.../storage/LocalFileConfigurationStorageTest.java | 31 ++
...ilablePartitionsRecoveryByFilterUpdateTest.java | 2 +-
10 files changed, 604 insertions(+), 17 deletions(-)
diff --git
a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterInitializer.java
b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterInitializer.java
index 96662b60904..dbdb3c6bfbe 100644
---
a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterInitializer.java
+++
b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterInitializer.java
@@ -36,6 +36,7 @@ import
org.apache.ignite.internal.cluster.management.network.messages.CmgInitMes
import
org.apache.ignite.internal.cluster.management.network.messages.CmgMessagesFactory;
import
org.apache.ignite.internal.cluster.management.network.messages.InitCompleteMessage;
import
org.apache.ignite.internal.cluster.management.network.messages.InitErrorMessage;
+import
org.apache.ignite.internal.configuration.validation.ConfigurationDuplicatesValidator;
import
org.apache.ignite.internal.configuration.validation.ConfigurationValidator;
import org.apache.ignite.internal.logger.IgniteLogger;
import org.apache.ignite.internal.logger.Loggers;
@@ -138,15 +139,16 @@ public class ClusterInitializer {
LOG.info("Resolved CMG nodes[nodes={}]", cmgNodes);
- String initialClusterConfiguration =
patchClusterConfigurationWithDynamicDefaults(clusterConfiguration);
- validateConfiguration(initialClusterConfiguration);
+ String patchedClusterConfiguration =
patchClusterConfigurationWithDynamicDefaults(clusterConfiguration);
+
+ validateConfiguration(patchedClusterConfiguration,
clusterConfiguration);
CmgInitMessage initMessage = msgFactory.cmgInitMessage()
.metaStorageNodes(msNodeNameSet)
.cmgNodes(cmgNodeNameSet)
.clusterName(clusterName)
.clusterId(UUID.randomUUID())
- .initialClusterConfiguration(initialClusterConfiguration)
+ .initialClusterConfiguration(patchedClusterConfiguration)
.build();
return invokeMessage(cmgNodes, initMessage)
@@ -261,12 +263,15 @@ public class ClusterInitializer {
return
configurationDynamicDefaultsPatcher.patchWithDynamicDefaults(hocon == null ? ""
: hocon);
}
- private void validateConfiguration(String hocon) {
- if (hocon != null) {
- List<ValidationIssue> issues =
clusterConfigurationValidator.validateHocon(hocon);
- if (!issues.isEmpty()) {
- throw new ConfigurationValidationException(issues);
- }
+ private void validateConfiguration(String patchedHocon, @Nullable String
initialHocon) {
+ List<ValidationIssue> issues =
clusterConfigurationValidator.validateHocon(patchedHocon);
+
+ if (initialHocon != null) {
+
issues.addAll(ConfigurationDuplicatesValidator.validate(initialHocon));
+ }
+
+ if (!issues.isEmpty()) {
+ throw new ConfigurationValidationException(issues);
}
}
}
diff --git
a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
index 8edf8959809..5f3aba72972 100644
---
a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
+++
b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
@@ -17,6 +17,7 @@
package org.apache.ignite.configuration.validation;
+import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@@ -24,8 +25,8 @@ import java.util.stream.Collectors;
* Configuration validation exception.
*/
public class ConfigurationValidationException extends RuntimeException {
- /** List of configuration validation issues. */
- private List<ValidationIssue> issues;
+ /** Collection of configuration validation issues. */
+ private Collection<ValidationIssue> issues;
/**
* Constructor.
@@ -41,10 +42,10 @@ public class ConfigurationValidationException extends
RuntimeException {
*
* @param issues List of issues occurred during validation.
*/
- public ConfigurationValidationException(List<ValidationIssue> issues) {
+ public ConfigurationValidationException(Collection<ValidationIssue>
issues) {
super(createMessageFromIssues(issues));
- this.issues = issues;
+ this.issues = List.copyOf(issues);
}
/**
@@ -52,11 +53,11 @@ public class ConfigurationValidationException extends
RuntimeException {
*
* @return List of issues occurred during validation.
*/
- public List<ValidationIssue> getIssues() {
+ public Collection<ValidationIssue> getIssues() {
return issues;
}
- private static String createMessageFromIssues(List<ValidationIssue>
issues) {
+ private static String createMessageFromIssues(Collection<ValidationIssue>
issues) {
return "Validation did not pass for keys: "
+ issues.stream().map(issue -> "[" + issue.key() + ", " +
issue.message() + "]").collect(Collectors.joining(", "));
}
diff --git
a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
index d78bb28a1e2..0fd598c1ddf 100644
---
a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
+++
b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
@@ -17,6 +17,7 @@
package org.apache.ignite.configuration.validation;
+import java.util.Objects;
import org.apache.ignite.internal.tostring.IgniteToStringInclude;
import org.apache.ignite.internal.tostring.S;
@@ -65,4 +66,24 @@ public class ValidationIssue {
public String toString() {
return S.toString(ValidationIssue.class, this);
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ ValidationIssue that = (ValidationIssue) obj;
+
+ return key.equals(that.key) && message.equals(that.message);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, message);
+ }
}
diff --git
a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/validation/ConfigurationDuplicatesValidator.java
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/validation/ConfigurationDuplicatesValidator.java
new file mode 100644
index 00000000000..1c252757f6e
--- /dev/null
+++
b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/validation/ConfigurationDuplicatesValidator.java
@@ -0,0 +1,329 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.configuration.validation;
+
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.impl.Parseable;
+import com.typesafe.config.parser.ConfigDocument;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Validates that there are no duplicate keys in the configuration.
+ *
+ * <p>Imagine configuration like this:
+ * <pre>
+ * root {
+ * arrayField: [
+ * arrayMember1: { name = "123" },
+ * arrayMember2: { name = "234" }
+ * ]
+ * }
+ * </pre>
+ *
+ * <p>This configuration can be represented like this:</p>
+ * <ul>
+ * <li>root children: [ ConfigNodePath("root"),
ConfigNodeComplexValue(field) ]</li>
+ * <li>
+ * arrayField children: [ ConfigNodePath("arrayField"),
ConfigNodeArray(arrayMember1), ConfigNodeArray(arrayMember2) ]
+ * </li>
+ * <li>arrayMember1 children: [ ConfigNodeComplexValue(name) ]</li>
+ * <li>arrayMember2 children: [ ConfigNodeComplexValue(name) ]</li>
+ * <li>name children: [ ConfigNodePath("name"), Value("123") ]</li>
+ * <li>name children: [ ConfigNodePath("name"), Value("234") ]</li>
+ * </ul>
+ */
+public class ConfigurationDuplicatesValidator {
+ /**
+ * Validates that there are no duplicate keys in the passed configuration.
+ *
+ * @param cfg configuration in HOCON or JSON format.
+ */
+ public static Collection<ValidationIssue> validate(String cfg) {
+ Object root = getConfigRoot(cfg);
+
+ Queue<Node> queue = new ArrayDeque<>();
+ queue.add(new Node(root, null, null));
+
+ Set<String> paths = new HashSet<>();
+ Set<ValidationIssue> issues = new HashSet<>();
+
+ while (!queue.isEmpty()) {
+ Node currentNode = queue.poll();
+ List<Object> nodeChildren =
getChildrenOrEmpty(currentNode.configNode);
+
+ if (nodeChildren.isEmpty()) {
+ continue;
+ }
+
+ Path currentPath = ConfigNodePath.path(currentNode.basePath,
nodeChildren, currentNode.indexInArray);
+
+ if (currentPath != null) {
+ String pathString = currentPath.toString();
+
+ if (!paths.add(pathString)) {
+ issues.add(new ValidationIssue(pathString, "Duplicated
key"));
+ }
+ } else {
+ currentPath = currentNode.basePath;
+ }
+
+ int index = 0;
+ for (Object child : nodeChildren) {
+ if (ConfigNodeComplexValue.isInstance(child) ||
ConfigNodeField.isInstance(child)) {
+ Integer indexInArray =
ConfigNodeArray.isInstance(currentNode.configNode)
+ ? index
+ : null;
+
+ queue.add(new Node(child, currentPath, indexInArray));
+ index++;
+ }
+ }
+ }
+
+ return issues;
+ }
+
+ private static Object getConfigRoot(String cfg) {
+ ConfigDocument configDocument = Parseable.newString(cfg,
ConfigParseOptions.defaults()).parseConfigDocument();
+ assert SimpleConfigDocument.isInstance(configDocument);
+
+ Object root = SimpleConfigDocument.root(configDocument);
+ assert ConfigNodeComplexValue.isInstance(root);
+
+ return root;
+ }
+
+ private static List<Object> getChildrenOrEmpty(Object configNode) {
+ if (ConfigNodeComplexValue.isInstance(configNode)) {
+ return ConfigNodeComplexValue.children(configNode);
+ } else if (ConfigNodeField.isInstance(configNode)) {
+ return ConfigNodeField.children(configNode);
+ } else {
+ return List.of();
+ }
+ }
+
+ private static class ConfigNodeComplexValue {
+ private static final Class<?> CLASS;
+ private static final Field CHILDREN_FIELD;
+
+ static {
+ try {
+ CLASS =
Class.forName("com.typesafe.config.impl.ConfigNodeComplexValue");
+
+ CHILDREN_FIELD = CLASS.getDeclaredField("children");
+ CHILDREN_FIELD.setAccessible(true);
+ } catch (Exception e) {
+ // Shouldn't happen unless library structure is changed
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean isInstance(Object configNode) {
+ return CLASS.isInstance(configNode);
+ }
+
+ private static List<Object> children(Object configNodeComplexValue) {
+ try {
+ return (List<Object>)
CHILDREN_FIELD.get(configNodeComplexValue);
+ } catch (IllegalAccessException e) {
+ // Shouldn't happen
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private static class ConfigNodeArray {
+ private static final Class<?> CLASS;
+
+ static {
+ try {
+ CLASS =
Class.forName("com.typesafe.config.impl.ConfigNodeArray");
+ } catch (Exception e) {
+ // Shouldn't happen unless library structure is changed
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean isInstance(Object configNode) {
+ return CLASS.isInstance(configNode);
+ }
+ }
+
+ private static class ConfigNodeField {
+ private static final Class<?> CLASS;
+ private static final Field CHILDREN_FIELD;
+
+ static {
+ try {
+ CLASS =
Class.forName("com.typesafe.config.impl.ConfigNodeField");
+
+ CHILDREN_FIELD = CLASS.getDeclaredField("children");
+ CHILDREN_FIELD.setAccessible(true);
+ } catch (Exception e) {
+ // Shouldn't happen unless library structure is changed
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean isInstance(Object configNode) {
+ return CLASS.isInstance(configNode);
+ }
+
+ private static List<Object> children(Object configNode) {
+ try {
+ return (List<Object>) CHILDREN_FIELD.get(configNode);
+ } catch (IllegalAccessException e) {
+ // Shouldn't happen
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private static class SimpleConfigDocument {
+ private static final Class<?> CLASS;
+ private static final Field NODE_TREE_FIELD;
+
+ static {
+ try {
+ CLASS =
Class.forName("com.typesafe.config.impl.SimpleConfigDocument");
+
+ NODE_TREE_FIELD = CLASS.getDeclaredField("configNodeTree");
+ NODE_TREE_FIELD.setAccessible(true);
+ } catch (Exception e) {
+ // Shouldn't happen unless library structure is changed
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean isInstance(Object configDocument) {
+ return CLASS.isInstance(configDocument);
+ }
+
+ private static Object root(Object simpleConfigDocument) {
+ try {
+ return NODE_TREE_FIELD.get(simpleConfigDocument);
+ } catch (IllegalAccessException e) {
+ // Shouldn't happen
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private static class ConfigNodePath {
+ private static final Class<?> CLASS;
+ private static final Class<?> PATH_CLASS;
+ private static final Field PATH_FIELD;
+ private static final Method PATH_RENDER_METHOD;
+
+ static {
+ try {
+ CLASS =
Class.forName("com.typesafe.config.impl.ConfigNodePath");
+ PATH_CLASS = Class.forName("com.typesafe.config.impl.Path");
+
+ PATH_FIELD = CLASS.getDeclaredField("path");
+ PATH_FIELD.setAccessible(true);
+
+ PATH_RENDER_METHOD = PATH_CLASS.getDeclaredMethod("render");
+ PATH_RENDER_METHOD.setAccessible(true);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean isInstance(Object configNode) {
+ return CLASS.isInstance(configNode);
+ }
+
+ /** Returns full path to the given node. Elements of array don't have
a NodePath child, so index is used instead. */
+ private static @Nullable Path path(@Nullable Path basePath,
List<Object> children, @Nullable Integer index) {
+ if (index != null) {
+ return new Path(basePath, "[" + index + "]");
+ }
+
+ Object pathNode = null;
+
+ for (Object child : children) {
+ if (isInstance(child)) {
+ pathNode = child;
+ }
+ }
+
+ if (pathNode == null) {
+ return null;
+ }
+
+ try {
+ Object path = PATH_FIELD.get(pathNode);
+
+ return new Path(basePath,
PATH_RENDER_METHOD.invoke(path).toString());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private static class Node {
+ private final Object configNode;
+ private final @Nullable Path basePath;
+ private final @Nullable Integer indexInArray;
+
+ private Node(Object configNode, @Nullable Path basePath, @Nullable
Integer indexInArray) {
+ this.configNode = configNode;
+ this.basePath = basePath;
+ this.indexInArray = indexInArray;
+ }
+ }
+
+ private static class Path {
+ private final @Nullable Path basePath;
+ private final String path;
+
+ private Path(@Nullable Path basePath, String path) {
+ this.basePath = basePath;
+ this.path = path;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+
+ appendTo(builder);
+
+ return builder.toString();
+ }
+
+ private void appendTo(StringBuilder builder) {
+ if (basePath != null) {
+ basePath.appendTo(builder);
+ builder.append('.');
+ }
+
+ builder.append(path);
+ }
+ }
+}
diff --git
a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/validation/ConfigurationDuplicatesValidatorTest.java
b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/validation/ConfigurationDuplicatesValidatorTest.java
new file mode 100644
index 00000000000..9d441a773a8
--- /dev/null
+++
b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/validation/ConfigurationDuplicatesValidatorTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.configuration.validation;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.empty;
+
+import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.junit.jupiter.api.Test;
+
+class ConfigurationDuplicatesValidatorTest extends BaseIgniteAbstractTest {
+ private static final String DUPLICATE_CONFIG = "ignite {\n"
+ + " network: {\n"
+ + " port: 456,\n"
+ + " nodeFinder.netClusterNodes: [ \"localhost\" ]\n"
+ + " },\n"
+ + " network: {}\n"
+ + " network.nodeFinder.netClusterNodes: [ \"localhost\" ],\n"
+ + " storage.profiles: {\n"
+ + " test.engine: test, \n"
+ + " default_aipersist.engine: aipersist, \n"
+ + " default_aipersist.size: 123, \n"
+ + " test.engine: aimem \n"
+ + " },\n"
+ + " storage {\n"
+ + " profiles = [\n"
+ + " {\n"
+ + " name = persistent,\n"
+ + " name = persistent\n"
+ + " }\n"
+ + " ]\n"
+ + " },\n"
+ + " clientConnector.port: 123,\n"
+ + " rest {\n"
+ + " port: 123,\n"
+ + " port: 456\n"
+ + " },\n"
+ + " compute { threadPoolSize: 1 },\n"
+ + " compute { threadPoolSize: 2 }\n"
+ + " failureHandler.dumpThreadsOnFailure: false\n"
+ + "}";
+
+ private static final String OK_CONFIG = "ignite {\n"
+ + " network: {\n"
+ + " port: 456,\n"
+ + " nodeFinder.netClusterNodes: [ {} ]\n"
+ + " },\n"
+ + " storage {\n"
+ + " profiles = [\n"
+ + " {\n"
+ + " name = persistent,\n"
+ + " engine = aipersist\n"
+ + " }\n"
+ + " {\n"
+ + " name = rocksdb-example,\n"
+ + " engine = rocksdb\n"
+ + " }\n"
+ + " {\n"
+ + " name = in-memory,\n"
+ + " engine = aimem\n"
+ + " }\n"
+ + " ]\n"
+ + " },\n"
+ + " clientConnector.port: 123,\n"
+ + " rest {\n"
+ + " port: 123\n"
+ + " },\n"
+ + " compute.threadPoolSize: 1,\n"
+ + " failureHandler.dumpThreadsOnFailure: false\n"
+ + "}";
+
+ @Test
+ void testValidationSucceededWithNoDuplicates() {
+ assertThat(ConfigurationDuplicatesValidator.validate(OK_CONFIG),
empty());
+ }
+
+ @Test
+ void testValidationFailedWithDuplicates() {
+ assertThat(ConfigurationDuplicatesValidator.validate(DUPLICATE_CONFIG),
+ containsInAnyOrder(
+ new ValidationIssue("ignite.network", "Duplicated
key"),
+ new
ValidationIssue("ignite.network.nodeFinder.netClusterNodes", "Duplicated key"),
+ new
ValidationIssue("ignite.storage.profiles.[0].name", "Duplicated key"),
+ new ValidationIssue("ignite.storage.profiles",
"Duplicated key"),
+ new
ValidationIssue("ignite.storage.profiles.test.engine", "Duplicated key"),
+ new ValidationIssue("ignite.rest.port", "Duplicated
key"),
+ new ValidationIssue("ignite.compute", "Duplicated
key"),
+ new ValidationIssue("ignite.compute.threadPoolSize",
"Duplicated key")
+ )
+ );
+ }
+}
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItClusterConfigurationTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItClusterConfigurationTest.java
new file mode 100644
index 00000000000..d58fcfa500a
--- /dev/null
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItClusterConfigurationTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.configuration;
+
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCause;
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+
+import java.nio.file.Path;
+import org.apache.ignite.IgniteServer;
+import org.apache.ignite.InitParameters;
+import
org.apache.ignite.configuration.validation.ConfigurationValidationException;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.apache.ignite.internal.testframework.TestIgnitionManager;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Integration test for checking Cluster configuration.
+ */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItClusterConfigurationTest extends BaseIgniteAbstractTest {
+ private IgniteServer node;
+
+ @AfterEach
+ void tearDown() {
+ if (node != null) {
+ node.shutdown();
+ }
+ }
+
+ @Test
+ void testValidationFailsWithDuplicates(TestInfo testInfo, @WorkDirectory
Path workDir) {
+ node = TestIgnitionManager.start(testNodeName(testInfo, 0), null,
workDir);
+
+ var parameters = InitParameters.builder()
+ .metaStorageNodes(node)
+ .clusterName("cluster")
+ .clusterConfiguration("ignite { metaStorage {
idleSyncTimeInterval: 50, idleSyncTimeInterval: 100 }}")
+ .build();
+
+ assertThrowsWithCause(
+ () -> TestIgnitionManager.init(node, parameters),
+ ConfigurationValidationException.class,
+ "Validation did not pass for keys:
[ignite.metaStorage.idleSyncTimeInterval, Duplicated key]");
+ }
+}
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItNodeBootstrapConfigurationTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItNodeBootstrapConfigurationTest.java
index bb826188e94..0c21dae9db6 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItNodeBootstrapConfigurationTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItNodeBootstrapConfigurationTest.java
@@ -100,4 +100,21 @@ public class ItNodeBootstrapConfigurationTest {
ConfigurationValidationException.class,
"Validation did not pass for keys: [ignite.rest.ssl.keyStore,
Key store file doesn't exist at bad_path]");
}
+
+ @Test
+ public void testConfigurationValidationFailsWithDuplicates(TestInfo
testInfo) {
+ String config = "ignite {\n"
+ + " rest: {\n"
+ + " ssl: {\n"
+ + " enabled: false,\n"
+ + " enabled: true\n"
+ + " }\n"
+ + " }\n"
+ + "}";
+
+ assertThrowsWithCause(
+ () -> TestIgnitionManager.start(testNodeName(testInfo, 0),
config, workDir),
+ ConfigurationValidationException.class,
+ "Validation did not pass for keys: [ignite.rest.ssl.enabled,
Duplicated key]");
+ }
}
diff --git
a/modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java
b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java
index 96e431c447b..23e532f79f4 100644
---
a/modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java
+++
b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java
@@ -40,6 +40,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
+import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
@@ -54,6 +55,7 @@ import
org.apache.ignite.configuration.ConfigurationDynamicDefaultsPatcher;
import org.apache.ignite.configuration.ConfigurationModule;
import org.apache.ignite.configuration.annotation.ConfigurationType;
import
org.apache.ignite.configuration.validation.ConfigurationValidationException;
+import org.apache.ignite.configuration.validation.ValidationIssue;
import
org.apache.ignite.internal.configuration.ConfigurationDynamicDefaultsPatcherImpl;
import org.apache.ignite.internal.configuration.ConfigurationTreeGenerator;
import org.apache.ignite.internal.configuration.NodeConfigCreateException;
@@ -62,6 +64,7 @@ import
org.apache.ignite.internal.configuration.NodeConfigWriteException;
import org.apache.ignite.internal.configuration.SuperRoot;
import org.apache.ignite.internal.configuration.hocon.HoconConverter;
import org.apache.ignite.internal.configuration.tree.ConverterToMapVisitor;
+import
org.apache.ignite.internal.configuration.validation.ConfigurationDuplicatesValidator;
import org.apache.ignite.internal.future.InFlightFutures;
import org.apache.ignite.internal.logger.IgniteLogger;
import org.apache.ignite.internal.logger.Loggers;
@@ -188,10 +191,16 @@ public class LocalFileConfigurationStorage implements
ConfigurationStorage {
try {
String confString = Files.readString(configPath.toAbsolutePath());
+ Collection<ValidationIssue> duplicates =
ConfigurationDuplicatesValidator.validate(confString);
+
+ if (!duplicates.isEmpty()) {
+ throw new ConfigurationValidationException(duplicates);
+ }
+
ConfigParseOptions parseOptions =
ConfigParseOptions.defaults().setSyntax(ConfigSyntax.CONF).setAllowMissing(false);
return ConfigFactory.parseString(patch(confString, module),
parseOptions);
- } catch (Parse | ConfigurationValidationException | IOException e) {
+ } catch (Parse | IOException e) {
throw new NodeConfigParseException("Failed to parse config content
from file " + configPath, e);
}
}
diff --git
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorageTest.java
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorageTest.java
index bae8dc18c6b..ca9fe676b36 100644
---
a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorageTest.java
+++
b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorageTest.java
@@ -17,6 +17,7 @@
package org.apache.ignite.internal.configuration.storage;
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrows;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.allOf;
@@ -44,6 +45,7 @@ import org.apache.ignite.configuration.annotation.ConfigValue;
import org.apache.ignite.configuration.annotation.ConfigurationRoot;
import org.apache.ignite.configuration.annotation.NamedConfigValue;
import org.apache.ignite.configuration.annotation.Value;
+import
org.apache.ignite.configuration.validation.ConfigurationValidationException;
import org.apache.ignite.internal.configuration.ConfigurationTreeGenerator;
import org.apache.ignite.internal.configuration.TestConfigurationChanger;
import
org.apache.ignite.internal.configuration.validation.ConfigurationValidatorImpl;
@@ -536,6 +538,35 @@ public class LocalFileConfigurationStorageTest {
assertDoesNotThrow(storage::readDataOnRecovery);
}
+ @Test
+ void testValidateDuplicates() throws IOException {
+ // Given config in JSON format
+ String fileContent
+ = "top {\n"
+ + " inner {\n"
+ + " boolVal=false,\n"
+ + " boolVal=true\n"
+ + " someConfigurationValue {\n"
+ + " intVal=1\n"
+ + " strVal=foo\n"
+ + " }\n"
+ + " strVal=foo\n"
+ + " }\n"
+ + " shortVal=3\n"
+ + "}\n";
+
+ Path configFile = getConfigFile();
+
+ Files.write(configFile, fileContent.getBytes(StandardCharsets.UTF_8));
+
+ // And storage detects duplicates
+ assertThrows(
+ ConfigurationValidationException.class,
+ changer::start,
+ "Validation did not pass for keys: [top.inner.boolVal,
Duplicated key]"
+ );
+ }
+
private String configFileContent() throws IOException {
return Files.readString(getConfigFile());
}
diff --git
a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/distributed/disaster/ItHighAvailablePartitionsRecoveryByFilterUpdateTest.java
b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/distributed/disaster/ItHighAvailablePartitionsRecoveryByFilterUpdateTest.java
index f64ea96a128..18d1a24ed7c 100644
---
a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/distributed/disaster/ItHighAvailablePartitionsRecoveryByFilterUpdateTest.java
+++
b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/distributed/disaster/ItHighAvailablePartitionsRecoveryByFilterUpdateTest.java
@@ -132,7 +132,7 @@ public class
ItHighAvailablePartitionsRecoveryByFilterUpdateTest extends Abstrac
) {
return "ignite {\n"
+ " nodeAttributes.nodeAttributes: " + nodeAtrributes + ",\n"
- + " storage.profiles: " + storageProfiles + ",\n"
+ + (storageProfiles == null ? "" : " storage.profiles: " +
storageProfiles + ",\n")
+ " network: {\n"
+ " port: {},\n"
+ " nodeFinder: {\n"