You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ib...@apache.org on 2021/02/03 13:10:11 UTC

[ignite-3] branch main updated: IGNITE-14094 Storage interface for configuration module + basic algorithm to store configuration in there.

This is an automated email from the ASF dual-hosted git repository.

ibessonov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 05992d2  IGNITE-14094 Storage interface for configuration module + basic algorithm to store configuration in there.
05992d2 is described below

commit 05992d2c6b90cdb46980814f4114e12399aec559
Author: Semyon Danilov <sa...@yandex.ru>
AuthorDate: Wed Feb 3 16:09:54 2021 +0300

    IGNITE-14094 Storage interface for configuration module + basic algorithm to store configuration in there.
    
    Signed-off-by: ibessonov <be...@gmail.com>
---
 .../processor/internal/Processor.java              |   4 +-
 .../ignite/configuration/sample/UsageTest.java     |  26 +-
 .../sample/storage/ConfigurationChangerTest.java   | 280 +++++++++++++++++++++
 .../sample/storage/TestConfigurationStorage.java   |  98 ++++++++
 ...tion.java => ConfigurationChangeException.java} |  23 +-
 .../ignite/configuration/ConfigurationChanger.java | 245 ++++++++++++++++++
 .../apache/ignite/configuration/Configurator.java  |  32 +--
 .../storage/ConfigurationStorage.java              |  53 ++--
 .../ConfigurationStorageListener.java}             |  16 +-
 .../Data.java}                                     |  41 ++-
 .../ConfigurationValidationException.java          |  13 +
 ...lidationException.java => ValidationIssue.java} |  10 +-
 .../java/org/apache/ignite/rest/RestModule.java    |  15 +-
 .../java/org/apache/ignite/app/IgniteRunner.java   |  27 +-
 .../ignite/configuration/ConfigurationModule.java  |   6 +-
 15 files changed, 756 insertions(+), 133 deletions(-)

diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
index 43c6fee..f8fe5ef 100644
--- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
+++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Processor.java
@@ -966,7 +966,7 @@ public class Processor extends AbstractProcessor {
                     MethodSpec.Builder nodeChangeMtdBuilder = MethodSpec.methodBuilder(changeMtdName)
                         .addAnnotation(Override.class)
                         .addModifiers(PUBLIC)
-                        .returns(changeClsName);
+                        .returns(nodeClsName);
 
                     if (valAnnotation != null) {
                         nodeChangeMtdBuilder
@@ -1024,7 +1024,7 @@ public class Processor extends AbstractProcessor {
                     MethodSpec.Builder nodeInitMtdBuilder = MethodSpec.methodBuilder(initMtdName)
                         .addAnnotation(Override.class)
                         .addModifiers(PUBLIC)
-                        .returns(initClsName);
+                        .returns(nodeClsName);
 
                     if (valAnnotation != null) {
                         nodeInitMtdBuilder
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/UsageTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/UsageTest.java
index b78d62c..89ee565 100644
--- a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/UsageTest.java
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/UsageTest.java
@@ -17,14 +17,10 @@
 
 package org.apache.ignite.configuration.sample;
 
-import java.io.Serializable;
 import java.util.Collections;
-import java.util.function.Consumer;
 import org.apache.ignite.configuration.ConfigurationRegistry;
 import org.apache.ignite.configuration.Configurator;
 import org.apache.ignite.configuration.internal.NamedList;
-import org.apache.ignite.configuration.storage.ConfigurationStorage;
-import org.apache.ignite.configuration.storage.StorageException;
 import org.apache.ignite.configuration.validation.ConfigurationValidationException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -35,21 +31,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
  * Simple usage test of generated configuration schema.
  */
 public class UsageTest {
-    private final ConfigurationStorage storage = new ConfigurationStorage() {
-        @Override public <T extends Serializable> void save(String propertyName, T object) throws StorageException {
-
-        }
-
-        @Override public <T extends Serializable> T get(String propertyName) throws StorageException {
-            return null;
-        }
-
-        @Override
-        public <T extends Serializable> void listen(String key, Consumer<T> listener) throws StorageException {
-
-        }
-    };
-
     /**
      * Test creation of configuration and calling configuration API methods.
      */
@@ -66,7 +47,6 @@ public class UsageTest {
         );
 
         final Configurator<LocalConfigurationImpl> configurator = Configurator.create(
-            storage,
             LocalConfigurationImpl::new,
             initLocal
         );
@@ -111,13 +91,11 @@ public class UsageTest {
                     .withTimeout(autoAdjustTimeout))
         );
 
-        Configurator<LocalConfigurationImpl> localConf = Configurator.create(storage,
-            LocalConfigurationImpl::new, initLocal);
+        Configurator<LocalConfigurationImpl> localConf = Configurator.create(LocalConfigurationImpl::new, initLocal);
 
         sysConf.registerConfigurator(localConf);
 
-        Configurator<NetworkConfigurationImpl> networkConf = Configurator.create(storage,
-            NetworkConfigurationImpl::new, initNetwork);
+        Configurator<NetworkConfigurationImpl> networkConf = Configurator.create(NetworkConfigurationImpl::new, initNetwork);
 
         sysConf.registerConfigurator(networkConf);
 
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/ConfigurationChangerTest.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/ConfigurationChangerTest.java
new file mode 100644
index 0000000..9eab60f
--- /dev/null
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/ConfigurationChangerTest.java
@@ -0,0 +1,280 @@
+/*
+ * 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.configuration.sample.storage;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Map;
+import org.apache.ignite.configuration.ConfigurationChangeException;
+import org.apache.ignite.configuration.ConfigurationChanger;
+import org.apache.ignite.configuration.Configurator;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.annotation.Config;
+import org.apache.ignite.configuration.annotation.ConfigValue;
+import org.apache.ignite.configuration.annotation.NamedConfigValue;
+import org.apache.ignite.configuration.annotation.Value;
+import org.apache.ignite.configuration.sample.storage.impl.ANode;
+import org.apache.ignite.configuration.storage.Data;
+import org.apache.ignite.configuration.validation.ConfigurationValidationException;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsMapContaining.hasEntry;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test configuration changer.
+ */
+public class ConfigurationChangerTest {
+    /** Root configuration key. */
+    private static final RootKey<?> KEY = () -> "key";
+
+    /** */
+    @Config
+    public static class AConfigurationSchema {
+        /** */
+        @ConfigValue
+        private BConfigurationSchema child;
+
+        /** */
+        @NamedConfigValue
+        private CConfigurationSchema elements;
+    }
+
+    /** */
+    @Config
+    public static class BConfigurationSchema {
+        /** */
+        @Value(immutable = true)
+        private int intCfg;
+
+        /** */
+        @Value
+        private String strCfg;
+    }
+
+    /** */
+    @Config
+    public static class CConfigurationSchema {
+        /** */
+        @Value
+        private String strCfg;
+    }
+
+    /**
+     * Test simple change of configuration.
+     */
+    @Test
+    public void testSimpleConfigurationChange() {
+        final TestConfigurationStorage storage = new TestConfigurationStorage();
+
+        final ConfiguratorController configuratorController = new ConfiguratorController();
+        final Configurator<?> configurator = configuratorController.configurator();
+
+        ANode data = new ANode()
+            .initChild(init -> init.initIntCfg(1).initStrCfg("1"))
+            .initElements(change -> change.put("a", init -> init.initStrCfg("1")));
+
+        final ConfigurationChanger changer = new ConfigurationChanger(storage);
+        changer.init();
+
+        changer.registerConfiguration(KEY, configurator);
+
+        changer.change(Collections.singletonMap(KEY, data));
+
+        final Data dataFromStorage = storage.readAll();
+        final Map<String, Serializable> dataMap = dataFromStorage.values();
+
+        assertEquals(3, dataMap.size());
+        assertThat(dataMap, hasEntry("key.child.intCfg", 1));
+        assertThat(dataMap, hasEntry("key.child.strCfg", "1"));
+        assertThat(dataMap, hasEntry("key.elements.a.strCfg", "1"));
+    }
+
+    /**
+     * Test subsequent change of configuration via different changers.
+     */
+    @Test
+    public void testModifiedFromAnotherStorage() {
+        final TestConfigurationStorage storage = new TestConfigurationStorage();
+
+        final ConfiguratorController configuratorController = new ConfiguratorController();
+        final Configurator<?> configurator = configuratorController.configurator();
+
+        ANode data1 = new ANode()
+            .initChild(init -> init.initIntCfg(1).initStrCfg("1"))
+            .initElements(change -> change.put("a", init -> init.initStrCfg("1")));
+
+        ANode data2 = new ANode()
+            .initChild(init -> init.initIntCfg(2).initStrCfg("2"))
+            .initElements(change -> change
+                .put("a", init -> init.initStrCfg("2"))
+                .put("b", init -> init.initStrCfg("2"))
+            );
+
+        final ConfigurationChanger changer1 = new ConfigurationChanger(storage);
+        changer1.init();
+
+        final ConfigurationChanger changer2 = new ConfigurationChanger(storage);
+        changer2.init();
+
+        changer1.registerConfiguration(KEY, configurator);
+        changer2.registerConfiguration(KEY, configurator);
+
+        changer1.change(Collections.singletonMap(KEY, data1));
+        changer2.change(Collections.singletonMap(KEY, data2));
+
+        final Data dataFromStorage = storage.readAll();
+        final Map<String, Serializable> dataMap = dataFromStorage.values();
+
+        assertEquals(4, dataMap.size());
+        assertThat(dataMap, hasEntry("key.child.intCfg", 2));
+        assertThat(dataMap, hasEntry("key.child.strCfg", "2"));
+        assertThat(dataMap, hasEntry("key.elements.a.strCfg", "2"));
+        assertThat(dataMap, hasEntry("key.elements.b.strCfg", "2"));
+    }
+
+    /**
+     * Test that subsequent change of configuration is failed if changes are incompatible.
+     */
+    @Test
+    public void testModifiedFromAnotherStorageWithIncompatibleChanges() {
+        final TestConfigurationStorage storage = new TestConfigurationStorage();
+
+        final ConfiguratorController configuratorController = new ConfiguratorController();
+        final Configurator<?> configurator = configuratorController.configurator();
+
+        ANode data1 = new ANode()
+            .initChild(init -> init.initIntCfg(1).initStrCfg("1"))
+            .initElements(change -> change.put("a", init -> init.initStrCfg("1")));
+
+        ANode data2 = new ANode()
+            .initChild(init -> init.initIntCfg(2).initStrCfg("2"))
+            .initElements(change -> change
+                .put("a", init -> init.initStrCfg("2"))
+                .put("b", init -> init.initStrCfg("2"))
+            );
+
+        final ConfigurationChanger changer1 = new ConfigurationChanger(storage);
+        changer1.init();
+
+        final ConfigurationChanger changer2 = new ConfigurationChanger(storage);
+        changer2.init();
+
+        changer1.registerConfiguration(KEY, configurator);
+        changer2.registerConfiguration(KEY, configurator);
+
+        changer1.change(Collections.singletonMap(KEY, data1));
+
+        configuratorController.hasIssues(true);
+
+        assertThrows(ConfigurationValidationException.class, () -> changer2.change(Collections.singletonMap(KEY, data2)));
+
+        final Data dataFromStorage = storage.readAll();
+        final Map<String, Serializable> dataMap = dataFromStorage.values();
+
+        assertEquals(3, dataMap.size());
+        assertThat(dataMap, hasEntry("key.child.intCfg", 1));
+        assertThat(dataMap, hasEntry("key.child.strCfg", "1"));
+        assertThat(dataMap, hasEntry("key.elements.a.strCfg", "1"));
+    }
+
+    /**
+     * Test that init and change fail with right exception if storage is inaccessible.
+     */
+    @Test
+    public void testFailedToWrite() {
+        final TestConfigurationStorage storage = new TestConfigurationStorage();
+
+        final ConfiguratorController configuratorController = new ConfiguratorController();
+        final Configurator<?> configurator = configuratorController.configurator();
+
+        ANode data = new ANode();
+
+        final ConfigurationChanger changer = new ConfigurationChanger(storage);
+
+        storage.fail(true);
+
+        assertThrows(ConfigurationChangeException.class, changer::init);
+
+        storage.fail(false);
+
+        changer.init();
+
+        changer.registerConfiguration(KEY, configurator);
+
+        storage.fail(true);
+
+        assertThrows(ConfigurationChangeException.class, () -> changer.change(Collections.singletonMap(KEY, data)));
+
+        storage.fail(false);
+
+        final Data dataFromStorage = storage.readAll();
+        final Map<String, Serializable> dataMap = dataFromStorage.values();
+
+        assertEquals(0, dataMap.size());
+    }
+
+    /**
+     * Wrapper for Configurator mock to control validation.
+     */
+    private static class ConfiguratorController {
+        /** Configurator. */
+        final Configurator<?> configurator;
+
+        /** Whether validate method should return issues. */
+        private boolean hasIssues;
+
+        /** Constructor. */
+        private ConfiguratorController() {
+            this(false);
+        }
+
+        /** Constructor. */
+        private ConfiguratorController(boolean hasIssues) {
+            this.hasIssues = hasIssues;
+
+            configurator = Mockito.mock(Configurator.class);
+
+            Mockito.when(configurator.validateChanges(Mockito.any())).then(mock -> {
+                if (this.hasIssues)
+                    return Collections.singletonList(new ValidationIssue());
+
+                return Collections.emptyList();
+            });
+        }
+
+        /**
+         * Set has issues flag.
+         * @param hasIssues Has issues flag.
+         */
+        public void hasIssues(boolean hasIssues) {
+            this.hasIssues = hasIssues;
+        }
+
+        /**
+         * Get configurator.
+         * @return Configurator.
+         */
+        public Configurator<?> configurator() {
+            return configurator;
+        }
+    }
+}
diff --git a/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/TestConfigurationStorage.java b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/TestConfigurationStorage.java
new file mode 100644
index 0000000..e5b2228
--- /dev/null
+++ b/modules/configuration-annotation-processor/src/test/java/org/apache/ignite/configuration/sample/storage/TestConfigurationStorage.java
@@ -0,0 +1,98 @@
+/*
+ * 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.configuration.sample.storage;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.ignite.configuration.storage.ConfigurationStorage;
+import org.apache.ignite.configuration.storage.ConfigurationStorageListener;
+import org.apache.ignite.configuration.storage.Data;
+import org.apache.ignite.configuration.storage.StorageException;
+
+/**
+ * Test configuration storage.
+ */
+public class TestConfigurationStorage implements ConfigurationStorage {
+    /** Map to store values. */
+    private Map<String, Serializable> map = new ConcurrentHashMap<>();
+
+    /** Change listeners. */
+    private List<ConfigurationStorageListener> listeners = new ArrayList<>();
+
+    /** Storage version. */
+    private AtomicInteger version = new AtomicInteger(0);
+
+    /** Should fail on every operation. */
+    private boolean fail = false;
+
+    /**
+     * Set fail flag.
+     * @param fail Fail flag.
+     */
+    public void fail(boolean fail) {
+        this.fail = fail;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Data readAll() throws StorageException {
+        if (fail)
+            throw new StorageException("Failed to read data");
+
+        return new Data(new HashMap<>(map), version.get());
+    }
+
+    /** {@inheritDoc} */
+    @Override public synchronized boolean write(Map<String, Serializable> newValues, int sentVersion) throws StorageException {
+        if (fail)
+            throw new StorageException("Failed to write data");
+
+        if (sentVersion != version.get())
+            return false;
+
+        map.putAll(newValues);
+
+        version.incrementAndGet();
+
+        listeners.forEach(listener -> listener.onEntriesChanged(new Data(newValues, version.get())));
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Set<String> keys() throws StorageException {
+        if (fail)
+            throw new StorageException("Failed to get keys");
+
+        return map.keySet();
+    }
+
+    /** {@inheritDoc} */
+    @Override public void addListener(ConfigurationStorageListener listener) {
+        listeners.add(listener);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void removeListener(ConfigurationStorageListener listener) {
+        listeners.remove(listener);
+    }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChangeException.java
similarity index 63%
copy from modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
copy to modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChangeException.java
index 20aec33..e9c2097 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChangeException.java
@@ -14,15 +14,26 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package org.apache.ignite.configuration.validation;
+package org.apache.ignite.configuration;
 
 /**
- * Configuration validation exception.
+ * Configuration change exception.
  */
-public class ConfigurationValidationException extends RuntimeException {
-    /** Constructor. */
-    public ConfigurationValidationException(String message) {
+public class ConfigurationChangeException extends RuntimeException {
+    /**
+     * Constructor.
+     * @param message Error message.
+     */
+    public ConfigurationChangeException(String message) {
         super(message);
     }
+
+    /**
+     * Constructor.
+     * @param message Error message.
+     * @param cause Cause.
+     */
+    public ConfigurationChangeException(String message, Throwable cause) {
+        super(message, cause);
+    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java
new file mode 100644
index 0000000..5c884ce
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java
@@ -0,0 +1,245 @@
+/*
+ * 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.configuration;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.storage.ConfigurationStorage;
+import org.apache.ignite.configuration.storage.Data;
+import org.apache.ignite.configuration.storage.StorageException;
+import org.apache.ignite.configuration.tree.ConfigurationVisitor;
+import org.apache.ignite.configuration.tree.InnerNode;
+import org.apache.ignite.configuration.tree.NamedListNode;
+import org.apache.ignite.configuration.tree.TraversableTreeNode;
+import org.apache.ignite.configuration.util.ConfigurationUtil;
+import org.apache.ignite.configuration.validation.ConfigurationValidationException;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+
+/**
+ * Class that handles configuration changes, by validating them, passing to storage and listening to storage updates.
+ */
+public class ConfigurationChanger {
+    /** Map of configurations' configurators. */
+    private Map<RootKey<?>, Configurator<?>> registry = new HashMap<>();
+
+    /** Storage. */
+    private ConfigurationStorage configurationStorage;
+
+    /** Changer's last known version of storage. */
+    private final AtomicInteger version = new AtomicInteger(0);
+
+    /** Constructor. */
+    public ConfigurationChanger(ConfigurationStorage configurationStorage) {
+        this.configurationStorage = configurationStorage;
+    }
+
+    /**
+     * Initialize changer.
+     */
+    public void init() throws ConfigurationChangeException {
+        final Data data;
+
+        try {
+            data = configurationStorage.readAll();
+        }
+        catch (StorageException e) {
+            throw new ConfigurationChangeException("Failed to initialize configuration: " + e.getMessage(), e);
+        }
+
+        version.set(data.version());
+
+        configurationStorage.addListener(this::updateFromListener);
+
+        // TODO: IGNITE-14118 iterate over data and fill Configurators
+    }
+
+    /**
+     * Add configurator.
+     * @param key Root configuration key of the configurator.
+     * @param configurator Configuration's configurator.
+     */
+    public void registerConfiguration(RootKey<?> key, Configurator<?> configurator) {
+        registry.put(key, configurator);
+    }
+
+    /**
+     * Change configuration.
+     * @param changes Map of changes by root key.
+     */
+    public void change(Map<RootKey<?>, TraversableTreeNode> changes) throws ConfigurationChangeException,
+        ConfigurationValidationException {
+        Map<String, Serializable> allChanges = changes.entrySet().stream()
+            .map((Map.Entry<RootKey<?>, TraversableTreeNode> change) -> convertChangesToMap(change.getKey(), change.getValue()))
+            .flatMap(map -> map.entrySet().stream())
+            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+        boolean writing = true;
+
+        List<ValidationIssue> validationIssues = Collections.emptyList();
+
+        while (writing) {
+            final ValidationResult validationResult = validate(changes);
+
+            validationIssues = validationResult.issues();
+
+            final int version = validationResult.version();
+
+            if (validationIssues.isEmpty())
+                try {
+                    writing = !configurationStorage.write(allChanges, version);
+                }
+                catch (StorageException e) {
+                    throw new ConfigurationChangeException("Failed to change configuration: " + e.getMessage(), e);
+                }
+            else
+                break;
+        }
+
+        if (!validationIssues.isEmpty())
+            throw new ConfigurationValidationException(validationIssues);
+    }
+
+    /**
+     * Update configuration from storage listener.
+     * @param changedEntries Changed data.
+     */
+    private synchronized void updateFromListener(Data changedEntries) {
+        // TODO: IGNITE-14118 add tree update
+        version.set(changedEntries.version());
+    }
+
+    /**
+     * Validate configuration changes.
+     * @param changes Configuration changes.
+     * @return Validation results.
+     */
+    private synchronized ValidationResult validate(Map<RootKey<?>, TraversableTreeNode> changes) {
+        final int version = this.version.get();
+
+        List<ValidationIssue> issues = new ArrayList<>();
+
+        for (Map.Entry<RootKey<?>, TraversableTreeNode> entry : changes.entrySet()) {
+            RootKey<?> rootKey = entry.getKey();
+            TraversableTreeNode changesForRoot = entry.getValue();
+
+            final Configurator<?> configurator = registry.get(rootKey);
+
+            List<ValidationIssue> list = configurator.validateChanges(changesForRoot);
+            issues.addAll(list);
+        }
+
+        return new ValidationResult(issues, version);
+    }
+
+    /**
+     * Convert a traversable tree to a map of qualified keys to values.
+     * @param rootKey Root configuration key.
+     * @param node Tree.
+     * @return Map of changes.
+     */
+    private Map<String, Serializable> convertChangesToMap(RootKey<?> rootKey, TraversableTreeNode node) {
+        Map<String, Serializable> values = new HashMap<>();
+
+        node.accept(rootKey.key(), new ConfigurationVisitor() {
+            /** Current key, aggregated by visitor. */
+            StringBuilder currentKey = new StringBuilder();
+
+            /** {@inheritDoc} */
+            @Override public void visitLeafNode(String key, Serializable val) {
+                values.put(currentKey.toString() + key, val);
+            }
+
+            /** {@inheritDoc} */
+            @Override public void visitInnerNode(String key, InnerNode node) {
+                if (node == null)
+                    return;
+
+                int previousKeyLength = currentKey.length();
+
+                currentKey.append(key).append('.');
+
+                node.traverseChildren(this);
+
+                currentKey.setLength(previousKeyLength);
+            }
+
+            /** {@inheritDoc} */
+            @Override public <N extends InnerNode> void visitNamedListNode(String key, NamedListNode<N> node) {
+                int previousKeyLength = currentKey.length();
+
+                if (key != null)
+                    currentKey.append(key).append('.');
+
+                for (String namedListKey : node.namedListKeys()) {
+                    int loopPreviousKeyLength = currentKey.length();
+
+                    currentKey.append(ConfigurationUtil.escape(namedListKey)).append('.');
+
+                    node.get(namedListKey).traverseChildren(this);
+
+                    currentKey.setLength(loopPreviousKeyLength);
+                }
+
+                currentKey.setLength(previousKeyLength);
+            }
+        });
+        return values;
+    }
+
+    /**
+     * Results of the validation.
+     */
+    private static final class ValidationResult {
+        /** List of issues. */
+        private final List<ValidationIssue> issues;
+
+        /** Version of configuration that changes were validated against. */
+        private final int version;
+
+        /**
+         * Constructor.
+         * @param issues List of issues.
+         * @param version Version.
+         */
+        private ValidationResult(List<ValidationIssue> issues, int version) {
+            this.issues = issues;
+            this.version = version;
+        }
+
+        /**
+         * Get issues.
+         * @return Issues.
+         */
+        public List<ValidationIssue> issues() {
+            return issues;
+        }
+
+        /**
+         * Get version.
+         * @return Version.
+         */
+        public int version() {
+            return version;
+        }
+    }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/Configurator.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/Configurator.java
index 3e3592a..d3f42c9 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/Configurator.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/Configurator.java
@@ -29,10 +29,11 @@ import org.apache.ignite.configuration.internal.DynamicConfiguration;
 import org.apache.ignite.configuration.internal.DynamicProperty;
 import org.apache.ignite.configuration.internal.Modifier;
 import org.apache.ignite.configuration.internal.selector.Selector;
-import org.apache.ignite.configuration.storage.ConfigurationStorage;
+import org.apache.ignite.configuration.internal.validation.MemberKey;
+import org.apache.ignite.configuration.tree.TraversableTreeNode;
 import org.apache.ignite.configuration.validation.ConfigurationValidationException;
 import org.apache.ignite.configuration.validation.FieldValidator;
-import org.apache.ignite.configuration.internal.validation.MemberKey;
+import org.apache.ignite.configuration.validation.ValidationIssue;
 
 /**
  * Convenient wrapper for configuration root. Provides access to configuration tree, stores validators, performs actions
@@ -40,9 +41,6 @@ import org.apache.ignite.configuration.internal.validation.MemberKey;
  * @param <T> Type of configuration root.
  */
 public class Configurator<T extends DynamicConfiguration<?, ?, ?>> {
-    /** Storage for the configuration tree. */
-    private final ConfigurationStorage storage;
-
     /** Root of the configuration tree. */
     private final T root;
 
@@ -51,7 +49,6 @@ public class Configurator<T extends DynamicConfiguration<?, ?, ?>> {
 
     /**
      *
-     * @param storage
      * @param rootBuilder
      * @param <VIEW>
      * @param <INIT>
@@ -60,15 +57,13 @@ public class Configurator<T extends DynamicConfiguration<?, ?, ?>> {
      * @return
      */
     public static <VIEW, INIT, CHANGE, CONF extends DynamicConfiguration<VIEW, INIT, CHANGE>> Configurator<CONF> create(
-        ConfigurationStorage storage,
         Function<Configurator<CONF>, CONF> rootBuilder
     ) {
-        return new Configurator<>(storage, rootBuilder, null);
+        return new Configurator<>(rootBuilder, null);
     }
 
     /**
      *
-     * @param storage
      * @param rootBuilder
      * @param init
      * @param <VIEW>
@@ -78,25 +73,20 @@ public class Configurator<T extends DynamicConfiguration<?, ?, ?>> {
      * @return
      */
     public static <VIEW, INIT, CHANGE, CONF extends DynamicConfiguration<VIEW, INIT, CHANGE>> Configurator<CONF> create(
-        ConfigurationStorage storage,
         Function<Configurator<CONF>, CONF> rootBuilder,
         INIT init
     ) {
-        return new Configurator<>(storage, rootBuilder, init);
+        return new Configurator<>(rootBuilder, init);
     }
 
     /**
      * Constructor.
-     * @param storage Configuration storage.
      * @param rootBuilder Function, that creates configuration root.
      */
     private <VIEW, INIT, CHANGE, CONF extends DynamicConfiguration<VIEW, INIT, CHANGE>> Configurator(
-        ConfigurationStorage storage,
         Function<Configurator<CONF>, CONF> rootBuilder,
         INIT init
     ) {
-        this.storage = storage;
-
         final CONF built = rootBuilder.apply((Configurator<CONF>) this);
 
         if (init != null)
@@ -121,7 +111,7 @@ public class Configurator<T extends DynamicConfiguration<?, ?, ?>> {
     }
 
     /**
-     * 
+     *
      */
     public Class<?> getChangeType() {
         Type sClass = root.getClass().getGenericSuperclass();
@@ -154,6 +144,7 @@ public class Configurator<T extends DynamicConfiguration<?, ?, ?>> {
         final TARGET select = selector.select(copy);
         select.changeWithoutValidation(newValue);
         copy.validate(root);
+
         selector.select(root).changeWithoutValidation(newValue);
     }
 
@@ -210,13 +201,16 @@ public class Configurator<T extends DynamicConfiguration<?, ?, ?>> {
      * @param <PROP> Type of the property.
      */
     public <PROP extends Serializable> void onAttached(DynamicProperty<PROP> property) {
-        final String key = property.key();
         property.addListener(new PropertyListener<PROP, PROP>() {
             /** {@inheritDoc} */
             @Override public void update(PROP newValue, ConfigurationProperty<PROP, PROP> modifier) {
-                storage.save(key, newValue);
+//                storage.save(key, newValue);
             }
         });
-        storage.listen(key, property::setSilently);
+//        storage.listen(key, property::setSilently);
+    }
+
+    public List<ValidationIssue> validateChanges(TraversableTreeNode changes) {
+        return Collections.emptyList();
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorage.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorage.java
index 1c77a9a..66b8e83 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorage.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorage.java
@@ -17,39 +17,46 @@
 package org.apache.ignite.configuration.storage;
 
 import java.io.Serializable;
-import java.util.function.Consumer;
+import java.util.Map;
+import java.util.Set;
 
 /**
- * Storage interface for configuration.
+ * Common interface for configuration storage.
  */
 public interface ConfigurationStorage {
     /**
-     * Save configuration property.
-     *
-     * @param propertyName Fully qualified name of the property.
-     * @param object Object, that represents the value of the property.
-     * @param <T> Type of the property.
-     * @throws StorageException If failed to save object.
+     * Read all configuration values and current storage version.
+     * @return Values and version.
+     * @throws StorageException If failed to retrieve data.
      */
-    <T extends Serializable> void save(String propertyName, T object) throws StorageException;
+    Data readAll() throws StorageException;
 
     /**
-     * Get property value from storage.
-     *
-     * @param propertyName Fully qualified name of the property.
-     * @param <T> Type of the property.
-     * @return Object, that represents the value of the property.
-     * @throws StorageException If failed to retrieve object frm configuration storage.
+     * Write key-value pairs into the storage with last known version.
+     * @param newValues Key-value pairs.
+     * @param version Last known version.
+     * @return {@code true} if successfully written, {@code false} if version of the storage is different from the passed
+     * argument.
+     * @throws StorageException If failed to write data.
      */
-    <T extends Serializable> T get(String propertyName) throws StorageException;
+    boolean write(Map<String, Serializable> newValues, int version) throws StorageException;
 
     /**
-     * Listen for the property change in the storage.
-     *
-     * @param key Key to listen on.
-     * @param listener Listener function.
-     * @param <T> Type of the property.
-     * @throws StorageException If failed to attach listener to configuration storage.
+     * Get all the keys of the configuration storage.
+     * @return Set of keys.
+     * @throws StorageException If failed to retrieve keys.
      */
-    <T extends Serializable> void listen(String key, Consumer<T> listener) throws StorageException;
+    Set<String> keys() throws StorageException;
+
+    /**
+     * Add listener to the storage that notifies of data changes..
+     * @param listener Listener.
+     */
+    void addListener(ConfigurationStorageListener listener);
+
+    /**
+     * Remove storage listener.
+     * @param listener Listener.
+     */
+    void removeListener(ConfigurationStorageListener listener);
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorageListener.java
similarity index 69%
copy from modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
copy to modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorageListener.java
index 20aec33..f8cb7b4 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/ConfigurationStorageListener.java
@@ -14,15 +14,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-package org.apache.ignite.configuration.validation;
+package org.apache.ignite.configuration.storage;
 
 /**
- * Configuration validation exception.
+ * Configuration storage listener for changes.
  */
-public class ConfigurationValidationException extends RuntimeException {
-    /** Constructor. */
-    public ConfigurationValidationException(String message) {
-        super(message);
-    }
+public interface ConfigurationStorageListener {
+    /**
+     * Method called when entries in storage change.
+     * @param changedEntries Changed entries, key-value pairs and new version of the storage.
+     */
+    void onEntriesChanged(Data changedEntries);
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/Data.java
similarity index 51%
copy from modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
copy to modules/configuration/src/main/java/org/apache/ignite/configuration/storage/Data.java
index 20aec33..eea2afb 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/storage/Data.java
@@ -14,15 +14,44 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.ignite.configuration.storage;
 
-package org.apache.ignite.configuration.validation;
+import java.io.Serializable;
+import java.util.Map;
 
 /**
- * Configuration validation exception.
+ * Represents data in configuration storage.
  */
-public class ConfigurationValidationException extends RuntimeException {
-    /** Constructor. */
-    public ConfigurationValidationException(String message) {
-        super(message);
+public class Data {
+    /** Values. */
+    private final Map<String, Serializable> values;
+
+    /** Configuration storage version. */
+    private final int version;
+
+    /**
+     * Constructor.
+     * @param values Values.
+     * @param version Version.
+     */
+    public Data(Map<String, Serializable> values, int version) {
+        this.values = values;
+        this.version = version;
+    }
+
+    /**
+     * Get values.
+     * @return Values.
+     */
+    public Map<String, Serializable> values() {
+        return values;
+    }
+
+    /**
+     * Get version.
+     * @return version.
+     */
+    public int version() {
+        return version;
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
index 20aec33..73af1f2 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
@@ -17,12 +17,25 @@
 
 package org.apache.ignite.configuration.validation;
 
+import java.util.List;
+
 /**
  * Configuration validation exception.
  */
 public class ConfigurationValidationException extends RuntimeException {
+    /** List of configuration validation issues. */
+    private List<ValidationIssue> issues;
+
     /** Constructor. */
     public ConfigurationValidationException(String message) {
         super(message);
     }
+
+    public ConfigurationValidationException(List<ValidationIssue> issues) {
+        this.issues = issues;
+    }
+
+    public List<ValidationIssue> getIssues() {
+        return issues;
+    }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
similarity index 78%
copy from modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
copy to modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
index 20aec33..4594a53 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ConfigurationValidationException.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
@@ -14,15 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.apache.ignite.configuration.validation;
 
-/**
- * Configuration validation exception.
- */
-public class ConfigurationValidationException extends RuntimeException {
-    /** Constructor. */
-    public ConfigurationValidationException(String message) {
-        super(message);
-    }
+public class ValidationIssue {
 }
diff --git a/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java b/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
index 7a77b05..a00e217 100644
--- a/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
+++ b/modules/rest/src/main/java/org/apache/ignite/rest/RestModule.java
@@ -20,18 +20,17 @@ package org.apache.ignite.rest;
 import com.google.gson.JsonSyntaxException;
 import io.javalin.Javalin;
 import java.io.Reader;
-import org.apache.ignite.configuration.Configurator;
 import org.apache.ignite.configuration.ConfigurationRegistry;
+import org.apache.ignite.configuration.Configurator;
 import org.apache.ignite.configuration.internal.selector.SelectorNotFoundException;
-import org.apache.ignite.rest.presentation.ConfigurationPresentation;
-import org.apache.ignite.rest.presentation.FormatConverter;
-import org.apache.ignite.rest.presentation.json.JsonConverter;
-import org.apache.ignite.rest.presentation.json.JsonPresentation;
-import org.apache.ignite.configuration.storage.ConfigurationStorage;
 import org.apache.ignite.configuration.validation.ConfigurationValidationException;
 import org.apache.ignite.rest.configuration.InitRest;
 import org.apache.ignite.rest.configuration.RestConfigurationImpl;
 import org.apache.ignite.rest.configuration.Selectors;
+import org.apache.ignite.rest.presentation.ConfigurationPresentation;
+import org.apache.ignite.rest.presentation.FormatConverter;
+import org.apache.ignite.rest.presentation.json.JsonConverter;
+import org.apache.ignite.rest.presentation.json.JsonPresentation;
 import org.slf4j.Logger;
 
 /**
@@ -65,7 +64,7 @@ public class RestModule {
     }
 
     /** */
-    public void prepareStart(ConfigurationRegistry sysConfig, Reader moduleConfReader, ConfigurationStorage storage) {
+    public void prepareStart(ConfigurationRegistry sysConfig, Reader moduleConfReader) {
         try {
             Class.forName(Selectors.class.getName());
         }
@@ -79,7 +78,7 @@ public class RestModule {
 
         FormatConverter converter = new JsonConverter();
 
-        Configurator<RestConfigurationImpl> restConf = Configurator.create(storage, RestConfigurationImpl::new,
+        Configurator<RestConfigurationImpl> restConf = Configurator.create(RestConfigurationImpl::new,
             converter.convertFrom(moduleConfReader, "rest", InitRest.class));
 
         sysConfig.registerConfigurator(restConf);
diff --git a/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java b/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
index cb26484..3e5aaf8 100644
--- a/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
+++ b/modules/runner/src/main/java/org/apache/ignite/app/IgniteRunner.java
@@ -21,13 +21,10 @@ import java.io.BufferedReader;
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.StringReader;
 import java.util.Arrays;
 import java.util.stream.Collectors;
-import java.io.Serializable;
-import java.io.StringReader;
-import java.util.function.Consumer;
 import org.apache.ignite.configuration.ConfigurationModule;
-import org.apache.ignite.configuration.storage.ConfigurationStorage;
 import org.apache.ignite.rest.RestModule;
 import org.apache.ignite.utils.IgniteProperties;
 import org.slf4j.Logger;
@@ -66,24 +63,6 @@ public class IgniteRunner {
     /** */
     private static final Logger log = LoggerFactory.getLogger(IgniteRunner.class);
 
-    /** */
-    private static final ConfigurationStorage STORAGE = new ConfigurationStorage() {
-        /** {@inheritDoc} */
-        @Override public <T extends Serializable> void save(String propertyName, T object) {
-
-        }
-
-        /** {@inheritDoc} */
-        @Override public <T extends Serializable> T get(String propertyName) {
-            return null;
-        }
-
-        /** {@inheritDoc} */
-        @Override public <T extends Serializable> void listen(String key, Consumer<T> listener) {
-
-        }
-    };
-
     /**
      * It is possible to start application with a custom configuration in form of json file other than that in resources.
      *
@@ -124,9 +103,9 @@ public class IgniteRunner {
                 bldr.append(str);
             }
 
-            restModule.prepareStart(confModule.configurationRegistry(), new StringReader(bldr.toString()), STORAGE);
+            restModule.prepareStart(confModule.configurationRegistry(), new StringReader(bldr.toString()));
 
-            confModule.bootstrap(new StringReader(bldr.toString()), STORAGE);
+            confModule.bootstrap(new StringReader(bldr.toString()));
         }
         finally {
             if (confReader != null)
diff --git a/modules/runner/src/main/java/org/apache/ignite/configuration/ConfigurationModule.java b/modules/runner/src/main/java/org/apache/ignite/configuration/ConfigurationModule.java
index d6155f0..fb222ca 100644
--- a/modules/runner/src/main/java/org/apache/ignite/configuration/ConfigurationModule.java
+++ b/modules/runner/src/main/java/org/apache/ignite/configuration/ConfigurationModule.java
@@ -18,11 +18,9 @@
 package org.apache.ignite.configuration;
 
 import java.io.Reader;
-
 import org.apache.ignite.configuration.extended.InitLocal;
 import org.apache.ignite.configuration.extended.LocalConfigurationImpl;
 import org.apache.ignite.configuration.extended.Selectors;
-import org.apache.ignite.configuration.storage.ConfigurationStorage;
 import org.apache.ignite.rest.presentation.FormatConverter;
 import org.apache.ignite.rest.presentation.json.JsonConverter;
 
@@ -49,11 +47,11 @@ public class ConfigurationModule {
     private final ConfigurationRegistry confRegistry = new ConfigurationRegistry();
 
     /** */
-    public void bootstrap(Reader confReader, ConfigurationStorage storage) {
+    public void bootstrap(Reader confReader) {
         FormatConverter converter = new JsonConverter();
 
         Configurator<LocalConfigurationImpl> configurator =
-            Configurator.create(storage, LocalConfigurationImpl::new, converter.convertFrom(confReader, "local", InitLocal.class));
+            Configurator.create(LocalConfigurationImpl::new, converter.convertFrom(confReader, "local", InitLocal.class));
 
         localConfigurator = configurator;