Add integration test with cluster-singleton
[controller.git] / opendaylight / md-sal / sal-distributed-eos-native / src / test / java / org / opendaylight / controller / cluster / akka / eos / service / ClusterSingletonIntegrationTest.java
diff --git a/opendaylight/md-sal/sal-distributed-eos-native/src/test/java/org/opendaylight/controller/cluster/akka/eos/service/ClusterSingletonIntegrationTest.java b/opendaylight/md-sal/sal-distributed-eos-native/src/test/java/org/opendaylight/controller/cluster/akka/eos/service/ClusterSingletonIntegrationTest.java
new file mode 100644 (file)
index 0000000..bef4304
--- /dev/null
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.cluster.akka.eos.service;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import akka.actor.testkit.typed.javadsl.ActorTestKit;
+import akka.actor.typed.javadsl.Adapter;
+import akka.cluster.Member;
+import akka.cluster.MemberStatus;
+import akka.cluster.typed.Cluster;
+import akka.testkit.javadsl.TestKit;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.awaitility.Awaitility;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.opendaylight.controller.cluster.akka.eos.AbstractNativeEosTest;
+import org.opendaylight.mdsal.eos.dom.api.DOMEntityOwnershipService;
+import org.opendaylight.mdsal.singleton.common.api.ClusterSingletonService;
+import org.opendaylight.mdsal.singleton.common.api.ClusterSingletonServiceRegistration;
+import org.opendaylight.mdsal.singleton.common.api.ServiceGroupIdentifier;
+import org.opendaylight.mdsal.singleton.dom.impl.DOMClusterSingletonServiceProviderImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ClusterSingletonIntegrationTest extends AbstractNativeEosTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClusterSingletonIntegrationTest.class);
+
+    private MockNativeEntityOwnershipService node1;
+    private MockNativeEntityOwnershipService node2;
+    private MockNativeEntityOwnershipService node3;
+
+    private MockSingletonService singletonNode1;
+    private MockSingletonService singletonNode2;
+    private MockSingletonService singletonNode3;
+
+
+    @Before
+    public void setUp() throws Exception {
+        node1 = startupNativeService(2550, List.of("member-1"), THREE_NODE_SEED_NODES);
+        node2 = startupNativeService(2551, List.of("member-2"), THREE_NODE_SEED_NODES);
+        node3 = startupNativeService(2552, List.of("member-3"), THREE_NODE_SEED_NODES);
+
+        singletonNode1 = new MockSingletonService(node1);
+        singletonNode1.initializeProvider();
+
+        singletonNode2 = new MockSingletonService(node2);
+        singletonNode2.initializeProvider();
+
+        singletonNode3 = new MockSingletonService(node3);
+        singletonNode3.initializeProvider();
+
+        waitUntillNodeReady(node3);
+    }
+
+    @After
+    public void tearDown() {
+        ActorTestKit.shutdown(Adapter.toTyped(node1.getActorSystem()), Duration.ofSeconds(20));
+        ActorTestKit.shutdown(Adapter.toTyped(node2.getActorSystem()), Duration.ofSeconds(20));
+        ActorTestKit.shutdown(Adapter.toTyped(node3.getActorSystem()), Duration.ofSeconds(20));
+    }
+
+    @Test
+    public void testSingletonOwnershipNotDropped() {
+        final MockClusterSingletonService service = new MockClusterSingletonService("member-1", "service-1");
+        singletonNode1.registerClusterSingletonService(service);
+
+        verifyServiceActive(service);
+
+        final MockClusterSingletonService service2 = new MockClusterSingletonService("member-2", "service-1");
+        singletonNode2.registerClusterSingletonService(service2);
+
+        verifyServiceInactive(service2, 2);
+    }
+
+    @Test
+    public void testSingletonOwnershipHandoff() {
+        final MockClusterSingletonService service = new MockClusterSingletonService("member-1", "service-1");
+        final ClusterSingletonServiceRegistration registration =
+                singletonNode1.registerClusterSingletonService(service);
+
+        verifyServiceActive(service);
+
+        final MockClusterSingletonService service2 = new MockClusterSingletonService("member-2", "service-1");
+        singletonNode2.registerClusterSingletonService(service2);
+
+        verifyServiceInactive(service2, 2);
+
+        registration.close();
+        verifyServiceInactive(service);
+        verifyServiceActive(service2);
+    }
+
+    @Test
+    public void testSingletonOwnershipHandoffOnNodeShutdown() throws Exception {
+        MockClusterSingletonService service2 = new MockClusterSingletonService("member-2", "service-1");
+        ClusterSingletonServiceRegistration registration2 =
+                singletonNode2.registerClusterSingletonService(service2);
+
+        verifyServiceActive(service2);
+
+        final MockClusterSingletonService service3 = new MockClusterSingletonService("member-3", "service-1");
+        final ClusterSingletonServiceRegistration registration3 =
+                singletonNode3.registerClusterSingletonService(service3);
+
+        verifyServiceInactive(service3, 2);
+
+        LOG.debug("Shutting down node2");
+        TestKit.shutdownActorSystem(node2.getActorSystem());
+        verifyServiceActive(service3);
+
+        node2 = startupNativeService(2551, List.of("member-1"), THREE_NODE_SEED_NODES);
+        singletonNode2 = new MockSingletonService(node2);
+        singletonNode2.initializeProvider();
+
+        waitUntillNodeReady(node2);
+        service2 = new MockClusterSingletonService("member-2", "service-1");
+        singletonNode2.registerClusterSingletonService(service2);
+
+        verifyServiceActive(service3);
+        verifyServiceInactive(service2, 5);
+    }
+
+    private void waitUntillNodeReady(MockNativeEntityOwnershipService node) {
+        // need to wait until all nodes are ready
+        final Cluster cluster = Cluster.get(Adapter.toTyped(node.getActorSystem()));
+        Awaitility.await().atMost(Duration.ofSeconds(20)).until(() -> {
+            final List<Member> members = new ArrayList<>();
+            cluster.state().getMembers().forEach(members::add);
+            if (members.size() != 3) {
+                return false;
+            }
+
+            for (final Member member : members) {
+                if (!member.status().equals(MemberStatus.up())) {
+                    return false;
+                }
+            }
+
+            return true;
+        });
+    }
+
+    private static void verifyServiceActive(MockClusterSingletonService service) {
+        await().untilAsserted(() -> assertTrue(service.isActivated()));
+    }
+
+    private static void verifyServiceActive(MockClusterSingletonService service, long delay) {
+        await().pollDelay(delay, TimeUnit.SECONDS).untilAsserted(() -> assertTrue(service.isActivated()));
+    }
+
+    private static void verifyServiceInactive(MockClusterSingletonService service) {
+        await().untilAsserted(() -> assertFalse(service.isActivated()));
+    }
+
+    private static void verifyServiceInactive(MockClusterSingletonService service, long delay) {
+        await().pollDelay(delay, TimeUnit.SECONDS).untilAsserted(() -> assertFalse(service.isActivated()));
+    }
+
+    private static class MockClusterSingletonService implements ClusterSingletonService {
+
+        private final String member;
+        private final ServiceGroupIdentifier identifier;
+        private boolean activated = false;
+
+        MockClusterSingletonService(String member, String identifier) {
+            this.member = member;
+            this.identifier = ServiceGroupIdentifier.create(identifier);
+        }
+
+        @Override
+        public void instantiateServiceInstance() {
+            LOG.debug("{} : Activating service: {}", member, identifier);
+            activated = true;
+        }
+
+        @Override
+        public ListenableFuture<? extends Object> closeServiceInstance() {
+            LOG.debug("{} : Closing service: {}", member, identifier);
+            activated = false;
+            return Futures.immediateFuture(null);
+        }
+
+        @Override
+        public ServiceGroupIdentifier getIdentifier() {
+            return identifier;
+        }
+
+        public boolean isActivated() {
+            return activated;
+        }
+    }
+
+    private static class MockSingletonService extends DOMClusterSingletonServiceProviderImpl {
+        MockSingletonService(DOMEntityOwnershipService entityOwnershipService) {
+            super(entityOwnershipService);
+        }
+    }
+}