You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ignite.apache.org by GitBox <gi...@apache.org> on 2023/01/17 19:20:04 UTC

[GitHub] [ignite-3] sashapolo opened a new pull request, #1542: IGNITE-18554 Remove MetaStorage learners on topology events @sashapolo

sashapolo opened a new pull request, #1542:
URL: https://github.com/apache/ignite-3/pull/1542

   https://issues.apache.org/jira/browse/IGNITE-18554


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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sashapolo commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sashapolo (via GitHub)" <gi...@apache.org>.
sashapolo commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1091571518


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+            }
+        });
+
+        logicalTopology.addEventListener(new LogicalTopologyEventListener() {
+            // Try to add a node as a Learner when it appears in the Logical Topology. Even though usually remote nodes add themselves as

Review Comment:
   Normal flow:
   1. A node gets discovered by the CMG leader using the Physical Topology.
   2. The node gets validated on the CMG leader.
   3. The CMG leader sends MS node IDs to the joining node.
   4. The joining node starts a local Raft server and executes `addLearners` to add itself as a learner.
   5. The joining node executes `onJoinReady` and gets added to the Logical Topology.
   
   Now imagine that after that a network partition happens for a long enough time for the node to get kicked out of the Logical Topology. As soon as that happens, it gets removed from the MS learners as well. When the network partition is fixed, the node will be added back to the Logical Topology, but it will not be added as MS learner (because the node was not restarted and the code that starts the Raft server will not be executed).



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sanpwc commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sanpwc (via GitHub)" <gi...@apache.org>.
sanpwc commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1104710077


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyService;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.PeersAndLearners;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listener for updating the list of Meta Storage Raft group listeners.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final String nodeName;
+
+    private final LogicalTopologyService logicalTopologyService;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we usually start a bunch of async operations inside the event listener and need
+     * them to finish in the same order.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopologyService logicalTopologyService,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.nodeName = clusterService.localConfiguration().getName();
+        this.logicalTopologyService = logicalTopologyService;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        synchronized (serializationFutureMux) {
+            registerTopologyEventListeners();
+
+            // Update learner configuration (in case we missed some topology updates) and initialize the serialization future.
+            serializationFuture = executeIfLeaderImpl(this::resetLearners);
+        }
+    }
+
+    private void registerTopologyEventListeners() {
+        logicalTopologyService.addEventListener(new LogicalTopologyEventListener() {
+            @Override
+            public void onNodeValidated(ClusterNode validatedNode) {
+                executeIfLeader((service, term) -> addLearner(service.raftGroupService(), validatedNode));

Review Comment:
   1. We do call onNodeValidated, ..., onNodeLeft are called within raft thread, is that correct?
   2. If true
       2.a You didn't introduce any synchronous long running logic, e.g. network communication as a reaction to such events, did you? serializationFuture?
       2.b You didn't introduce any logic that may prevent CMG state-machine idempotentancy, did you? Meaning, that if we'll have an exception within onSmt on Node A and won't have such an exception on Node B, CMG listener won't be effected, is that true?
   3. Who is responsible for exception handling for serializationFuture? E.g. logging.
   



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sashapolo commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sashapolo (via GitHub)" <gi...@apache.org>.
sashapolo commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1092035813


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+            }
+        });
+
+        logicalTopology.addEventListener(new LogicalTopologyEventListener() {
+            // Try to add a node as a Learner when it appears in the Logical Topology. Even though usually remote nodes add themselves as

Review Comment:
   Do you suggest that if a node leaves the logical topology for whatever reason, should re-run the joining process even if it was not restarted?



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sashapolo commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sashapolo (via GitHub)" <gi...@apache.org>.
sashapolo commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1092099507


##########
modules/metastorage/src/integrationTest/java/org/apache/ignite/internal/metastorage/impl/ItMetaStorageMultipleNodesTest.java:
##########
@@ -0,0 +1,338 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.notExists;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.revision;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.noop;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.put;
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willBe;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
+import org.apache.ignite.internal.cluster.management.raft.ClusterStateStorage;
+import org.apache.ignite.internal.cluster.management.raft.TestClusterStateStorage;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopologyImpl;
+import org.apache.ignite.internal.configuration.testframework.ConfigurationExtension;
+import org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
+import org.apache.ignite.internal.hlc.HybridClockImpl;
+import org.apache.ignite.internal.manager.IgniteComponent;
+import org.apache.ignite.internal.metastorage.Entry;
+import org.apache.ignite.internal.metastorage.EntryEvent;
+import org.apache.ignite.internal.metastorage.WatchEvent;
+import org.apache.ignite.internal.metastorage.WatchListener;
+import org.apache.ignite.internal.metastorage.server.SimpleInMemoryKeyValueStorage;
+import org.apache.ignite.internal.network.message.ScaleCubeMessage;
+import org.apache.ignite.internal.raft.Loza;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftManager;
+import org.apache.ignite.internal.raft.configuration.RaftConfiguration;
+import org.apache.ignite.internal.testframework.IgniteAbstractTest;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.vault.VaultManager;
+import org.apache.ignite.internal.vault.inmemory.InMemoryVaultService;
+import org.apache.ignite.lang.ByteArray;
+import org.apache.ignite.lang.NodeStoppingException;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.DefaultMessagingService;
+import org.apache.ignite.network.NetworkAddress;
+import org.apache.ignite.network.StaticNodeFinder;
+import org.apache.ignite.utils.ClusterServiceTestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Tests for scenarios when Meta Storage nodes join and leave a cluster.
+ */
+@ExtendWith(ConfigurationExtension.class)
+public class ItMetaStorageMultipleNodesTest extends IgniteAbstractTest {
+    private static class Node {
+        private final VaultManager vaultManager;
+
+        private final ClusterService clusterService;
+
+        private final RaftManager raftManager;
+
+        private final ClusterStateStorage clusterStateStorage = new TestClusterStateStorage();
+
+        private final ClusterManagementGroupManager cmgManager;
+
+        private final MetaStorageManagerImpl metaStorageManager;
+
+        Node(ClusterService clusterService, RaftConfiguration raftConfiguration, Path dataPath) {
+            this.clusterService = clusterService;
+
+            this.vaultManager = new VaultManager(new InMemoryVaultService());
+
+            Path basePath = dataPath.resolve(name());
+
+            this.raftManager = new Loza(
+                    clusterService,
+                    raftConfiguration,
+                    basePath.resolve("raft"),
+                    new HybridClockImpl()
+            );
+
+            var logicalTopology = new LogicalTopologyImpl(clusterStateStorage);
+
+            this.cmgManager = new ClusterManagementGroupManager(
+                    vaultManager,
+                    clusterService,
+                    raftManager,
+                    clusterStateStorage,
+                    logicalTopology
+            );
+
+            this.metaStorageManager = new MetaStorageManagerImpl(
+                    vaultManager,
+                    clusterService,
+                    cmgManager,
+                    logicalTopology,
+                    raftManager,
+                    new SimpleInMemoryKeyValueStorage(name())
+            );
+        }
+
+        void start() throws NodeStoppingException {
+            List<IgniteComponent> components =
+                    List.of(vaultManager, clusterService, raftManager, clusterStateStorage, cmgManager, metaStorageManager);
+
+            components.forEach(IgniteComponent::start);
+
+            metaStorageManager.deployWatches();
+        }
+
+        String name() {
+            return clusterService.localConfiguration().getName();
+        }
+
+        void stop() throws Exception {
+            List<IgniteComponent> components =
+                    List.of(metaStorageManager, cmgManager, raftManager, clusterStateStorage, clusterService, vaultManager);
+
+            Stream<AutoCloseable> beforeNodeStop = components.stream().map(c -> c::beforeNodeStop);
+
+            Stream<AutoCloseable> nodeStop = components.stream().map(c -> c::stop);
+
+            IgniteUtils.closeAll(Stream.concat(beforeNodeStop, nodeStop));
+        }
+
+        CompletableFuture<Set<String>> getMetaStorageLearners() {
+            return metaStorageManager
+                    .metaStorageServiceFuture()
+                    .thenApply(MetaStorageServiceImpl::raftGroupService)
+                    .thenCompose(service -> service.refreshMembers(false).thenApply(v -> service.learners()))
+                    .thenApply(learners -> learners.stream().map(Peer::consistentId).collect(toSet()));
+        }
+
+        void startDroppingMessagesTo(Node recipient) {
+            ((DefaultMessagingService) clusterService.messagingService())
+                    .dropMessages((recipientConsistentId, message) ->
+                            recipient.name().equals(recipientConsistentId) && message instanceof ScaleCubeMessage);
+        }
+
+        void stopDroppingMessages() {
+            ((DefaultMessagingService) clusterService.messagingService()).stopDroppingMessages();
+        }
+    }
+
+    private final List<Node> nodes = new ArrayList<>();
+
+    @InjectConfiguration
+    private RaftConfiguration raftConfiguration;
+
+    private Node startNode(TestInfo testInfo) throws NodeStoppingException {
+        var nodeFinder = new StaticNodeFinder(List.of(new NetworkAddress("localhost", 10_000)));
+
+        ClusterService clusterService = ClusterServiceTestUtils.clusterService(testInfo, 10_000 + nodes.size(), nodeFinder);
+
+        var node = new Node(clusterService, raftConfiguration, workDir);
+
+        node.start();
+
+        nodes.add(node);
+
+        return node;
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        IgniteUtils.closeAll(nodes.stream().map(node -> node::stop));
+    }
+
+    /**
+     * Tests that an incoming node gets registered as a Learner and receives Meta Storage updates.
+     */
+    @Test
+    void testLearnerJoin(TestInfo testInfo) throws NodeStoppingException {
+        Node firstNode = startNode(testInfo);
+
+        firstNode.cmgManager.initCluster(List.of(firstNode.name()), List.of(firstNode.name()), "test");
+
+        var key = new ByteArray("foo");
+        byte[] value = "bar".getBytes(StandardCharsets.UTF_8);
+
+        CompletableFuture<Boolean> invokeFuture = firstNode.metaStorageManager.invoke(notExists(key), put(key, value), noop());
+
+        assertThat(invokeFuture, willBe(true));
+
+        Node secondNode = startNode(testInfo);
+
+        // Check that reading remote data works correctly.
+        assertThat(secondNode.metaStorageManager.get(key).thenApply(Entry::value), willBe(value));
+
+        // Check that the new node will receive events.
+        var awaitFuture = new CompletableFuture<EntryEvent>();
+
+        secondNode.metaStorageManager.registerExactWatch(key, new WatchListener() {
+            @Override
+            public void onUpdate(WatchEvent event) {
+                // Skip the first update event, because it's not guaranteed to arrive here (insert may have happened before the watch was
+                // registered).
+                if (event.revision() != 1) {
+                    awaitFuture.complete(event.entryEvent());
+                }
+            }
+
+            @Override
+            public void onError(Throwable e) {
+                awaitFuture.completeExceptionally(e);
+            }
+        });
+
+        byte[] newValue = "baz".getBytes(StandardCharsets.UTF_8);
+
+        invokeFuture = firstNode.metaStorageManager.invoke(revision(key).eq(1), put(key, newValue), noop());
+
+        assertThat(invokeFuture, willBe(true));
+
+        var expectedEntryEvent = new EntryEvent(
+                new EntryImpl(key.bytes(), value, 1, 1),
+                new EntryImpl(key.bytes(), newValue, 2, 2)
+        );
+
+        assertThat(awaitFuture, willBe(expectedEntryEvent));
+
+        // Check that the second node has been registered as a learner.
+        assertThat(firstNode.getMetaStorageLearners(), willBe(Set.of(secondNode.name())));
+    }
+
+    /**
+     * Tests a case when a node leaves the physical topology without entering the logical topology.
+     */
+    @Test
+    void testLearnerLeavePhysicalTopology(TestInfo testInfo) throws Exception {
+        Node firstNode = startNode(testInfo);
+        Node secondNode = startNode(testInfo);
+
+        firstNode.cmgManager.initCluster(List.of(firstNode.name()), List.of(firstNode.name()), "test");
+
+        // Try reading some data to make sure that Raft has been configured correctly.
+        assertThat(secondNode.metaStorageManager.get(new ByteArray("test")).thenApply(Entry::value), willBe(nullValue()));
+
+        // Check that the second node has been registered as a learner.
+        assertThat(firstNode.getMetaStorageLearners(), willBe(Set.of(secondNode.name())));
+
+        // Stop the second node.
+        secondNode.stop();
+
+        nodes.remove(1);
+
+        assertTrue(waitForCondition(() -> firstNode.getMetaStorageLearners().join().isEmpty(), 10_000));
+    }
+
+    /**
+     * Tests a case when a node leaves the physical topology without entering the logical topology.
+     */
+    @Test
+    void testLearnerLeaveLogicalTopology(TestInfo testInfo) throws Exception {
+        Node firstNode = startNode(testInfo);
+        Node secondNode = startNode(testInfo);
+
+        firstNode.cmgManager.initCluster(List.of(firstNode.name()), List.of(firstNode.name()), "test");
+
+        assertThat(allOf(firstNode.cmgManager.onJoinReady(), secondNode.cmgManager.onJoinReady()), willCompleteSuccessfully());
+
+        CompletableFuture<Set<String>> logicalTopologyNodes = firstNode.cmgManager
+                .logicalTopology()
+                .thenApply(logicalTopology -> logicalTopology.nodes().stream().map(ClusterNode::name).collect(toSet()));
+
+        assertThat(logicalTopologyNodes, willBe(Set.of(firstNode.name(), secondNode.name())));
+
+        // Try reading some data to make sure that Raft has been configured correctly.
+        assertThat(secondNode.metaStorageManager.get(new ByteArray("test")).thenApply(Entry::value), willBe(nullValue()));
+
+        // Check that the second node has been registered as a learner.
+        assertThat(firstNode.getMetaStorageLearners(), willBe(Set.of(secondNode.name())));
+
+        // Stop the second node.
+        secondNode.stop();
+
+        nodes.remove(1);
+
+        assertTrue(waitForCondition(() -> firstNode.getMetaStorageLearners().join().isEmpty(), 10_000));
+    }
+
+    /**
+     * Tests a scenario when a node gets kicked out of the Logical Topology due to a network partition. It should then be able to join
+     * the Meta Storage Raft group successfully.
+     */
+    @Test
+    void testLearnerLeaveAndJoinBecauseOfNetworkPartition(TestInfo testInfo) throws Exception {
+        Node firstNode = startNode(testInfo);
+        Node secondNode = startNode(testInfo);
+
+        firstNode.cmgManager.initCluster(List.of(firstNode.name()), List.of(firstNode.name()), "test");
+
+        assertThat(allOf(firstNode.cmgManager.onJoinReady(), secondNode.cmgManager.onJoinReady()), willCompleteSuccessfully());
+
+        CompletableFuture<Set<String>> logicalTopologyNodes = firstNode.cmgManager
+                .logicalTopology()
+                .thenApply(logicalTopology -> logicalTopology.nodes().stream().map(ClusterNode::name).collect(toSet()));
+
+        assertThat(logicalTopologyNodes, willBe(Set.of(firstNode.name(), secondNode.name())));
+
+        assertThat(firstNode.getMetaStorageLearners(), willBe(Set.of(secondNode.name())));
+
+        // Make first node lose the second node from the Physical and Logical topologies.
+        firstNode.startDroppingMessagesTo(secondNode);
+
+        assertTrue(waitForCondition(() -> firstNode.getMetaStorageLearners().join().isEmpty(), 10_000));
+
+        // Make the first node discover the second node again. The second node should be added as a Meta Storage Learner again.
+        firstNode.stopDroppingMessages();
+
+        assertTrue(waitForCondition(() -> firstNode.getMetaStorageLearners().join().equals(Set.of(secondNode.name())), 10_000));
+    }
+}

Review Comment:
   I'm writing such test, it's going to take a while(



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sashapolo commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sashapolo (via GitHub)" <gi...@apache.org>.
sashapolo commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1091588972


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));

Review Comment:
   > Are there any order guarantees between messaging and network topology events?
   
   I don't think so, at least there's no explicit guarantee. Technically these can be separate subsystems.
   
   > In other words is it possible that we will have a race between processing nodeJoinRequest and handling onDissapeared, meaning the race between adding and removing the learner in this particular case? 
   
   Can you please clarify the situation?



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sanpwc commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sanpwc (via GitHub)" <gi...@apache.org>.
sanpwc commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1091989894


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+            }
+        });
+
+        logicalTopology.addEventListener(new LogicalTopologyEventListener() {
+            // Try to add a node as a Learner when it appears in the Logical Topology. Even though usually remote nodes add themselves as
+            // learners, this call allows us to handle the case when a network partition occurs and a node gets kicked out of the Logical
+            // Topology. Since the remote node simply gets disconnected and not restarted, it should be able to re-join the cluster even
+            // though it will skip the step when it adds itself as a Learner.
+            @Override
+            public void onAppeared(ClusterNode appearedNode, LogicalTopologySnapshot newTopology) {
+                executeIfLeader(service -> addLearner(service.raftGroupService(), appearedNode));
+            }
+
+            // Try to remove a Meta Storage Learner when a node leaves the Logical Topology. This approach protects from network glitches
+            // when a node leaves and re-joins the Physical Topology.
+            @Override
+            public void onDisappeared(ClusterNode disappearedNode, LogicalTopologySnapshot newTopology) {

Review Comment:
   My fault. We do noting on physical.onAppeared(A), so there's no race.



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sanpwc commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sanpwc (via GitHub)" <gi...@apache.org>.
sanpwc commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1092213703


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+            }
+        });
+
+        logicalTopology.addEventListener(new LogicalTopologyEventListener() {
+            // Try to add a node as a Learner when it appears in the Logical Topology. Even though usually remote nodes add themselves as
+            // learners, this call allows us to handle the case when a network partition occurs and a node gets kicked out of the Logical
+            // Topology. Since the remote node simply gets disconnected and not restarted, it should be able to re-join the cluster even
+            // though it will skip the step when it adds itself as a Learner.
+            @Override
+            public void onAppeared(ClusterNode appearedNode, LogicalTopologySnapshot newTopology) {
+                executeIfLeader(service -> addLearner(service.raftGroupService(), appearedNode));
+            }
+
+            // Try to remove a Meta Storage Learner when a node leaves the Logical Topology. This approach protects from network glitches
+            // when a node leaves and re-joins the Physical Topology.
+            @Override
+            public void onDisappeared(ClusterNode disappearedNode, LogicalTopologySnapshot newTopology) {

Review Comment:
   However, seems, that we may have a race on CMG leadership transfer.
   1. C joins the topology and register itself as a learner.
   2. B becomes MS leader and fires executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology())); However B doesn't know yet that C is part of logical topology, so it removes it.
   3. Yep, you do expect that when onAppeared event will be fired you'll add C (which is wrong from my point of view), however...
   4. ...A becomes MS leader, without B receiving  onAppeared(C), and A already received onAppeared(C), so no one will restore C as a learner.



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sashapolo commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sashapolo (via GitHub)" <gi...@apache.org>.
sashapolo commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1091590067


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+            }
+        });
+
+        logicalTopology.addEventListener(new LogicalTopologyEventListener() {
+            // Try to add a node as a Learner when it appears in the Logical Topology. Even though usually remote nodes add themselves as
+            // learners, this call allows us to handle the case when a network partition occurs and a node gets kicked out of the Logical
+            // Topology. Since the remote node simply gets disconnected and not restarted, it should be able to re-join the cluster even
+            // though it will skip the step when it adds itself as a Learner.
+            @Override
+            public void onAppeared(ClusterNode appearedNode, LogicalTopologySnapshot newTopology) {
+                executeIfLeader(service -> addLearner(service.raftGroupService(), appearedNode));
+            }
+
+            // Try to remove a Meta Storage Learner when a node leaves the Logical Topology. This approach protects from network glitches
+            // when a node leaves and re-joins the Physical Topology.
+            @Override
+            public void onDisappeared(ClusterNode disappearedNode, LogicalTopologySnapshot newTopology) {

Review Comment:
   Again, can you please clarify the situation a little bit?



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sashapolo commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sashapolo (via GitHub)" <gi...@apache.org>.
sashapolo commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1091728965


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));

Review Comment:
   yes, this can happen =(



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sanpwc commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sanpwc (via GitHub)" <gi...@apache.org>.
sanpwc commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1091697689


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));

