Alexey Kukushkin created IGNITE-28874:
-----------------------------------------

             Summary: Ignite service is never redeployed when its node fails
                 Key: IGNITE-28874
                 URL: https://issues.apache.org/jira/browse/IGNITE-28874
             Project: Ignite
          Issue Type: Bug
    Affects Versions: 2.18
            Reporter: Alexey Kukushkin


It is expected that an Ignite service is always redeployed on another available 
node after its node fails. However, it does not happen if the next node that 
Ignite chooses for the service also fails during the service redeployment. See 
the reproducer below.

{code:java}
package sandbox.ignite;

import org.apache.ignite.Ignite;
import org.apache.ignite.Ignition;
import org.apache.ignite.cluster.ClusterNode;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.failure.NoOpFailureHandler;
import org.apache.ignite.lang.IgnitePredicate;
import org.apache.ignite.services.Service;
import org.apache.ignite.services.ServiceConfiguration;
import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
import org.junit.jupiter.api.RepeatedTest;

import java.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class CfAll44439Test {
    private final static int FAILURE_DETECTION_TIMEOUT = 2_000;

    /**
     * EXPECTED: An Ignite service is always redeployed on the next available 
node after its node fails.
     * ACTUAL: The service is never redeployed if the next available node 
chosen for it fails during
     * the service redeployment.
     */
    @RepeatedTest(10)
    public void serviceIsRedeployedAfterItsNodeFails() throws 
InterruptedException {
        // GIVEN an Ignite cluster consisting of 1 server and 3 client nodes 
with a 2-second failure detection timeout
        var aliveClients = new HashSet<>(Set.of("client1", "client2", 
"client3"));
        try (final var server1 = 
Ignition.start(getIgniteConfiguration("server1", false))) {
            for (final var clientName : aliveClients) {
                Ignition.start(getIgniteConfiguration(clientName, true));
            }

            // AND an Ignite service named "SingletonService" is deployed on 
one of the client nodes as a Cluster
            // Singleton. The "SingletonService" is initially deployed without 
any delays. However, redeploying
            // the service takes 4 times the failure detection timeout (8 
seconds).
            final var service = new SingletonService(Duration.ofMillis(4 * 
FAILURE_DETECTION_TIMEOUT));
            final var serviceName = SingletonService.class.getName();
            final var serviceConfig = new ServiceConfiguration()
                .setService(service)
                .setName(serviceName)
                .setNodeFilter(new AllClientsPredicate())
                .setMaxPerNodeCount(0)
                .setTotalCount(1);
            server1.services().deploy(serviceConfig);

            final var initialServiceNode = getServiceNodeConsistentId(server1, 
serviceName);
            assertTrue(
                initialServiceNode.isPresent(),
                "Initial setup: all clients are alive, the service is on " + 
initialServiceNode);

            // AND the node hosting the "SingletonService" stops
            Ignition.stop(initialServiceNode.orElseThrow(), true);
            aliveClients.remove(initialServiceNode.orElseThrow());

            var nextServiceNode = getServiceNodeConsistentId(server1, 
serviceName);
            assertTrue(
                nextServiceNode.isEmpty(),
                "The service is not deployed immediately after its node is 
killed since redeployment takes " +
                    "(4 x timeout)");

            // AND 4 seconds passes
            Thread.sleep(2 * FAILURE_DETECTION_TIMEOUT);

            nextServiceNode = getServiceNodeConsistentId(server1, serviceName);
            assertTrue(
                nextServiceNode.isEmpty(),
                "The service is still not deployed after (2 x timeout) since 
redeployment takes (4 x timeout)");

            // AND the next node Ignite scheduled to deploy the service on stops
            // NOTE: at this moment the service is still not redeployed, but 
Ignite has already chosen the next
            // node for deployment. There is no API to query that node, so we 
pick one of the two remaining
            // client nodes at random. This means there is a 50% chance of 
picking the wrong node, and thus
            // the test reproduces the problem with 50% probability. The test 
is repeated to increase that probability.
            var nextKilledClient = aliveClients.iterator().next();
            Ignition.stop(nextKilledClient, true);
            aliveClients.remove(nextKilledClient);

            nextServiceNode = getServiceNodeConsistentId(server1, serviceName);
            assertTrue(
                nextServiceNode.isEmpty(),
                "The service is still not deployed immediately after another 
node is killed");

            // WHEN another 12 seconds pass, making the total time 16 seconds, 
which is twice the
            // service's 8-second redeployment time.
            Thread.sleep(6 * FAILURE_DETECTION_TIMEOUT);

            // THEN the service is redeployed on the remaining client node
            nextServiceNode = getServiceNodeConsistentId(server1, serviceName);
            final var remainingClient = aliveClients.iterator().next();
            assertTrue(
                nextServiceNode.isPresent() && 
nextServiceNode.orElseThrow().equals(remainingClient),
                "The service must be deployed on the remaining " + 
remainingClient + " after (8 x timeout) since " +
                    "the elapsed time significantly exceeds the service's (4 x 
timeout) redeployment time");

            Ignition.stop(remainingClient, true);
        }
    }

    private static Optional<String> getServiceNodeConsistentId(Ignite ignite, 
String serviceName) {
        return ignite.services().serviceDescriptors().stream()
            .filter(s -> s.name().equals(serviceName))
            .findFirst()
            .flatMap(descriptor -> descriptor
                .topologySnapshot()
                .keySet()
                .stream()
                .findFirst()
                .flatMap(uuid -> 
Optional.ofNullable(ignite.cluster().node(uuid)))
                .map(node -> node.consistentId().toString()));
    }

    private static IgniteConfiguration getIgniteConfiguration(final String 
igniteName, final boolean isClient) {
        return new IgniteConfiguration()
            .setClientMode(isClient)
            .setIgniteInstanceName(igniteName)
            .setConsistentId(igniteName)
            .setFailureDetectionTimeout(FAILURE_DETECTION_TIMEOUT)
            .setClientFailureDetectionTimeout(FAILURE_DETECTION_TIMEOUT)
            .setFailureHandler(new NoOpFailureHandler())
            .setDiscoverySpi(
                new TcpDiscoverySpi()
                    .setIpFinder(new 
TcpDiscoveryVmIpFinder().setAddresses(Collections.singleton("127.0.0.1:49500")))
                    .setLocalPort(49500)
            );
    }

    private static class SingletonService implements Service {
        private static boolean initiallyDeployed = false;
        private final Duration redeployDuration;

        private SingletonService(final Duration redeployDuration) {
            this.redeployDuration = redeployDuration;
        }

        @Override
        public void init() {
            try {
                if (initiallyDeployed) {
                    Thread.sleep(redeployDuration);
                } else {
                    initiallyDeployed = true;
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private static class AllClientsPredicate implements 
IgnitePredicate<ClusterNode> {
        @Override
        public boolean apply(final ClusterNode clusterNode) {
            return clusterNode.isClient();
        }
    }
}
{code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to