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

petrov-mg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 86943b54d24 IGNITE-28990 Extended IgniteClusterNode API with Ignite 
Features getter (#13488)
86943b54d24 is described below

commit 86943b54d2436a41d38d8361f6446c59936d43d8
Author: Mikhail Petrov <[email protected]>
AuthorDate: Mon Aug 17 20:56:00 2026 +0300

    IGNITE-28990 Extended IgniteClusterNode API with Ignite Features getter 
(#13488)
---
 .../jol/GridAffinityAssignmentJolBenchmark.java    |   5 +-
 .../ignite/internal/CoreMessagesProvider.java      |   2 +
 .../managers/discovery/IgniteClusterNode.java      |   8 ++
 .../wal/reader/StandaloneGridKernalContext.java    |   8 +-
 .../rollingupgrade/RollingUpgradeClusterData.java  |  13 ++-
 .../rollingupgrade/RollingUpgradeProcessor.java    | 101 ++++++---------------
 .../feature/IgniteNodeFeatureSet.java              |  87 ++++++++++++++----
 .../discovery/isolated/IsolatedDiscoverySpi.java   |   8 +-
 .../spi/discovery/isolated/IsolatedNode.java       |  13 ++-
 .../ignite/spi/discovery/tcp/ClientImpl.java       |   2 +-
 .../ignite/spi/discovery/tcp/ServerImpl.java       |   6 +-
 .../ignite/spi/discovery/tcp/TcpDiscoverySpi.java  |  17 +---
 .../discovery/tcp/internal/TcpDiscoveryNode.java   |  26 +++++-
 .../tcp/messages/TcpDiscoveryHandshakeRequest.java |   7 +-
 .../messages/TcpDiscoveryHandshakeResponse.java    |   7 +-
 .../affinity/GridAffinityAssignmentV2Test.java     |   4 +-
 .../spi/discovery/zk/ZookeeperDiscoverySpi.java    |  13 ++-
 .../zk/internal/ZookeeperClusterNode.java          |  17 ++++
 .../zk/internal/ZookeeperDiscoverySpiTestBase.java |  14 +--
 19 files changed, 218 insertions(+), 140 deletions(-)

diff --git 
a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jol/GridAffinityAssignmentJolBenchmark.java
 
b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jol/GridAffinityAssignmentJolBenchmark.java
index df474379073..1477f3357ed 100644
--- 
a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jol/GridAffinityAssignmentJolBenchmark.java
+++ 
b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jol/GridAffinityAssignmentJolBenchmark.java
@@ -42,6 +42,8 @@ import 
org.apache.ignite.spi.discovery.DiscoveryMetricsProvider;
 import org.apache.ignite.spi.discovery.tcp.internal.TcpDiscoveryNode;
 import org.openjdk.jol.info.GraphLayout;
 
+import static 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet.LOCAL_CORE_FEATURES;
+
 /**
  *
  */
@@ -333,7 +335,8 @@ public class GridAffinityAssignmentJolBenchmark {
             0,
             metrics,
             ver,
-            "Node_" + idx
+            "Node_" + idx,
+            LOCAL_CORE_FEATURES
         );
         node.setAttributes(Collections.emptyMap());
 
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java
index ef05f8b47fd..f7537f0b20f 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java
@@ -264,6 +264,7 @@ import 
org.apache.ignite.internal.processors.rest.handlers.task.GridTaskResultRe
 import 
org.apache.ignite.internal.processors.rollingupgrade.RollingUpgradeClusterData;
 import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteCoreFeatureSet;
 import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteFeatureSet;
+import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet;
 import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgnitePluginFeatureSet;
 import org.apache.ignite.internal.processors.security.SecurityContextWrapper;
 import 
org.apache.ignite.internal.processors.service.LazyServiceConfigurationMessage;
@@ -756,6 +757,7 @@ public class CoreMessagesProvider extends 
AbstractMessageFactoryProvider {
         register(IgniteCoreFeatureSet.class);
         register(IgnitePluginFeatureSet.class);
         register(RollingUpgradeClusterData.class);
+        register(IgniteNodeFeatureSet.class);
 
         assert msgIdx <= MAX_MESSAGE_ID;
     }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/managers/discovery/IgniteClusterNode.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/managers/discovery/IgniteClusterNode.java
index 34ba45683ec..78abd199589 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/managers/discovery/IgniteClusterNode.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/managers/discovery/IgniteClusterNode.java
@@ -22,11 +22,19 @@ import java.util.Map;
 import org.apache.ignite.cache.CacheMetrics;
 import org.apache.ignite.cluster.ClusterMetrics;
 import org.apache.ignite.cluster.ClusterNode;
+import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet;
 
 /**
  *
  */
 public interface IgniteClusterNode extends ClusterNode {
+    /**
+     * Gets the set of Ignite features supported by this node.
+     *
+     * @return Ignite Node Feature set.
+     */
+    public IgniteNodeFeatureSet features();
+
     /**
      * Sets consistent globally unique node ID which survives node restarts.
      *
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java
index 44a5bdabcf5..835cfb15d14 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/wal/reader/StandaloneGridKernalContext.java
@@ -91,7 +91,6 @@ import 
org.apache.ignite.internal.processors.query.GridQueryProcessor;
 import org.apache.ignite.internal.processors.resource.GridResourceProcessor;
 import org.apache.ignite.internal.processors.rest.IgniteRestProcessor;
 import 
org.apache.ignite.internal.processors.rollingupgrade.RollingUpgradeProcessor;
-import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteCoreFeatureSet;
 import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet;
 import 
org.apache.ignite.internal.processors.schedule.IgniteScheduleProcessorAdapter;
 import org.apache.ignite.internal.processors.security.IgniteSecurity;
@@ -116,6 +115,8 @@ import 
org.apache.ignite.spi.metric.noop.NoopMetricExporterSpi;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import static 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet.LOCAL_CORE_FEATURES;
+
 /**
  * Dummy context for offline utilities. All grid components registered in the 
standalone context
  * must be properly stopped since the lifecycle of them are controlled by 
kernal.
@@ -179,9 +180,6 @@ public class StandaloneGridKernalContext implements 
GridKernalContext {
     /** Operation context dispacther. */
     private final OperationContextDispatcher opCtxDispatcher = new 
OperationContextDispatcher();
 
-    /** */
-    private final IgniteNodeFeatureSet locNodeFeatures = new 
IgniteNodeFeatureSet(IgniteCoreFeatureSet.local());
-
     /**
      * @param log Logger.
      * @param ft Node file tree.
@@ -324,7 +322,7 @@ public class StandaloneGridKernalContext implements 
GridKernalContext {
 
     /** {@inheritDoc} */
     @Override public IgniteNodeFeatureSet localNodeFeatures() {
-        return locNodeFeatures;
+        return LOCAL_CORE_FEATURES;
     }
 
     /** {@inheritDoc} */
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeClusterData.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeClusterData.java
index 1c86dbda18f..e842017f355 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeClusterData.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeClusterData.java
@@ -19,7 +19,6 @@ package org.apache.ignite.internal.processors.rollingupgrade;
 
 import java.util.UUID;
 import org.apache.ignite.internal.Order;
-import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteComponentFeatureSet;
 import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet;
 import org.apache.ignite.plugin.extensions.communication.Message;
 import org.jetbrains.annotations.Nullable;
@@ -36,11 +35,11 @@ public class RollingUpgradeClusterData implements Message {
 
     /** */
     @Order(2)
-    IgniteComponentFeatureSet[] activeFeatures;
+    IgniteNodeFeatureSet activeFeatures;
 
     /** */
     @Order(3)
-    @Nullable IgniteComponentFeatureSet[] prevActiveFeatures;
+    @Nullable IgniteNodeFeatureSet prevActiveFeatures;
 
     /** */
     public RollingUpgradeClusterData() {
@@ -56,17 +55,17 @@ public class RollingUpgradeClusterData implements Message {
     ) {
         this.isVersionUpgradeEnabled = isVersionUpgradeEnabled;
         this.curFinalizeProcId = curFinalizeProcId;
-        this.activeFeatures = activeFeatures.values();
-        this.prevActiveFeatures = prevActiveFeatures == null ? null : 
prevActiveFeatures.values();
+        this.activeFeatures = activeFeatures;
+        this.prevActiveFeatures = prevActiveFeatures;
     }
 
     /** */
     public IgniteNodeFeatureSet activeFeatures() {
-        return new IgniteNodeFeatureSet(activeFeatures);
+        return activeFeatures;
     }
 
     /** */
     @Nullable public IgniteNodeFeatureSet previousActiveFeatures() {
-        return prevActiveFeatures == null ? null : new 
IgniteNodeFeatureSet(prevActiveFeatures);
+        return prevActiveFeatures;
     }
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeProcessor.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeProcessor.java
index 76318e8a3dd..690b1f29521 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeProcessor.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/RollingUpgradeProcessor.java
@@ -17,8 +17,8 @@
 
 package org.apache.ignite.internal.processors.rollingupgrade;
 
+import java.util.Collection;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
@@ -33,6 +33,7 @@ import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.events.DiscoveryEvent;
 import org.apache.ignite.internal.GridKernalContext;
 import org.apache.ignite.internal.IgniteInternalFuture;
+import org.apache.ignite.internal.managers.discovery.IgniteClusterNode;
 import org.apache.ignite.internal.processors.GridProcessorAdapter;
 import 
org.apache.ignite.internal.processors.nodevalidation.DiscoveryNodeValidationProcessor;
 import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteComponentFeatureSet;
@@ -57,7 +58,6 @@ import static 
org.apache.ignite.events.EventType.EVT_NODE_JOINED;
 import static org.apache.ignite.events.EventType.EVT_NODE_LEFT;
 import static org.apache.ignite.events.EventType.EVT_NODE_VALIDATION_FAILED;
 import static 
org.apache.ignite.internal.GridComponent.DiscoveryDataExchangeType.ROLLING_UPGRADE_PROC;
-import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_IGNITE_FEATURES;
 import static 
org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.RU_ABORT_VERSION_FINALIZATION;
 import static 
org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.RU_COMPLETE_VERSION_FINALIZATION;
 import static 
org.apache.ignite.internal.util.distributed.DistributedProcess.DistributedProcessType.RU_ENABLE;
@@ -174,19 +174,12 @@ public class RollingUpgradeProcessor extends 
GridProcessorAdapter implements Dis
     /** */
     IgniteComponentUpgradeState state(String cmpName) {
         synchronized (topGuard) {
-            return detectComponentUpgradeState(clusterFeatures(), cmpName);
+            return detectComponentUpgradeState(clusterNodes(), cmpName);
         }
     }
 
     /** {@inheritDoc} */
     @Override public void start() throws IgniteCheckedException {
-        ctx.addNodeAttribute(
-            ATTR_IGNITE_FEATURES,
-            U.marshal(
-                ctx.marshallerContext().jdkMarshaller(),
-                featureMgr.localVersionFeatures().values())
-        );
-
         ctx.event().addLocalEventListener(
             evt -> {
                 synchronized (topGuard) {
@@ -236,16 +229,7 @@ public class RollingUpgradeProcessor extends 
GridProcessorAdapter implements Dis
                         " cluster version finalization process is complete 
[joiningNode=" + joiningNode + ']');
             }
 
-            IgniteNodeFeatureSet joiningNodeFeatures;
-
-            try {
-                joiningNodeFeatures = extractNodeFeatures(joiningNode);
-            }
-            catch (IgniteCheckedException e) {
-                return new IgniteNodeValidationResult(
-                    joiningNode.id(),
-                    "Failed to resolve joining node features [joiningNode=" + 
joiningNode + ", errMsg=" + e.getMessage() + ']');
-            }
+            IgniteNodeFeatureSet joiningNodeFeatures = 
extractNodeFeatures(joiningNode);
 
             if (isVerUpgradeEnabled) {
                 if (!joiningNode.isClient() && 
!joiningNodeFeatures.components().containsAll(featureMgr.activeFeatures().components()))
 {
@@ -258,10 +242,10 @@ public class RollingUpgradeProcessor extends 
GridProcessorAdapter implements Dis
                     );
                 }
 
-                Map<ClusterNode, IgniteNodeFeatureSet> clusterFeatures = 
clusterFeatures();
+                Set<ClusterNode> clusterNodes = clusterNodes();
 
                 for (IgniteComponentFeatureSet rmtCmpFeatures : 
joiningNodeFeatures.values()) {
-                    IgniteComponentUpgradeState state = 
detectComponentUpgradeState(clusterFeatures, rmtCmpFeatures.componentName());
+                    IgniteComponentUpgradeState state = 
detectComponentUpgradeState(clusterNodes, rmtCmpFeatures.componentName());
 
                     if (!state.isCompatible(rmtCmpFeatures.version())) {
                         return new IgniteNodeValidationResult(
@@ -316,11 +300,8 @@ public class RollingUpgradeProcessor extends 
GridProcessorAdapter implements Dis
     }
 
     /** */
-    private IgniteComponentUpgradeState detectComponentUpgradeState(
-        Map<ClusterNode, IgniteNodeFeatureSet> clusterFeatures,
-        String cmpName
-    ) {
-        SortedSet<IgniteProductVersion> clusterCmpVersions = 
distinctClusterComponentVersions(clusterFeatures, cmpName);
+    private IgniteComponentUpgradeState 
detectComponentUpgradeState(Collection<ClusterNode> clusterNodes, String 
cmpName) {
+        SortedSet<IgniteProductVersion> clusterCmpVersions = 
distinctClusterComponentVersions(clusterNodes, cmpName);
 
         assert !clusterCmpVersions.isEmpty() && clusterCmpVersions.size() <= 2 
: "Cluster nodes must run no more than" +
             " two versions of the component [cmpName=" + cmpName + ", 
clusterCmpVersions=" + clusterCmpVersions.size() + "]";
@@ -342,20 +323,17 @@ public class RollingUpgradeProcessor extends 
GridProcessorAdapter implements Dis
     }
 
     /** */
-    private SortedSet<IgniteProductVersion> distinctClusterComponentVersions(
-        Map<ClusterNode, IgniteNodeFeatureSet> clusterFeatures,
-        String cmpName
-    ) {
+    private SortedSet<IgniteProductVersion> 
distinctClusterComponentVersions(Collection<ClusterNode> clusterNodes, String 
cmpName) {
         SortedSet<IgniteProductVersion> distinctCmpVersions = new 
TreeSet<>(Comparator.nullsFirst(Comparator.naturalOrder()));
 
-        clusterFeatures.forEach((node, nodeFeatures) -> {
-            IgniteComponentFeatureSet cmpFeatures = 
nodeFeatures.componentFeatures(cmpName);
+        for (ClusterNode node : clusterNodes) {
+            IgniteComponentFeatureSet cmpFeatures = 
extractNodeFeatures(node).componentFeatures(cmpName);
 
             if (node.isClient() && cmpFeatures == null)
-                return; // Components are optional on client nodes, even when 
they are configured on servers.
+                continue; // Components are optional on client nodes, even 
when they are configured on servers.
 
             distinctCmpVersions.add(cmpFeatures == null ? null : 
cmpFeatures.version());
-        });
+        }
 
         return distinctCmpVersions;
     }
@@ -371,17 +349,17 @@ public class RollingUpgradeProcessor extends 
GridProcessorAdapter implements Dis
 
     /** */
     private boolean isReadyForVersionFinalization() {
-        Map<ClusterNode, IgniteNodeFeatureSet> clusterFeatures = 
clusterFeatures();
+        Collection<ClusterNode> clusterNodes = clusterNodes();
 
         Set<IgniteNodeFeatureSet> distinctServerNodeFeatureSets = new 
HashSet<>();
         Set<IgniteNodeFeatureSet> distinctClientNodeFeatureSets = new 
HashSet<>();
 
-        clusterFeatures.forEach((node, features) -> {
+        for (ClusterNode node : clusterNodes) {
             if (!node.isClient())
-                distinctServerNodeFeatureSets.add(features);
+                distinctServerNodeFeatureSets.add(extractNodeFeatures(node));
             else
-                distinctClientNodeFeatureSets.add(features);
-        });
+                distinctClientNodeFeatureSets.add(extractNodeFeatures(node));
+        }
 
         if (distinctServerNodeFeatureSets.size() != 1)
             return false;
@@ -402,15 +380,10 @@ public class RollingUpgradeProcessor extends 
GridProcessorAdapter implements Dis
     }
 
     /** */
-    private IgniteNodeFeatureSet extractNodeFeatures(ClusterNode node) throws 
IgniteCheckedException {
-        byte[] attrVal = node.attribute(ATTR_IGNITE_FEATURES);
+    private static IgniteNodeFeatureSet extractNodeFeatures(ClusterNode node) {
+        assert node instanceof IgniteClusterNode : node;
 
-        IgniteComponentFeatureSet[] nodeFeatures = U.unmarshal(
-            ctx.marshallerContext().jdkMarshaller(),
-            attrVal,
-            U.resolveClassLoader(ctx.config()));
-
-        return new IgniteNodeFeatureSet(nodeFeatures);
+        return ((IgniteClusterNode)node).features();
     }
 
     /** */
@@ -418,25 +391,6 @@ public class RollingUpgradeProcessor extends 
GridProcessorAdapter implements Dis
         return F.isEmpty(errors) ? null : F.firstValue(errors);
     }
 
-    /** */
-    private Map<ClusterNode, IgniteNodeFeatureSet> clusterFeatures() {
-        assert Thread.holdsLock(topGuard);
-
-        try {
-            Map<ClusterNode, IgniteNodeFeatureSet> res = new HashMap<>();
-
-            for (ClusterNode node : clusterNodes())
-                res.put(node, extractNodeFeatures(node));
-
-            return res;
-        }
-        catch (IgniteCheckedException e) {
-            U.error(log, "Failed to resolve cluster features", e);
-
-            throw new IgniteException("Failed to resolve cluster features", e);
-        }
-    }
-
     /** */
     private Set<ClusterNode> clusterNodes() {
         assert Thread.holdsLock(topGuard);
@@ -560,13 +514,14 @@ public class RollingUpgradeProcessor extends 
GridProcessorAdapter implements Dis
 
                 curFinalizeProcId = reqId;
 
-                if (!isReadyForVersionFinalization())
+                if (!isReadyForVersionFinalization()) {
                     return new GridFinishedFuture<>(new IgniteException(
-                    "Cluster version finalization failed. The cluster contains 
nodes running" +
-                        " different versions of one or more components. Retry 
the operation after upgrading" +
-                        " all cluster node components to the same version" +
-                        " [clusterFeatures=" + 
clusterFeatures().entrySet().stream().collect(
-                        Collectors.toMap(e -> e.getKey().id(), 
Map.Entry::getValue)) + ']'));
+                        "Cluster version finalization failed. The cluster 
contains nodes running" +
+                            " different versions of one or more components. 
Retry the operation after upgrading" +
+                            " all cluster node components to the same version" 
+
+                            " [clusterFeatures=" + 
clusterNodes().stream().collect(Collectors.toMap(
+                            ClusterNode::id, 
RollingUpgradeProcessor::extractNodeFeatures)) + ']'));
+                }
 
                 return new GridFinishedFuture<>();
             }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/feature/IgniteNodeFeatureSet.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/feature/IgniteNodeFeatureSet.java
index ef06cb57fbf..d00439e4156 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/feature/IgniteNodeFeatureSet.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/processors/rollingupgrade/feature/IgniteNodeFeatureSet.java
@@ -17,6 +17,11 @@
 
 package org.apache.ignite.internal.processors.rollingupgrade.feature;
 
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -24,36 +29,56 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.ignite.IgniteException;
-import org.apache.ignite.internal.util.tostring.GridToStringExclude;
+import org.apache.ignite.internal.Order;
+import org.apache.ignite.plugin.extensions.communication.Message;
 import org.jetbrains.annotations.Nullable;
 
 /**
  * Represents a set of {@link IgniteFeature}s supported by an Ignite node. 
Ignite is divided into independent components.
  * Each component is associated with its version and a set of {@link 
IgniteFeature}s.
  */
-public class IgniteNodeFeatureSet {
+public class IgniteNodeFeatureSet implements Message, Externalizable {
     /** */
-    @GridToStringExclude
-    private final Map<String, IgniteComponentFeatureSet> features;
+    private static final long serialVersionUID = 0L;
 
     /** */
-    public IgniteNodeFeatureSet(IgniteComponentFeatureSet... features) {
-        this.features = indexByComponentName(features);
+    public static final IgniteNodeFeatureSet LOCAL_CORE_FEATURES = new 
IgniteNodeFeatureSet(new IgniteComponentFeatureSet[] {
+        IgniteCoreFeatureSet.local()
+    });
+
+    /** */
+    @Order(0)
+    IgniteComponentFeatureSet[] features;
+
+    /** */
+    @Nullable private volatile Map<String, IgniteComponentFeatureSet> 
featuresByComponent;
+
+    /** */
+    public IgniteNodeFeatureSet() {
+        // No-op.
+    }
+
+    /** */
+    public IgniteNodeFeatureSet(IgniteComponentFeatureSet[] features) {
+        assert features != null;
+
+        this.features = features;
+        this.featuresByComponent = indexByComponentName(features);
     }
 
     /** */
     public Set<String> components() {
-        return Collections.unmodifiableSet(features.keySet());
+        return Collections.unmodifiableSet(featuresByComponent().keySet());
     }
 
     /** */
     public IgniteComponentFeatureSet[] values() {
-        return features.values().toArray(IgniteComponentFeatureSet[]::new);
+        return features;
     }
 
     /** */
     @Nullable public IgniteComponentFeatureSet componentFeatures(String 
cmpName) {
-        return features.get(cmpName);
+        return featuresByComponent().get(cmpName);
     }
 
     /** */
@@ -61,8 +86,8 @@ public class IgniteNodeFeatureSet {
         if (!components().containsAll(other.components()))
             return false;
 
-        for (IgniteComponentFeatureSet otherCmpFeatures : 
other.features.values()) {
-            if 
(!otherCmpFeatures.equals(features.get(otherCmpFeatures.componentName())))
+        for (IgniteComponentFeatureSet otherCmpFeatures : other.features) {
+            if 
(!otherCmpFeatures.equals(featuresByComponent().get(otherCmpFeatures.componentName())))
                 return false;
         }
 
@@ -71,11 +96,41 @@ public class IgniteNodeFeatureSet {
 
     /** */
     public boolean contains(IgniteFeature feature) {
-        IgniteComponentFeatureSet cmpFeatures = 
features.get(feature.componentName());
+        IgniteComponentFeatureSet cmpFeatures = 
featuresByComponent().get(feature.componentName());
 
         return cmpFeatures != null && cmpFeatures.contains(feature.id());
     }
 
+    /** */
+    private Map<String, IgniteComponentFeatureSet> featuresByComponent() {
+        Map<String, IgniteComponentFeatureSet> featuresByComponent = 
this.featuresByComponent;
+
+        if (featuresByComponent != null)
+            return featuresByComponent;
+
+        featuresByComponent = indexByComponentName(features);
+
+        this.featuresByComponent = featuresByComponent;
+
+        return featuresByComponent;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeExternal(ObjectOutput out) throws IOException {
+        out.writeInt(features.length);
+
+        for (IgniteComponentFeatureSet feature : features)
+            out.writeObject(feature);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readExternal(ObjectInput in) throws IOException, 
ClassNotFoundException {
+        features = new IgniteComponentFeatureSet[in.readInt()];
+
+        for (int i = 0; i < features.length; i++)
+            features[i] = (IgniteComponentFeatureSet)in.readObject();
+    }
+
     /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (o == null || getClass() != o.getClass())
@@ -83,21 +138,21 @@ public class IgniteNodeFeatureSet {
 
         IgniteNodeFeatureSet other = (IgniteNodeFeatureSet)o;
 
-        return Objects.equals(features, other.features);
+        return Objects.equals(featuresByComponent(), 
other.featuresByComponent());
     }
 
     /** {@inheritDoc} */
     @Override public int hashCode() {
-        return Objects.hashCode(features);
+        return Objects.hashCode(featuresByComponent());
     }
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return 
features.values().stream().map(IgniteComponentFeatureSet::toString).collect(Collectors.joining(",
 ", "[", "]"));
+        return 
Arrays.stream(features).map(IgniteComponentFeatureSet::toString).collect(Collectors.joining(",
 ", "[", "]"));
     }
 
     /** */
-    private static Map<String, IgniteComponentFeatureSet> 
indexByComponentName(IgniteComponentFeatureSet... features) {
+    private static Map<String, IgniteComponentFeatureSet> 
indexByComponentName(IgniteComponentFeatureSet[] features) {
         Map<String, IgniteComponentFeatureSet> res = new HashMap<>();
 
         for (IgniteComponentFeatureSet compFeatures : features) {
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/isolated/IsolatedDiscoverySpi.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/isolated/IsolatedDiscoverySpi.java
index 7a46fa1e0c0..d5556f56816 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/isolated/IsolatedDiscoverySpi.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/isolated/IsolatedDiscoverySpi.java
@@ -54,6 +54,7 @@ import static java.util.Collections.singleton;
 import static org.apache.ignite.events.EventType.EVT_NODE_JOINED;
 import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_SECURITY_CREDENTIALS;
 import static 
org.apache.ignite.internal.events.DiscoveryCustomEvent.EVT_DISCOVERY_CUSTOM_EVT;
+import static 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet.LOCAL_CORE_FEATURES;
 import static 
org.apache.ignite.internal.processors.security.SecurityUtils.authenticateLocalNode;
 import static 
org.apache.ignite.internal.processors.security.SecurityUtils.withSecurityContext;
 
@@ -125,7 +126,12 @@ public class IsolatedDiscoverySpi extends IgniteSpiAdapter 
implements IgniteDisc
 
     /** {@inheritDoc} */
     @Override public void setNodeAttributes(Map<String, Object> attrs, 
IgniteProductVersion ver) {
-        locNode = new IsolatedNode(ignite.configuration().getNodeId(), attrs, 
ver);
+        locNode = new IsolatedNode(
+            ignite.configuration().getNodeId(),
+            attrs,
+            ver,
+            ignite instanceof IgniteKernal ? 
ignite.context().localNodeFeatures() : LOCAL_CORE_FEATURES
+        );
     }
 
     /** {@inheritDoc} */
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/isolated/IsolatedNode.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/isolated/IsolatedNode.java
index 2489d95900b..3e18ddea775 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/isolated/IsolatedNode.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/isolated/IsolatedNode.java
@@ -27,6 +27,7 @@ import org.apache.ignite.cache.CacheMetrics;
 import org.apache.ignite.cluster.ClusterMetrics;
 import org.apache.ignite.internal.ClusterMetricsSnapshot;
 import org.apache.ignite.internal.managers.discovery.IgniteClusterNode;
+import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteProductVersion;
 
@@ -48,6 +49,9 @@ public class IsolatedNode implements IgniteClusterNode {
     /** Node attributes. */
     private Map<String, Object> attrs;
 
+    /** Node features. */
+    private final IgniteNodeFeatureSet features;
+
     /** */
     private volatile ClusterMetrics metrics = new ClusterMetricsSnapshot();
 
@@ -58,11 +62,13 @@ public class IsolatedNode implements IgniteClusterNode {
      * @param id Node ID.
      * @param attrs Node attributes.
      * @param ver Node version.
+     * @param features Node features.
      */
-    public IsolatedNode(UUID id, Map<String, Object> attrs, 
IgniteProductVersion ver) {
+    public IsolatedNode(UUID id, Map<String, Object> attrs, 
IgniteProductVersion ver, IgniteNodeFeatureSet features) {
         this.id = id;
         this.attrs = U.sealMap(attrs);
         this.ver = ver;
+        this.features = features;
     }
 
     /** {@inheritDoc} */
@@ -75,6 +81,11 @@ public class IsolatedNode implements IgniteClusterNode {
         return consistentId;
     }
 
+    /** {@inheritDoc} */
+    @Override public IgniteNodeFeatureSet features() {
+        return features;
+    }
+
     /** {@inheritDoc} */
     @Override public <T> T attribute(String name) {
         return (T)attrs.get(name);
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ClientImpl.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ClientImpl.java
index e6d27b3bb48..53b1dbfc935 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ClientImpl.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ClientImpl.java
@@ -709,7 +709,7 @@ class ClientImpl extends TcpDiscoveryImpl {
 
                 TcpDiscoveryIoSession ses = createSession(sock);
 
-                TcpDiscoveryHandshakeRequest req = new 
TcpDiscoveryHandshakeRequest(locNodeId, spi.localNodeFeatures());
+                TcpDiscoveryHandshakeRequest req = new 
TcpDiscoveryHandshakeRequest(locNodeId, locNode.features());
 
                 req.client(true);
                 req.dcId(locNode.dataCenterId());
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ServerImpl.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ServerImpl.java
index d326fd8769a..c97661427c1 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ServerImpl.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/ServerImpl.java
@@ -1507,7 +1507,7 @@ class ServerImpl extends TcpDiscoveryImpl {
 
                 openSock = true;
 
-                TcpDiscoveryHandshakeRequest req = new 
TcpDiscoveryHandshakeRequest(locNodeId, spi.localNodeFeatures());
+                TcpDiscoveryHandshakeRequest req = new 
TcpDiscoveryHandshakeRequest(locNodeId, locNode.features());
 
                 // Handshake.
                 spi.writeMessage(ses, req, 
timeoutHelper.nextTimeoutChunk(spi.getSocketTimeout()));
@@ -3419,7 +3419,7 @@ class ServerImpl extends TcpDiscoveryImpl {
                                 openSock = true;
 
                                 // Handshake.
-                                TcpDiscoveryHandshakeRequest hndMsg = new 
TcpDiscoveryHandshakeRequest(locNodeId, spi.localNodeFeatures());
+                                TcpDiscoveryHandshakeRequest hndMsg = new 
TcpDiscoveryHandshakeRequest(locNodeId, locNode.features());
 
                                 if (sndState != null) {
                                     // If want a forced connection, we set the 
change-topology node flag to current node id.
@@ -6602,7 +6602,7 @@ class ServerImpl extends TcpDiscoveryImpl {
                     TcpDiscoveryHandshakeResponse res = new 
TcpDiscoveryHandshakeResponse(
                         locNodeId,
                         locNode.internalOrder(),
-                        spi.localNodeFeatures()
+                        locNode.features()
                     );
 
                     if (req.client()) {
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java
index b92ca657ad7..84da71e3cec 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java
@@ -472,9 +472,6 @@ public class TcpDiscoverySpi extends IgniteSpiAdapter 
implements IgniteDiscovery
     /** For test purposes. */
     private boolean skipAddrsRandomization = false;
 
-    /** */
-    private IgniteNodeFeatureSet locNodeFeatures;
-
     /**
      * Gets current SPI state.
      *
@@ -1196,8 +1193,6 @@ public class TcpDiscoverySpi extends IgniteSpiAdapter 
implements IgniteDiscovery
         // Init local node.
         initAddresses();
 
-        locNodeFeatures = ((IgniteEx)ignite).context().localNodeFeatures();
-
         locNode = new TcpDiscoveryNode(
             ignite.configuration().getNodeId(),
             addrs.get1(),
@@ -1205,7 +1200,8 @@ public class TcpDiscoverySpi extends IgniteSpiAdapter 
implements IgniteDiscovery
             srvPort,
             metricsProvider,
             locNodeVer,
-            consistentId());
+            consistentId(),
+            ignite.context().localNodeFeatures());
 
         if (addExtAddrAttr) {
             Collection<InetSocketAddress> extAddrs = addrRslvr == null ? null :
@@ -1695,11 +1691,6 @@ public class TcpDiscoverySpi extends IgniteSpiAdapter 
implements IgniteDiscovery
         }
     }
 
-    /** */
-    IgniteNodeFeatureSet localNodeFeatures() {
-        return locNodeFeatures;
-    }
-
     /** */
     void validateRemoteFeatures(IgniteNodeFeatureSet rmtFeatures) throws 
IgniteCheckedException {
         if (rmtFeatures == null) {
@@ -1709,7 +1700,7 @@ public class TcpDiscoverySpi extends IgniteSpiAdapter 
implements IgniteDiscovery
         }
 
         for (IgniteComponentFeatureSet rmtCmpFeatures : rmtFeatures.values()) {
-            IgniteComponentFeatureSet locCmpFeatures = 
locNodeFeatures.componentFeatures(rmtCmpFeatures.componentName());
+            IgniteComponentFeatureSet locCmpFeatures = 
locNode.features().componentFeatures(rmtCmpFeatures.componentName());
 
             if (locCmpFeatures == null)
                 continue;
@@ -1724,7 +1715,7 @@ public class TcpDiscoverySpi extends IgniteSpiAdapter 
implements IgniteDiscovery
 
             if (!src.isUpgradableTo(target)) {
                 throw new UnsupportedNodeVersionException("Remote node 
component versions are not supported" +
-                    " [locComponents=" + locNodeFeatures +
+                    " [locComponents=" + locNode.features() +
                     ", rmtComponents=" + rmtFeatures + ']');
             }
         }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/internal/TcpDiscoveryNode.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/internal/TcpDiscoveryNode.java
index 8f1bee5ceb2..f31b3366c30 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/internal/TcpDiscoveryNode.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/internal/TcpDiscoveryNode.java
@@ -38,6 +38,7 @@ import org.apache.ignite.internal.IgniteNodeAttributes;
 import org.apache.ignite.internal.Marshalled;
 import org.apache.ignite.internal.Order;
 import org.apache.ignite.internal.managers.discovery.IgniteClusterNode;
+import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet;
 import org.apache.ignite.internal.util.lang.GridMetadataAwareAdapter;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
 import org.apache.ignite.internal.util.tostring.GridToStringInclude;
@@ -154,6 +155,11 @@ public class TcpDiscoveryNode extends 
GridMetadataAwareAdapter implements Ignite
     @Order(10)
     UUID clientRouterNodeId;
 
+    /** Node features. */
+    @GridToStringExclude
+    @Order(11)
+    IgniteNodeFeatureSet features;
+
     /** */
     @GridToStringExclude
     private transient volatile InetSocketAddress lastSuccessfulAddr;
@@ -183,6 +189,7 @@ public class TcpDiscoveryNode extends 
GridMetadataAwareAdapter implements Ignite
      * @param metricsProvider Metrics provider.
      * @param ver Version.
      * @param consistentId Node consistent ID.
+     * @param features Node features.
      */
     public TcpDiscoveryNode(UUID id,
         Collection<String> addrs,
@@ -190,11 +197,13 @@ public class TcpDiscoveryNode extends 
GridMetadataAwareAdapter implements Ignite
         int discPort,
         DiscoveryMetricsProvider metricsProvider,
         IgniteProductVersion ver,
-        Serializable consistentId
+        Serializable consistentId,
+        IgniteNodeFeatureSet features
     ) {
         assert id != null;
         assert metricsProvider != null;
         assert ver != null;
+        assert features != null;
 
         this.id = id;
 
@@ -207,6 +216,7 @@ public class TcpDiscoveryNode extends 
GridMetadataAwareAdapter implements Ignite
         this.discPort = discPort;
         this.metricsProvider = metricsProvider;
         this.ver = ver;
+        this.features = features;
 
         this.consistentId = consistentId != null ? consistentId : 
U.consistentId(sortedAddrs, discPort);
 
@@ -359,6 +369,11 @@ public class TcpDiscoveryNode extends 
GridMetadataAwareAdapter implements Ignite
         return ver;
     }
 
+    /** {@inheritDoc} */
+    @Override public IgniteNodeFeatureSet features() {
+        return features;
+    }
+
     /**
      * @param ver Version.
      */
@@ -544,7 +559,14 @@ public class TcpDiscoveryNode extends 
GridMetadataAwareAdapter implements Ignite
      */
     public TcpDiscoveryNode clientReconnectNode(Map<String, Object> nodeAttrs) 
{
         TcpDiscoveryNode node = new TcpDiscoveryNode(
-            id, addrs, hostNames, discPort, metricsProvider, ver, null
+            id,
+            addrs,
+            hostNames,
+            discPort,
+            metricsProvider,
+            ver,
+            null,
+            features
         );
 
         node.attrs = Collections.unmodifiableMap(new HashMap<>(nodeAttrs));
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/messages/TcpDiscoveryHandshakeRequest.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/messages/TcpDiscoveryHandshakeRequest.java
index d2a33c6d059..560be2a9c16 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/messages/TcpDiscoveryHandshakeRequest.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/messages/TcpDiscoveryHandshakeRequest.java
@@ -19,7 +19,6 @@ package org.apache.ignite.spi.discovery.tcp.messages;
 
 import java.util.UUID;
 import org.apache.ignite.internal.Order;
-import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteComponentFeatureSet;
 import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.plugin.extensions.communication.MessageFactory;
@@ -39,7 +38,7 @@ public class TcpDiscoveryHandshakeRequest extends 
TcpDiscoveryAbstractMessage {
 
     /** */
     @Order(2)
-    IgniteComponentFeatureSet[] nodeFeatures;
+    IgniteNodeFeatureSet nodeFeatures;
 
     /**
      * Default constructor for {@link MessageFactory}.
@@ -57,7 +56,7 @@ public class TcpDiscoveryHandshakeRequest extends 
TcpDiscoveryAbstractMessage {
     public TcpDiscoveryHandshakeRequest(UUID creatorNodeId, 
IgniteNodeFeatureSet locNodeFeatures) {
         super(creatorNodeId);
 
-        this.nodeFeatures = locNodeFeatures.values();
+        this.nodeFeatures = locNodeFeatures;
     }
 
     /**
@@ -90,7 +89,7 @@ public class TcpDiscoveryHandshakeRequest extends 
TcpDiscoveryAbstractMessage {
 
     /** @return Features supported by the sender node. */
     public IgniteNodeFeatureSet nodeFeatures() {
-        return nodeFeatures == null ? null : new 
IgniteNodeFeatureSet(nodeFeatures);
+        return nodeFeatures;
     }
 
     /** {@inheritDoc} */
diff --git 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/messages/TcpDiscoveryHandshakeResponse.java
 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/messages/TcpDiscoveryHandshakeResponse.java
index d11e0814ef8..0c936624017 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/messages/TcpDiscoveryHandshakeResponse.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/messages/TcpDiscoveryHandshakeResponse.java
@@ -21,7 +21,6 @@ import java.net.InetSocketAddress;
 import java.util.Collection;
 import java.util.UUID;
 import org.apache.ignite.internal.Order;
-import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteComponentFeatureSet;
 import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.S;
@@ -46,7 +45,7 @@ public class TcpDiscoveryHandshakeResponse extends 
TcpDiscoveryAbstractMessage {
 
     /** */
     @Order(3)
-    IgniteComponentFeatureSet[] nodeFeatures;
+    IgniteNodeFeatureSet nodeFeatures;
 
     /**
      * Default constructor for {@link MessageFactory}.
@@ -67,7 +66,7 @@ public class TcpDiscoveryHandshakeResponse extends 
TcpDiscoveryAbstractMessage {
 
         order = locNodeOrder;
 
-        this.nodeFeatures = locNodeFeatures.values();
+        this.nodeFeatures = locNodeFeatures;
     }
 
     /**
@@ -115,7 +114,7 @@ public class TcpDiscoveryHandshakeResponse extends 
TcpDiscoveryAbstractMessage {
 
     /** @return Features supported by the sender node. */
     public IgniteNodeFeatureSet nodeFeatures() {
-        return nodeFeatures == null ? null : new 
IgniteNodeFeatureSet(nodeFeatures);
+        return nodeFeatures;
     }
 
     /** {@inheritDoc} */
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignmentV2Test.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignmentV2Test.java
index f994cee551f..c1c81095fba 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignmentV2Test.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/processors/affinity/GridAffinityAssignmentV2Test.java
@@ -40,6 +40,7 @@ import 
org.apache.ignite.spi.discovery.DiscoveryMetricsProvider;
 import org.apache.ignite.spi.discovery.tcp.internal.TcpDiscoveryNode;
 import org.junit.Test;
 
+import static 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet.LOCAL_CORE_FEATURES;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotSame;
@@ -249,7 +250,8 @@ public class GridAffinityAssignmentV2Test {
             0,
             metrics,
             v,
-            consistentId
+            consistentId,
+            LOCAL_CORE_FEATURES
         );
 
         node.setAttributes(Collections.emptyMap());
diff --git 
a/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/zk/ZookeeperDiscoverySpi.java
 
b/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/zk/ZookeeperDiscoverySpi.java
index 8f9b3008583..37b96375777 100644
--- 
a/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/zk/ZookeeperDiscoverySpi.java
+++ 
b/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/zk/ZookeeperDiscoverySpi.java
@@ -23,9 +23,11 @@ import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.configuration.IgniteConfiguration;
@@ -39,6 +41,7 @@ import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteBiTuple;
 import org.apache.ignite.lang.IgniteProductVersion;
+import org.apache.ignite.marshaller.Marshallers;
 import 
org.apache.ignite.plugin.extensions.communication.MessageFactoryProvider;
 import org.apache.ignite.resources.LoggerResource;
 import org.apache.ignite.spi.IgniteSpiAdapter;
@@ -66,6 +69,7 @@ import org.jetbrains.annotations.Nullable;
 
 import static 
org.apache.ignite.IgniteSystemProperties.IGNITE_CONSISTENT_ID_BY_HOST_WITHOUT_PORT;
 import static org.apache.ignite.IgniteSystemProperties.getBoolean;
+import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_IGNITE_FEATURES;
 import static 
org.apache.ignite.internal.managers.discovery.GridDiscoveryManager.DISCO_METRICS;
 
 /**
@@ -364,8 +368,15 @@ public class ZookeeperDiscoverySpi extends 
IgniteSpiAdapter implements IgniteDis
             log.debug("Node version to set: " + ver);
         }
 
-        locNodeAttrs = attrs;
         locNodeVer = ver;
+        locNodeAttrs = new HashMap<>(attrs);
+
+        try {
+            locNodeAttrs.put(ATTR_IGNITE_FEATURES, 
Marshallers.jdk().marshal(ignite.context().localNodeFeatures()));
+        }
+        catch (IgniteCheckedException e) {
+            throw new IgniteSpiException("Failed to initialize Ignite Node 
Features attribute", e);
+        }
     }
 
     /** {@inheritDoc} */
diff --git 
a/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/zk/internal/ZookeeperClusterNode.java
 
b/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/zk/internal/ZookeeperClusterNode.java
index bd657912425..198f9b275d0 100644
--- 
a/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/zk/internal/ZookeeperClusterNode.java
+++ 
b/modules/zookeeper/src/main/java/org/apache/ignite/spi/discovery/zk/internal/ZookeeperClusterNode.java
@@ -27,19 +27,24 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
+import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.cache.CacheMetrics;
 import org.apache.ignite.cluster.ClusterMetrics;
 import org.apache.ignite.internal.ClusterMetricsSnapshot;
 import org.apache.ignite.internal.IgniteNodeAttributes;
 import org.apache.ignite.internal.SecurityCredentialsAttrFilterPredicate;
 import org.apache.ignite.internal.managers.discovery.IgniteClusterNode;
+import 
org.apache.ignite.internal.processors.rollingupgrade.feature.IgniteNodeFeatureSet;
 import org.apache.ignite.internal.util.tostring.GridToStringExclude;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.lang.IgniteProductVersion;
+import org.apache.ignite.marshaller.Marshallers;
+import org.apache.ignite.spi.IgniteSpiException;
 import org.apache.ignite.spi.discovery.DiscoveryMetricsProvider;
 import org.jetbrains.annotations.Nullable;
 
+import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_IGNITE_FEATURES;
 import static 
org.apache.ignite.internal.IgniteNodeAttributes.ATTR_NODE_CONSISTENT_ID;
 import static org.apache.ignite.internal.util.lang.ClusterNodeFunc.eqNodes;
 
@@ -144,6 +149,18 @@ public class ZookeeperClusterNode implements 
IgniteClusterNode, Externalizable,
         return id;
     }
 
+    /** {@inheritDoc} */
+    @Override public IgniteNodeFeatureSet features() {
+        byte[] nodeFeaturesAttr = attribute(ATTR_IGNITE_FEATURES);
+
+        try {
+            return Marshallers.jdk().unmarshal(nodeFeaturesAttr, 
U.gridClassLoader());
+        }
+        catch (IgniteCheckedException e) {
+            throw new IgniteSpiException("Failed to resolve Ignite Node 
Features", e);
+        }
+    }
+
     /** {@inheritDoc} */
     @Override public Object consistentId() {
         return consistentId;
diff --git 
a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/zk/internal/ZookeeperDiscoverySpiTestBase.java
 
b/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/zk/internal/ZookeeperDiscoverySpiTestBase.java
index 929908e2ac8..79661366303 100644
--- 
a/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/zk/internal/ZookeeperDiscoverySpiTestBase.java
+++ 
b/modules/zookeeper/src/test/java/org/apache/ignite/spi/discovery/zk/internal/ZookeeperDiscoverySpiTestBase.java
@@ -67,11 +67,11 @@ import org.apache.ignite.lang.IgniteFuture;
 import org.apache.ignite.lang.IgniteInClosure;
 import org.apache.ignite.lang.IgniteOutClosure;
 import org.apache.ignite.lang.IgnitePredicate;
+import org.apache.ignite.lang.IgniteProductVersion;
 import org.apache.ignite.plugin.extensions.communication.Message;
 import org.apache.ignite.plugin.security.SecurityCredentials;
 import org.apache.ignite.plugin.segmentation.SegmentationPolicy;
 import org.apache.ignite.resources.IgniteInstanceResource;
-import org.apache.ignite.spi.IgniteSpiException;
 import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi;
 import org.apache.ignite.spi.communication.tcp.internal.GridNioServerWrapper;
 import org.apache.ignite.spi.discovery.DiscoverySpiNodeAuthenticator;
@@ -934,13 +934,13 @@ class ZookeeperDiscoverySpiTestBase extends 
GridCommonAbstractTest {
 
     /** */
     private static class TestAuthZookeeperDiscoverySpi extends 
TestZookeeperDiscoverySpi {
-        /** */
-        @Override public void spiStart(@Nullable String igniteInstanceName) 
throws IgniteSpiException {
-            ((IgniteEx)ignite).context().addNodeAttribute(
-                ATTR_SECURITY_CREDENTIALS,
-                new SecurityCredentials(null, null, igniteInstanceName));
+        /** {@inheritDoc} */
+        @Override public void setNodeAttributes(Map<String, Object> attrs, 
IgniteProductVersion ver) {
+            Map<String, Object> attrsWithCreds = new HashMap<>(attrs);
+
+            attrsWithCreds.put(ATTR_SECURITY_CREDENTIALS, new 
SecurityCredentials(null, null, igniteInstanceName));
 
-            super.spiStart(igniteInstanceName);
+            super.setNodeAttributes(attrsWithCreds, ver);
         }
     }
 }

Reply via email to