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 2025/04/30 05:39:37 UTC

(ignite-3) branch main updated: IGNITE-25048 Support configuration deprecation (#5714)

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 bc6fa5451d3 IGNITE-25048 Support configuration deprecation (#5714)
bc6fa5451d3 is described below

commit bc6fa5451d3163f53dad8d6c6c5904f81fca7a57
Author: Ivan Bessonov <be...@gmail.com>
AuthorDate: Wed Apr 30 08:39:32 2025 +0300

    IGNITE-25048 Support configuration deprecation (#5714)
---
 .../java/org/apache/ignite/client/TestServer.java  |   3 +-
 .../processor/ConfigurationProcessor.java          |  24 +-
 .../ignite/configuration/ConfigurationModule.java  |  19 +
 .../presentation/HoconPresentationTest.java        |   3 +-
 .../configuration/ConfigurationChanger.java        |  63 ++-
 .../configuration/ConfigurationManager.java        |   6 +-
 .../configuration/ConfigurationMigrator.java       |  33 ++
 .../configuration/ConfigurationRegistry.java       |  12 +-
 .../configuration/util/ConfigurationFlattener.java |  65 ++-
 .../configuration/util/ConfigurationUtil.java      |  28 +-
 .../configuration/ConfigurationChangerTest.java    |   6 +-
 .../configuration/ConfigurationRegistryTest.java   |  27 +-
 .../InjectedValueConfigurationTest.java            |   3 +-
 .../asm/ConfigurationTreeGeneratorTest.java        |   3 +-
 .../deprecation/DeprecatedConfigurationTest.java   | 459 +++++++++++++++++++++
 .../configuration/direct/DirectPropertiesTest.java |   3 +-
 .../configuration/hocon/HoconConverterTest.java    |   3 +-
 .../ConfigurationAnyListenerTest.java              |   3 +-
 .../notifications/ConfigurationListenerTest.java   |   3 +-
 .../internal/configuration/sample/UsageTest.java   |   6 +-
 .../configuration/tree/InternalIdTest.java         |   3 +-
 .../configuration/tree/NamedListNodeTest.java      |   3 +-
 .../configuration/util/ConfigurationUtilTest.java  |   6 +-
 .../configuration/TestConfigurationChanger.java    |  10 +-
 ...niteDistributionZoneManagerNodeRestartTest.java |   6 +-
 .../rebalance/ItRebalanceDistributedTest.java      |   6 +-
 .../metastorage/server/WatchProcessor.java         |   2 +-
 .../network/utils/ClusterServiceTestUtils.java     |   3 +-
 .../partition/replicator/fixtures/Node.java        |   6 +-
 .../ignite/internal/rest/RestComponentTest.java    |   3 +-
 .../internal/rest/configuration/TestFactory.java   |   3 +-
 .../configuration/generator/DefaultsGenerator.java |   2 +-
 .../ItDistributedConfigurationPropertiesTest.java  |   3 +-
 .../runner/app/ItIgniteNodeRestartTest.java        |   6 +-
 .../org/apache/ignite/internal/app/IgniteImpl.java |   6 +-
 .../internal/configuration/CompoundModule.java     |   5 +
 .../storage/LocalFileConfigurationStorageTest.java |   3 +-
 37 files changed, 728 insertions(+), 120 deletions(-)

diff --git a/modules/client/src/test/java/org/apache/ignite/client/TestServer.java b/modules/client/src/test/java/org/apache/ignite/client/TestServer.java
index 34194725490..b7fb490f655 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/TestServer.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/TestServer.java
@@ -183,7 +183,8 @@ public class TestServer implements AutoCloseable {
                 List.of(NodeConfiguration.KEY),
                 new TestConfigurationStorage(LOCAL),
                 generator,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         ComponentContext componentContext = new ComponentContext();
diff --git a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java
index 9139376e79e..477640b3369 100644
--- a/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java
+++ b/modules/configuration-annotation-processor/src/main/java/org/apache/ignite/internal/configuration/processor/ConfigurationProcessor.java
@@ -292,11 +292,16 @@ public class ConfigurationProcessor extends AbstractProcessor {
         // Get configuration types (VIEW, CHANGE and so on)
         TypeName interfaceGetMethodType = getInterfaceGetMethodType(field);
 
-        MethodSpec interfaceGetMethod = MethodSpec.methodBuilder(fieldName)
+        MethodSpec.Builder builder = MethodSpec.methodBuilder(fieldName)
                 .addModifiers(PUBLIC, ABSTRACT)
                 .returns(interfaceGetMethodType)
-                .addJavadoc("@see $T#" + field, field.getEnclosingElement())
-                .build();
+                .addJavadoc("@see $T#" + field, field.getEnclosingElement());
+
+        if (field.getAnnotation(Deprecated.class) != null) {
+            builder.addAnnotation(Deprecated.class);
+        }
+
+        MethodSpec interfaceGetMethod = builder.build();
 
         configurationInterfaceBuilder.addMethod(interfaceGetMethod);
     }
@@ -474,6 +479,11 @@ public class ConfigurationProcessor extends AbstractProcessor {
                     .addJavadoc("@see $T#" + field, field.getEnclosingElement())
                     .returns(viewFieldType);
 
+            boolean isDeprecated = field.getAnnotation(Deprecated.class) != null;
+            if (isDeprecated) {
+                getMtdBuilder.addAnnotation(Deprecated.class);
+            }
+
             viewClsBuilder.addMethod(getMtdBuilder.build());
 
             // Read only.
@@ -488,6 +498,10 @@ public class ConfigurationProcessor extends AbstractProcessor {
                     .addJavadoc("@see $T#" + field, field.getEnclosingElement())
                     .returns(changeClsName);
 
+            if (isDeprecated) {
+                changeMtdBuilder.addAnnotation(Deprecated.class);
+            }
+
             if (containsAnyAnnotation(field, Value.class, InjectedValue.class)) {
                 if (schemaFieldType.getKind() == TypeKind.ARRAY) {
                     changeMtdBuilder.varargs(true);
@@ -503,6 +517,10 @@ public class ConfigurationProcessor extends AbstractProcessor {
                         .addJavadoc("@see $T#" + field, field.getEnclosingElement())
                         .returns(changeFieldType);
 
+                if (isDeprecated) {
+                    shortChangeMtdBuilder.addAnnotation(Deprecated.class);
+                }
+
                 changeClsBuilder.addMethod(shortChangeMtdBuilder.build());
             }
 
diff --git a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationModule.java b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationModule.java
index e94a7993f88..f3e928d1eac 100644
--- a/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationModule.java
+++ b/modules/configuration-api/src/main/java/org/apache/ignite/configuration/ConfigurationModule.java
@@ -117,4 +117,23 @@ public interface ConfigurationModule {
     default void patchConfigurationWithDynamicDefaults(SuperRootChange rootChange) {
         // No-op.
     }
+
+    /**
+     * Logic to perform configuration migration when Ignite version is upgraded. Main task - replace deprecated configuration values with
+     * their non-deprecated cousins.
+     *
+     * <p>Typical implementation should look something like this:
+     * <pre><code>
+     *      var barValue = superRoot.viewRoot(KEY).foo().oldConfiguration().bar()
+     *
+     *      if (barValue != BAR_DEFAULT) { // Usually implies explicitly set value.
+     *          superRoot.changeRoot(KEY).changeNewFoo().changeBar(barValue)
+     *      }
+     * </code></pre>
+     *
+     * @param superRootChange Super root change instance.
+     */
+    default void migrateDeprecatedConfigurations(SuperRootChange superRootChange) {
+        // No-op.
+    }
 }
diff --git a/modules/configuration-presentation/src/test/java/org/apache/ignite/internal/configuration/presentation/HoconPresentationTest.java b/modules/configuration-presentation/src/test/java/org/apache/ignite/internal/configuration/presentation/HoconPresentationTest.java
index d72a9c6696e..c8e2dfc4d3a 100644
--- a/modules/configuration-presentation/src/test/java/org/apache/ignite/internal/configuration/presentation/HoconPresentationTest.java
+++ b/modules/configuration-presentation/src/test/java/org/apache/ignite/internal/configuration/presentation/HoconPresentationTest.java
@@ -88,7 +88,8 @@ public class HoconPresentationTest {
                 List.of(TestRootConfiguration.KEY),
                 new TestConfigurationStorage(LOCAL),
                 generator,
-                ConfigurationValidatorImpl.withDefaultValidators(generator, Set.of(validator))
+                ConfigurationValidatorImpl.withDefaultValidators(generator, Set.of(validator)),
+                changer -> {}
         );
 
         assertThat(cfgRegistry.startAsync(new ComponentContext()), willCompleteSuccessfully());
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
index 009a47da218..5e4df905c86 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationChanger.java
@@ -94,6 +94,9 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
     /** Configuration validator. */
     private final ConfigurationValidator configurationValidator;
 
+    /** Configuration migrator. */
+    private final ConfigurationMigrator migrator;
+
     /** Storage trees. */
     private volatile StorageRoots storageRoots;
 
@@ -144,8 +147,8 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
         /** Immutable forest, so to say. */
         private final SuperRoot roots;
 
-        /** Version associated with the currently known storage state. */
-        private final long version;
+        /** Full storage data. */
+        private final Data data;
 
         /** Future that signifies update of current configuration. */
         private final CompletableFuture<Void> changeFuture = new CompletableFuture<>();
@@ -155,12 +158,12 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
          *
          * @param rootsWithoutDefaults Forest without the defaults
          * @param roots Forest with the defaults filled in
-         * @param version Version associated with the currently known storage state.
+         * @param data Configuration storage state.
          */
-        private StorageRoots(SuperRoot rootsWithoutDefaults, SuperRoot roots, long version) {
+        private StorageRoots(SuperRoot rootsWithoutDefaults, SuperRoot roots, Data data) {
             this.rootsWithoutDefaults = rootsWithoutDefaults;
             this.roots = roots;
-            this.version = version;
+            this.data = data;
 
             makeImmutable(roots);
             makeImmutable(rootsWithoutDefaults);
@@ -203,19 +206,23 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
      * @param rootKeys Configuration root keys.
      * @param storage Configuration storage.
      * @param configurationValidator Configuration validator.
+     * @param migrator Configuration migrator.
      * @throws IllegalArgumentException If the configuration type of the root keys is not equal to the storage type.
      */
     public ConfigurationChanger(
             ConfigurationUpdateListener configurationUpdateListener,
             Collection<RootKey<?, ?>> rootKeys,
             ConfigurationStorage storage,
-            ConfigurationValidator configurationValidator) {
+            ConfigurationValidator configurationValidator,
+            ConfigurationMigrator migrator
+    ) {
         checkConfigurationType(rootKeys, storage);
 
         this.configurationUpdateListener = configurationUpdateListener;
         this.storage = storage;
         this.configurationValidator = configurationValidator;
         this.rootKeys = rootKeys.stream().collect(toMap(RootKey::key, identity()));
+        this.migrator = migrator;
     }
 
     /**
@@ -292,7 +299,7 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
         // The root WITHOUT the defaults is used to calculate which properties to write to the underlying storage,
         // in other words it allows us to persist the defaults from the code.
         // After the storage listener fires for the first time both roots are supposed to become equal.
-        storageRoots = new StorageRoots(superRootNoDefaults, superRoot, data.changeId());
+        storageRoots = new StorageRoots(superRootNoDefaults, superRoot, data);
 
         storage.registerConfigurationListener(configurationStorageListener());
 
@@ -310,7 +317,7 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
     private void persistDefaults() {
         // If the storage version is 0, it indicates that the storage is empty.
         // In this case, write the defaults along with the initial configuration.
-        ConfigurationSource cfgSrc = storageRoots.version == 0 ? initialConfiguration : ConfigurationUtil.EMPTY_CFG_SRC;
+        ConfigurationSource cfgSrc = storageRoots.data.changeId() == 0 ? initialConfiguration : ConfigurationUtil.EMPTY_CFG_SRC;
 
         changeInternally(cfgSrc, true)
                 .whenComplete((v, e) -> {
@@ -596,7 +603,7 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
             // This read and the following comparison MUST be performed while holding read lock.
             StorageRoots localRoots = storageRoots;
 
-            if (localRoots.version < storageRevision) {
+            if (localRoots.data.changeId() < storageRevision) {
                 // Need to wait for the configuration updates from the storage, then try to update again (loop).
                 return localRoots.changeFuture.thenCompose(v -> changeInternally(src, onStartup));
             }
@@ -611,7 +618,11 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
 
             addDefaults(changes);
 
+            migrator.migrate(new SuperRootChangeImpl(changes));
+
             Map<String, Serializable> allChanges = createFlattenedUpdatesMap(localRoots.rootsWithoutDefaults, changes);
+            dropUnnecessarilyDeletedKeys(allChanges, localRoots);
+
             if (allChanges.isEmpty() && onStartup) {
                 // We don't want an empty storage update if this is the initialization changer.
                 return nullCompletedFuture();
@@ -625,7 +636,7 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
             // still try to write the empty update, because local configuration can be obsolete. If this is the case, then the CAS will
             // fail and the update will be recalculated and there is a chance that the new local configuration will produce a non-empty
             // update.
-            return storage.write(allChanges, localRoots.version)
+            return storage.write(allChanges, localRoots.data.changeId())
                     .thenCompose(casWroteSuccessfully -> {
                         if (casWroteSuccessfully) {
                             return localRoots.changeFuture;
@@ -675,7 +686,7 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
 
             long newChangeId = changedEntries.changeId();
 
-            var newStorageRoots = new StorageRoots(newSuperNoDefaults, newSuperRoot, newChangeId);
+            var newStorageRoots = new StorageRoots(newSuperNoDefaults, newSuperRoot, mergeData(oldStorageRoots.data, changedEntries));
 
             rwLock.writeLock().lock();
 
@@ -700,6 +711,29 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
         };
     }
 
+    private static Data mergeData(Data currentData, Data delta) {
+        assert delta.changeId() > currentData.changeId() : currentData.changeId() + " " + delta.changeId();
+
+        Map<String, Serializable> newState = new HashMap<>(currentData.values());
+
+        for (Entry<String, ? extends Serializable> entry : delta.values().entrySet()) {
+            if (entry.getValue() == null) {
+                newState.remove(entry.getKey());
+            } else {
+                newState.put(entry.getKey(), entry.getValue());
+            }
+        }
+
+        return new Data(newState, delta.changeId());
+    }
+
+    /**
+     * Remove keys from {@code allChanges}, that are associated with nulls in this map, and already absent in {@link StorageRoots#data}.
+     */
+    private static void dropUnnecessarilyDeletedKeys(Map<String, Serializable> allChanges, StorageRoots localRoots) {
+        allChanges.entrySet().removeIf(entry -> entry.getValue() == null && !localRoots.data.values().containsKey(entry.getKey()));
+    }
+
     /**
      * Notifies all listeners of the current configuration.
      *
@@ -710,7 +744,12 @@ public abstract class ConfigurationChanger implements DynamicConfigurationChange
 
         long notificationCount = notificationListenerCnt.incrementAndGet();
 
-        return configurationUpdateListener.onConfigurationUpdated(null, storageRoots.roots, storageRoots.version, notificationCount);
+        return configurationUpdateListener.onConfigurationUpdated(
+                null,
+                storageRoots.roots,
+                storageRoots.data.changeId(),
+                notificationCount
+        );
     }
 
     /** {@inheritDoc} */
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java
index 23c7f81a96a..a99f9947e93 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationManager.java
@@ -45,13 +45,15 @@ public class ConfigurationManager implements IgniteComponent {
             Collection<RootKey<?, ?>> rootKeys,
             ConfigurationStorage storage,
             ConfigurationTreeGenerator generator,
-            ConfigurationValidator configurationValidator
+            ConfigurationValidator configurationValidator,
+            ConfigurationMigrator migrator
     ) {
         registry = new ConfigurationRegistry(
                 rootKeys,
                 storage,
                 generator,
-                configurationValidator
+                configurationValidator,
+                migrator
         );
     }
 
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationMigrator.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationMigrator.java
new file mode 100644
index 00000000000..33cb87681d3
--- /dev/null
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationMigrator.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.configuration;
+
+import org.apache.ignite.configuration.ConfigurationModule;
+import org.apache.ignite.configuration.SuperRootChange;
+
+/**
+ * Class that is used to perform configuration migration when Ignite version is upgraded. Main task - replace deprecated configuration
+ * values with their non-deprecated cousins. In production code expected to be based on
+ * {@link ConfigurationModule#migrateDeprecatedConfigurations(SuperRootChange)}. Could be anything in tests.
+ *
+ * @see ConfigurationModule#migrateDeprecatedConfigurations(SuperRootChange)
+ */
+@FunctionalInterface
+public interface ConfigurationMigrator {
+    void migrate(SuperRootChange superRoot);
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationRegistry.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationRegistry.java
index ac074ba17b0..e242077e179 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationRegistry.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/ConfigurationRegistry.java
@@ -63,23 +63,17 @@ public class ConfigurationRegistry implements IgniteComponent {
 
     /**
      * Constructor.
-     *
-     * @param rootKeys                    Configuration root keys.
-     * @param storage                     Configuration storage.
-     * @param generator                   Configuration tree generator.
-     * @param configurationValidator      Configuration validator.
-     * @throws IllegalArgumentException If the configuration type of the root keys is not equal to the storage type, or if the schema or its
-     *                                  extensions are not valid.
      */
     public ConfigurationRegistry(
             Collection<RootKey<?, ?>> rootKeys,
             ConfigurationStorage storage,
             ConfigurationTreeGenerator generator,
-            ConfigurationValidator configurationValidator
+            ConfigurationValidator configurationValidator,
+            ConfigurationMigrator migrator
     ) {
         checkConfigurationType(rootKeys, storage);
 
-        changer = new ConfigurationChanger(notificationUpdateListener(), rootKeys, storage, configurationValidator) {
+        changer = new ConfigurationChanger(notificationUpdateListener(), rootKeys, storage, configurationValidator, migrator) {
             @Override
             public InnerNode createRootNode(RootKey<?, ?> rootKey) {
                 return generator.instantiateNode(rootKey.schemaClass());
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java
index 282a06cfb5e..f7e8f54af7b 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationFlattener.java
@@ -114,11 +114,15 @@ public class ConfigurationFlattener {
         /** {@inheritDoc} */
         @Override
         public Void doVisitLeafNode(Field field, String key, Serializable newVal) {
+            boolean isDeprecated = field.isAnnotationPresent(Deprecated.class);
+
             // Read same value from old tree.
             Serializable oldVal = oldInnerNodesStack.element().traverseChild(key, ConfigurationUtil.leafNodeVisitor(), true);
 
             // Do not put duplicates into the resulting map.
-            if (singleTreeTraversal || !Objects.deepEquals(oldVal, newVal)) {
+            if (isDeprecated || singleTreeTraversal || !Objects.deepEquals(oldVal, newVal)) {
+                boolean deletion = this.deletion || isDeprecated;
+
                 resMap.put(currentKey(), deletion ? null : newVal);
             }
 
@@ -128,14 +132,11 @@ public class ConfigurationFlattener {
         /** {@inheritDoc} */
         @Override
         public Void doVisitInnerNode(Field field, String key, InnerNode newNode) {
+            boolean isDeprecated = field != null && field.isAnnotationPresent(Deprecated.class);
+
             // Read same node from old tree.
             InnerNode oldNode = oldInnerNodesStack.element().traverseChild(key, ConfigurationUtil.innerNodeVisitor(), true);
 
-            // Skip subtree that has not changed.
-            if (oldNode == newNode && !singleTreeTraversal) {
-                return null;
-            }
-
             // In case inner node is null in both trees,
             // see LocalFileConfigurationStorageTest#innerNodeWithPartialContent – the node is someConfigurationValue.
             if (oldNode == null && newNode == null) {
@@ -143,14 +144,16 @@ public class ConfigurationFlattener {
             }
 
             if (oldNode == null) {
-                visitAsymmetricInnerNode(newNode, false);
+                visitAsymmetricInnerNode(newNode, isDeprecated);
             } else if (oldNode.schemaType() != newNode.schemaType()) {
                 // At the moment, we do not separate the general fields from the fields of
                 // specific instances of the polymorphic configuration, so we will assume
                 // that all the fields have changed, perhaps we will fix this later.
                 visitAsymmetricInnerNode(oldNode, true);
 
-                visitAsymmetricInnerNode(newNode, false);
+                visitAsymmetricInnerNode(newNode, isDeprecated);
+            } else if (isDeprecated) {
+                visitAsymmetricInnerNode(newNode, true);
             } else {
                 oldInnerNodesStack.push(oldNode);
 
@@ -165,18 +168,14 @@ public class ConfigurationFlattener {
         /** {@inheritDoc} */
         @Override
         public Void doVisitNamedListNode(Field field, String key, NamedListNode<?> newNode) {
+            boolean isDeprecated = field.isAnnotationPresent(Deprecated.class);
+
             // Read same named list node from old tree.
             NamedListNode<?> oldNode =
                     oldInnerNodesStack.element().traverseChild(key, ConfigurationUtil.namedListNodeVisitor(), true);
 
-            // Skip subtree that has not changed.
-            if (oldNode == newNode && !singleTreeTraversal) {
-                return null;
-            }
-
             // Old keys ordering can be ignored if we either create or delete everything.
-            Map<String, Integer> oldKeysToOrderIdxMap = singleTreeTraversal ? null
-                    : keysToOrderIdx(oldNode);
+            Map<String, Integer> oldKeysToOrderIdxMap = singleTreeTraversal ? null : keysToOrderIdx(oldNode);
 
             // New keys ordering can be ignored if we delete everything.
             Map<String, Integer> newKeysToOrderIdxMap = deletion ? null : keysToOrderIdx(newNode);
@@ -187,7 +186,7 @@ public class ConfigurationFlattener {
                 String namedListFullKey = currentKey();
 
                 withTracking(newNodeInternalId.toString(), false, false, () -> {
-                    InnerNode newNamedElement = newNode.getInnerNode(newNodeKey);
+                    InnerNode newNamedElement = isDeprecated ? null : newNode.getInnerNode(newNodeKey);
 
                     String oldNodeKey = oldNode.keyByInternalId(newNodeInternalId);
                     InnerNode oldNamedElement = oldNode.getInnerNode(oldNodeKey);
@@ -197,27 +196,23 @@ public class ConfigurationFlattener {
                         return null;
                     }
 
-                    // Skip element that has not changed.
-                    // Its index can be different though, so we don't "continue" straight away.
-                    if (singleTreeTraversal || oldNamedElement != newNamedElement) {
-                        if (newNamedElement == null) {
-                            visitAsymmetricInnerNode(oldNamedElement, true);
-                        } else if (oldNamedElement == null) {
-                            visitAsymmetricInnerNode(newNamedElement, false);
-                        } else if (newNamedElement.schemaType() != oldNamedElement.schemaType()) {
-                            // At the moment, we do not separate the general fields from the fields of
-                            // specific instances of the polymorphic configuration, so we will assume
-                            // that all the fields have changed, perhaps we will fix this later.
-                            visitAsymmetricInnerNode(oldNamedElement, true);
-
-                            visitAsymmetricInnerNode(newNamedElement, false);
-                        } else {
-                            oldInnerNodesStack.push(oldNamedElement);
+                    if (newNamedElement == null) {
+                        visitAsymmetricInnerNode(oldNamedElement, true);
+                    } else if (oldNamedElement == null) {
+                        visitAsymmetricInnerNode(newNamedElement, isDeprecated);
+                    } else if (newNamedElement.schemaType() != oldNamedElement.schemaType()) {
+                        // At the moment, we do not separate the general fields from the fields of
+                        // specific instances of the polymorphic configuration, so we will assume
+                        // that all the fields have changed, perhaps we will fix this later.
+                        visitAsymmetricInnerNode(oldNamedElement, true);
+
+                        visitAsymmetricInnerNode(newNamedElement, isDeprecated);
+                    } else {
+                        oldInnerNodesStack.push(oldNamedElement);
 
-                            newNamedElement.traverseChildren(this, true);
+                        newNamedElement.traverseChildren(this, true);
 
-                            oldInnerNodesStack.pop();
-                        }
+                        oldInnerNodesStack.pop();
                     }
 
                     Integer newIdx = newKeysToOrderIdxMap == null ? null : newKeysToOrderIdxMap.get(newNodeKey);
diff --git a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
index a0e1a28b967..f2890cc5734 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/internal/configuration/util/ConfigurationUtil.java
@@ -38,6 +38,7 @@ import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.NoSuchElementException;
 import java.util.Queue;
 import java.util.RandomAccess;
@@ -223,7 +224,7 @@ public class ConfigurationUtil {
     public static Map<String, ?> toPrefixMap(Map<String, ? extends Serializable> rawConfig) {
         Map<String, Object> res = new HashMap<>();
 
-        for (Map.Entry<String, ? extends Serializable> entry : rawConfig.entrySet()) {
+        for (Entry<String, ? extends Serializable> entry : rawConfig.entrySet()) {
             List<String> keys = split(entry.getKey());
 
             assert keys instanceof RandomAccess : keys.getClass();
@@ -755,9 +756,7 @@ public class ConfigurationUtil {
      * @param prefixMap Prefix map, constructed from the storage notification data or its subtree.
      */
     public static void compressDeletedEntries(Map<String, ?> prefixMap) {
-        for (Iterator<? extends Map.Entry<String, ?>> it = prefixMap.entrySet().iterator(); it.hasNext(); ) {
-            Map.Entry<String, ?> entry = it.next();
-
+        for (Entry<String, ?> entry : prefixMap.entrySet()) {
             Object value = entry.getValue();
 
             if (value instanceof Map) {
@@ -768,10 +767,6 @@ public class ConfigurationUtil {
                 if (map.containsKey(NamedListNode.NAME) && map.get(NamedListNode.NAME) == null) {
                     entry.setValue(null);
                 }
-            } else if (value == null) {
-                // If there was a change in the type of polymorphic configuration,
-                // then the fields of the old configuration will be {@code null}, so we can get rid of them.
-                it.remove();
             }
         }
 
@@ -998,7 +993,9 @@ public class ConfigurationUtil {
                 return;
             }
 
-            for (Map.Entry<String, ?> entry : map.entrySet()) {
+            assert node instanceof InnerNode : node;
+
+            for (Entry<String, ?> entry : map.entrySet()) {
                 String key = entry.getKey();
                 Object val = entry.getValue();
 
@@ -1010,7 +1007,16 @@ public class ConfigurationUtil {
                 }
 
                 if (val == null) {
-                    node.construct(key, null, true);
+                    try {
+                        // If we received a null value, there are two options:
+                        //  - we changed a type of the polymorphic configuration, and some values must disappear. We'll get an exception.
+                        //  - we deleted deprecated configuration value from configuration storage. The value in tree must be replaced with
+                        //    default in such a case.
+                        ((InnerNode) node).constructDefault(key);
+                    } catch (NoSuchElementException ignore) {
+                        assert ((InnerNode) node).isPolymorphic()
+                                : "Constructing property " + key + " failed in " + node + " and it is not polymorphic.";
+                    }
                 } else if (val instanceof Map) {
                     node.construct(key, new InnerConfigurationSource((Map<String, ?>) val), true);
                 } else {
@@ -1034,7 +1040,7 @@ public class ConfigurationUtil {
             // This list must be mutable and RandomAccess.
             var orderedKeys = new ArrayList<>(((NamedListView<?>) node).namedListKeys());
 
-            for (Map.Entry<String, ?> entry : map.entrySet()) {
+            for (Entry<String, ?> entry : map.entrySet()) {
                 String internalIdStr = entry.getKey();
 
                 // This is the mapping of internal ids to names. Skip it.
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java
index f868c6f04aa..fae6f64ce63 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/ConfigurationChangerTest.java
@@ -226,7 +226,8 @@ public class ConfigurationChangerTest {
                 List.of(KEY),
                 storage,
                 generator,
-                ConfigurationValidatorImpl.withDefaultValidators(generator, Set.of(validator))
+                ConfigurationValidatorImpl.withDefaultValidators(generator, Set.of(validator)),
+                change -> {}
         );
 
         changer2.start();
@@ -606,7 +607,8 @@ public class ConfigurationChangerTest {
                 List.of(rootKey),
                 storage,
                 generator,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                change -> {}
         );
     }
 
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/ConfigurationRegistryTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/ConfigurationRegistryTest.java
index 785946b9def..8e50996e211 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/ConfigurationRegistryTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/ConfigurationRegistryTest.java
@@ -65,7 +65,8 @@ public class ConfigurationRegistryTest {
                                 List.of(ExtendedFirstRootConfigurationSchema.class),
                                 List.of()
                         ),
-                        new TestConfigurationValidator()
+                        new TestConfigurationValidator(),
+                        changer -> {}
                 )
         );
 
@@ -78,7 +79,8 @@ public class ConfigurationRegistryTest {
                         List.of(ExtendedFirstRootConfigurationSchema.class),
                         List.of()
                 ),
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(configRegistry.stopAsync(new ComponentContext()), willCompleteSuccessfully());
@@ -97,7 +99,8 @@ public class ConfigurationRegistryTest {
                                 List.of(),
                                 List.of(Second0PolymorphicConfigurationSchema.class)
                         ),
-                        new TestConfigurationValidator()
+                        new TestConfigurationValidator(),
+                        changer -> {}
                 )
         );
 
@@ -112,7 +115,8 @@ public class ConfigurationRegistryTest {
                                 List.of(),
                                 List.of(First0PolymorphicConfigurationSchema.class, ErrorFirst0PolymorphicConfigurationSchema.class)
                         ),
-                        new TestConfigurationValidator()
+                        new TestConfigurationValidator(),
+                        changer -> {}
                 )
         );
 
@@ -131,7 +135,8 @@ public class ConfigurationRegistryTest {
                                 Third1PolymorphicConfigurationSchema.class
                         )
                 ),
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(configRegistry.stopAsync(new ComponentContext()), willCompleteSuccessfully());
@@ -145,7 +150,8 @@ public class ConfigurationRegistryTest {
                         List.of(ThirdRootConfiguration.KEY),
                         new TestConfigurationStorage(LOCAL),
                         new ConfigurationTreeGenerator(ThirdRootConfiguration.KEY),
-                        new TestConfigurationValidator()
+                        new TestConfigurationValidator(),
+                        changer -> {}
                 )
         );
 
@@ -172,7 +178,8 @@ public class ConfigurationRegistryTest {
                         List.of(),
                         List.of(Fourth0PolymorphicConfigurationSchema.class)
                 ),
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
@@ -204,7 +211,8 @@ public class ConfigurationRegistryTest {
                                 Fourth0PolymorphicConfigurationSchema.class
                         )
                 ),
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
 
@@ -232,7 +240,8 @@ public class ConfigurationRegistryTest {
                 List.of(FirstRootConfiguration.KEY, SecondRootConfiguration.KEY),
                 storage,
                 new ConfigurationTreeGenerator(FirstRootConfiguration.KEY, SecondRootConfiguration.KEY),
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/InjectedValueConfigurationTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/InjectedValueConfigurationTest.java
index 5748ec4fa83..5a3535adb58 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/InjectedValueConfigurationTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/InjectedValueConfigurationTest.java
@@ -80,7 +80,8 @@ public class InjectedValueConfigurationTest {
                 roots,
                 new TestConfigurationStorage(LOCAL),
                 new ConfigurationTreeGenerator(roots, List.of(), List.of()),
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/asm/ConfigurationTreeGeneratorTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/asm/ConfigurationTreeGeneratorTest.java
index ace55ac0129..1b988968dc7 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/asm/ConfigurationTreeGeneratorTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/asm/ConfigurationTreeGeneratorTest.java
@@ -119,7 +119,8 @@ public class ConfigurationTreeGeneratorTest {
                 rootKeys,
                 new TestConfigurationStorage(LOCAL),
                 generator,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                change -> {}
         );
 
         changer.start();
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/deprecation/DeprecatedConfigurationTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/deprecation/DeprecatedConfigurationTest.java
new file mode 100644
index 00000000000..ebfe0851fce
--- /dev/null
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/deprecation/DeprecatedConfigurationTest.java
@@ -0,0 +1,459 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.configuration.deprecation;
+
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import org.apache.ignite.configuration.RootKey;
+import org.apache.ignite.configuration.SuperRootChange;
+import org.apache.ignite.configuration.annotation.ConfigValue;
+import org.apache.ignite.configuration.annotation.ConfigurationRoot;
+import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.annotation.NamedConfigValue;
+import org.apache.ignite.configuration.annotation.Value;
+import org.apache.ignite.internal.configuration.ConfigurationChanger;
+import org.apache.ignite.internal.configuration.ConfigurationMigrator;
+import org.apache.ignite.internal.configuration.ConfigurationTreeGenerator;
+import org.apache.ignite.internal.configuration.SuperRoot;
+import org.apache.ignite.internal.configuration.SuperRootChangeImpl;
+import org.apache.ignite.internal.configuration.TestConfigurationChanger;
+import org.apache.ignite.internal.configuration.storage.ConfigurationStorage;
+import org.apache.ignite.internal.configuration.storage.Data;
+import org.apache.ignite.internal.configuration.storage.TestConfigurationStorage;
+import org.apache.ignite.internal.configuration.tree.ConfigurationSource;
+import org.apache.ignite.internal.configuration.tree.ConstructableTreeNode;
+import org.apache.ignite.internal.configuration.tree.InnerNode;
+import org.apache.ignite.internal.configuration.tree.TraversableTreeNodeTest.ChildConfigurationSchema;
+import org.apache.ignite.internal.configuration.tree.TraversableTreeNodeTest.NamedElementConfigurationSchema;
+import org.apache.ignite.internal.configuration.util.ConfigurationUtil;
+import org.apache.ignite.internal.configuration.validation.TestConfigurationValidator;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Tests for configuration schemas with {@link Deprecated} properties.
+ */
+@ExtendWith(MockitoExtension.class)
+public class DeprecatedConfigurationTest extends BaseIgniteAbstractTest {
+    private static final ConfigurationType TEST_CONFIGURATION_TYPE = ConfigurationType.LOCAL;
+
+    private ConfigurationStorage storage;
+
+    /**
+     * Argument captor for {@link ConfigurationStorage#write(Map, long)}.
+     */
+    @Captor
+    private ArgumentCaptor<Map<String, Serializable>> lastWriteCapture;
+
+    /**
+     * Configuration root schema without any deprecations.
+     */
+    @ConfigurationRoot(rootName = "root", type = ConfigurationType.LOCAL)
+    public static class BeforeDeprecationConfigurationSchema {
+        @Value(hasDefault = true)
+        public int intValue = 10;
+
+        @ConfigValue
+        public ChildConfigurationSchema child;
+
+        @NamedConfigValue
+        public NamedElementConfigurationSchema list;
+    }
+
+    /**
+     * Configuration root schema with deprecated primitive configuration field.
+     */
+    @ConfigurationRoot(rootName = "root", type = ConfigurationType.LOCAL)
+    public static class DeprecatedValueConfigurationSchema {
+        @Deprecated
+        @Value(hasDefault = true)
+        public int intValue = 10;
+
+        @ConfigValue
+        public ChildConfigurationSchema child;
+
+        @NamedConfigValue
+        public NamedElementConfigurationSchema list;
+    }
+
+    /**
+     * Configuration root schema with deprecated child configuration field.
+     */
+    @ConfigurationRoot(rootName = "root", type = ConfigurationType.LOCAL)
+    public static class DeprecatedChildConfigurationSchema {
+        @Value(hasDefault = true)
+        public int intValue = 10;
+
+        @Deprecated
+        @ConfigValue
+        public ChildConfigurationSchema child;
+
+        @NamedConfigValue
+        public NamedElementConfigurationSchema list;
+    }
+
+    /**
+     * Configuration root schema with deprecated named list configuration field.
+     */
+    @ConfigurationRoot(rootName = "root", type = ConfigurationType.LOCAL)
+    public static class DeprecatedNamedListConfigurationSchema {
+        @Value(hasDefault = true)
+        public int intValue = 10;
+
+        @ConfigValue
+        public ChildConfigurationSchema child;
+
+        @Deprecated
+        @NamedConfigValue
+        public NamedElementConfigurationSchema list;
+    }
+
+    @BeforeEach
+    void setUp() {
+        storage = spy(new TestConfigurationStorage(TEST_CONFIGURATION_TYPE));
+
+        when(storage.write(lastWriteCapture.capture(), anyLong())).thenAnswer(InvocationOnMock::callRealMethod);
+    }
+
+    @AfterEach
+    void tearDown() {
+        storage.close();
+    }
+
+    /**
+     * Common assertions for the initial state of the test. In all other tests we rely on invariants stated here.
+     */
+    @Test
+    void testInitialState() {
+        withConfigurationChanger(BeforeDeprecationConfiguration.KEY, true,  change -> {}, changer -> {
+            assertEquals(
+                    Map.of(
+                            "root.intValue", 10,
+                            "root.child.my-int-cfg", 99
+                    ),
+                    lastWriteCapture.getValue()
+            );
+
+            Data data = getData();
+
+            assertEquals(1, data.changeId());
+            assertEquals(
+                    Map.of(
+                            "root.intValue", 10,
+                            "root.child.my-int-cfg", 99
+                    ),
+                    data.values()
+            );
+        });
+    }
+
+    @Test
+    void testDeprecatedPrimitiveConfiguration() {
+        withConfigurationChanger(BeforeDeprecationConfiguration.KEY, true, change -> {}, changer -> {});
+
+        withConfigurationChanger(DeprecatedValueConfiguration.KEY, false, change -> {}, changer -> {
+            //noinspection CastToIncompatibleInterface
+            var root = (DeprecatedValueView) changer.superRoot().getRoot(DeprecatedValueConfiguration.KEY);
+            // Deprecated value should still be read as a default.
+            assertEquals(10, root.intValue());
+
+            // Check that deprecated value has been deleted while starting the changer.
+            assertEquals(
+                    mapWithNulls("root.intValue", null),
+                    lastWriteCapture.getValue()
+            );
+
+            // Check that deprecated value is not present in the storage anymore.
+            Data data = getData();
+            assertEquals(2, data.changeId());
+            assertEquals(
+                    Map.of("root.child.my-int-cfg", 99),
+                    data.values()
+            );
+
+            // Deprecated values should NOT be re-written if they're already absent.
+            changeConfiguration(changer, superRoot ->
+                    superRoot.changeRoot(DeprecatedValueConfiguration.KEY).changeChild().changeIntCfg(100)
+            );
+
+            assertEquals(
+                    Map.of("root.child.my-int-cfg", 100),
+                    lastWriteCapture.getValue()
+            );
+
+            data = getData();
+            assertEquals(3, data.changeId());
+            assertEquals(
+                    Map.of("root.child.my-int-cfg", 100),
+                    data.values()
+            );
+        });
+    }
+
+    @Test
+    void testConfigurationMigrator() {
+        withConfigurationChanger(BeforeDeprecationConfiguration.KEY, true, change -> {}, changer -> {});
+
+        ConfigurationMigrator migrator = superRootChange -> {
+            // Check one more time that an old value can be read, just to show that we can do that inside of migrator.
+            assertEquals(10, superRootChange.viewRoot(DeprecatedValueConfiguration.KEY).intValue());
+
+            superRootChange.changeRoot(DeprecatedValueConfiguration.KEY).changeChild().changeIntCfg(100);
+        };
+
+        withConfigurationChanger(DeprecatedValueConfiguration.KEY, false, migrator, changer -> {
+            //noinspection CastToIncompatibleInterface
+            var root = (DeprecatedValueView) changer.superRoot().getRoot(DeprecatedValueConfiguration.KEY);
+            // Migrator completed its task.
+            assertEquals(100, root.child().intCfg());
+
+            // Check that deprecated value has been deleted while starting the changer, and that migrator did its job at the same time.
+            assertEquals(
+                    mapWithNulls(
+                            "root.intValue", null,
+                            "root.child.my-int-cfg", 100
+                    ),
+                    lastWriteCapture.getValue()
+            );
+        });
+    }
+
+    @Test
+    void testDeprecatedChildConfiguration() {
+        withConfigurationChanger(BeforeDeprecationConfiguration.KEY, true, change -> {}, changer -> {});
+
+        withConfigurationChanger(DeprecatedChildConfiguration.KEY, false, change -> {}, changer -> {
+            //noinspection CastToIncompatibleInterface
+            var root = (DeprecatedChildView) changer.superRoot().getRoot(DeprecatedChildConfiguration.KEY);
+            assertNotNull(root.child());
+            assertNull(root.child().strCfg()); // This test never really did anything to "strValue", that's why it's null.
+            assertEquals(99, root.child().intCfg());
+
+            // Check that deprecated value has been deleted while starting the changer.
+            assertEquals(
+                    mapWithNulls(
+                            "root.child.my-int-cfg", null
+                    ),
+                    lastWriteCapture.getValue()
+            );
+
+            // Check that deprecated value is not present in the storage anymore.
+            Data data = getData();
+
+            assertEquals(2, data.changeId());
+            assertEquals(
+                    Map.of("root.intValue", 10),
+                    data.values()
+            );
+
+            // Deprecated values should NOT be re-written if they're already absent.
+            changeConfiguration(changer, superRoot ->
+                    superRoot.changeRoot(DeprecatedChildConfiguration.KEY).changeIntValue(20)
+            );
+
+            assertEquals(
+                    Map.of("root.intValue", 20),
+                    lastWriteCapture.getValue()
+            );
+
+            data = getData();
+            assertEquals(3, data.changeId());
+            assertEquals(
+                    Map.of("root.intValue", 20),
+                    data.values()
+            );
+        });
+    }
+
+    @Test
+    void testDeprecatedNamedListConfiguration() {
+        AtomicReference<UUID> internalIdReference = new AtomicReference<>();
+
+        withConfigurationChanger(BeforeDeprecationConfiguration.KEY, true, change -> {}, changer -> {
+            // Add named list element for the test.
+            changeConfiguration(changer, superRoot -> {
+                superRoot.changeRoot(BeforeDeprecationConfiguration.KEY)
+                        .changeList()
+                        .create("foo", namedElementChange ->
+                                internalIdReference.set(((InnerNode) namedElementChange).internalId())
+                        );
+            });
+
+            UUID internalId = internalIdReference.get();
+            assertNotNull(internalId);
+
+            // Check that all expected properties have been added.
+            assertEquals(
+                    Map.of(
+                            "root.list." + internalId + ".<order>", 0,
+                            "root.list." + internalId + ".<name>", "foo",
+                            "root.list.<ids>.foo", internalId
+                    ),
+                    lastWriteCapture.getValue()
+            );
+
+            // Check storage state.
+            Data data = getData();
+
+            assertEquals(2, data.changeId());
+            assertEquals(
+                    Map.of(
+                            "root.intValue", 10,
+                            "root.child.my-int-cfg", 99,
+                            "root.list.<ids>.foo", internalId,
+                            "root.list." + internalId + ".<order>", 0,
+                            "root.list." + internalId + ".<name>", "foo"
+                    ),
+                    data.values()
+            );
+        });
+
+        withConfigurationChanger(DeprecatedNamedListConfiguration.KEY, false, change -> {}, changer -> {
+            //noinspection CastToIncompatibleInterface
+            var root = (DeprecatedNamedListView) changer.superRoot().getRoot(DeprecatedNamedListConfiguration.KEY);
+            assertNotNull(root.list());
+
+            UUID internalId = internalIdReference.get();
+
+            // Check that deprecated value has been deleted while starting the changer.
+            assertEquals(
+                    mapWithNulls(
+                            "root.list." + internalId + ".<order>", null,
+                            "root.list." + internalId + ".<name>", null,
+                            "root.list.<ids>.foo", null
+                    ),
+                    lastWriteCapture.getValue()
+            );
+
+            // Check that deprecated value is not present in the storage anymore.
+            Data data = getData();
+
+            assertEquals(3, data.changeId());
+            assertEquals(
+                    Map.of(
+                            "root.intValue", 10,
+                            "root.child.my-int-cfg", 99
+                    ),
+                    data.values()
+            );
+
+            // Deprecated values should NOT be re-written if they're already absent.
+            changeConfiguration(changer, superRoot ->
+                    superRoot.changeRoot(DeprecatedNamedListConfiguration.KEY).changeIntValue(20)
+            );
+
+            assertEquals(
+                    Map.of("root.intValue", 20),
+                    lastWriteCapture.getValue()
+            );
+
+            data = getData();
+            assertEquals(4, data.changeId());
+            assertEquals(
+                    Map.of(
+                            "root.intValue", 20,
+                            "root.child.my-int-cfg", 99
+                    ),
+                    data.values()
+            );
+        });
+    }
+
+    private void withConfigurationChanger(
+            RootKey<?, ?> rootKey,
+            boolean init,
+            ConfigurationMigrator migrator,
+            Consumer<ConfigurationChanger> action
+    ) {
+        ConfigurationChanger changer = new TestConfigurationChanger(
+                List.of(rootKey),
+                storage,
+                new ConfigurationTreeGenerator(rootKey),
+                new TestConfigurationValidator(),
+                migrator
+        );
+
+        if (init) {
+            changer.initializeConfigurationWith(ConfigurationUtil.EMPTY_CFG_SRC);
+        }
+
+        changer.start();
+
+        try {
+            action.accept(changer);
+        } finally {
+            changer.stop();
+        }
+    }
+
+    private static void changeConfiguration(ConfigurationChanger changer, Consumer<SuperRootChange> changeClosure) {
+        CompletableFuture<Void> changeFuture = changer.change(new ConfigurationSource() {
+            @Override
+            public void descend(ConstructableTreeNode node) {
+                changeClosure.accept(new SuperRootChangeImpl((SuperRoot) node));
+            }
+        });
+
+        assertThat(changeFuture, willCompleteSuccessfully());
+    }
+
+    private Data getData() {
+        CompletableFuture<Data> dataFuture = storage.readDataOnRecovery();
+
+        assertThat(dataFuture, willCompleteSuccessfully());
+
+        return dataFuture.join();
+    }
+
+    /**
+     * {@link Map#of} does not support null values.
+     */
+    private static <K, V> Map<K, V> mapWithNulls(Object... pairs) {
+        assertEquals(0, pairs.length % 2);
+
+        Map<K, V> map = new HashMap<>();
+
+        for (int i = 0; i < pairs.length / 2; i++) {
+            map.put((K) pairs[i * 2], (V) pairs[i * 2 + 1]);
+        }
+
+        return map;
+    }
+}
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/direct/DirectPropertiesTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/direct/DirectPropertiesTest.java
index c3f64e8e1e3..454af8834bd 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/direct/DirectPropertiesTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/direct/DirectPropertiesTest.java
@@ -110,7 +110,8 @@ public class DirectPropertiesTest {
                 List.of(DirectConfiguration.KEY),
                 new TestConfigurationStorage(LOCAL),
                 new ConfigurationTreeGenerator(DirectConfiguration.KEY),
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java
index 13383c608cb..91b00bc285a 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/hocon/HoconConverterTest.java
@@ -232,7 +232,8 @@ public class HoconConverterTest {
                                 HoconSecondPolymorphicInstanceConfigurationSchema.class
                         )
                 ),
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationAnyListenerTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationAnyListenerTest.java
index ce4aaf811c5..54739efec60 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationAnyListenerTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationAnyListenerTest.java
@@ -168,7 +168,8 @@ public class ConfigurationAnyListenerTest {
                 List.of(RootConfiguration.KEY),
                 new TestConfigurationStorage(LOCAL),
                 generator,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java
index 8d1b6d090bc..ec78048fa04 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/notifications/ConfigurationListenerTest.java
@@ -189,7 +189,8 @@ public class ConfigurationListenerTest {
                 List.of(ParentConfiguration.KEY),
                 storage,
                 generator,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/sample/UsageTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/sample/UsageTest.java
index c38c0886086..1e29fb70dd4 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/sample/UsageTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/sample/UsageTest.java
@@ -55,7 +55,8 @@ public class UsageTest {
                 List.of(LocalConfiguration.KEY),
                 new TestConfigurationStorage(LOCAL),
                 new ConfigurationTreeGenerator(LocalConfiguration.KEY),
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
@@ -114,7 +115,8 @@ public class UsageTest {
                 List.of(NetworkConfiguration.KEY, LocalConfiguration.KEY),
                 new TestConfigurationStorage(LOCAL),
                 new ConfigurationTreeGenerator(NetworkConfiguration.KEY, LocalConfiguration.KEY),
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/InternalIdTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/InternalIdTest.java
index 478ceb29b2f..169e03027e7 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/InternalIdTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/InternalIdTest.java
@@ -95,7 +95,8 @@ public class InternalIdTest {
                 List.of(InternalIdParentConfiguration.KEY),
                 new TestConfigurationStorage(LOCAL),
                 generator,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         assertThat(registry.startAsync(new ComponentContext()), willCompleteSuccessfully());
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListNodeTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListNodeTest.java
index 7c334618a5f..98ac215513a 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListNodeTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/tree/NamedListNodeTest.java
@@ -110,7 +110,8 @@ public class NamedListNodeTest {
                 List.of(FirstConfiguration.KEY),
                 storage,
                 cgen,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                change -> {}
         );
 
         changer.start();
diff --git a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
index 486fd0a812e..0ec72f49b7d 100644
--- a/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
+++ b/modules/configuration/src/test/java/org/apache/ignite/internal/configuration/util/ConfigurationUtilTest.java
@@ -1020,7 +1020,11 @@ public class ConfigurationUtilTest {
 
         Map<String, Object> exp = new HashMap<>();
 
-        exp.put("0", Map.of("first", "1"));
+        Map<String, Object> expMapZero = new HashMap<>();
+        expMapZero.put("first", "1");
+        expMapZero.put("second", null);
+
+        exp.put("0", expMapZero);
         exp.put("1", null);
         exp.put("2", Map.of("strVal", "foo", "intVal", 10));
 
diff --git a/modules/configuration/src/testFixtures/java/org/apache/ignite/internal/configuration/TestConfigurationChanger.java b/modules/configuration/src/testFixtures/java/org/apache/ignite/internal/configuration/TestConfigurationChanger.java
index efd464fb657..c1a4498e9fd 100644
--- a/modules/configuration/src/testFixtures/java/org/apache/ignite/internal/configuration/TestConfigurationChanger.java
+++ b/modules/configuration/src/testFixtures/java/org/apache/ignite/internal/configuration/TestConfigurationChanger.java
@@ -36,19 +36,15 @@ public class TestConfigurationChanger extends ConfigurationChanger {
 
     /**
      * Constructor.
-     *
-     * @param rootKeys                    Configuration root keys.
-     * @param storage                     Configuration storage.
-     * @param generator                   Runtime implementations tree generator for node classes.
-     * @throws IllegalArgumentException If the configuration type of the root keys is not equal to the storage type.
      */
     public TestConfigurationChanger(
             Collection<RootKey<?, ?>> rootKeys,
             ConfigurationStorage storage,
             ConfigurationTreeGenerator generator,
-            ConfigurationValidator configurationValidator
+            ConfigurationValidator validator,
+            ConfigurationMigrator migrator
     ) {
-        super(noOpListener(), rootKeys, storage, configurationValidator);
+        super(noOpListener(), rootKeys, storage, validator, migrator);
 
         this.generator = generator;
     }
diff --git a/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/distributionzones/ItIgniteDistributionZoneManagerNodeRestartTest.java b/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/distributionzones/ItIgniteDistributionZoneManagerNodeRestartTest.java
index d0a86e349d4..1b69cc019c3 100644
--- a/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/distributionzones/ItIgniteDistributionZoneManagerNodeRestartTest.java
+++ b/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/distributionzones/ItIgniteDistributionZoneManagerNodeRestartTest.java
@@ -221,7 +221,8 @@ public class ItIgniteDistributionZoneManagerNodeRestartTest extends BaseIgniteRe
                 modules.local().rootKeys(),
                 new LocalFileConfigurationStorage(configFile, localConfigurationGenerator, modules.local()),
                 localConfigurationGenerator,
-                ConfigurationValidatorImpl.withDefaultValidators(localConfigurationGenerator, modules.local().validators())
+                ConfigurationValidatorImpl.withDefaultValidators(localConfigurationGenerator, modules.local().validators()),
+                changer -> {}
         );
 
         NetworkConfiguration networkConfiguration = nodeCfgMgr.configurationRegistry()
@@ -288,7 +289,8 @@ public class ItIgniteDistributionZoneManagerNodeRestartTest extends BaseIgniteRe
                 modules.distributed().rootKeys(),
                 cfgStorage,
                 distributedConfigurationGenerator,
-                ConfigurationValidatorImpl.withDefaultValidators(distributedConfigurationGenerator, validators)
+                ConfigurationValidatorImpl.withDefaultValidators(distributedConfigurationGenerator, validators),
+                changer -> {}
         );
 
         ConfigurationRegistry clusterConfigRegistry = clusterCfgMgr.configurationRegistry();
diff --git a/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceDistributedTest.java b/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceDistributedTest.java
index 82f39c771da..2e146dbbcd1 100644
--- a/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceDistributedTest.java
+++ b/modules/distribution-zones/src/integrationTest/java/org/apache/ignite/internal/rebalance/ItRebalanceDistributedTest.java
@@ -1223,7 +1223,8 @@ public class ItRebalanceDistributedTest extends BaseIgniteAbstractTest {
                     List.of(NodeConfiguration.KEY),
                     new LocalFileConfigurationStorage(configPath, nodeCfgGenerator, null),
                     nodeCfgGenerator,
-                    new TestConfigurationValidator()
+                    new TestConfigurationValidator(),
+                    changer -> {}
             );
 
             clusterService = ClusterServiceTestUtils.clusterService(
@@ -1376,7 +1377,8 @@ public class ItRebalanceDistributedTest extends BaseIgniteAbstractTest {
                     clusterCfgGenerator,
                     ConfigurationValidatorImpl.withDefaultValidators(
                             clusterCfgGenerator, Set.of(new NonNegativeIntegerNumberSystemPropertyValueValidator(REBALANCE_RETRY_DELAY_MS))
-                    )
+                    ),
+                    changer -> {}
             );
 
             ConfigurationRegistry clusterConfigRegistry = clusterCfgMgr.configurationRegistry();
diff --git a/modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/server/WatchProcessor.java b/modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/server/WatchProcessor.java
index d48ab17b474..34cf9713d0b 100644
--- a/modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/server/WatchProcessor.java
+++ b/modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/server/WatchProcessor.java
@@ -400,7 +400,7 @@ public class WatchProcessor implements ManuallyCloseable {
 
             if (!nodeStopping) {
                 LOG.error("Notification chain encountered an error, so no notifications will be ever fired for subsequent revisions "
-                        + "until a restart. Notifying the FailureManager. Additional info: '{}'", additionalInfoSupplier.get(), e);
+                        + "until a restart. Notifying the FailureManager. Additional info: '{}'", additionalInfoSupplier.get());
 
                 failureProcessor.process(new FailureContext(CRITICAL_ERROR, e));
             } else {
diff --git a/modules/network/src/testFixtures/java/org/apache/ignite/internal/network/utils/ClusterServiceTestUtils.java b/modules/network/src/testFixtures/java/org/apache/ignite/internal/network/utils/ClusterServiceTestUtils.java
index 9d1dfc7434b..10eeb600447 100644
--- a/modules/network/src/testFixtures/java/org/apache/ignite/internal/network/utils/ClusterServiceTestUtils.java
+++ b/modules/network/src/testFixtures/java/org/apache/ignite/internal/network/utils/ClusterServiceTestUtils.java
@@ -221,7 +221,8 @@ public class ClusterServiceTestUtils {
                 Collections.singleton(NodeConfiguration.KEY),
                 new TestConfigurationStorage(ConfigurationType.LOCAL),
                 generator,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         NetworkConfiguration networkConfiguration = nodeConfigurationMgr.configurationRegistry()
diff --git a/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/fixtures/Node.java b/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/fixtures/Node.java
index bceb3b9e133..d0fb870f97b 100644
--- a/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/fixtures/Node.java
+++ b/modules/partition-replicator/src/integrationTest/java/org/apache/ignite/internal/partition/replicator/fixtures/Node.java
@@ -360,7 +360,8 @@ public class Node {
                 List.of(NodeConfiguration.KEY),
                 new LocalFileConfigurationStorage(configPath, nodeCfgGenerator, null),
                 nodeCfgGenerator,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         var clusterIdHolder = new ClusterIdHolder();
@@ -540,7 +541,8 @@ public class Node {
                 List.of(ClusterConfiguration.KEY),
                 cfgStorage,
                 clusterCfgGenerator,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
 
         ConfigurationRegistry clusterConfigRegistry = clusterCfgMgr.configurationRegistry();
diff --git a/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java b/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java
index 1070943fb5a..93e3368d087 100644
--- a/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java
+++ b/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java
@@ -91,7 +91,8 @@ public class RestComponentTest extends BaseIgniteAbstractTest {
                 List.of(NodeConfiguration.KEY),
                 new TestConfigurationStorage(LOCAL),
                 generator,
-                new TestConfigurationValidator()
+                new TestConfigurationValidator(),
+                changer -> {}
         );
         assertThat(configurationManager.startAsync(new ComponentContext()), willCompleteSuccessfully());
 
diff --git a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestFactory.java b/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestFactory.java
index 7073b05e4db..357d6bed353 100644
--- a/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestFactory.java
+++ b/modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/TestFactory.java
@@ -64,7 +64,8 @@ public class TestFactory {
                 List.of(TestRootConfiguration.KEY),
                 new TestConfigurationStorage(LOCAL),
                 generator,
-                ConfigurationValidatorImpl.withDefaultValidators(generator, Set.of(validator))
+                ConfigurationValidatorImpl.withDefaultValidators(generator, Set.of(validator)),
+                changer -> {}
         );
 
         assertThat(configurationRegistry.startAsync(new ComponentContext()), willCompleteSuccessfully());
diff --git a/modules/runner/src/defaultsGenerator/java/org/apache/ignite/internal/configuration/generator/DefaultsGenerator.java b/modules/runner/src/defaultsGenerator/java/org/apache/ignite/internal/configuration/generator/DefaultsGenerator.java
index 5dfd62dbd8d..f9af33af58a 100644
--- a/modules/runner/src/defaultsGenerator/java/org/apache/ignite/internal/configuration/generator/DefaultsGenerator.java
+++ b/modules/runner/src/defaultsGenerator/java/org/apache/ignite/internal/configuration/generator/DefaultsGenerator.java
@@ -95,7 +95,7 @@ public class DefaultsGenerator {
 
         ConfigurationUpdateListener empty = (oldRoot, newRoot, storageRevision, notificationNumber) -> nullCompletedFuture();
 
-        return new ConfigurationChanger(empty, modules.local().rootKeys(), storage, configurationValidator) {
+        return new ConfigurationChanger(empty, modules.local().rootKeys(), storage, configurationValidator, c -> {}) {
             @Override
             public InnerNode createRootNode(RootKey<?, ?> rootKey) {
                 return localConfigurationGenerator.instantiateNode(rootKey.schemaClass());
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItDistributedConfigurationPropertiesTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItDistributedConfigurationPropertiesTest.java
index 0955e9c4122..679b93c204d 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItDistributedConfigurationPropertiesTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/configuration/ItDistributedConfigurationPropertiesTest.java
@@ -270,7 +270,8 @@ public class ItDistributedConfigurationPropertiesTest extends BaseIgniteAbstract
                     List.of(DistributedConfiguration.KEY),
                     distributedCfgStorage,
                     generator,
-                    new TestConfigurationValidator()
+                    new TestConfigurationValidator(),
+                    changer -> {}
             );
         }
 
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java
index 9cb13fe98d8..e1986bdc48c 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteNodeRestartTest.java
@@ -373,7 +373,8 @@ public class ItIgniteNodeRestartTest extends BaseIgniteRestartTest {
                 modules.local().rootKeys(),
                 new LocalFileConfigurationStorage(configFile, localConfigurationGenerator, modules.local()),
                 localConfigurationGenerator,
-                ConfigurationValidatorImpl.withDefaultValidators(localConfigurationGenerator, modules.local().validators())
+                ConfigurationValidatorImpl.withDefaultValidators(localConfigurationGenerator, modules.local().validators()),
+                changer -> {}
         );
 
         NetworkConfiguration networkConfiguration = nodeCfgMgr.configurationRegistry()
@@ -550,7 +551,8 @@ public class ItIgniteNodeRestartTest extends BaseIgniteRestartTest {
                 modules.distributed().rootKeys(),
                 cfgStorage,
                 distributedConfigurationGenerator,
-                ConfigurationValidatorImpl.withDefaultValidators(distributedConfigurationGenerator, modules.distributed().validators())
+                ConfigurationValidatorImpl.withDefaultValidators(distributedConfigurationGenerator, modules.distributed().validators()),
+                changer -> {}
         );
 
         ConfigurationRegistry clusterConfigRegistry = clusterCfgMgr.configurationRegistry();
diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
index c9f5fa7c869..863fc8b1c5f 100644
--- a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
+++ b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
@@ -547,7 +547,8 @@ public class IgniteImpl implements Ignite {
                 modules.local().rootKeys(),
                 localFileConfigurationStorage,
                 localConfigurationGenerator,
-                localConfigurationValidator
+                localConfigurationValidator,
+                modules.local()::migrateDeprecatedConfigurations
         );
 
         // Start local configuration to be able to read all local properties.
@@ -763,7 +764,8 @@ public class IgniteImpl implements Ignite {
                 modules.distributed().rootKeys(),
                 cfgStorage,
                 distributedConfigurationGenerator,
-                distributedCfgValidator
+                distributedCfgValidator,
+                modules.distributed()::migrateDeprecatedConfigurations
         );
 
         ConfigurationRegistry clusterConfigRegistry = clusterCfgMgr.configurationRegistry();
diff --git a/modules/runner/src/main/java/org/apache/ignite/internal/configuration/CompoundModule.java b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/CompoundModule.java
index f64aac2d51c..fc19c9d9f24 100644
--- a/modules/runner/src/main/java/org/apache/ignite/internal/configuration/CompoundModule.java
+++ b/modules/runner/src/main/java/org/apache/ignite/internal/configuration/CompoundModule.java
@@ -84,4 +84,9 @@ public class CompoundModule implements ConfigurationModule {
     public void patchConfigurationWithDynamicDefaults(SuperRootChange rootChange) {
         modules.forEach(module -> module.patchConfigurationWithDynamicDefaults(rootChange));
     }
+
+    @Override
+    public void migrateDeprecatedConfigurations(SuperRootChange superRootChange) {
+        modules.forEach(module -> module.migrateDeprecatedConfigurations(superRootChange));
+    }
 }
diff --git a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorageTest.java b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorageTest.java
index 0bd47c0078d..d412736b106 100644
--- a/modules/runner/src/test/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorageTest.java
+++ b/modules/runner/src/test/java/org/apache/ignite/internal/configuration/storage/LocalFileConfigurationStorageTest.java
@@ -97,7 +97,8 @@ public class LocalFileConfigurationStorageTest {
                 List.of(TopConfiguration.KEY),
                 storage,
                 treeGenerator,
-                new ConfigurationValidatorImpl(treeGenerator, Set.of())
+                new ConfigurationValidatorImpl(treeGenerator, Set.of()),
+                change -> {}
         );
     }