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/03/03 16:05:29 UTC

[ignite-3] branch main updated: IGNITE-14182 Implemented naming list configuration elements removal. (#58)

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 81921cb  IGNITE-14182 Implemented naming list configuration elements removal. (#58)
81921cb is described below

commit 81921cb21b427484b3ff81455eb5bfcc1a961f6a
Author: ibessonov <be...@gmail.com>
AuthorDate: Wed Mar 3 19:05:20 2021 +0300

    IGNITE-14182 Implemented naming list configuration elements removal. (#58)
---
 .../processor/internal/Processor.java              |  79 +++++++------
 .../configuration/processor/internal/Utils.java    |   9 ++
 .../ignite/configuration/sample/UsageTest.java     |   7 ++
 .../sample/storage/TestConfigurationStorage.java   |  11 +-
 .../ignite/configuration/ConfigurationChanger.java |  39 ++++++-
 ...nfigurator.java => NamedConfigurationTree.java} |  31 +++---
 .../configuration/annotation/NamedConfigValue.java |   2 -
 .../ignite/configuration/annotation/Value.java     |   2 +-
 .../configuration/internal/ConfigurationNode.java  |   4 +-
 .../internal/DynamicConfiguration.java             |  14 ++-
 .../configuration/internal/DynamicProperty.java    |  12 +-
 .../ignite/configuration/internal/NamedList.java   |  44 --------
 .../internal/NamedListConfiguration.java           |  18 ++-
 .../internal/util/ConfigurationUtil.java           | 122 ++++++++++++++++-----
 .../ITScaleCubeNetworkClusterMessagingTest.java    |   2 +
 15 files changed, 233 insertions(+), 163 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 2ec61bf..898346c 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
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.configuration.processor.internal;
 
-import com.squareup.javapoet.AnnotationSpec;
 import com.squareup.javapoet.ArrayTypeName;
 import com.squareup.javapoet.ClassName;
 import com.squareup.javapoet.CodeBlock;
@@ -57,6 +56,7 @@ import org.apache.ignite.configuration.ConfigurationChanger;
 import org.apache.ignite.configuration.ConfigurationRegistry;
 import org.apache.ignite.configuration.ConfigurationTree;
 import org.apache.ignite.configuration.ConfigurationValue;
+import org.apache.ignite.configuration.NamedConfigurationTree;
 import org.apache.ignite.configuration.RootKey;
 import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.ConfigValue;
@@ -80,6 +80,7 @@ import static javax.lang.model.element.Modifier.FINAL;
 import static javax.lang.model.element.Modifier.PRIVATE;
 import static javax.lang.model.element.Modifier.PUBLIC;
 import static javax.lang.model.element.Modifier.STATIC;
+import static org.apache.ignite.configuration.processor.internal.Utils.suppressWarningsUnchecked;
 
 /**
  * Annotation processor that produces configuration classes.
@@ -181,7 +182,7 @@ public class Processor extends AbstractProcessor {
                 // Get configuration types (VIEW, INIT, CHANGE and so on)
                 final ConfigurationFieldTypes types = getTypes(field);
 
-                TypeName getMethodType = types.getGetMethodType();
+                TypeName fieldType = types.getFieldType();
                 TypeName viewClassType = types.getViewClassType();
                 TypeName initClassType = types.getInitClassType();
                 TypeName changeClassType = types.getChangeClassType();
@@ -198,13 +199,13 @@ public class Processor extends AbstractProcessor {
                     // Create DynamicConfiguration (descendant) field
                     final FieldSpec nestedConfigField =
                         FieldSpec
-                            .builder(getMethodType, fieldName, Modifier.PRIVATE, FINAL)
+                            .builder(fieldType, fieldName, Modifier.PRIVATE, FINAL)
                             .build();
 
                     configurationClassBuilder.addField(nestedConfigField);
 
                     // Constructor statement
-                    constructorBodyBuilder.addStatement("add($L = new $T(keys, $S, rootKey, changer))", fieldName, getMethodType, fieldName);
+                    constructorBodyBuilder.addStatement("add($L = new $T(keys, $S, rootKey, changer))", fieldName, fieldType, fieldName);
                 }
 
                 final NamedConfigValue namedConfigAnnotation = field.getAnnotation(NamedConfigValue.class);
@@ -216,11 +217,9 @@ public class Processor extends AbstractProcessor {
                         );
                     }
 
-                    ClassName fieldType = Utils.getConfigurationName((ClassName) baseType);
-
                     // Create NamedListConfiguration<> field
                     final FieldSpec nestedConfigField = FieldSpec.builder(
-                        getMethodType,
+                        fieldType,
                         fieldName,
                         Modifier.PRIVATE,
                         FINAL
@@ -232,9 +231,9 @@ public class Processor extends AbstractProcessor {
                     constructorBodyBuilder.addStatement(
                         "add($L = new $T(keys, $S, rootKey, changer, (p, k) -> new $T(p, k, rootKey, changer)))",
                         fieldName,
-                        getMethodType,
+                        fieldType,
                         fieldName,
-                        fieldType
+                        Utils.getConfigurationName((ClassName) baseType)
                     );
                 }
 
@@ -250,7 +249,7 @@ public class Processor extends AbstractProcessor {
                     }
 
                     // Create value (DynamicProperty<>) field
-                    final FieldSpec generatedField = FieldSpec.builder(getMethodType, fieldName, Modifier.PRIVATE, FINAL).build();
+                    final FieldSpec generatedField = FieldSpec.builder(fieldType, fieldName, Modifier.PRIVATE, FINAL).build();
 
                     configurationClassBuilder.addField(generatedField);
 
@@ -259,11 +258,11 @@ public class Processor extends AbstractProcessor {
                     // Constructor statement
                     constructorBodyBuilder.addStatement(
                         "add($L = new $T(keys, $S, rootKey, changer), $L)",
-                        fieldName, getMethodType, fieldName, validatorsBlock
+                        fieldName, fieldType, fieldName, validatorsBlock
                     );
                 }
 
-                configDesc.getFields().add(new ConfigurationElement(getMethodType, fieldName, viewClassType, initClassType, changeClassType));
+                configDesc.getFields().add(new ConfigurationElement(fieldType, fieldName, viewClassType, initClassType, changeClassType));
 
                 createGetters(configurationClassBuilder, configurationInterfaceBuilder, fieldName, types);
             }
@@ -341,14 +340,20 @@ public class Processor extends AbstractProcessor {
             .build();
         configurationInterfaceBuilder.addMethod(interfaceGetMethod);
 
-        MethodSpec getMethod = MethodSpec.methodBuilder(fieldName)
+        MethodSpec.Builder getMethodBuilder = MethodSpec.methodBuilder(fieldName)
             .addAnnotation(Override.class)
             .addJavadoc("{@inheritDoc}")
             .addModifiers(PUBLIC, FINAL)
-            .returns(types.getGetMethodType())
-            .addStatement("return $L", fieldName)
-            .build();
-        configurationClassBuilder.addMethod(getMethod);
+            .returns(types.getInterfaceGetMethodType());
+
+        if (Utils.isNamedConfiguration(types.getFieldType())) {
+            getMethodBuilder.addAnnotation(suppressWarningsUnchecked())
+                .addStatement("return ($T)$L", NamedConfigurationTree.class, fieldName);
+        }
+        else
+            getMethodBuilder.addStatement("return $L", fieldName);
+
+        configurationClassBuilder.addMethod(getMethodBuilder.build());
     }
 
     /**
@@ -357,7 +362,7 @@ public class Processor extends AbstractProcessor {
      * @return Bundle with all types for configuration
      */
     private ConfigurationFieldTypes getTypes(final VariableElement field) {
-        TypeName getMethodType = null;
+        TypeName fieldType = null;
         TypeName interfaceGetMethodType = null;
 
         final TypeName baseType = TypeName.get(field.asType());
@@ -369,10 +374,10 @@ public class Processor extends AbstractProcessor {
 
         final ConfigValue confAnnotation = field.getAnnotation(ConfigValue.class);
         if (confAnnotation != null) {
-            getMethodType = Utils.getConfigurationName((ClassName) baseType);
+            fieldType = Utils.getConfigurationName((ClassName) baseType);
             interfaceGetMethodType = Utils.getConfigurationInterfaceName((ClassName) baseType);
 
-            unwrappedType = getMethodType;
+            unwrappedType = fieldType;
             viewClassType = Utils.getViewName((ClassName) baseType);
             initClassType = Utils.getInitName((ClassName) baseType);
             changeClassType = Utils.getChangeName((ClassName) baseType);
@@ -380,16 +385,14 @@ public class Processor extends AbstractProcessor {
 
         final NamedConfigValue namedConfigAnnotation = field.getAnnotation(NamedConfigValue.class);
         if (namedConfigAnnotation != null) {
-            ClassName fieldType = Utils.getConfigurationName((ClassName) baseType);
-            //TODO IGNITE-14182 This is BS, interface name must be used instead.
-            ClassName interfaceFieldType = Utils.getConfigurationName((ClassName) baseType);
+            ClassName interfaceGetType = Utils.getConfigurationInterfaceName((ClassName) baseType);
 
             viewClassType = Utils.getViewName((ClassName) baseType);
             initClassType = Utils.getInitName((ClassName) baseType);
             changeClassType = Utils.getChangeName((ClassName) baseType);
 
-            getMethodType = ParameterizedTypeName.get(ClassName.get(NamedListConfiguration.class), viewClassType, fieldType, initClassType, changeClassType);
-            interfaceGetMethodType = ParameterizedTypeName.get(ClassName.get(NamedListConfiguration.class), viewClassType, interfaceFieldType, initClassType, changeClassType);
+            fieldType = ParameterizedTypeName.get(ClassName.get(NamedListConfiguration.class), interfaceGetType, viewClassType, changeClassType, initClassType);
+            interfaceGetMethodType = ParameterizedTypeName.get(ClassName.get(NamedConfigurationTree.class), interfaceGetType, viewClassType, changeClassType, initClassType);
         }
 
         final Value valueAnnotation = field.getAnnotation(Value.class);
@@ -403,11 +406,11 @@ public class Processor extends AbstractProcessor {
                 genericType = genericType.box();
             }
 
-            getMethodType = ParameterizedTypeName.get(dynPropClass, genericType);
+            fieldType = ParameterizedTypeName.get(dynPropClass, genericType);
             interfaceGetMethodType = ParameterizedTypeName.get(confValueClass, genericType);
         }
 
-        return new ConfigurationFieldTypes(getMethodType, unwrappedType, viewClassType, initClassType, changeClassType, interfaceGetMethodType);
+        return new ConfigurationFieldTypes(fieldType, unwrappedType, viewClassType, initClassType, changeClassType, interfaceGetMethodType);
     }
 
     /**
@@ -415,7 +418,7 @@ public class Processor extends AbstractProcessor {
      */
     private static class ConfigurationFieldTypes {
         /** Field get method type. */
-        private final TypeName getMethodType;
+        private final TypeName fieldType;
 
         /** Configuration type (if marked with @ConfigValue or @NamedConfig), or original type (if marked with @Value) */
         private final TypeName unwrappedType;
@@ -432,8 +435,8 @@ public class Processor extends AbstractProcessor {
         /** Get method type for public interface. */
         private final TypeName interfaceGetMethodType;
 
-        private ConfigurationFieldTypes(TypeName getMethodType, TypeName unwrappedType, TypeName viewClassType, TypeName initClassType, TypeName changeClassType, TypeName interfaceGetMethodType) {
-            this.getMethodType = getMethodType;
+        private ConfigurationFieldTypes(TypeName fieldType, TypeName unwrappedType, TypeName viewClassType, TypeName initClassType, TypeName changeClassType, TypeName interfaceGetMethodType) {
+            this.fieldType = fieldType;
             this.unwrappedType = unwrappedType;
             this.viewClassType = viewClassType;
             this.initClassType = initClassType;
@@ -447,8 +450,8 @@ public class Processor extends AbstractProcessor {
         }
 
         /** */
-        public TypeName getGetMethodType() {
-            return getMethodType;
+        public TypeName getFieldType() {
+            return fieldType;
         }
 
         /** */
@@ -711,11 +714,7 @@ public class Processor extends AbstractProcessor {
                             nodeChangeMtdBuilder.addStatement("$L.accept($L)", paramName, fieldName);
                         }
                         else {
-                            nodeChangeMtdBuilder.addAnnotation(
-                                AnnotationSpec.builder(SuppressWarnings.class)
-                                    .addMember("value", "$S", "unchecked")
-                                    .build()
-                            );
+                            nodeChangeMtdBuilder.addAnnotation(suppressWarningsUnchecked());
 
                             nodeChangeMtdBuilder.addStatement("$L.accept((NamedListChange)$L)", paramName, fieldName);
                         }
@@ -776,11 +775,7 @@ public class Processor extends AbstractProcessor {
                             nodeInitMtdBuilder.addStatement("$L.accept($L)", paramName, fieldName);
                         }
                         else {
-                            nodeInitMtdBuilder.addAnnotation(
-                                AnnotationSpec.builder(SuppressWarnings.class)
-                                    .addMember("value", "$S", "unchecked")
-                                    .build()
-                            );
+                            nodeInitMtdBuilder.addAnnotation(suppressWarningsUnchecked());
 
                             nodeInitMtdBuilder.addStatement("$L.accept((NamedListChange)$L)", paramName, fieldName);
                         }
diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java
index e08cf13..f11fe9b 100644
--- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java
+++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/configuration/processor/internal/Utils.java
@@ -16,6 +16,7 @@
  */
 package org.apache.ignite.configuration.processor.internal;
 
+import com.squareup.javapoet.AnnotationSpec;
 import com.squareup.javapoet.ClassName;
 import com.squareup.javapoet.CodeBlock;
 import com.squareup.javapoet.FieldSpec;
@@ -222,4 +223,12 @@ public class Utils {
         throw new ProcessorException(type + " is not a NamedListConfiguration class");
     }
 
+    /**
+     * @return {@code @SuppressWarnings("unchecked")} annotation spec object.
+     */
+    public static AnnotationSpec suppressWarningsUnchecked() {
+        return AnnotationSpec.builder(SuppressWarnings.class)
+            .addMember("value", "$S", "unchecked")
+            .build();
+    }
 }
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 ba6dcd2..4eb513c 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
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -54,6 +55,7 @@ public class UsageTest {
                 )
             )
         ).get(1, SECONDS);
+
         assertTrue(root.baseline().autoAdjust().enabled().value());
 
         root.baseline().autoAdjust().enabled().update(false).get(1, SECONDS);
@@ -73,6 +75,11 @@ public class UsageTest {
 
         root.baseline().nodes().get("node1").change(node -> node.changeAutoAdjustEnabled(false)).get(1, SECONDS);
         assertFalse(root.value().baseline().nodes().get("node1").autoAdjustEnabled());
+
+        root.baseline().nodes().change(nodes -> nodes.delete("node1")).get(1, SECONDS);
+
+        assertNull(root.baseline().nodes().get("node1"));
+        assertNull(root.value().baseline().nodes().get("node1"));
     }
 
     /**
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
index f29a6af..5c2d405 100644
--- 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
@@ -55,7 +55,7 @@ public class TestConfigurationStorage implements ConfigurationStorage {
     }
 
     /** {@inheritDoc} */
-    @Override public Data readAll() throws StorageException {
+    @Override public synchronized Data readAll() throws StorageException {
         if (fail)
             throw new StorageException("Failed to read data");
 
@@ -70,7 +70,12 @@ public class TestConfigurationStorage implements ConfigurationStorage {
         if (sentVersion != version.get())
             return CompletableFuture.completedFuture(false);
 
-        map.putAll(newValues);
+        for (Map.Entry<String, Serializable> entry : newValues.entrySet()) {
+            if (entry.getValue() != null)
+                map.put(entry.getKey(), entry.getValue());
+            else
+                map.remove(entry.getKey());
+        }
 
         version.incrementAndGet();
 
@@ -80,7 +85,7 @@ public class TestConfigurationStorage implements ConfigurationStorage {
     }
 
     /** {@inheritDoc} */
-    @Override public Set<String> keys() throws StorageException {
+    @Override public synchronized Set<String> keys() throws StorageException {
         if (fail)
             throw new StorageException("Failed to get keys");
 
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
index 1ee2d04..b6f91fb 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/ConfigurationChanger.java
@@ -173,7 +173,7 @@ public class ConfigurationChanger {
 
         if (storagesTypes.size() != 1) {
             return CompletableFuture.failedFuture(
-                new ConfigurationChangeException("Cannot change configurations belonging to different roots")
+                new ConfigurationChangeException("Cannot change configurations belonging to different storages.")
             );
         }
 
@@ -199,10 +199,10 @@ public class ConfigurationChanger {
         ConfigurationStorage storage,
         CompletableFuture<?> fut
     ) {
-        Map<String, Serializable> allChanges = changes.entrySet().stream()
-            .map((Map.Entry<RootKey<?>, TraversableTreeNode> change) -> nodeToFlatMap(change.getKey(), change.getValue()))
-            .flatMap(map -> map.entrySet().stream())
-            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+        Map<String, Serializable> allChanges = new HashMap<>();
+
+        for (Map.Entry<RootKey<?>, TraversableTreeNode> change : changes.entrySet())
+            allChanges.putAll(nodeToFlatMap(change.getKey(), getRootNode(change.getKey()), change.getValue()));
 
         StorageRoots roots = storagesRootsMap.get(storage.getClass());
 
@@ -243,8 +243,9 @@ public class ConfigurationChanger {
 
         Map<String, ?> dataValuesPrefixMap = ConfigurationUtil.toPrefixMap(changedEntries.values());
 
+        compressDeletedEntries(dataValuesPrefixMap);
+
         for (RootKey<?> rootKey : oldStorageRoots.roots.keySet()) {
-            //TODO IGNITE-14182 Remove is not yet supported here.
             Map<String, ?> rootPrefixMap = (Map<String, ?>)dataValuesPrefixMap.get(rootKey.key());
 
             if (rootPrefixMap != null) {
@@ -264,6 +265,32 @@ public class ConfigurationChanger {
     }
 
     /**
+     * "Compress" prefix map - this means that deleted named list elements will be represented as a single {@code null}
+     * objects instead of a number of nullified configuration leaves.
+     *
+     * @param prefixMap Prefix map, constructed from the storage notification data or its subtree.
+     */
+    private void compressDeletedEntries(Map<String, ?> prefixMap) {
+        // Here we basically assume that if prefix subtree contains single null child then all its childrens are nulls.
+        Set<String> keysForRemoval = prefixMap.entrySet().stream()
+            .filter(entry ->
+                entry.getValue() instanceof Map && ((Map<?, ?>)entry.getValue()).containsValue(null)
+            )
+            .map(Map.Entry::getKey)
+            .collect(Collectors.toSet());
+
+        // Replace all such elements will nulls, signifying that these are deleted named list elements.
+        for (String key : keysForRemoval)
+            prefixMap.put(key, null);
+
+        // Continue recursively.
+        for (Object value : prefixMap.values()) {
+            if (value instanceof Map)
+                compressDeletedEntries((Map<String, ?>)value);
+        }
+    }
+
+    /**
      * Validate configuration changes.
      *
      * @param storageRoots Storage roots.
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/PublicConfigurator.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/NamedConfigurationTree.java
similarity index 54%
rename from modules/configuration/src/main/java/org/apache/ignite/configuration/PublicConfigurator.java
rename to modules/configuration/src/main/java/org/apache/ignite/configuration/NamedConfigurationTree.java
index 7b8f953..e4f096c 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/PublicConfigurator.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/NamedConfigurationTree.java
@@ -17,26 +17,23 @@
 
 package org.apache.ignite.configuration;
 
-import org.apache.ignite.configuration.internal.DynamicConfiguration;
+import org.apache.ignite.configuration.tree.NamedListChange;
+import org.apache.ignite.configuration.tree.NamedListView;
 
 /**
- * Public configurator.
- * @param <T> Public type.
+ * Configuration tree representing arbitrary set of named underlying configuration tree of the same type.
+ *
+ * @param <T> Type of the underlying configuration tree.
+ * @param <VALUE> Value type of the underlying node.
+ * @param <CHANGE> Type of the object that changes underlying nodes values.
  */
-public class PublicConfigurator<T extends ConfigurationTree<?, ?>> {
-    /** Configuration root. */
-    private T root;
-
-    public <VIEW, INIT, CHANGE> PublicConfigurator(Configurator<? extends DynamicConfiguration<VIEW, INIT, CHANGE>> configurator) {
-        final ConfigurationTree<VIEW, CHANGE> root = configurator.getRoot();
-        this.root = (T) root;
-    }
-
+public interface NamedConfigurationTree<T extends ConfigurationProperty<VIEW, CHANGE>, VIEW, CHANGE, INIT>
+    extends ConfigurationTree<NamedListView<VIEW>, NamedListChange<CHANGE, INIT>>
+{
     /**
-     * Get root of the configuration.
-     * @return Configuration root.
+     * Get named configuration by name.
+     * @param name Name.
+     * @return Configuration.
      */
-    public T getRoot() {
-        return root;
-    }
+    T get(String name);
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/NamedConfigValue.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/NamedConfigValue.java
index f3d3f77..5ecc62b 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/NamedConfigValue.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/NamedConfigValue.java
@@ -21,7 +21,6 @@ import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 import org.apache.ignite.configuration.internal.NamedListConfiguration;
-import org.apache.ignite.configuration.internal.NamedList;
 
 import static java.lang.annotation.ElementType.FIELD;
 import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -42,7 +41,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
  *
  * }
  * </pre>
- * @see NamedList
  */
 @Target({ FIELD })
 @Retention(SOURCE)
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Value.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Value.java
index 4459890..b39c484 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Value.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/annotation/Value.java
@@ -28,7 +28,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
 /**
  * This annotation marks configuration schema field as a configuration tree leaf.
  * Every field annotated with this annotation will produce a {@link DynamicProperty} field in generated configuration class.
- * <br/> Type must be one of the following:
+ * <br/> Type must be one of the following (or array of one of the following):
  * <ul>
  *     <li>boolean</li>
  *     <li>int</li>
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java
index 62bbba5..12461a8 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/ConfigurationNode.java
@@ -93,7 +93,7 @@ public abstract class ConfigurationNode<VIEW> {
                 if (cachedRootNode == oldRootNode) {
                     cachedRootNode = newRootNode;
 
-                    refreshValue0(newVal);
+                    beforeRefreshValue(newVal);
 
                     return val = newVal;
                 }
@@ -128,5 +128,5 @@ public abstract class ConfigurationNode<VIEW> {
      *
      * @param newValue New configuration value.
      */
-    protected abstract void refreshValue0(VIEW newValue);
+    protected abstract void beforeRefreshValue(VIEW newValue);
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java
index 5b2bbeb..97c217f 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicConfiguration.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.configuration.internal;
 
 import java.io.Serializable;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -25,7 +26,6 @@ import java.util.Objects;
 import java.util.RandomAccess;
 import java.util.concurrent.Future;
 import java.util.function.Consumer;
-import java.util.stream.Collectors;
 import org.apache.ignite.configuration.ConfigurationChanger;
 import org.apache.ignite.configuration.ConfigurationProperty;
 import org.apache.ignite.configuration.ConfigurationTree;
@@ -99,18 +99,20 @@ public abstract class DynamicConfiguration<VIEW, INIT, CHANGE> extends Configura
         else {
             assert keys instanceof RandomAccess;
 
+            // Transform inner node closure into update tree.
             rootNodeChange.construct(keys.get(1), new ConfigurationSource() {
-                private int i = 1;
+                private int level = 1;
 
                 @Override public void descend(ConstructableTreeNode node) {
-                    if (++i == keys.size())
+                    if (++level == keys.size())
                         change.accept((CHANGE)node);
                     else
-                        node.construct(keys.get(i), this);
+                        node.construct(keys.get(level), this);
                 }
             });
         }
 
+        // Use resulting tree as update request for the storage.
         return changer.change(Map.of(rootKey, rootNodeChange));
     }
 
@@ -126,11 +128,11 @@ public abstract class DynamicConfiguration<VIEW, INIT, CHANGE> extends Configura
 
     /** {@inheritDoc} */
     @Override public Map<String, ConfigurationProperty<?, ?>> members() {
-        return members.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+        return Collections.unmodifiableMap(members);
     }
 
     /** {@inheritDoc} */
-    @Override protected void refreshValue0(VIEW newValue) {
+    @Override protected void beforeRefreshValue(VIEW newValue) {
         // No-op.
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java
index 883ad93..1beb83a 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/DynamicProperty.java
@@ -79,17 +79,18 @@ public class DynamicProperty<T extends Serializable> extends ConfigurationNode<T
         assert keys instanceof RandomAccess;
         assert !keys.isEmpty();
 
+        // Transform leaf value into update tree.
         rootNodeChange.construct(keys.get(1), new ConfigurationSource() {
-            private int i = 1;
+            private int level = 1;
 
             @Override public void descend(ConstructableTreeNode node) {
-                assert i < keys.size() - 1;
+                assert level < keys.size() - 1;
 
-                node.construct(keys.get(++i), this);
+                node.construct(keys.get(++level), this);
             }
 
             @Override public <T> T unwrap(Class<T> clazz) {
-                assert i == keys.size() - 1;
+                assert level == keys.size() - 1;
 
                 assert clazz.isInstance(newValue);
 
@@ -97,6 +98,7 @@ public class DynamicProperty<T extends Serializable> extends ConfigurationNode<T
             }
         });
 
+        // Use resulting tree as update request for the storage.
         return changer.change(Map.of(rootKey, rootNodeChange));
     }
 
@@ -106,7 +108,7 @@ public class DynamicProperty<T extends Serializable> extends ConfigurationNode<T
     }
 
     /** {@inheritDoc} */
-    @Override protected void refreshValue0(T newValue) {
+    @Override protected void beforeRefreshValue(T newValue) {
         // No-op.
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedList.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedList.java
deleted file mode 100644
index edce953..0000000
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedList.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.internal;
-
-import java.util.Map;
-
-/**
- * This class holds named configurations in VIEW object.
- */
-public class NamedList<T> {
-    /** Named values. */
-    private final Map<String, T> values;
-
-    /**
-     * Constructor.
-     * @param values Named values.
-     */
-    public NamedList(Map<String, T> values) {
-        this.values = values;
-    }
-
-    /**
-     * Get named values.
-     * @return Named values.
-     */
-    public Map<String, T> getValues() {
-        return values;
-    }
-}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java
index 5f3f450..107f06f 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/NamedListConfiguration.java
@@ -23,6 +23,7 @@ import java.util.Map;
 import java.util.function.BiFunction;
 import org.apache.ignite.configuration.ConfigurationChanger;
 import org.apache.ignite.configuration.ConfigurationProperty;
+import org.apache.ignite.configuration.NamedConfigurationTree;
 import org.apache.ignite.configuration.RootKey;
 import org.apache.ignite.configuration.tree.NamedListChange;
 import org.apache.ignite.configuration.tree.NamedListInit;
@@ -31,8 +32,10 @@ import org.apache.ignite.configuration.tree.NamedListView;
 /**
  * Named configuration wrapper.
  */
-public class NamedListConfiguration<VIEW, T extends ConfigurationProperty<VIEW, CHANGE>, INIT, CHANGE>
-    extends DynamicConfiguration<NamedListView<VIEW>, NamedListInit<INIT>, NamedListChange<CHANGE, INIT>> {
+public class NamedListConfiguration<T extends ConfigurationProperty<VIEW, CHANGE>, VIEW, CHANGE, INIT>
+    extends DynamicConfiguration<NamedListView<VIEW>, NamedListInit<INIT>, NamedListChange<CHANGE, INIT>>
+    implements NamedConfigurationTree<T, VIEW, CHANGE, INIT>
+{
     /** Creator of named configuration. */
     private final BiFunction<List<String>, String, T> creator;
 
@@ -57,20 +60,15 @@ public class NamedListConfiguration<VIEW, T extends ConfigurationProperty<VIEW,
         this.creator = creator;
     }
 
-    /**
-     * Get named configuration by name.
-     * @param name Name.
-     * @return Configuration.
-     */
-    public T get(String name) {
+    /** {@inheritDoc} */
+    @Override public T get(String name) {
         refreshValue();
 
-        //TODO IGNITE-14182 Exceptions.
         return values.get(name);
     }
 
     /** {@inheritDoc} */
-    @Override protected synchronized void refreshValue0(NamedListView<VIEW> newValue) {
+    @Override protected synchronized void beforeRefreshValue(NamedListView<VIEW> newValue) {
         Map<String, T> oldValues = this.values;
         Map<String, T> newValues = new HashMap<>();
 
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java
index 2d491b9..1c09c97 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/internal/util/ConfigurationUtil.java
@@ -275,64 +275,136 @@ public class ConfigurationUtil {
 
     /**
      * Convert a traversable tree to a map of qualified keys to values.
+     *
      * @param rootKey Root configuration key.
-     * @param node Tree.
+     * @param curRoot Current root tree.
+     * @param updates Tree with updates.
      * @return Map of changes.
      */
-    public static Map<String, Serializable> nodeToFlatMap(RootKey<?> rootKey, TraversableTreeNode node) {
-        Map<String, Serializable> values = new HashMap<>();
+    public static Map<String, Serializable> nodeToFlatMap(
+        RootKey<?> rootKey,
+        TraversableTreeNode curRoot,
+        TraversableTreeNode updates
+    ) {
+        return updates.accept(rootKey.key(), new ConfigurationVisitor<>() {
+            /** Resulting flat map. */
+            private Map<String, Serializable> values = new HashMap<>();
 
-        node.accept(rootKey.key(), new ConfigurationVisitor<>() {
             /** Current key, aggregated by visitor. */
-            StringBuilder currentKey = new StringBuilder();
+            private StringBuilder currentKey = new StringBuilder();
+
+            /** Current keys list, almost the same as {@link #currentKey}. */
+            private List<String> currentPath = new ArrayList<>();
+
+            /** Write nulls instead of actual values. Makes sense for deletions from named lists. */
+            private boolean writeNulls;
 
             /** {@inheritDoc} */
-            @Override public Void visitLeafNode(String key, Serializable val) {
+            @Override public Map<String, Serializable> visitLeafNode(String key, Serializable val) {
                 if (val != null)
-                    values.put(currentKey.toString() + key, val);
+                    values.put(currentKey.toString() + key, writeNulls ? null : val);
 
-                return null;
+                return values;
             }
 
             /** {@inheritDoc} */
-            @Override public Void visitInnerNode(String key, InnerNode node) {
+            @Override public Map<String, Serializable> visitInnerNode(String key, InnerNode node) {
                 if (node == null)
                     return null;
 
-                int previousKeyLength = currentKey.length();
-
-                currentKey.append(key).append('.');
+                int previousKeyLength = startVisit(key, false);
 
                 node.traverseChildren(this);
 
-                currentKey.setLength(previousKeyLength);
+                endVisit(previousKeyLength);
 
-                return null;
+                return values;
             }
 
             /** {@inheritDoc} */
-            @Override public <N extends InnerNode> Void visitNamedListNode(String key, NamedListNode<N> node) {
-                int previousKeyLength = currentKey.length();
-
-                if (key != null)
-                    currentKey.append(key).append('.');
+            @Override public <N extends InnerNode> Map<String, Serializable> visitNamedListNode(String key, NamedListNode<N> node) {
+                int previousKeyLength = startVisit(key, false);
 
                 for (String namedListKey : node.namedListKeys()) {
-                    int loopPreviousKeyLength = currentKey.length();
+                    int loopPreviousKeyLength = startVisit(namedListKey, true);
 
-                    currentKey.append(ConfigurationUtil.escape(namedListKey)).append('.');
+                    N namedElement = node.get(namedListKey);
 
-                    node.get(namedListKey).traverseChildren(this);
+                    if (namedElement == null)
+                        visitDeletedNamedListElement();
+                    else
+                        namedElement.traverseChildren(this);
 
-                    currentKey.setLength(loopPreviousKeyLength);
+                    endVisit(loopPreviousKeyLength);
                 }
 
+                endVisit(previousKeyLength);
+
+                return values;
+            }
+
+            /**
+             * Prepares values of {@link #currentKey} and {@link #currentPath} for further processing.
+             *
+             * @param key Key.
+             * @param escape Whether we need to escape the key before appending it to {@link #currentKey}.
+             * @return Previous length of {@link #currentKey} so it can be passed to {@link #endVisit(int)} later.
+             */
+            private int startVisit(String key, boolean escape) {
+                int previousKeyLength = currentKey.length();
+
+                currentKey.append(escape ? ConfigurationUtil.escape(key) : key).append('.');
+
+                if (!writeNulls)
+                    currentPath.add(key);
+
+                return previousKeyLength;
+            }
+
+            /**
+             * Puts {@link #currentKey} and {@link #currentPath} in the same state as they were before
+             * {@link #startVisit(String, boolean)}.
+             *
+             * @param previousKeyLength Value return by corresponding {@link #startVisit(String, boolean)} invocation.
+             */
+            private void endVisit(int previousKeyLength) {
                 currentKey.setLength(previousKeyLength);
 
-                return null;
+                if (!writeNulls)
+                    currentPath.remove(currentPath.size() - 1);
+            }
+
+            /**
+             * Here we must list all joined keys belonging to deleted element. The only way to do it is to traverse
+             * cached configuration tree and write {@code null} into all values.
+             */
+            private void visitDeletedNamedListElement() {
+                // It must be impossible to delete something inside of the element that we're currently deleting.
+                assert !writeNulls;
+
+                Object originalNamedElement = null;
+
+                try {
+                    // This code can in fact be better optimized for deletion scenario,
+                    // but there's no point in doing that, since the operation is so rare and it will
+                    // complicate code even more.
+                    originalNamedElement = find(currentPath.subList(1, currentPath.size()), curRoot);
+                }
+                catch (KeyNotFoundException ignore) {
+                    // May happen, not a big deal. This means that element never existed in the first place.
+                }
+
+                if (originalNamedElement != null) {
+                    assert originalNamedElement instanceof InnerNode : currentPath;
+
+                    writeNulls = true;
+
+                    ((InnerNode)originalNamedElement).traverseChildren(this);
+
+                    writeNulls = false;
+                }
             }
         });
-        return values;
     }
 
     /**
diff --git a/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ITScaleCubeNetworkClusterMessagingTest.java b/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ITScaleCubeNetworkClusterMessagingTest.java
index 89e6376..67e8977 100644
--- a/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ITScaleCubeNetworkClusterMessagingTest.java
+++ b/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ITScaleCubeNetworkClusterMessagingTest.java
@@ -47,6 +47,8 @@ class ITScaleCubeNetworkClusterMessagingTest {
 
             iterator.remove();
         }
+
+        TestNetworkHandlersProvider.MESSAGE_STORAGE.clear();
     }
 
     /** */