ibessonov commented on code in PR #1640:
URL: https://github.com/apache/ignite-3/pull/1640#discussion_r1102402474


##########
modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/configuration/ItConfigCommandTest.java:
##########
@@ -67,6 +67,31 @@ void addConfigKeyValue() {
         );
     }
 
+    @Test
+    @DisplayName("Should update config with hocon format when valid 
cluster-endpoint-url is given")
+    void addNodeConfigKeyValue() {
+        // When update default data storage to rocksdb
+        execute("node", "config", "update", "--node-url", NODE_URL,
+                "network.nodeFinder.netClusterNodes : [ \"localhost:3344\", 
\"localhost:3345\" ]");
+
+        // Then
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertErrOutputIsEmpty,
+                this::assertOutputIsNotEmpty
+        );
+
+        // When read the updated cluster configuration
+        execute("node", "config", "show", "--node-url", NODE_URL);
+
+        // Then
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertErrOutputIsEmpty,
+                () -> assertOutputContains("\"netClusterNodes\" : [ 
\"localhost:3344\", \"localhost:3345\" ]")

Review Comment:
   Do we render it as JSON? Is this convenient?



##########
modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java:
##########
@@ -84,12 +84,12 @@ public void stop() throws Exception {
     /**
      * Bootstrap configuration manager with customer user cfg.
      *
-     * @param hoconStr Customer configuration in hocon format.
+     * @param hoconPath Customer configuration in hocon format.
      * @throws InterruptedException If thread is interrupted during bootstrap.
      * @throws ExecutionException   If configuration update failed for some 
reason.
      */
-    public void bootstrap(@Language("HOCON") String hoconStr) throws 
InterruptedException, ExecutionException {
-        ConfigObject hoconCfg = ConfigFactory.parseString(hoconStr).root();
+    public void bootstrap(Path hoconPath) throws InterruptedException, 
ExecutionException {

Review Comment:
   `configPath` maybe? Hocon is a data format. When people say JsonPath or 
xmlPath, they don't mean the path to the file, they mean the path inside of the 
file I guess.



##########
modules/core/src/test/java/org/apache/ignite/internal/lang/ErrorGroupsArchTest.java:
##########
@@ -0,0 +1,89 @@
+/*
+ * 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.lang;
+
+import com.tngtech.archunit.base.Optional;
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.domain.JavaField;
+import com.tngtech.archunit.core.importer.Location;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.junit.LocationProvider;
+import com.tngtech.archunit.lang.ArchCondition;
+import com.tngtech.archunit.lang.ArchRule;
+import com.tngtech.archunit.lang.ConditionEvents;
+import com.tngtech.archunit.lang.SimpleConditionEvent;
+import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Set;
+import 
org.apache.ignite.internal.lang.ErrorGroupsArchTest.CoreLocationProvider;
+import org.apache.ignite.lang.ErrorGroups;
+
+/**
+ * Test that all error code groups has correct naming.
+ */
+@AnalyzeClasses(
+        packages = "org.apache.ignite.lang",
+        locations = CoreLocationProvider.class)
+public class ErrorGroupsArchTest {
+
+    static class CoreLocationProvider implements LocationProvider {
+        @Override
+        public Set<Location> get(Class<?> testClass) {
+            // ignite-3/modules/core
+            Path modulesRoot = Path.of("").toAbsolutePath();
+
+            return Set.of(Location.of(modulesRoot));
+        }
+    }
+
+    @SuppressWarnings("unused")
+    @ArchTest
+    public static final ArchRule IGNITE_EXCEPTIONS_HAVE_REQUIRED_CONSTRUCTORS 
= ArchRuleDefinition.classes().that().areNestedClasses()
+            .should(new ArchCondition<>("") {
+                @Override
+                public void check(JavaClass javaClass, ConditionEvents 
conditionEvents) {
+                    Optional<JavaClass> enclosingClass = 
javaClass.getEnclosingClass();
+                    if (enclosingClass.isPresent() && 
enclosingClass.get().getFullName().equals(ErrorGroups.class.getName())) {
+                        boolean groupFound = false;
+                        for (JavaField field : javaClass.getFields()) {
+                            if 
(Objects.equals(field.getType().toErasure().reflect(), ErrorGroups.class)) {
+                                if (!groupFound) {
+                                    groupFound = true;
+                                    if (!field.getName().endsWith("GROUP")) {
+                                        
conditionEvents.add(SimpleConditionEvent.violated(field,
+                                                "Group definition field name 
must end with GROUP suffix"));
+                                    }
+                                } else {
+                                    
conditionEvents.add(SimpleConditionEvent.violated(field,
+                                            "Each error group class must have 
only one error group definition."));
+                                }
+                            }
+                            if 
(Objects.equals(field.getRawType().getFullName(), "int")) {
+                                if (!field.getName().endsWith("ERR")) {
+                                    
conditionEvents.add(SimpleConditionEvent.violated(field,
+                                            "Error code definition field name 
must end with ERR suffix"));
+                                }
+                            }
+                        }
+                    }
+                }
+            });
+

Review Comment:
   ```suggestion
   ```



##########
modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java:
##########
@@ -532,7 +535,7 @@ private static ConfigurationModules 
loadConfigurationModules(@Nullable ClassLoad
      *         previously use default values. Please pay attention that 
previously specified properties are searched in the
      *         {@code workDir} specified by the user.
      */
-    public CompletableFuture<Ignite> start(@Language("HOCON") @Nullable String 
cfg) {
+    public CompletableFuture<Ignite> start(@NotNull NodeBootstrapConfiguration 
cfg) {

Review Comment:
   We don't use `@NotNull`, please remove it



##########
modules/core/src/main/java/org/apache/ignite/lang/ErrorGroups.java:
##########
@@ -359,4 +359,24 @@ public static class Network {
         /** Unresolvable consistent ID. */
         public static final int UNRESOLVABLE_CONSISTENT_ID_ERR = 
NETWORK_ERR_GROUP.registerErrorCode(1);
     }
+
+    /**
+     * Node configuration error group.
+     */
+    public static class NodeConfiguration {
+        /**
+         * Node configuration error group.
+         */
+        public static final ErrorGroup NODE_CONFIGURATION_ERR_GROUP = 
ErrorGroup.newGroup("NODECFG", 12);
+
+        /**
+         * Config read error.
+         */
+        public static final int CONFIG_READ_ERR = 
NODE_CONFIGURATION_ERR_GROUP.registerErrorCode(1);
+
+        public static final int CONFIG_FILE_CREATE_ERR = 
NODE_CONFIGURATION_ERR_GROUP.registerErrorCode(2);

Review Comment:
   These errors lack javadocs



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();
+
+    private final AtomicReference<ConfigurationStorageListener> lsnrRef = new 
AtomicReference<>();
+
+    private final ExecutorService threadPool = Executors.newFixedThreadPool(4, 
new NamedThreadFactory("loc-cfg-file", LOG));
+
+    private final InFlightFutures futureTracker = new InFlightFutures();
+
+    public LocalFileConfigurationStorage(NodeBootstrapConfiguration 
configuration) {
+        this.configPath = configuration.config();
+        ensureConfigFile();
+    }
+
+    @Override
+    public CompletableFuture<Data> readDataOnRecovery() {
+        return readAllLatest("").thenApply(stringMap -> new Data(stringMap, 
0));
+    }
+
+    @Override
+    public CompletableFuture<Map<String, ? extends Serializable>> 
readAllLatest(String prefix) {
+        ensureConfigFile();
+        Map<String, Serializable> map = new HashMap<>();
+        if (!lastState.isEmpty()) {
+            for (Entry<String, ConfigValue> e : 
parseConfigOptions().entrySet()) {
+                String key = e.getKey();
+                if (key.startsWith(prefix)) {
+                    ConfigValue value = e.getValue();
+                    if (value.valueType() == ConfigValueType.LIST) {
+                        map.put(key, toArray(lastState.get(key).getClass(), 
(ConfigList) value));

Review Comment:
   There's no validation. Why don't we read values from `lastState`? It would 
be so much safer



##########
modules/core/src/test/java/org/apache/ignite/internal/lang/ErrorGroupsArchTest.java:
##########
@@ -0,0 +1,89 @@
+/*
+ * 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.lang;
+
+import com.tngtech.archunit.base.Optional;
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.domain.JavaField;
+import com.tngtech.archunit.core.importer.Location;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.junit.LocationProvider;
+import com.tngtech.archunit.lang.ArchCondition;
+import com.tngtech.archunit.lang.ArchRule;
+import com.tngtech.archunit.lang.ConditionEvents;
+import com.tngtech.archunit.lang.SimpleConditionEvent;
+import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Set;
+import 
org.apache.ignite.internal.lang.ErrorGroupsArchTest.CoreLocationProvider;
+import org.apache.ignite.lang.ErrorGroups;
+
+/**
+ * Test that all error code groups has correct naming.
+ */
+@AnalyzeClasses(
+        packages = "org.apache.ignite.lang",
+        locations = CoreLocationProvider.class)
+public class ErrorGroupsArchTest {
+
+    static class CoreLocationProvider implements LocationProvider {
+        @Override
+        public Set<Location> get(Class<?> testClass) {
+            // ignite-3/modules/core
+            Path modulesRoot = Path.of("").toAbsolutePath();
+
+            return Set.of(Location.of(modulesRoot));
+        }
+    }
+
+    @SuppressWarnings("unused")
+    @ArchTest
+    public static final ArchRule IGNITE_EXCEPTIONS_HAVE_REQUIRED_CONSTRUCTORS 
= ArchRuleDefinition.classes().that().areNestedClasses()

Review Comment:
   I thought we already have such test somewhere, and that it would cover all 
modules.
   Maybe the name of the constant is misleading?



##########
modules/core/src/test/java/org/apache/ignite/internal/lang/ErrorGroupsArchTest.java:
##########
@@ -0,0 +1,89 @@
+/*
+ * 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.lang;
+
+import com.tngtech.archunit.base.Optional;
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.domain.JavaField;
+import com.tngtech.archunit.core.importer.Location;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.junit.LocationProvider;
+import com.tngtech.archunit.lang.ArchCondition;
+import com.tngtech.archunit.lang.ArchRule;
+import com.tngtech.archunit.lang.ConditionEvents;
+import com.tngtech.archunit.lang.SimpleConditionEvent;
+import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Set;
+import 
org.apache.ignite.internal.lang.ErrorGroupsArchTest.CoreLocationProvider;
+import org.apache.ignite.lang.ErrorGroups;
+
+/**
+ * Test that all error code groups has correct naming.
+ */
+@AnalyzeClasses(
+        packages = "org.apache.ignite.lang",
+        locations = CoreLocationProvider.class)
+public class ErrorGroupsArchTest {
+
+    static class CoreLocationProvider implements LocationProvider {
+        @Override
+        public Set<Location> get(Class<?> testClass) {
+            // ignite-3/modules/core
+            Path modulesRoot = Path.of("").toAbsolutePath();
+
+            return Set.of(Location.of(modulesRoot));
+        }
+    }
+
+    @SuppressWarnings("unused")
+    @ArchTest
+    public static final ArchRule IGNITE_EXCEPTIONS_HAVE_REQUIRED_CONSTRUCTORS 
= ArchRuleDefinition.classes().that().areNestedClasses()
+            .should(new ArchCondition<>("") {

Review Comment:
   Why is description empty?



##########
modules/core/src/test/java/org/apache/ignite/internal/lang/ErrorGroupsArchTest.java:
##########
@@ -0,0 +1,89 @@
+/*
+ * 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.lang;
+
+import com.tngtech.archunit.base.Optional;
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.domain.JavaField;
+import com.tngtech.archunit.core.importer.Location;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.junit.LocationProvider;
+import com.tngtech.archunit.lang.ArchCondition;
+import com.tngtech.archunit.lang.ArchRule;
+import com.tngtech.archunit.lang.ConditionEvents;
+import com.tngtech.archunit.lang.SimpleConditionEvent;
+import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Set;
+import 
org.apache.ignite.internal.lang.ErrorGroupsArchTest.CoreLocationProvider;
+import org.apache.ignite.lang.ErrorGroups;
+
+/**
+ * Test that all error code groups has correct naming.
+ */
+@AnalyzeClasses(
+        packages = "org.apache.ignite.lang",
+        locations = CoreLocationProvider.class)
+public class ErrorGroupsArchTest {
+
+    static class CoreLocationProvider implements LocationProvider {
+        @Override
+        public Set<Location> get(Class<?> testClass) {
+            // ignite-3/modules/core
+            Path modulesRoot = Path.of("").toAbsolutePath();
+
+            return Set.of(Location.of(modulesRoot));
+        }
+    }
+
+    @SuppressWarnings("unused")
+    @ArchTest
+    public static final ArchRule IGNITE_EXCEPTIONS_HAVE_REQUIRED_CONSTRUCTORS 
= ArchRuleDefinition.classes().that().areNestedClasses()
+            .should(new ArchCondition<>("") {
+                @Override
+                public void check(JavaClass javaClass, ConditionEvents 
conditionEvents) {
+                    Optional<JavaClass> enclosingClass = 
javaClass.getEnclosingClass();
+                    if (enclosingClass.isPresent() && 
enclosingClass.get().getFullName().equals(ErrorGroups.class.getName())) {
+                        boolean groupFound = false;
+                        for (JavaField field : javaClass.getFields()) {
+                            if 
(Objects.equals(field.getType().toErasure().reflect(), ErrorGroups.class)) {
+                                if (!groupFound) {
+                                    groupFound = true;
+                                    if (!field.getName().endsWith("GROUP")) {
+                                        
conditionEvents.add(SimpleConditionEvent.violated(field,
+                                                "Group definition field name 
must end with GROUP suffix"));
+                                    }
+                                } else {
+                                    
conditionEvents.add(SimpleConditionEvent.violated(field,
+                                            "Each error group class must have 
only one error group definition."));
+                                }
+                            }
+                            if 
(Objects.equals(field.getRawType().getFullName(), "int")) {

Review Comment:
   Same question here. Why not comparing the type with `int.class` or 
`Integer.TYPE`?



##########
modules/core/src/test/java/org/apache/ignite/internal/lang/ErrorGroupsArchTest.java:
##########
@@ -0,0 +1,89 @@
+/*
+ * 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.lang;
+
+import com.tngtech.archunit.base.Optional;
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.domain.JavaField;
+import com.tngtech.archunit.core.importer.Location;
+import com.tngtech.archunit.junit.AnalyzeClasses;
+import com.tngtech.archunit.junit.ArchTest;
+import com.tngtech.archunit.junit.LocationProvider;
+import com.tngtech.archunit.lang.ArchCondition;
+import com.tngtech.archunit.lang.ArchRule;
+import com.tngtech.archunit.lang.ConditionEvents;
+import com.tngtech.archunit.lang.SimpleConditionEvent;
+import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Set;
+import 
org.apache.ignite.internal.lang.ErrorGroupsArchTest.CoreLocationProvider;
+import org.apache.ignite.lang.ErrorGroups;
+
+/**
+ * Test that all error code groups has correct naming.
+ */
+@AnalyzeClasses(
+        packages = "org.apache.ignite.lang",
+        locations = CoreLocationProvider.class)
+public class ErrorGroupsArchTest {
+
+    static class CoreLocationProvider implements LocationProvider {
+        @Override
+        public Set<Location> get(Class<?> testClass) {
+            // ignite-3/modules/core
+            Path modulesRoot = Path.of("").toAbsolutePath();
+
+            return Set.of(Location.of(modulesRoot));
+        }
+    }
+
+    @SuppressWarnings("unused")
+    @ArchTest
+    public static final ArchRule IGNITE_EXCEPTIONS_HAVE_REQUIRED_CONSTRUCTORS 
= ArchRuleDefinition.classes().that().areNestedClasses()
+            .should(new ArchCondition<>("") {
+                @Override
+                public void check(JavaClass javaClass, ConditionEvents 
conditionEvents) {
+                    Optional<JavaClass> enclosingClass = 
javaClass.getEnclosingClass();
+                    if (enclosingClass.isPresent() && 
enclosingClass.get().getFullName().equals(ErrorGroups.class.getName())) {
+                        boolean groupFound = false;
+                        for (JavaField field : javaClass.getFields()) {
+                            if 
(Objects.equals(field.getType().toErasure().reflect(), ErrorGroups.class)) {

Review Comment:
   You can compare classes using `==`, why don't you do it?



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();
+
+    private final AtomicReference<ConfigurationStorageListener> lsnrRef = new 
AtomicReference<>();
+
+    private final ExecutorService threadPool = Executors.newFixedThreadPool(4, 
new NamedThreadFactory("loc-cfg-file", LOG));
+
+    private final InFlightFutures futureTracker = new InFlightFutures();
+
+    public LocalFileConfigurationStorage(NodeBootstrapConfiguration 
configuration) {
+        this.configPath = configuration.config();
+        ensureConfigFile();
+    }
+
+    @Override
+    public CompletableFuture<Data> readDataOnRecovery() {
+        return readAllLatest("").thenApply(stringMap -> new Data(stringMap, 
0));
+    }
+
+    @Override
+    public CompletableFuture<Map<String, ? extends Serializable>> 
readAllLatest(String prefix) {
+        ensureConfigFile();
+        Map<String, Serializable> map = new HashMap<>();
+        if (!lastState.isEmpty()) {
+            for (Entry<String, ConfigValue> e : 
parseConfigOptions().entrySet()) {
+                String key = e.getKey();
+                if (key.startsWith(prefix)) {
+                    ConfigValue value = e.getValue();
+                    if (value.valueType() == ConfigValueType.LIST) {
+                        map.put(key, toArray(lastState.get(key).getClass(), 
(ConfigList) value));
+                    } else {
+                        map.put(key, (Serializable) value.unwrapped());
+                    }
+                }
+            }
+        }
+        return CompletableFuture.completedFuture(map);
+    }
+
+    @Override
+    public CompletableFuture<Serializable> readLatest(String key) {
+        ensureConfigFile();
+        return CompletableFuture.completedFuture((Serializable) 
parseConfigOptions().getValue(key).unwrapped());
+    }
+
+    @Override
+    public CompletableFuture<Boolean> write(Map<String, ? extends 
Serializable> newValues, long ver) {
+        ensureConfigFile();
+        saveValues(newValues);
+        lastState.putAll(newValues);
+        runAsync(() -> lsnrRef.get().onEntriesChanged(new Data(newValues, 0)));
+        return CompletableFuture.completedFuture(true);
+    }
+
+    private void runAsync(Runnable runnable) {
+        CompletableFuture<Void> future = CompletableFuture.runAsync(runnable, 
threadPool);
+
+        futureTracker.registerFuture(future);
+    }
+
+    @Override
+    public void registerConfigurationListener(ConfigurationStorageListener 
lsnr) {
+        if (!lsnrRef.compareAndSet(null, lsnr)) {
+            LOG.debug("Configuration listener has already been set");
+        }
+    }
+
+    @Override
+    public ConfigurationType type() {
+        return ConfigurationType.LOCAL;
+    }
+
+    @Override
+    public CompletableFuture<Long> lastRevision() {
+        return CompletableFuture.completedFuture(0L);
+    }
+
+    @Override
+    public CompletableFuture<Void> writeConfigurationRevision(long 
prevRevision, long currentRevision) {
+        return CompletableFuture.completedFuture(null);
+    }
+
+    @Override
+    public void close() {
+        IgniteUtils.shutdownAndAwaitTermination(threadPool, 10, 
TimeUnit.SECONDS);
+
+        futureTracker.cancelInFlightFutures();
+    }
+
+    private String toString(Map<String, ? extends Serializable> values) {
+        Map<String, Object> map = 
values.entrySet().stream().collect(Collectors.toMap(Entry::getKey, stringEntry 
-> {
+            Serializable value = stringEntry.getValue();
+            if (value.getClass().isArray()) {
+                return Arrays.asList((Object[]) value);
+            }
+            return value;
+        }));
+        Config other = ConfigFactory.parseMap(map);
+        Config newConfig = parseConfigOptions().withFallback(other).resolve();
+        return newConfig.isEmpty()
+                ? ""
+                : 
newConfig.root().render(ConfigRenderOptions.defaults().setJson(false));
+    }
+
+    private void saveValues(Map<String, ? extends Serializable> values) {
+        try {
+            Files.write(configPath, 
toString(values).getBytes(StandardCharsets.UTF_8));

Review Comment:
   Not safe file writing, once again. At first, you should write data in 
another file, and only then you have a right to corrupt/delete old file, 
because new one is already on disk.
   On top of that, we need a "recovery" procedure that would delete tmp file or 
maybe rename it to proper config file if it's already missing



##########
modules/core/src/main/java/org/apache/ignite/lang/ErrorGroups.java:
##########
@@ -359,4 +359,24 @@ public static class Network {
         /** Unresolvable consistent ID. */
         public static final int UNRESOLVABLE_CONSISTENT_ID_ERR = 
NETWORK_ERR_GROUP.registerErrorCode(1);
     }
+
+    /**
+     * Node configuration error group.
+     */
+    public static class NodeConfiguration {
+        /**
+         * Node configuration error group.
+         */
+        public static final ErrorGroup NODE_CONFIGURATION_ERR_GROUP = 
ErrorGroup.newGroup("NODECFG", 12);
+
+        /**
+         * Config read error.
+         */
+        public static final int CONFIG_READ_ERR = 
NODE_CONFIGURATION_ERR_GROUP.registerErrorCode(1);
+
+        public static final int CONFIG_FILE_CREATE_ERR = 
NODE_CONFIGURATION_ERR_GROUP.registerErrorCode(2);
+
+        public static final int CONFIG_WRITE_ERR = 
NODE_CONFIGURATION_ERR_GROUP.registerErrorCode(3);
+

Review Comment:
   ```suggestion
   ```



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();
+
+    private final AtomicReference<ConfigurationStorageListener> lsnrRef = new 
AtomicReference<>();
+
+    private final ExecutorService threadPool = Executors.newFixedThreadPool(4, 
new NamedThreadFactory("loc-cfg-file", LOG));
+
+    private final InFlightFutures futureTracker = new InFlightFutures();
+
+    public LocalFileConfigurationStorage(NodeBootstrapConfiguration 
configuration) {
+        this.configPath = configuration.config();
+        ensureConfigFile();
+    }
+
+    @Override
+    public CompletableFuture<Data> readDataOnRecovery() {
+        return readAllLatest("").thenApply(stringMap -> new Data(stringMap, 
0));
+    }
+
+    @Override
+    public CompletableFuture<Map<String, ? extends Serializable>> 
readAllLatest(String prefix) {
+        ensureConfigFile();
+        Map<String, Serializable> map = new HashMap<>();
+        if (!lastState.isEmpty()) {
+            for (Entry<String, ConfigValue> e : 
parseConfigOptions().entrySet()) {
+                String key = e.getKey();
+                if (key.startsWith(prefix)) {
+                    ConfigValue value = e.getValue();
+                    if (value.valueType() == ConfigValueType.LIST) {
+                        map.put(key, toArray(lastState.get(key).getClass(), 
(ConfigList) value));
+                    } else {
+                        map.put(key, (Serializable) value.unwrapped());
+                    }
+                }
+            }
+        }
+        return CompletableFuture.completedFuture(map);
+    }
+
+    @Override
+    public CompletableFuture<Serializable> readLatest(String key) {
+        ensureConfigFile();
+        return CompletableFuture.completedFuture((Serializable) 
parseConfigOptions().getValue(key).unwrapped());
+    }
+
+    @Override
+    public CompletableFuture<Boolean> write(Map<String, ? extends 
Serializable> newValues, long ver) {
+        ensureConfigFile();
+        saveValues(newValues);
+        lastState.putAll(newValues);
+        runAsync(() -> lsnrRef.get().onEntriesChanged(new Data(newValues, 0)));
+        return CompletableFuture.completedFuture(true);
+    }
+
+    private void runAsync(Runnable runnable) {
+        CompletableFuture<Void> future = CompletableFuture.runAsync(runnable, 
threadPool);
+
+        futureTracker.registerFuture(future);
+    }
+
+    @Override
+    public void registerConfigurationListener(ConfigurationStorageListener 
lsnr) {
+        if (!lsnrRef.compareAndSet(null, lsnr)) {
+            LOG.debug("Configuration listener has already been set");
+        }
+    }
+
+    @Override
+    public ConfigurationType type() {
+        return ConfigurationType.LOCAL;
+    }
+
+    @Override
+    public CompletableFuture<Long> lastRevision() {
+        return CompletableFuture.completedFuture(0L);

Review Comment:
   Wow, why? Class is clearly not completed.
   Too bad that 
`org.apache.ignite.internal.configuration.storage.ConfigurationStorageTest` is 
not sophisticated enough, that's on us...



##########
modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java:
##########
@@ -261,7 +261,10 @@ public class IgniteImpl implements Ignite {
      * @param serviceProviderClassLoader The class loader to be used to load 
provider-configuration files and provider classes, or
      *         {@code null} if the system class loader (or, failing that the 
bootstrap class loader) is to be used.
      */
-    IgniteImpl(String name, Path workDir, @Nullable ClassLoader 
serviceProviderClassLoader) {
+    IgniteImpl(String name,
+            NodeBootstrapConfiguration configuration,
+            Path workDir,
+            @Nullable ClassLoader serviceProviderClassLoader) {

Review Comment:
   ```suggestion
               @Nullable ClassLoader serviceProviderClassLoader
   ) {
   ```



##########
modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java:
##########
@@ -261,7 +261,10 @@ public class IgniteImpl implements Ignite {
      * @param serviceProviderClassLoader The class loader to be used to load 
provider-configuration files and provider classes, or
      *         {@code null} if the system class loader (or, failing that the 
bootstrap class loader) is to be used.
      */
-    IgniteImpl(String name, Path workDir, @Nullable ClassLoader 
serviceProviderClassLoader) {
+    IgniteImpl(String name,
+            NodeBootstrapConfiguration configuration,
+            Path workDir,
+            @Nullable ClassLoader serviceProviderClassLoader) {

Review Comment:
   Looks prettier this way :)



##########
modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java:
##########
@@ -548,14 +551,11 @@ public CompletableFuture<Ignite> start(@Language("HOCON") 
@Nullable String cfg)
             lifecycleManager.startComponent(nodeCfgMgr);
 
             // Node configuration manager bootstrap.
-            if (cfg != null) {
-                try {
-                    nodeCfgMgr.bootstrap(cfg);
-                } catch (Exception e) {
-                    throw new IgniteException("Unable to parse user-specific 
configuration", e);
-                }
-            } else {
-                nodeCfgMgr.configurationRegistry().initializeDefaults();
+
+            try {
+                nodeCfgMgr.bootstrap(cfg.config());
+            } catch (Exception e) {
+                throw new IgniteException("Unable to parse user-specific 
configuration", e);

Review Comment:
   You don't use configuration error codes here, why?



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/NodeBootstrapConfiguration.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicReference;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Node bootstrap configuration provider interface.
+ */
+@FunctionalInterface
+public interface NodeBootstrapConfiguration {
+    /**
+     * Path to node configuration file.
+     *
+     * @return Path to node configuration file in HOCON format.
+     */
+    Path config();

Review Comment:
   `configPath` maybe?



##########
modules/runner/src/test/java/org/apache/ignite/internal/configuration/storage/ConfigurationStorageTest.java:
##########
@@ -78,4 +78,12 @@ public void testReadAllLatest() {
 
         assertThat(latestData, willBe(anEmptyMap()));
     }
+
+    @Test
+    public void testRealConfig() {
+        Map<String, Serializable> data = Map.of("network.port", 3344,
+                "network.portRange", 5, "network.nodeFinder.netClusterNode", 
new String[] {"localhost:3344", "localhost:3345"});
+
+        storage.write(data, 0);

Review Comment:
   Don't we need to check something after writing data?
   What's up with the test name? What do you mean by "real config"?



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();
+
+    private final AtomicReference<ConfigurationStorageListener> lsnrRef = new 
AtomicReference<>();
+
+    private final ExecutorService threadPool = Executors.newFixedThreadPool(4, 
new NamedThreadFactory("loc-cfg-file", LOG));
+
+    private final InFlightFutures futureTracker = new InFlightFutures();
+
+    public LocalFileConfigurationStorage(NodeBootstrapConfiguration 
configuration) {
+        this.configPath = configuration.config();
+        ensureConfigFile();
+    }
+
+    @Override
+    public CompletableFuture<Data> readDataOnRecovery() {
+        return readAllLatest("").thenApply(stringMap -> new Data(stringMap, 
0));
+    }
+
+    @Override
+    public CompletableFuture<Map<String, ? extends Serializable>> 
readAllLatest(String prefix) {
+        ensureConfigFile();
+        Map<String, Serializable> map = new HashMap<>();
+        if (!lastState.isEmpty()) {
+            for (Entry<String, ConfigValue> e : 
parseConfigOptions().entrySet()) {
+                String key = e.getKey();
+                if (key.startsWith(prefix)) {
+                    ConfigValue value = e.getValue();
+                    if (value.valueType() == ConfigValueType.LIST) {
+                        map.put(key, toArray(lastState.get(key).getClass(), 
(ConfigList) value));
+                    } else {
+                        map.put(key, (Serializable) value.unwrapped());
+                    }
+                }
+            }
+        }
+        return CompletableFuture.completedFuture(map);
+    }
+
+    @Override
+    public CompletableFuture<Serializable> readLatest(String key) {
+        ensureConfigFile();
+        return CompletableFuture.completedFuture((Serializable) 
parseConfigOptions().getValue(key).unwrapped());
+    }
+
+    @Override
+    public CompletableFuture<Boolean> write(Map<String, ? extends 
Serializable> newValues, long ver) {
+        ensureConfigFile();
+        saveValues(newValues);
+        lastState.putAll(newValues);
+        runAsync(() -> lsnrRef.get().onEntriesChanged(new Data(newValues, 0)));
+        return CompletableFuture.completedFuture(true);
+    }
+
+    private void runAsync(Runnable runnable) {
+        CompletableFuture<Void> future = CompletableFuture.runAsync(runnable, 
threadPool);
+
+        futureTracker.registerFuture(future);
+    }
+
+    @Override
+    public void registerConfigurationListener(ConfigurationStorageListener 
lsnr) {
+        if (!lsnrRef.compareAndSet(null, lsnr)) {
+            LOG.debug("Configuration listener has already been set");
+        }
+    }
+
+    @Override
+    public ConfigurationType type() {
+        return ConfigurationType.LOCAL;
+    }
+
+    @Override
+    public CompletableFuture<Long> lastRevision() {
+        return CompletableFuture.completedFuture(0L);
+    }
+
+    @Override
+    public CompletableFuture<Void> writeConfigurationRevision(long 
prevRevision, long currentRevision) {
+        return CompletableFuture.completedFuture(null);
+    }
+
+    @Override
+    public void close() {
+        IgniteUtils.shutdownAndAwaitTermination(threadPool, 10, 
TimeUnit.SECONDS);
+
+        futureTracker.cancelInFlightFutures();
+    }
+
+    private String toString(Map<String, ? extends Serializable> values) {
+        Map<String, Object> map = 
values.entrySet().stream().collect(Collectors.toMap(Entry::getKey, stringEntry 
-> {
+            Serializable value = stringEntry.getValue();
+            if (value.getClass().isArray()) {
+                return Arrays.asList((Object[]) value);
+            }
+            return value;
+        }));
+        Config other = ConfigFactory.parseMap(map);
+        Config newConfig = parseConfigOptions().withFallback(other).resolve();
+        return newConfig.isEmpty()
+                ? ""
+                : 
newConfig.root().render(ConfigRenderOptions.defaults().setJson(false));
+    }
+
+    private void saveValues(Map<String, ? extends Serializable> values) {
+        try {
+            Files.write(configPath, 
toString(values).getBytes(StandardCharsets.UTF_8));
+        } catch (IOException e) {
+            throw new NodeConfigWriteException(
+                    "Failed to write values " + values + " to config file.", 
e);
+        }
+    }
+
+    private Config parseConfigOptions() {
+        return ConfigFactory.parseFile(
+                configPath.toFile(),
+                ConfigParseOptions.defaults().setAllowMissing(false));
+    }
+
+    private void ensureConfigFile() {

Review Comment:
   Name of the method doesn't reflect what it does. Please document it at least



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/NodeBootstrapConfiguration.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicReference;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Node bootstrap configuration provider interface.
+ */
+@FunctionalInterface
+public interface NodeBootstrapConfiguration {
+    /**
+     * Path to node configuration file.
+     *
+     * @return Path to node configuration file in HOCON format.
+     */
+    Path config();
+
+

Review Comment:
   ```suggestion
   
   ```



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/NodeBootstrapConfiguration.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicReference;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Node bootstrap configuration provider interface.
+ */
+@FunctionalInterface
+public interface NodeBootstrapConfiguration {
+    /**
+     * Path to node configuration file.
+     *
+     * @return Path to node configuration file in HOCON format.
+     */
+    Path config();
+
+
+    /**
+     * Simple config file provider.
+     *
+     * @param configPath path to node bootstrap configuration.
+     * @return Simple implementation with provided configuration file.
+     */
+    static NodeBootstrapConfiguration directFile(@NotNull Path configPath) {
+        return () -> configPath;
+    }
+
+    /**
+     * Return node bootstrap configuration with content from {@param is}.
+     *
+     * @param is configuration content.
+     * @param workDir dir for configuration file location.

Review Comment:
   ```suggestion
        * @param is Configuration content.
        * @param workDir Dir for configuration file location.
   ```



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();
+
+    private final AtomicReference<ConfigurationStorageListener> lsnrRef = new 
AtomicReference<>();
+
+    private final ExecutorService threadPool = Executors.newFixedThreadPool(4, 
new NamedThreadFactory("loc-cfg-file", LOG));
+
+    private final InFlightFutures futureTracker = new InFlightFutures();
+
+    public LocalFileConfigurationStorage(NodeBootstrapConfiguration 
configuration) {
+        this.configPath = configuration.config();
+        ensureConfigFile();
+    }
+
+    @Override
+    public CompletableFuture<Data> readDataOnRecovery() {
+        return readAllLatest("").thenApply(stringMap -> new Data(stringMap, 
0));
+    }
+
+    @Override
+    public CompletableFuture<Map<String, ? extends Serializable>> 
readAllLatest(String prefix) {
+        ensureConfigFile();
+        Map<String, Serializable> map = new HashMap<>();
+        if (!lastState.isEmpty()) {
+            for (Entry<String, ConfigValue> e : 
parseConfigOptions().entrySet()) {
+                String key = e.getKey();
+                if (key.startsWith(prefix)) {
+                    ConfigValue value = e.getValue();
+                    if (value.valueType() == ConfigValueType.LIST) {
+                        map.put(key, toArray(lastState.get(key).getClass(), 
(ConfigList) value));
+                    } else {
+                        map.put(key, (Serializable) value.unwrapped());
+                    }
+                }
+            }
+        }
+        return CompletableFuture.completedFuture(map);
+    }
+
+    @Override
+    public CompletableFuture<Serializable> readLatest(String key) {
+        ensureConfigFile();
+        return CompletableFuture.completedFuture((Serializable) 
parseConfigOptions().getValue(key).unwrapped());
+    }
+
+    @Override
+    public CompletableFuture<Boolean> write(Map<String, ? extends 
Serializable> newValues, long ver) {

Review Comment:
   `ver` is completely ignored, as far as I see



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/NodeBootstrapConfiguration.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicReference;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Node bootstrap configuration provider interface.
+ */
+@FunctionalInterface
+public interface NodeBootstrapConfiguration {
+    /**
+     * Path to node configuration file.
+     *
+     * @return Path to node configuration file in HOCON format.
+     */
+    Path config();
+
+
+    /**
+     * Simple config file provider.
+     *
+     * @param configPath path to node bootstrap configuration.
+     * @return Simple implementation with provided configuration file.
+     */
+    static NodeBootstrapConfiguration directFile(@NotNull Path configPath) {

Review Comment:
   Please remove all occasions of `@NotNull`



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/NodeBootstrapConfiguration.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicReference;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Node bootstrap configuration provider interface.
+ */
+@FunctionalInterface
+public interface NodeBootstrapConfiguration {
+    /**
+     * Path to node configuration file.
+     *
+     * @return Path to node configuration file in HOCON format.
+     */
+    Path config();
+
+
+    /**
+     * Simple config file provider.
+     *
+     * @param configPath path to node bootstrap configuration.
+     * @return Simple implementation with provided configuration file.
+     */
+    static NodeBootstrapConfiguration directFile(@NotNull Path configPath) {
+        return () -> configPath;
+    }
+
+    /**
+     * Return node bootstrap configuration with content from {@param is}.
+     *
+     * @param is configuration content.
+     * @param workDir dir for configuration file location.

Review Comment:
   Please start parameters descriptions with capital letters



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/NodeBootstrapConfiguration.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicReference;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Node bootstrap configuration provider interface.
+ */
+@FunctionalInterface
+public interface NodeBootstrapConfiguration {
+    /**
+     * Path to node configuration file.
+     *
+     * @return Path to node configuration file in HOCON format.
+     */
+    Path config();
+
+
+    /**
+     * Simple config file provider.
+     *
+     * @param configPath path to node bootstrap configuration.
+     * @return Simple implementation with provided configuration file.
+     */
+    static NodeBootstrapConfiguration directFile(@NotNull Path configPath) {
+        return () -> configPath;
+    }
+
+    /**
+     * Return node bootstrap configuration with content from {@param is}.
+     *
+     * @param is configuration content.
+     * @param workDir dir for configuration file location.
+     * @return Node bootstrap configuration with lazy config file creation.
+     */
+    static NodeBootstrapConfiguration inputStream(@Nullable InputStream is, 
Path workDir) {
+        return new NodeBootstrapConfiguration() {
+
+            private final AtomicReference<Path> config = new 
AtomicReference<>();
+
+            @Override
+            public Path config() {
+                if (config.compareAndSet(null, createEmptyConfig(workDir))) {
+                    if (is != null) {
+                        try {
+                            Files.write(config.get(), is.readAllBytes());

Review Comment:
   This file write operation is not safe.
   1. Operation can be interrupted in the middle of the process
   2. You don't call fsync when you're done
   
   There must be an example of safe file writing somewhere in the code, and you 
can probably reuse that code



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/NodeBootstrapConfiguration.java:
##########
@@ -0,0 +1,119 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicReference;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Node bootstrap configuration provider interface.
+ */
+@FunctionalInterface
+public interface NodeBootstrapConfiguration {
+    /**
+     * Path to node configuration file.
+     *
+     * @return Path to node configuration file in HOCON format.
+     */
+    Path config();
+
+
+    /**
+     * Simple config file provider.
+     *
+     * @param configPath path to node bootstrap configuration.
+     * @return Simple implementation with provided configuration file.
+     */
+    static NodeBootstrapConfiguration directFile(@NotNull Path configPath) {
+        return () -> configPath;
+    }
+
+    /**
+     * Return node bootstrap configuration with content from {@param is}.
+     *
+     * @param is configuration content.
+     * @param workDir dir for configuration file location.
+     * @return Node bootstrap configuration with lazy config file creation.
+     */
+    static NodeBootstrapConfiguration inputStream(@Nullable InputStream is, 
Path workDir) {
+        return new NodeBootstrapConfiguration() {
+
+            private final AtomicReference<Path> config = new 
AtomicReference<>();
+
+            @Override
+            public Path config() {
+                if (config.compareAndSet(null, createEmptyConfig(workDir))) {
+                    if (is != null) {
+                        try {
+                            Files.write(config.get(), is.readAllBytes());
+                        } catch (IOException e) {
+                            throw new NodeConfigReadException("Failed to read 
config input stream.", e);
+                        }
+                    }
+                }
+                return config.get();
+            }
+        };
+    }
+
+    /**
+     * Return node bootstrap configuration with content from {@param 
plainConf}.
+     *
+     * @param plainConf configuration content.
+     * @param workDir dir for configuration file location.
+     * @return Node bootstrap configuration with lazy config file creation.
+     */
+    static NodeBootstrapConfiguration string(@Nullable @Language("HOCON") 
String plainConf, Path workDir) {
+        return inputStream(plainConf != null
+                ? new 
ByteArrayInputStream(plainConf.getBytes(StandardCharsets.UTF_8))
+                : null,
+                workDir);
+    }
+
+    /**
+     * Empty config provider.
+     *
+     * @param workDir Configuration file location.
+     * @return Node bootstrap configuration provider to empty config.
+     */
+    static NodeBootstrapConfiguration empty(Path workDir) {
+        return () -> createEmptyConfig(workDir);
+    }
+
+    private static Path createEmptyConfig(Path workDir) {
+        try {
+            Path config = workDir.resolve("ignite-config.conf");

Review Comment:
   What's a `*.conf` by the way? Is this a common file extension for 
configuration files?



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();

Review Comment:
   I'd rename it to `latest`, but it's up to you



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();
+
+    private final AtomicReference<ConfigurationStorageListener> lsnrRef = new 
AtomicReference<>();

Review Comment:
   Why did you shorten this name?



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();
+
+    private final AtomicReference<ConfigurationStorageListener> lsnrRef = new 
AtomicReference<>();
+
+    private final ExecutorService threadPool = Executors.newFixedThreadPool(4, 
new NamedThreadFactory("loc-cfg-file", LOG));
+
+    private final InFlightFutures futureTracker = new InFlightFutures();
+
+    public LocalFileConfigurationStorage(NodeBootstrapConfiguration 
configuration) {
+        this.configPath = configuration.config();
+        ensureConfigFile();
+    }
+
+    @Override
+    public CompletableFuture<Data> readDataOnRecovery() {
+        return readAllLatest("").thenApply(stringMap -> new Data(stringMap, 
0));
+    }

Review Comment:
   Shouldn't it be empty during the start?



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();
+
+    private final AtomicReference<ConfigurationStorageListener> lsnrRef = new 
AtomicReference<>();
+
+    private final ExecutorService threadPool = Executors.newFixedThreadPool(4, 
new NamedThreadFactory("loc-cfg-file", LOG));
+
+    private final InFlightFutures futureTracker = new InFlightFutures();
+
+    public LocalFileConfigurationStorage(NodeBootstrapConfiguration 
configuration) {
+        this.configPath = configuration.config();
+        ensureConfigFile();
+    }
+
+    @Override
+    public CompletableFuture<Data> readDataOnRecovery() {
+        return readAllLatest("").thenApply(stringMap -> new Data(stringMap, 
0));
+    }
+
+    @Override
+    public CompletableFuture<Map<String, ? extends Serializable>> 
readAllLatest(String prefix) {
+        ensureConfigFile();
+        Map<String, Serializable> map = new HashMap<>();
+        if (!lastState.isEmpty()) {
+            for (Entry<String, ConfigValue> e : 
parseConfigOptions().entrySet()) {
+                String key = e.getKey();
+                if (key.startsWith(prefix)) {
+                    ConfigValue value = e.getValue();
+                    if (value.valueType() == ConfigValueType.LIST) {
+                        map.put(key, toArray(lastState.get(key).getClass(), 
(ConfigList) value));
+                    } else {
+                        map.put(key, (Serializable) value.unwrapped());
+                    }
+                }
+            }
+        }
+        return CompletableFuture.completedFuture(map);
+    }
+
+    @Override
+    public CompletableFuture<Serializable> readLatest(String key) {
+        ensureConfigFile();
+        return CompletableFuture.completedFuture((Serializable) 
parseConfigOptions().getValue(key).unwrapped());
+    }
+
+    @Override
+    public CompletableFuture<Boolean> write(Map<String, ? extends 
Serializable> newValues, long ver) {

Review Comment:
   Is this method thread-safe? I guess not?
   Please explain why. If it doesn't need to be thread-safe, then please add a 
comment with justification



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();
+
+    private final AtomicReference<ConfigurationStorageListener> lsnrRef = new 
AtomicReference<>();
+
+    private final ExecutorService threadPool = Executors.newFixedThreadPool(4, 
new NamedThreadFactory("loc-cfg-file", LOG));
+
+    private final InFlightFutures futureTracker = new InFlightFutures();
+
+    public LocalFileConfigurationStorage(NodeBootstrapConfiguration 
configuration) {
+        this.configPath = configuration.config();
+        ensureConfigFile();
+    }
+
+    @Override
+    public CompletableFuture<Data> readDataOnRecovery() {
+        return readAllLatest("").thenApply(stringMap -> new Data(stringMap, 
0));
+    }
+
+    @Override
+    public CompletableFuture<Map<String, ? extends Serializable>> 
readAllLatest(String prefix) {
+        ensureConfigFile();
+        Map<String, Serializable> map = new HashMap<>();
+        if (!lastState.isEmpty()) {
+            for (Entry<String, ConfigValue> e : 
parseConfigOptions().entrySet()) {

Review Comment:
   There must be a huge TODO somewhere, telling that current code will break 
once we introduce a named list configuration to the set of node configurations.



##########
modules/runner/src/main/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorage.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * 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.storage;
+
+import static org.apache.ignite.internal.configuration.TypeUtils.box;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.unwrapPrimitive;
+import static 
org.apache.ignite.internal.configuration.util.ConfigurationUtil.wrongTypeException;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import com.typesafe.config.ConfigList;
+import com.typesafe.config.ConfigParseOptions;
+import com.typesafe.config.ConfigRenderOptions;
+import com.typesafe.config.ConfigValue;
+import com.typesafe.config.ConfigValueType;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.internal.configuration.NodeBootstrapConfiguration;
+import org.apache.ignite.internal.configuration.NodeConfigCreateException;
+import org.apache.ignite.internal.configuration.NodeConfigWriteException;
+import org.apache.ignite.internal.future.InFlightFutures;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.util.IgniteUtils;
+
+/**
+ * Implementation of {@link ConfigurationStorage} based on local file 
configuration storage.
+ */
+public class LocalFileConfigurationStorage implements ConfigurationStorage {
+    private static final IgniteLogger LOG = 
Loggers.forClass(LocalFileConfigurationStorage.class);
+
+    private final Path configPath;
+    private final Map<String, Serializable> lastState = new HashMap<>();
+
+    private final AtomicReference<ConfigurationStorageListener> lsnrRef = new 
AtomicReference<>();
+
+    private final ExecutorService threadPool = Executors.newFixedThreadPool(4, 
new NamedThreadFactory("loc-cfg-file", LOG));
+
+    private final InFlightFutures futureTracker = new InFlightFutures();
+
+    public LocalFileConfigurationStorage(NodeBootstrapConfiguration 
configuration) {
+        this.configPath = configuration.config();
+        ensureConfigFile();
+    }
+
+    @Override
+    public CompletableFuture<Data> readDataOnRecovery() {
+        return readAllLatest("").thenApply(stringMap -> new Data(stringMap, 
0));
+    }
+
+    @Override
+    public CompletableFuture<Map<String, ? extends Serializable>> 
readAllLatest(String prefix) {
+        ensureConfigFile();
+        Map<String, Serializable> map = new HashMap<>();
+        if (!lastState.isEmpty()) {
+            for (Entry<String, ConfigValue> e : 
parseConfigOptions().entrySet()) {
+                String key = e.getKey();
+                if (key.startsWith(prefix)) {
+                    ConfigValue value = e.getValue();
+                    if (value.valueType() == ConfigValueType.LIST) {
+                        map.put(key, toArray(lastState.get(key).getClass(), 
(ConfigList) value));
+                    } else {
+                        map.put(key, (Serializable) value.unwrapped());
+                    }
+                }
+            }
+        }
+        return CompletableFuture.completedFuture(map);
+    }
+
+    @Override
+    public CompletableFuture<Serializable> readLatest(String key) {
+        ensureConfigFile();
+        return CompletableFuture.completedFuture((Serializable) 
parseConfigOptions().getValue(key).unwrapped());
+    }
+
+    @Override
+    public CompletableFuture<Boolean> write(Map<String, ? extends 
Serializable> newValues, long ver) {
+        ensureConfigFile();
+        saveValues(newValues);
+        lastState.putAll(newValues);
+        runAsync(() -> lsnrRef.get().onEntriesChanged(new Data(newValues, 0)));
+        return CompletableFuture.completedFuture(true);
+    }
+
+    private void runAsync(Runnable runnable) {
+        CompletableFuture<Void> future = CompletableFuture.runAsync(runnable, 
threadPool);
+
+        futureTracker.registerFuture(future);
+    }
+
+    @Override
+    public void registerConfigurationListener(ConfigurationStorageListener 
lsnr) {
+        if (!lsnrRef.compareAndSet(null, lsnr)) {
+            LOG.debug("Configuration listener has already been set");
+        }
+    }
+
+    @Override
+    public ConfigurationType type() {
+        return ConfigurationType.LOCAL;
+    }
+
+    @Override
+    public CompletableFuture<Long> lastRevision() {
+        return CompletableFuture.completedFuture(0L);
+    }
+
+    @Override
+    public CompletableFuture<Void> writeConfigurationRevision(long 
prevRevision, long currentRevision) {
+        return CompletableFuture.completedFuture(null);
+    }
+
+    @Override
+    public void close() {
+        IgniteUtils.shutdownAndAwaitTermination(threadPool, 10, 
TimeUnit.SECONDS);
+
+        futureTracker.cancelInFlightFutures();
+    }
+
+    private String toString(Map<String, ? extends Serializable> values) {

Review Comment:
   Please add javadoc. It may not be clear at first glance that this method is 
used before saving configuration to file



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to