Review Comment:
   I mean following:
   0. There's node A up and running that is also CMG and MS Leader, meaning that both CMG and MS are build on top of single node A.
   1. Node B tries to join the cluster. At some point it sends `service.addLearners(List.of(localPeer))` message to the A.
   2. addLearner(B) processing hangs a bit. 
   3. Node A sees onDisappeared(B) and tries to remove B from learners, despite the fact that there's no B in the learners set.
   4. addLearner(B) wakes up and successfully adds B as a Learner.
   Thus we have a reordering between adding and removing Learner B.
   
   So, basically, my question is about possibility of such reordering. 



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sashapolo commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sashapolo (via GitHub)" <gi...@apache.org>.
sashapolo commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1104733255


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,230 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyService;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.PeersAndLearners;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listener for updating the list of Meta Storage Raft group listeners.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final String nodeName;
+
+    private final LogicalTopologyService logicalTopologyService;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we usually start a bunch of async operations inside the event listener and need
+     * them to finish in the same order.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopologyService logicalTopologyService,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.nodeName = clusterService.localConfiguration().getName();
+        this.logicalTopologyService = logicalTopologyService;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        synchronized (serializationFutureMux) {
+            registerTopologyEventListeners();
+
+            // Update learner configuration (in case we missed some topology updates) and initialize the serialization future.
+            serializationFuture = executeIfLeaderImpl(this::resetLearners);
+        }
+    }
+
+    private void registerTopologyEventListeners() {
+        logicalTopologyService.addEventListener(new LogicalTopologyEventListener() {
+            @Override
+            public void onNodeValidated(ClusterNode validatedNode) {
+                executeIfLeader((service, term) -> addLearner(service.raftGroupService(), validatedNode));

Review Comment:
   > We do call onNodeValidated, ..., onNodeLeft are called within raft thread, is that correct?
   
   yes
   
   > You didn't introduce any synchronous long running logic, e.g. network communication as a reaction to such events, did you? serializationFuture?
   
   How is it possible to introduce synchronous network calls in our code? All such stuff is done asynchronously. That's exactly why I need the `serializationFuture` - to chain a bunch of asynchronous calls to preserve Raft linearization guarantees. But I never wait on on this future.
   
   >  You didn't introduce any logic that may prevent CMG state-machine idempotentancy, did you? Meaning, that if we'll have an exception within onSmt on Node A and won't have such an exception on Node B, CMG listener won't be effected, is that true?
   
   Since all operations are asynchronous, I expect all exception to simply fail on the related thread and be possibly logged somewhere.
   
   > Who is responsible for exception handling for serializationFuture? E.g. logging.
   
   Logging is done inside the action that is passed to `executeIfLeaderImpl`. For example, see the `updateConfigUnderLock` method, it should do all the logging.



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sanpwc merged pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sanpwc (via GitHub)" <gi...@apache.org>.
sanpwc merged PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542


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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sashapolo commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sashapolo (via GitHub)" <gi...@apache.org>.
sashapolo commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1091566860


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {

Review Comment:
   Yes, both `addLearners` and `removeLearners` are idempotent, it's basically adding or removing a `PeerId` from a `HashSet`. See `modules/raft/src/main/java/org/apache/ignite/raft/jraft/conf/Configuration.java` `addLearner` and `removeLearner` methods.



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sashapolo closed pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sashapolo (via GitHub)" <gi...@apache.org>.
sashapolo closed pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events
URL: https://github.com/apache/ignite-3/pull/1542


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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sanpwc commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sanpwc (via GitHub)" <gi...@apache.org>.
sanpwc commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1090865667


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();

Review Comment:
   Do we need busyLock on over registerTopologyEventListeners? In other words, what will happen if topologyService.addEventHandler is called on "stopped" topologyService?



##########
modules/metastorage/src/integrationTest/java/org/apache/ignite/internal/metastorage/impl/ItMetaStorageMultipleNodesTest.java:
##########
@@ -0,0 +1,338 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.notExists;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.revision;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.noop;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.put;
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willBe;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
+import org.apache.ignite.internal.cluster.management.raft.ClusterStateStorage;
+import org.apache.ignite.internal.cluster.management.raft.TestClusterStateStorage;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopologyImpl;
+import org.apache.ignite.internal.configuration.testframework.ConfigurationExtension;
+import org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
+import org.apache.ignite.internal.hlc.HybridClockImpl;
+import org.apache.ignite.internal.manager.IgniteComponent;
+import org.apache.ignite.internal.metastorage.Entry;
+import org.apache.ignite.internal.metastorage.EntryEvent;
+import org.apache.ignite.internal.metastorage.WatchEvent;
+import org.apache.ignite.internal.metastorage.WatchListener;
+import org.apache.ignite.internal.metastorage.server.SimpleInMemoryKeyValueStorage;
+import org.apache.ignite.internal.network.message.ScaleCubeMessage;
+import org.apache.ignite.internal.raft.Loza;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftManager;
+import org.apache.ignite.internal.raft.configuration.RaftConfiguration;
+import org.apache.ignite.internal.testframework.IgniteAbstractTest;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.vault.VaultManager;
+import org.apache.ignite.internal.vault.inmemory.InMemoryVaultService;
+import org.apache.ignite.lang.ByteArray;
+import org.apache.ignite.lang.NodeStoppingException;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.DefaultMessagingService;
+import org.apache.ignite.network.NetworkAddress;
+import org.apache.ignite.network.StaticNodeFinder;
+import org.apache.ignite.utils.ClusterServiceTestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Tests for scenarios when Meta Storage nodes join and leave a cluster.
+ */
+@ExtendWith(ConfigurationExtension.class)
+public class ItMetaStorageMultipleNodesTest extends IgniteAbstractTest {
+    private static class Node {
+        private final VaultManager vaultManager;
+
+        private final ClusterService clusterService;
+
+        private final RaftManager raftManager;
+
+        private final ClusterStateStorage clusterStateStorage = new TestClusterStateStorage();
+
+        private final ClusterManagementGroupManager cmgManager;
+
+        private final MetaStorageManagerImpl metaStorageManager;
+
+        Node(ClusterService clusterService, RaftConfiguration raftConfiguration, Path dataPath) {
+            this.clusterService = clusterService;
+
+            this.vaultManager = new VaultManager(new InMemoryVaultService());
+
+            Path basePath = dataPath.resolve(name());
+
+            this.raftManager = new Loza(
+                    clusterService,
+                    raftConfiguration,
+                    basePath.resolve("raft"),
+                    new HybridClockImpl()
+            );
+
+            var logicalTopology = new LogicalTopologyImpl(clusterStateStorage);
+
+            this.cmgManager = new ClusterManagementGroupManager(
+                    vaultManager,
+                    clusterService,
+                    raftManager,
+                    clusterStateStorage,
+                    logicalTopology
+            );
+
+            this.metaStorageManager = new MetaStorageManagerImpl(
+                    vaultManager,
+                    clusterService,
+                    cmgManager,
+                    logicalTopology,
+                    raftManager,
+                    new SimpleInMemoryKeyValueStorage(name())
+            );
+        }
+
+        void start() throws NodeStoppingException {
+            List<IgniteComponent> components =
+                    List.of(vaultManager, clusterService, raftManager, clusterStateStorage, cmgManager, metaStorageManager);
+
+            components.forEach(IgniteComponent::start);
+
+            metaStorageManager.deployWatches();
+        }
+
+        String name() {
+            return clusterService.localConfiguration().getName();
+        }
+
+        void stop() throws Exception {
+            List<IgniteComponent> components =
+                    List.of(metaStorageManager, cmgManager, raftManager, clusterStateStorage, clusterService, vaultManager);
+
+            Stream<AutoCloseable> beforeNodeStop = components.stream().map(c -> c::beforeNodeStop);
+
+            Stream<AutoCloseable> nodeStop = components.stream().map(c -> c::stop);
+
+            IgniteUtils.closeAll(Stream.concat(beforeNodeStop, nodeStop));
+        }
+
+        CompletableFuture<Set<String>> getMetaStorageLearners() {
+            return metaStorageManager
+                    .metaStorageServiceFuture()
+                    .thenApply(MetaStorageServiceImpl::raftGroupService)
+                    .thenCompose(service -> service.refreshMembers(false).thenApply(v -> service.learners()))
+                    .thenApply(learners -> learners.stream().map(Peer::consistentId).collect(toSet()));
+        }
+
+        void startDroppingMessagesTo(Node recipient) {

Review Comment:
   It's better to rename the method, because it's only dropping ScaleCubeMessages. Something like startDroppingTopologyDiscoveryMessages will be less confusing from my point of view.



##########
modules/metastorage/src/integrationTest/java/org/apache/ignite/internal/metastorage/impl/ItMetaStorageMultipleNodesTest.java:
##########
@@ -0,0 +1,338 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.notExists;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.revision;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.noop;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.put;
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willBe;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
+import org.apache.ignite.internal.cluster.management.raft.ClusterStateStorage;
+import org.apache.ignite.internal.cluster.management.raft.TestClusterStateStorage;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopologyImpl;
+import org.apache.ignite.internal.configuration.testframework.ConfigurationExtension;
+import org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
+import org.apache.ignite.internal.hlc.HybridClockImpl;
+import org.apache.ignite.internal.manager.IgniteComponent;
+import org.apache.ignite.internal.metastorage.Entry;
+import org.apache.ignite.internal.metastorage.EntryEvent;
+import org.apache.ignite.internal.metastorage.WatchEvent;
+import org.apache.ignite.internal.metastorage.WatchListener;
+import org.apache.ignite.internal.metastorage.server.SimpleInMemoryKeyValueStorage;
+import org.apache.ignite.internal.network.message.ScaleCubeMessage;
+import org.apache.ignite.internal.raft.Loza;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftManager;
+import org.apache.ignite.internal.raft.configuration.RaftConfiguration;
+import org.apache.ignite.internal.testframework.IgniteAbstractTest;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.vault.VaultManager;
+import org.apache.ignite.internal.vault.inmemory.InMemoryVaultService;
+import org.apache.ignite.lang.ByteArray;
+import org.apache.ignite.lang.NodeStoppingException;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.DefaultMessagingService;
+import org.apache.ignite.network.NetworkAddress;
+import org.apache.ignite.network.StaticNodeFinder;
+import org.apache.ignite.utils.ClusterServiceTestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Tests for scenarios when Meta Storage nodes join and leave a cluster.
+ */
+@ExtendWith(ConfigurationExtension.class)
+public class ItMetaStorageMultipleNodesTest extends IgniteAbstractTest {
+    private static class Node {
+        private final VaultManager vaultManager;
+
+        private final ClusterService clusterService;
+
+        private final RaftManager raftManager;
+
+        private final ClusterStateStorage clusterStateStorage = new TestClusterStateStorage();
+
+        private final ClusterManagementGroupManager cmgManager;
+
+        private final MetaStorageManagerImpl metaStorageManager;
+
+        Node(ClusterService clusterService, RaftConfiguration raftConfiguration, Path dataPath) {
+            this.clusterService = clusterService;
+
+            this.vaultManager = new VaultManager(new InMemoryVaultService());
+
+            Path basePath = dataPath.resolve(name());
+
+            this.raftManager = new Loza(
+                    clusterService,
+                    raftConfiguration,
+                    basePath.resolve("raft"),
+                    new HybridClockImpl()
+            );
+
+            var logicalTopology = new LogicalTopologyImpl(clusterStateStorage);
+
+            this.cmgManager = new ClusterManagementGroupManager(
+                    vaultManager,
+                    clusterService,
+                    raftManager,
+                    clusterStateStorage,
+                    logicalTopology
+            );
+
+            this.metaStorageManager = new MetaStorageManagerImpl(
+                    vaultManager,
+                    clusterService,
+                    cmgManager,
+                    logicalTopology,
+                    raftManager,
+                    new SimpleInMemoryKeyValueStorage(name())
+            );
+        }
+
+        void start() throws NodeStoppingException {
+            List<IgniteComponent> components =
+                    List.of(vaultManager, clusterService, raftManager, clusterStateStorage, cmgManager, metaStorageManager);
+
+            components.forEach(IgniteComponent::start);
+
+            metaStorageManager.deployWatches();
+        }
+
+        String name() {
+            return clusterService.localConfiguration().getName();
+        }
+
+        void stop() throws Exception {
+            List<IgniteComponent> components =
+                    List.of(metaStorageManager, cmgManager, raftManager, clusterStateStorage, clusterService, vaultManager);
+
+            Stream<AutoCloseable> beforeNodeStop = components.stream().map(c -> c::beforeNodeStop);
+
+            Stream<AutoCloseable> nodeStop = components.stream().map(c -> c::stop);
+
+            IgniteUtils.closeAll(Stream.concat(beforeNodeStop, nodeStop));
+        }
+
+        CompletableFuture<Set<String>> getMetaStorageLearners() {
+            return metaStorageManager
+                    .metaStorageServiceFuture()
+                    .thenApply(MetaStorageServiceImpl::raftGroupService)
+                    .thenCompose(service -> service.refreshMembers(false).thenApply(v -> service.learners()))
+                    .thenApply(learners -> learners.stream().map(Peer::consistentId).collect(toSet()));
+        }
+
+        void startDroppingMessagesTo(Node recipient) {
+            ((DefaultMessagingService) clusterService.messagingService())
+                    .dropMessages((recipientConsistentId, message) ->
+                            recipient.name().equals(recipientConsistentId) && message instanceof ScaleCubeMessage);
+        }
+
+        void stopDroppingMessages() {
+            ((DefaultMessagingService) clusterService.messagingService()).stopDroppingMessages();
+        }
+    }
+
+    private final List<Node> nodes = new ArrayList<>();
+
+    @InjectConfiguration
+    private RaftConfiguration raftConfiguration;
+
+    private Node startNode(TestInfo testInfo) throws NodeStoppingException {
+        var nodeFinder = new StaticNodeFinder(List.of(new NetworkAddress("localhost", 10_000)));
+
+        ClusterService clusterService = ClusterServiceTestUtils.clusterService(testInfo, 10_000 + nodes.size(), nodeFinder);
+
+        var node = new Node(clusterService, raftConfiguration, workDir);
+
+        node.start();
+
+        nodes.add(node);
+
+        return node;
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        IgniteUtils.closeAll(nodes.stream().map(node -> node::stop));
+    }
+
+    /**
+     * Tests that an incoming node gets registered as a Learner and receives Meta Storage updates.
+     */
+    @Test
+    void testLearnerJoin(TestInfo testInfo) throws NodeStoppingException {
+        Node firstNode = startNode(testInfo);
+
+        firstNode.cmgManager.initCluster(List.of(firstNode.name()), List.of(firstNode.name()), "test");
+
+        var key = new ByteArray("foo");
+        byte[] value = "bar".getBytes(StandardCharsets.UTF_8);
+
+        CompletableFuture<Boolean> invokeFuture = firstNode.metaStorageManager.invoke(notExists(key), put(key, value), noop());
+
+        assertThat(invokeFuture, willBe(true));
+
+        Node secondNode = startNode(testInfo);
+
+        // Check that reading remote data works correctly.
+        assertThat(secondNode.metaStorageManager.get(key).thenApply(Entry::value), willBe(value));
+
+        // Check that the new node will receive events.
+        var awaitFuture = new CompletableFuture<EntryEvent>();
+
+        secondNode.metaStorageManager.registerExactWatch(key, new WatchListener() {
+            @Override
+            public void onUpdate(WatchEvent event) {
+                // Skip the first update event, because it's not guaranteed to arrive here (insert may have happened before the watch was
+                // registered).
+                if (event.revision() != 1) {
+                    awaitFuture.complete(event.entryEvent());
+                }
+            }
+
+            @Override
+            public void onError(Throwable e) {
+                awaitFuture.completeExceptionally(e);
+            }
+        });
+
+        byte[] newValue = "baz".getBytes(StandardCharsets.UTF_8);
+
+        invokeFuture = firstNode.metaStorageManager.invoke(revision(key).eq(1), put(key, newValue), noop());
+
+        assertThat(invokeFuture, willBe(true));
+
+        var expectedEntryEvent = new EntryEvent(
+                new EntryImpl(key.bytes(), value, 1, 1),
+                new EntryImpl(key.bytes(), newValue, 2, 2)
+        );
+
+        assertThat(awaitFuture, willBe(expectedEntryEvent));
+
+        // Check that the second node has been registered as a learner.
+        assertThat(firstNode.getMetaStorageLearners(), willBe(Set.of(secondNode.name())));
+    }
+
+    /**
+     * Tests a case when a node leaves the physical topology without entering the logical topology.
+     */
+    @Test
+    void testLearnerLeavePhysicalTopology(TestInfo testInfo) throws Exception {
+        Node firstNode = startNode(testInfo);
+        Node secondNode = startNode(testInfo);
+
+        firstNode.cmgManager.initCluster(List.of(firstNode.name()), List.of(firstNode.name()), "test");
+
+        // Try reading some data to make sure that Raft has been configured correctly.
+        assertThat(secondNode.metaStorageManager.get(new ByteArray("test")).thenApply(Entry::value), willBe(nullValue()));
+
+        // Check that the second node has been registered as a learner.
+        assertThat(firstNode.getMetaStorageLearners(), willBe(Set.of(secondNode.name())));
+
+        // Stop the second node.
+        secondNode.stop();
+
+        nodes.remove(1);
+
+        assertTrue(waitForCondition(() -> firstNode.getMetaStorageLearners().join().isEmpty(), 10_000));
+    }
+
+    /**
+     * Tests a case when a node leaves the physical topology without entering the logical topology.
+     */
+    @Test
+    void testLearnerLeaveLogicalTopology(TestInfo testInfo) throws Exception {
+        Node firstNode = startNode(testInfo);
+        Node secondNode = startNode(testInfo);
+
+        firstNode.cmgManager.initCluster(List.of(firstNode.name()), List.of(firstNode.name()), "test");
+
+        assertThat(allOf(firstNode.cmgManager.onJoinReady(), secondNode.cmgManager.onJoinReady()), willCompleteSuccessfully());
+
+        CompletableFuture<Set<String>> logicalTopologyNodes = firstNode.cmgManager
+                .logicalTopology()
+                .thenApply(logicalTopology -> logicalTopology.nodes().stream().map(ClusterNode::name).collect(toSet()));
+
+        assertThat(logicalTopologyNodes, willBe(Set.of(firstNode.name(), secondNode.name())));
+
+        // Try reading some data to make sure that Raft has been configured correctly.
+        assertThat(secondNode.metaStorageManager.get(new ByteArray("test")).thenApply(Entry::value), willBe(nullValue()));
+
+        // Check that the second node has been registered as a learner.
+        assertThat(firstNode.getMetaStorageLearners(), willBe(Set.of(secondNode.name())));
+
+        // Stop the second node.
+        secondNode.stop();
+
+        nodes.remove(1);
+
+        assertTrue(waitForCondition(() -> firstNode.getMetaStorageLearners().join().isEmpty(), 10_000));
+    }
+
+    /**
+     * Tests a scenario when a node gets kicked out of the Logical Topology due to a network partition. It should then be able to join
+     * the Meta Storage Raft group successfully.
+     */
+    @Test
+    void testLearnerLeaveAndJoinBecauseOfNetworkPartition(TestInfo testInfo) throws Exception {
+        Node firstNode = startNode(testInfo);
+        Node secondNode = startNode(testInfo);
+
+        firstNode.cmgManager.initCluster(List.of(firstNode.name()), List.of(firstNode.name()), "test");
+
+        assertThat(allOf(firstNode.cmgManager.onJoinReady(), secondNode.cmgManager.onJoinReady()), willCompleteSuccessfully());
+
+        CompletableFuture<Set<String>> logicalTopologyNodes = firstNode.cmgManager
+                .logicalTopology()
+                .thenApply(logicalTopology -> logicalTopology.nodes().stream().map(ClusterNode::name).collect(toSet()));
+
+        assertThat(logicalTopologyNodes, willBe(Set.of(firstNode.name(), secondNode.name())));
+
+        assertThat(firstNode.getMetaStorageLearners(), willBe(Set.of(secondNode.name())));
+
+        // Make first node lose the second node from the Physical and Logical topologies.
+        firstNode.startDroppingMessagesTo(secondNode);
+
+        assertTrue(waitForCondition(() -> firstNode.getMetaStorageLearners().join().isEmpty(), 10_000));
+
+        // Make the first node discover the second node again. The second node should be added as a Meta Storage Learner again.
+        firstNode.stopDroppingMessages();
+
+        assertTrue(waitForCondition(() -> firstNode.getMetaStorageLearners().join().equals(Set.of(secondNode.name())), 10_000));
+    }
+}

Review Comment:
   Is there a test that will check that node start will fail with corresponding public exception if we failed to add self as a learner?



##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));

Review Comment:
   Are there any order guarantees between messaging and network topology events?
   In other words is it possible that we will have a race between processing nodeJoinRequest and handling onDissapeared, meaning the race between adding and removing the learner in this particular case?



##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+            }
+        });
+
+        logicalTopology.addEventListener(new LogicalTopologyEventListener() {
+            // Try to add a node as a Learner when it appears in the Logical Topology. Even though usually remote nodes add themselves as

Review Comment:
   Could you please elaborate the flow in a greater detail? I didn't get the case.



##########
modules/metastorage/src/integrationTest/java/org/apache/ignite/internal/metastorage/impl/ItMetaStorageMultipleNodesTest.java:
##########
@@ -0,0 +1,338 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.notExists;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.revision;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.noop;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.put;
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willBe;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
+import org.apache.ignite.internal.cluster.management.raft.ClusterStateStorage;
+import org.apache.ignite.internal.cluster.management.raft.TestClusterStateStorage;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopologyImpl;
+import org.apache.ignite.internal.configuration.testframework.ConfigurationExtension;
+import org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
+import org.apache.ignite.internal.hlc.HybridClockImpl;
+import org.apache.ignite.internal.manager.IgniteComponent;
+import org.apache.ignite.internal.metastorage.Entry;
+import org.apache.ignite.internal.metastorage.EntryEvent;
+import org.apache.ignite.internal.metastorage.WatchEvent;
+import org.apache.ignite.internal.metastorage.WatchListener;
+import org.apache.ignite.internal.metastorage.server.SimpleInMemoryKeyValueStorage;
+import org.apache.ignite.internal.network.message.ScaleCubeMessage;
+import org.apache.ignite.internal.raft.Loza;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftManager;
+import org.apache.ignite.internal.raft.configuration.RaftConfiguration;
+import org.apache.ignite.internal.testframework.IgniteAbstractTest;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.vault.VaultManager;
+import org.apache.ignite.internal.vault.inmemory.InMemoryVaultService;
+import org.apache.ignite.lang.ByteArray;
+import org.apache.ignite.lang.NodeStoppingException;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.DefaultMessagingService;
+import org.apache.ignite.network.NetworkAddress;
+import org.apache.ignite.network.StaticNodeFinder;
+import org.apache.ignite.utils.ClusterServiceTestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Tests for scenarios when Meta Storage nodes join and leave a cluster.
+ */
+@ExtendWith(ConfigurationExtension.class)
+public class ItMetaStorageMultipleNodesTest extends IgniteAbstractTest {
+    private static class Node {
+        private final VaultManager vaultManager;
+
+        private final ClusterService clusterService;
+
+        private final RaftManager raftManager;
+
+        private final ClusterStateStorage clusterStateStorage = new TestClusterStateStorage();
+
+        private final ClusterManagementGroupManager cmgManager;
+
+        private final MetaStorageManagerImpl metaStorageManager;
+
+        Node(ClusterService clusterService, RaftConfiguration raftConfiguration, Path dataPath) {
+            this.clusterService = clusterService;
+
+            this.vaultManager = new VaultManager(new InMemoryVaultService());
+
+            Path basePath = dataPath.resolve(name());
+
+            this.raftManager = new Loza(
+                    clusterService,
+                    raftConfiguration,
+                    basePath.resolve("raft"),
+                    new HybridClockImpl()
+            );
+
+            var logicalTopology = new LogicalTopologyImpl(clusterStateStorage);
+
+            this.cmgManager = new ClusterManagementGroupManager(
+                    vaultManager,
+                    clusterService,
+                    raftManager,
+                    clusterStateStorage,
+                    logicalTopology
+            );
+
+            this.metaStorageManager = new MetaStorageManagerImpl(
+                    vaultManager,
+                    clusterService,
+                    cmgManager,
+                    logicalTopology,
+                    raftManager,
+                    new SimpleInMemoryKeyValueStorage(name())
+            );
+        }
+
+        void start() throws NodeStoppingException {
+            List<IgniteComponent> components =
+                    List.of(vaultManager, clusterService, raftManager, clusterStateStorage, cmgManager, metaStorageManager);
+
+            components.forEach(IgniteComponent::start);
+
+            metaStorageManager.deployWatches();
+        }
+
+        String name() {
+            return clusterService.localConfiguration().getName();
+        }
+
+        void stop() throws Exception {
+            List<IgniteComponent> components =
+                    List.of(metaStorageManager, cmgManager, raftManager, clusterStateStorage, clusterService, vaultManager);
+
+            Stream<AutoCloseable> beforeNodeStop = components.stream().map(c -> c::beforeNodeStop);
+
+            Stream<AutoCloseable> nodeStop = components.stream().map(c -> c::stop);
+
+            IgniteUtils.closeAll(Stream.concat(beforeNodeStop, nodeStop));
+        }
+
+        CompletableFuture<Set<String>> getMetaStorageLearners() {
+            return metaStorageManager
+                    .metaStorageServiceFuture()
+                    .thenApply(MetaStorageServiceImpl::raftGroupService)
+                    .thenCompose(service -> service.refreshMembers(false).thenApply(v -> service.learners()))
+                    .thenApply(learners -> learners.stream().map(Peer::consistentId).collect(toSet()));
+        }
+
+        void startDroppingMessagesTo(Node recipient) {
+            ((DefaultMessagingService) clusterService.messagingService())
+                    .dropMessages((recipientConsistentId, message) ->
+                            recipient.name().equals(recipientConsistentId) && message instanceof ScaleCubeMessage);
+        }
+
+        void stopDroppingMessages() {
+            ((DefaultMessagingService) clusterService.messagingService()).stopDroppingMessages();
+        }
+    }
+
+    private final List<Node> nodes = new ArrayList<>();
+
+    @InjectConfiguration
+    private RaftConfiguration raftConfiguration;
+
+    private Node startNode(TestInfo testInfo) throws NodeStoppingException {
+        var nodeFinder = new StaticNodeFinder(List.of(new NetworkAddress("localhost", 10_000)));
+
+        ClusterService clusterService = ClusterServiceTestUtils.clusterService(testInfo, 10_000 + nodes.size(), nodeFinder);
+
+        var node = new Node(clusterService, raftConfiguration, workDir);
+
+        node.start();
+
+        nodes.add(node);
+
+        return node;
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        IgniteUtils.closeAll(nodes.stream().map(node -> node::stop));
+    }
+
+    /**
+     * Tests that an incoming node gets registered as a Learner and receives Meta Storage updates.
+     */
+    @Test
+    void testLearnerJoin(TestInfo testInfo) throws NodeStoppingException {
+        Node firstNode = startNode(testInfo);
+
+        firstNode.cmgManager.initCluster(List.of(firstNode.name()), List.of(firstNode.name()), "test");
+
+        var key = new ByteArray("foo");
+        byte[] value = "bar".getBytes(StandardCharsets.UTF_8);
+
+        CompletableFuture<Boolean> invokeFuture = firstNode.metaStorageManager.invoke(notExists(key), put(key, value), noop());
+
+        assertThat(invokeFuture, willBe(true));
+
+        Node secondNode = startNode(testInfo);
+
+        // Check that reading remote data works correctly.
+        assertThat(secondNode.metaStorageManager.get(key).thenApply(Entry::value), willBe(value));
+
+        // Check that the new node will receive events.
+        var awaitFuture = new CompletableFuture<EntryEvent>();
+
+        secondNode.metaStorageManager.registerExactWatch(key, new WatchListener() {
+            @Override
+            public void onUpdate(WatchEvent event) {
+                // Skip the first update event, because it's not guaranteed to arrive here (insert may have happened before the watch was
+                // registered).
+                if (event.revision() != 1) {
+                    awaitFuture.complete(event.entryEvent());
+                }
+            }
+
+            @Override
+            public void onError(Throwable e) {
+                awaitFuture.completeExceptionally(e);
+            }
+        });
+
+        byte[] newValue = "baz".getBytes(StandardCharsets.UTF_8);
+
+        invokeFuture = firstNode.metaStorageManager.invoke(revision(key).eq(1), put(key, newValue), noop());
+
+        assertThat(invokeFuture, willBe(true));
+
+        var expectedEntryEvent = new EntryEvent(
+                new EntryImpl(key.bytes(), value, 1, 1),
+                new EntryImpl(key.bytes(), newValue, 2, 2)
+        );
+
+        assertThat(awaitFuture, willBe(expectedEntryEvent));
+
+        // Check that the second node has been registered as a learner.
+        assertThat(firstNode.getMetaStorageLearners(), willBe(Set.of(secondNode.name())));
+    }
+
+    /**
+     * Tests a case when a node leaves the physical topology without entering the logical topology.
+     */
+    @Test
+    void testLearnerLeavePhysicalTopology(TestInfo testInfo) throws Exception {

Review Comment:
   How do you guarantee  "...without entering the logical topology" logic?



##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {

Review Comment:
   All that operations should be idempotent, meaning that it's possible that after leader re-election we will try to call them one more time, e.g. remove already removed learner. Will it work properly?



##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+            }
+        });
+
+        logicalTopology.addEventListener(new LogicalTopologyEventListener() {
+            // Try to add a node as a Learner when it appears in the Logical Topology. Even though usually remote nodes add themselves as
+            // learners, this call allows us to handle the case when a network partition occurs and a node gets kicked out of the Logical
+            // Topology. Since the remote node simply gets disconnected and not restarted, it should be able to re-join the cluster even
+            // though it will skip the step when it adds itself as a Learner.
+            @Override
+            public void onAppeared(ClusterNode appearedNode, LogicalTopologySnapshot newTopology) {
+                executeIfLeader(service -> addLearner(service.raftGroupService(), appearedNode));
+            }
+
+            // Try to remove a Meta Storage Learner when a node leaves the Logical Topology. This approach protects from network glitches
+            // when a node leaves and re-joins the Physical Topology.
+            @Override
+            public void onDisappeared(ClusterNode disappearedNode, LogicalTopologySnapshot newTopology) {

Review Comment:
   Seems that we don't have happens before guarantees here. In other words it's possible to handle logical.onDisappeared(A) while processing physical.onAppeared(A) just because one is about in-raft CMG thread and another is network topology internals.



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sashapolo commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sashapolo (via GitHub)" <gi...@apache.org>.
sashapolo commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1091560147


##########
modules/metastorage/src/integrationTest/java/org/apache/ignite/internal/metastorage/impl/ItMetaStorageMultipleNodesTest.java:
##########
@@ -0,0 +1,338 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.stream.Collectors.toSet;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.notExists;
+import static org.apache.ignite.internal.metastorage.dsl.Conditions.revision;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.noop;
+import static org.apache.ignite.internal.metastorage.dsl.Operations.put;
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willBe;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
+import org.apache.ignite.internal.cluster.management.raft.ClusterStateStorage;
+import org.apache.ignite.internal.cluster.management.raft.TestClusterStateStorage;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopologyImpl;
+import org.apache.ignite.internal.configuration.testframework.ConfigurationExtension;
+import org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
+import org.apache.ignite.internal.hlc.HybridClockImpl;
+import org.apache.ignite.internal.manager.IgniteComponent;
+import org.apache.ignite.internal.metastorage.Entry;
+import org.apache.ignite.internal.metastorage.EntryEvent;
+import org.apache.ignite.internal.metastorage.WatchEvent;
+import org.apache.ignite.internal.metastorage.WatchListener;
+import org.apache.ignite.internal.metastorage.server.SimpleInMemoryKeyValueStorage;
+import org.apache.ignite.internal.network.message.ScaleCubeMessage;
+import org.apache.ignite.internal.raft.Loza;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftManager;
+import org.apache.ignite.internal.raft.configuration.RaftConfiguration;
+import org.apache.ignite.internal.testframework.IgniteAbstractTest;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.vault.VaultManager;
+import org.apache.ignite.internal.vault.inmemory.InMemoryVaultService;
+import org.apache.ignite.lang.ByteArray;
+import org.apache.ignite.lang.NodeStoppingException;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.DefaultMessagingService;
+import org.apache.ignite.network.NetworkAddress;
+import org.apache.ignite.network.StaticNodeFinder;
+import org.apache.ignite.utils.ClusterServiceTestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Tests for scenarios when Meta Storage nodes join and leave a cluster.
+ */
+@ExtendWith(ConfigurationExtension.class)
+public class ItMetaStorageMultipleNodesTest extends IgniteAbstractTest {
+    private static class Node {
+        private final VaultManager vaultManager;
+
+        private final ClusterService clusterService;
+
+        private final RaftManager raftManager;
+
+        private final ClusterStateStorage clusterStateStorage = new TestClusterStateStorage();
+
+        private final ClusterManagementGroupManager cmgManager;
+
+        private final MetaStorageManagerImpl metaStorageManager;
+
+        Node(ClusterService clusterService, RaftConfiguration raftConfiguration, Path dataPath) {
+            this.clusterService = clusterService;
+
+            this.vaultManager = new VaultManager(new InMemoryVaultService());
+
+            Path basePath = dataPath.resolve(name());
+
+            this.raftManager = new Loza(
+                    clusterService,
+                    raftConfiguration,
+                    basePath.resolve("raft"),
+                    new HybridClockImpl()
+            );
+
+            var logicalTopology = new LogicalTopologyImpl(clusterStateStorage);
+
+            this.cmgManager = new ClusterManagementGroupManager(
+                    vaultManager,
+                    clusterService,
+                    raftManager,
+                    clusterStateStorage,
+                    logicalTopology
+            );
+
+            this.metaStorageManager = new MetaStorageManagerImpl(
+                    vaultManager,
+                    clusterService,
+                    cmgManager,
+                    logicalTopology,
+                    raftManager,
+                    new SimpleInMemoryKeyValueStorage(name())
+            );
+        }
+
+        void start() throws NodeStoppingException {
+            List<IgniteComponent> components =
+                    List.of(vaultManager, clusterService, raftManager, clusterStateStorage, cmgManager, metaStorageManager);
+
+            components.forEach(IgniteComponent::start);
+
+            metaStorageManager.deployWatches();
+        }
+
+        String name() {
+            return clusterService.localConfiguration().getName();
+        }
+
+        void stop() throws Exception {
+            List<IgniteComponent> components =
+                    List.of(metaStorageManager, cmgManager, raftManager, clusterStateStorage, clusterService, vaultManager);
+
+            Stream<AutoCloseable> beforeNodeStop = components.stream().map(c -> c::beforeNodeStop);
+
+            Stream<AutoCloseable> nodeStop = components.stream().map(c -> c::stop);
+
+            IgniteUtils.closeAll(Stream.concat(beforeNodeStop, nodeStop));
+        }
+
+        CompletableFuture<Set<String>> getMetaStorageLearners() {
+            return metaStorageManager
+                    .metaStorageServiceFuture()
+                    .thenApply(MetaStorageServiceImpl::raftGroupService)
+                    .thenCompose(service -> service.refreshMembers(false).thenApply(v -> service.learners()))
+                    .thenApply(learners -> learners.stream().map(Peer::consistentId).collect(toSet()));
+        }
+
+        void startDroppingMessagesTo(Node recipient) {
+            ((DefaultMessagingService) clusterService.messagingService())
+                    .dropMessages((recipientConsistentId, message) ->
+                            recipient.name().equals(recipientConsistentId) && message instanceof ScaleCubeMessage);
+        }
+
+        void stopDroppingMessages() {
+            ((DefaultMessagingService) clusterService.messagingService()).stopDroppingMessages();
+        }
+    }
+
+    private final List<Node> nodes = new ArrayList<>();
+
+    @InjectConfiguration
+    private RaftConfiguration raftConfiguration;
+
+    private Node startNode(TestInfo testInfo) throws NodeStoppingException {
+        var nodeFinder = new StaticNodeFinder(List.of(new NetworkAddress("localhost", 10_000)));
+
+        ClusterService clusterService = ClusterServiceTestUtils.clusterService(testInfo, 10_000 + nodes.size(), nodeFinder);
+
+        var node = new Node(clusterService, raftConfiguration, workDir);
+
+        node.start();
+
+        nodes.add(node);
+
+        return node;
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        IgniteUtils.closeAll(nodes.stream().map(node -> node::stop));
+    }
+
+    /**
+     * Tests that an incoming node gets registered as a Learner and receives Meta Storage updates.
+     */
+    @Test
+    void testLearnerJoin(TestInfo testInfo) throws NodeStoppingException {
+        Node firstNode = startNode(testInfo);
+
+        firstNode.cmgManager.initCluster(List.of(firstNode.name()), List.of(firstNode.name()), "test");
+
+        var key = new ByteArray("foo");
+        byte[] value = "bar".getBytes(StandardCharsets.UTF_8);
+
+        CompletableFuture<Boolean> invokeFuture = firstNode.metaStorageManager.invoke(notExists(key), put(key, value), noop());
+
+        assertThat(invokeFuture, willBe(true));
+
+        Node secondNode = startNode(testInfo);
+
+        // Check that reading remote data works correctly.
+        assertThat(secondNode.metaStorageManager.get(key).thenApply(Entry::value), willBe(value));
+
+        // Check that the new node will receive events.
+        var awaitFuture = new CompletableFuture<EntryEvent>();
+
+        secondNode.metaStorageManager.registerExactWatch(key, new WatchListener() {
+            @Override
+            public void onUpdate(WatchEvent event) {
+                // Skip the first update event, because it's not guaranteed to arrive here (insert may have happened before the watch was
+                // registered).
+                if (event.revision() != 1) {
+                    awaitFuture.complete(event.entryEvent());
+                }
+            }
+
+            @Override
+            public void onError(Throwable e) {
+                awaitFuture.completeExceptionally(e);
+            }
+        });
+
+        byte[] newValue = "baz".getBytes(StandardCharsets.UTF_8);
+
+        invokeFuture = firstNode.metaStorageManager.invoke(revision(key).eq(1), put(key, newValue), noop());
+
+        assertThat(invokeFuture, willBe(true));
+
+        var expectedEntryEvent = new EntryEvent(
+                new EntryImpl(key.bytes(), value, 1, 1),
+                new EntryImpl(key.bytes(), newValue, 2, 2)
+        );
+
+        assertThat(awaitFuture, willBe(expectedEntryEvent));
+
+        // Check that the second node has been registered as a learner.
+        assertThat(firstNode.getMetaStorageLearners(), willBe(Set.of(secondNode.name())));
+    }
+
+    /**
+     * Tests a case when a node leaves the physical topology without entering the logical topology.
+     */
+    @Test
+    void testLearnerLeavePhysicalTopology(TestInfo testInfo) throws Exception {

Review Comment:
   I do not invoke `cmgManager.onJoinReady()`



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [ignite-3] sanpwc commented on a diff in pull request #1542: IGNITE-18554 Remove MetaStorage learners on topology events

Posted by "sanpwc (via GitHub)" <gi...@apache.org>.
sanpwc commented on code in PR #1542:
URL: https://github.com/apache/ignite-3/pull/1542#discussion_r1091954071


##########
modules/metastorage/src/main/java/org/apache/ignite/internal/metastorage/impl/MetaStorageRaftGroupEventsListener.java:
##########
@@ -0,0 +1,247 @@
+/*
+ * 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.metastorage.impl;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.apache.ignite.internal.cluster.management.topology.LogicalTopology;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologyEventListener;
+import org.apache.ignite.internal.cluster.management.topology.api.LogicalTopologySnapshot;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.raft.Peer;
+import org.apache.ignite.internal.raft.RaftGroupEventsListener;
+import org.apache.ignite.internal.raft.service.RaftGroupService;
+import org.apache.ignite.internal.util.IgniteSpinBusyLock;
+import org.apache.ignite.network.ClusterNode;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyEventHandler;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Raft Group Events listener that registers Logical Topology listeners for removing learners that have left the topology.
+ */
+public class MetaStorageRaftGroupEventsListener implements RaftGroupEventsListener {
+    private static final IgniteLogger LOG = Loggers.forClass(MetaStorageManagerImpl.class);
+
+    private final IgniteSpinBusyLock busyLock;
+
+    private final TopologyService topologyService;
+
+    private final LogicalTopology logicalTopology;
+
+    private final CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut;
+
+    /**
+     * Future required to enable serialized processing of topology events.
+     *
+     * <p>While Logical Topology events are linearized, we also concurrently react to Physical Topology changes and/or update Raft
+     * configuration after a new leader has been elected.
+     *
+     * <p>Multi-threaded access is guarded by {@code serializationFutureMux}.
+     */
+    private CompletableFuture<Void> serializationFuture = null;
+
+    private final Object serializationFutureMux = new Object();
+
+    /**
+     * The most recent version of the Logical Topology that this listener has observed.
+     */
+    private volatile long lastProcessedLogicalTopologyVersion = -1;
+
+    MetaStorageRaftGroupEventsListener(
+            IgniteSpinBusyLock busyLock,
+            ClusterService clusterService,
+            LogicalTopology logicalTopology,
+            CompletableFuture<MetaStorageServiceImpl> metaStorageSvcFut
+    ) {
+        this.busyLock = busyLock;
+        this.topologyService = clusterService.topologyService();
+        this.logicalTopology = logicalTopology;
+        this.metaStorageSvcFut = metaStorageSvcFut;
+    }
+
+    @Override
+    public void onLeaderElected(long term) {
+        registerTopologyEventListeners();
+
+        // Update learner configuration in case we missed some topology updates.
+        executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+    }
+
+    private void registerTopologyEventListeners() {
+        // Try to remove a Meta Storage Learner if a node leaves the Physical Topology. This handles the case when a node joins, but leaves
+        // before sending the Join Ready request (i.e. before it gets added to the Logical Topology).
+        topologyService.addEventHandler(new TopologyEventHandler() {
+            @Override
+            public void onDisappeared(ClusterNode member) {
+                executeIfLeader(service -> removeLearners(service.raftGroupService(), logicalTopology.getLogicalTopology()));
+            }
+        });
+
+        logicalTopology.addEventListener(new LogicalTopologyEventListener() {
+            // Try to add a node as a Learner when it appears in the Logical Topology. Even though usually remote nodes add themselves as

Review Comment:
   Got it. I would expect that joining process should be aborted if joining node was removed from physical topology and restarted from the very beginning if needed. Otherwise we may have inconsistencies in node id, etc. 



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

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org