Bug 5421 - Single cluster-wide service instances grouping 59/40859/13
authorVaclav Demcak <[email protected]>
Thu, 14 Apr 2016 09:32:29 +0000 (11:32 +0200)
committerRobert Varga <[email protected]>
Mon, 18 Jul 2016 11:51:16 +0000 (11:51 +0000)
ClusterSingletonService grouping means to run all registred service to same
Group on same ClusterNode. So we could say, service goup will always run on
same ClusterNode.

 * implement grouping functionality for ClusterSingletonService instancies
 * implement double candidate funct. for handling ClusterSingletonService
   service group instances
 * add junit test suite + import Mockito

Change-Id: If91a6f1bf786c42c32201899bc0bb826729f0585
Signed-off-by: Vaclav Demcak <[email protected]>
12 files changed:
common/artifacts/pom.xml
common/mdsal-common-api/pom.xml
singleton-service/mdsal-singleton-common-spi/pom.xml [new file with mode: 0644]
singleton-service/mdsal-singleton-common-spi/src/main/java/org/opendaylight/mdsal/singleton/common/spi/AbstractClusterSingletonServiceProviderImpl.java [new file with mode: 0644]
singleton-service/mdsal-singleton-common-spi/src/main/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceGroup.java [moved from singleton-service/mdsal-singleton-common-api/src/main/java/org/opendaylight/mdsal/singleton/common/api/ClusterSingletonServiceGroup.java with 94% similarity]
singleton-service/mdsal-singleton-common-spi/src/main/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceGroupImpl.java [new file with mode: 0644]
singleton-service/mdsal-singleton-common-spi/src/main/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceRegistrationDelegator.java [moved from singleton-service/mdsal-singleton-common-api/src/main/java/org/opendaylight/mdsal/singleton/common/api/ClusterSingletonServiceRegistrationDelegator.java with 87% similarity]
singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceGroupImplTest.java [new file with mode: 0644]
singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceRegistrationDelegatorTest.java [new file with mode: 0644]
singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/util/TestEntity.java [new file with mode: 0644]
singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/util/TestInstanceIdentifier.java [new file with mode: 0644]
singleton-service/pom.xml

index 8688f95cb1b6cdb4b6ce76a1d77f5b183e2c6002..f9eafaaa56345f4bf391b24b2de6f16e0f3711bd 100644 (file)
                 <artifactId>mdsal-singleton-common-api</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.opendaylight.mdsal</groupId>
+                <artifactId>mdsal-singleton-common-spi</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.opendaylight.mdsal</groupId>
                 <artifactId>mdsal-singleton-dom-api</artifactId>
index 74e3d6c4a362bb07cce836adf752959f9b57c50b..e5dee0a24aa6033b2a7e6f20bc7e90c6e73865a0 100644 (file)
       <artifactId>mockito-configuration</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <scm>
diff --git a/singleton-service/mdsal-singleton-common-spi/pom.xml b/singleton-service/mdsal-singleton-common-spi/pom.xml
new file mode 100644 (file)
index 0000000..28dc755
--- /dev/null
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.opendaylight.mdsal</groupId>
+    <artifactId>singleton-service</artifactId>
+    <version>2.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>mdsal-singleton-common-spi</artifactId>
+  <packaging>bundle</packaging>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.opendaylight.mdsal</groupId>
+      <artifactId>mdsal-eos-common-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.mdsal</groupId>
+      <artifactId>mdsal-singleton-common-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <artifactId>concepts</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <artifactId>util</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <artifactId>yang-common</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <artifactId>mockito-configuration</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <scm>
+    <connection>scm:git:http://git.opendaylight.org/gerrit/mdsal.git</connection>
+    <developerConnection>scm:git:ssh://git.opendaylight.org:29418/mdsal.git</developerConnection>
+    <tag>HEAD</tag>
+    <url>https://wiki.opendaylight.org/view/MD-SAL:Main</url>
+  </scm>
+
+  <!--
+      Maven Site Configuration
+
+      The following configuration is necessary for maven-site-plugin to
+      correctly identify the correct deployment path for OpenDaylight Maven
+      sites.
+  -->
+  <url>${odl.site.url}/${project.groupId}/${stream}/${project.artifactId}/</url>
+
+  <distributionManagement>
+    <site>
+      <id>opendaylight-site</id>
+      <url>${nexus.site.url}/${project.artifactId}/</url>
+    </site>
+  </distributionManagement>
+
+</project>
diff --git a/singleton-service/mdsal-singleton-common-spi/src/main/java/org/opendaylight/mdsal/singleton/common/spi/AbstractClusterSingletonServiceProviderImpl.java b/singleton-service/mdsal-singleton-common-spi/src/main/java/org/opendaylight/mdsal/singleton/common/spi/AbstractClusterSingletonServiceProviderImpl.java
new file mode 100644 (file)
index 0000000..02ee829
--- /dev/null
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.mdsal.singleton.common.spi;
+
+/**
+ * Abstract class {@link AbstractClusterSingletonServiceProviderImpl} represents implementations of
+ * {@link org.opendaylight.mdsal.singleton.common.api.CommonClusterSingletonServiceProvider}.
+ */
+public abstract class AbstractClusterSingletonServiceProviderImpl {
+
+    // TODO: add implementation
+}
@@ -6,12 +6,14 @@
  * and is available at http://www.eclipse.org/legal/epl-v10.html
  */
 
-package org.opendaylight.mdsal.singleton.common.api;
+package org.opendaylight.mdsal.singleton.common.spi;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import java.util.List;
 import org.opendaylight.mdsal.eos.common.api.GenericEntity;
 import org.opendaylight.mdsal.eos.common.api.GenericEntityOwnershipChange;
+import org.opendaylight.mdsal.singleton.common.api.ClusterSingletonService;
+import org.opendaylight.mdsal.singleton.common.api.ClusterSingletonServiceRegistration;
 import org.opendaylight.yangtools.concepts.Path;
 
 /**
diff --git a/singleton-service/mdsal-singleton-common-spi/src/main/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceGroupImpl.java b/singleton-service/mdsal-singleton-common-spi/src/main/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceGroupImpl.java
new file mode 100644 (file)
index 0000000..812b9ef
--- /dev/null
@@ -0,0 +1,406 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.mdsal.singleton.common.spi;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.base.Verify;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import org.opendaylight.mdsal.eos.common.api.EntityOwnershipChangeState;
+import org.opendaylight.mdsal.eos.common.api.GenericEntity;
+import org.opendaylight.mdsal.eos.common.api.GenericEntityOwnershipCandidateRegistration;
+import org.opendaylight.mdsal.eos.common.api.GenericEntityOwnershipChange;
+import org.opendaylight.mdsal.eos.common.api.GenericEntityOwnershipListener;
+import org.opendaylight.mdsal.eos.common.api.GenericEntityOwnershipService;
+import org.opendaylight.mdsal.singleton.common.api.ClusterSingletonService;
+import org.opendaylight.mdsal.singleton.common.api.ClusterSingletonServiceRegistration;
+import org.opendaylight.yangtools.concepts.Path;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of {@link ClusterSingletonServiceGroup}
+ *
+ * @param <P> the instance identifier path type
+ * @param <E> the GenericEntity type
+ * @param <C> the GenericEntityOwnershipChange type
+ * @param <G> the GenericEntityOwnershipListener type
+ * @param <S> the GenericEntityOwnershipService type
+ */
+@VisibleForTesting
+final class ClusterSingletonServiceGroupImpl<P extends Path<P>, E extends GenericEntity<P>,
+                                             C extends GenericEntityOwnershipChange<P, E>,
+                                             G extends GenericEntityOwnershipListener<P, C>,
+                                             S extends GenericEntityOwnershipService<P, E, G>>
+        implements ClusterSingletonServiceGroup<P, E, C> {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClusterSingletonServiceGroupImpl.class.getName());
+
+    private final S entityOwnershipService;
+    private final String clusterSingletonGroupIdentifier;
+    private final Semaphore clusterLock = new Semaphore(1);
+
+    /* Entity instances */
+    private final E serviceEntity;
+    private final E doubleCandidateEntity;
+
+    @GuardedBy("clusterLock")
+    private boolean hasOwnership = false;
+    @GuardedBy("clusterLock")
+    private final List<ClusterSingletonServiceRegistrationDelegator> serviceGroup = new LinkedList<>();
+    private final ConcurrentMap<String, ClusterSingletonServiceGroup<P, E, C>> allServiceGroups;
+
+    /* EOS Candidate Registrations */
+    private GenericEntityOwnershipCandidateRegistration<P, E> serviceEntityCandidateReg;
+    private GenericEntityOwnershipCandidateRegistration<P, E> asyncCloseEntityCandidateReg;
+
+    /**
+     * Class constructor
+     *
+     * @param clusterSingletonServiceGroupIdentifier not empty string as identifier
+     * @param mainEntity
+     * @param closeEntity
+     * @param entityOwnershipService
+     */
+    ClusterSingletonServiceGroupImpl(final String clusterSingletonServiceGroupIdentifier, final E mainEntity,
+            final E closeEntity, final S entityOwnershipService,
+            final ConcurrentMap<String, ClusterSingletonServiceGroup<P, E, C>> allServiceGroups) {
+        LOG.debug("New Instance of ClusterSingletonServiceGroup {}", clusterSingletonServiceGroupIdentifier);
+        Preconditions.checkArgument(!Strings.isNullOrEmpty(clusterSingletonServiceGroupIdentifier));
+        this.clusterSingletonGroupIdentifier = clusterSingletonServiceGroupIdentifier;
+        this.entityOwnershipService = Preconditions.checkNotNull(entityOwnershipService);
+        this.serviceEntity = Preconditions.checkNotNull(mainEntity);
+        this.doubleCandidateEntity = Preconditions.checkNotNull(closeEntity);
+        this.allServiceGroups = Preconditions.checkNotNull(allServiceGroups);
+    }
+
+    @Override
+    public final ListenableFuture<List<Void>> closeClusterSingletonGroup() {
+        LOG.debug("Close method for service Provider {}", clusterSingletonGroupIdentifier);
+        boolean needReleaseLock = false;
+        final ListenableFuture<List<Void>> destroyFuture;
+        try {
+            needReleaseLock = clusterLock.tryAcquire(10, TimeUnit.SECONDS);
+        } catch (final Exception e) {
+            LOG.warn("Unexpected Exception for service Provider {} in closing phase.", clusterSingletonGroupIdentifier, e);
+        } finally {
+            if (serviceEntityCandidateReg != null) {
+                serviceEntityCandidateReg.close();
+                serviceEntityCandidateReg = null;
+            }
+            final List<ListenableFuture<Void>> serviceCloseFutureList = new ArrayList<>();
+            if (hasOwnership) {
+                for (final ClusterSingletonServiceRegistrationDelegator service : serviceGroup) {
+                    serviceCloseFutureList.add(service.closeServiceInstance());
+                }
+            }
+            destroyFuture = Futures.allAsList(serviceCloseFutureList);
+            final Semaphore finalRelease = needReleaseLock ? clusterLock : null;
+            Futures.addCallback(destroyFuture, newAsyncCloseCallback(finalRelease));
+        }
+        return destroyFuture;
+    }
+
+    @Override
+    public final void initializationClusterSingletonGroup() {
+        LOG.debug("Initialization ClusterSingletonGroup {}", clusterSingletonGroupIdentifier);
+        boolean needReleaseLock = false;
+        boolean needCloseProviderInstance = false;
+        try {
+            clusterLock.acquire();
+            needReleaseLock = true;
+            Verify.verify(serviceGroup.isEmpty());
+            Verify.verify(!hasOwnership);
+            Verify.verify(serviceEntityCandidateReg == null);
+            serviceEntityCandidateReg = entityOwnershipService.registerCandidate(serviceEntity);
+        } catch (final Exception e) {
+            LOG.debug("Unexpected error by registration service Provider {}", clusterSingletonGroupIdentifier, e);
+            needCloseProviderInstance = true;
+        } finally {
+            closeResources(needReleaseLock, needCloseProviderInstance);
+        }
+    }
+
+    @Override
+    public final ClusterSingletonServiceRegistration registerService(final ClusterSingletonService service) {
+        LOG.debug("RegisterService method call for ClusterSingletonServiceGroup {}", clusterSingletonGroupIdentifier);
+        Verify.verify(clusterSingletonGroupIdentifier.equals(service.getIdentifier().getValue()));
+        boolean needReleaseLock = false;
+        boolean needCloseProviderInstance = false;
+        ClusterSingletonServiceRegistrationDelegator reg = null;
+        try {
+            clusterLock.acquire();
+            needReleaseLock = true;
+            Verify.verify(serviceEntityCandidateReg != null);
+            reg = new ClusterSingletonServiceRegistrationDelegator(service, this);
+            serviceGroup.add(reg);
+            if (hasOwnership) {
+                service.instantiateServiceInstance();
+            }
+        } catch (final Exception e) {
+            LOG.debug("Unexpected error by registration service Provider {}", clusterSingletonGroupIdentifier, e);
+            needCloseProviderInstance = true;
+        } finally {
+            closeResources(needReleaseLock, needCloseProviderInstance);
+        }
+        return reg;
+    }
+
+    @Override
+    public final void unregisterService(final ClusterSingletonService service) {
+        LOG.debug("UnregisterService method call for ClusterSingletonServiceGroup {}", clusterSingletonGroupIdentifier);
+        Verify.verify(clusterSingletonGroupIdentifier.equals(service.getIdentifier().getValue()));
+        boolean needReleaseLock = false;
+        boolean needCloseProviderInstance = false;
+        try {
+            clusterLock.acquire();
+            needReleaseLock = true;
+            serviceGroup.remove(service);
+            if (hasOwnership) {
+                service.closeServiceInstance();
+            }
+        } catch (final Exception e) {
+            LOG.debug("Unexpected error by registration service Provider {}", clusterSingletonGroupIdentifier, e);
+            needCloseProviderInstance = true;
+        } finally {
+            closeResources(needReleaseLock, needCloseProviderInstance);
+            if (serviceGroup.isEmpty()) {
+                this.closeClusterSingletonGroup();
+            }
+        }
+    }
+
+    @Override
+    public final void ownershipChanged(final C ownershipChange) {
+        LOG.debug("Ownership change {} for ClusterSingletonServiceGrou {}", ownershipChange,
+                clusterSingletonGroupIdentifier);
+        try {
+            if (ownershipChange.inJeopardy()) {
+                LOG.warn("Cluster Node lost connection to another cluster nodes {}", ownershipChange);
+                lostOwnership();
+                return;
+            }
+            if (serviceEntity.equals(ownershipChange.getEntity())) {
+                if (EntityOwnershipChangeState.LOCAL_OWNERSHIP_GRANTED.equals(ownershipChange.getState())) {
+                    /*
+                     * SLAVE to MASTER : ownershipChange.getState().isOwner() && !ownershipChange.getState().wasOwner()
+                     */
+                    tryToTakeOwnership();
+                } else if (EntityOwnershipChangeState.LOCAL_OWNERSHIP_LOST_NEW_OWNER.equals(ownershipChange.getState())
+                        || EntityOwnershipChangeState.LOCAL_OWNERSHIP_LOST_NO_OWNER
+                                .equals(ownershipChange.getState())) {
+                    /*
+                     * MASTER to SLAVE : !ownershipChange.getState().isOwner() && ownershipChange.getState().wasOwner()
+                     */
+                    lostOwnership();
+                } else {
+                    /* Not needed notifications */
+                    LOG.debug("Not processed entity OwnershipChange {} in service Provider {}", ownershipChange,
+                            clusterSingletonGroupIdentifier);
+                }
+            } else if (doubleCandidateEntity.equals(ownershipChange.getEntity())) {
+                if (EntityOwnershipChangeState.LOCAL_OWNERSHIP_GRANTED.equals(ownershipChange.getState())) {
+                    /*
+                     * SLAVE to MASTER : ownershipChange.getState().isOwner() && !ownershipChange.getState().wasOwner()
+                     */
+                    takeOwnership();
+                } else if (EntityOwnershipChangeState.LOCAL_OWNERSHIP_LOST_NEW_OWNER.equals(ownershipChange.getState())
+                        || EntityOwnershipChangeState.LOCAL_OWNERSHIP_LOST_NO_OWNER
+                                .equals(ownershipChange.getState())) {
+                    /*
+                     * MASTER to SLAVE : !ownershipChange.getState().isOwner() && ownershipChange.getState().wasOwner()
+                     */
+                    LOG.warn("Unexpected lost doubleCandidate {} leadership. Close service instance {}",
+                            doubleCandidateEntity, clusterSingletonGroupIdentifier);
+                    lostOwnership();
+                } else {
+                    /* Not needed notifications */
+                    LOG.debug("Not processed doubleCandidate OwnershipChange {} in service Provider {}",
+                            ownershipChange, clusterSingletonGroupIdentifier);
+                }
+            } else {
+                LOG.warn("Unexpected EntityOwnershipChangeEvent for entity {}", ownershipChange);
+            }
+        } catch (final Exception e) {
+            LOG.error("Unexpected Exception for service Provider {}", clusterSingletonGroupIdentifier, e);
+            // TODO : think about close ... is it necessary?
+        }
+    }
+
+    /*
+     * Help method to registerated DoubleCandidateEntity. It is first step
+     * before the actual instance take Leadership.
+     */
+    private void tryToTakeOwnership() {
+        LOG.debug("TryToTakeLeadership method for service Provider {}", clusterSingletonGroupIdentifier);
+        boolean needReleaseLock = false;
+        boolean needCloseProviderInstance = false;
+        try {
+            clusterLock.acquire();
+            needReleaseLock = true;
+            Verify.verify(serviceEntityCandidateReg != null);
+            Verify.verify(asyncCloseEntityCandidateReg == null);
+            asyncCloseEntityCandidateReg = entityOwnershipService.registerCandidate(doubleCandidateEntity);
+        } catch (final Exception e) {
+            LOG.error("Unexpected exception state for service Provider {} in TryToTakeLeadership",
+                    clusterSingletonGroupIdentifier, e);
+            needCloseProviderInstance = true;
+        } finally {
+            closeResources(needReleaseLock, needCloseProviderInstance);
+        }
+    }
+
+    /*
+     * Help method calls setupService method for create single cluster-wide service instance.
+     */
+    private void takeOwnership() {
+        LOG.debug("TakeLeadership method for service Provider {}", clusterSingletonGroupIdentifier);
+        boolean needReleaseLock = false;
+        boolean needCloseProviderInstance = false;
+        try {
+            clusterLock.acquire();
+            needReleaseLock = true;
+            Verify.verify(serviceEntityCandidateReg != null);
+            Verify.verify(asyncCloseEntityCandidateReg != null);
+            for (final ClusterSingletonServiceRegistrationDelegator service : serviceGroup) {
+                service.instantiateServiceInstance();
+            }
+            hasOwnership = true;
+        } catch (final Exception e) {
+            LOG.error("Unexpected exception state for service Provider {} in TakeLeadership",
+                    clusterSingletonGroupIdentifier, e);
+            needCloseProviderInstance = true;
+        } finally {
+            closeResources(needReleaseLock, needCloseProviderInstance);
+        }
+    }
+
+    /*
+     * Help method calls suspendService method for stop this single cluster-wide service instance.
+     * The last async. step has to close DoubleCandidateRegistration reference what should initialize
+     * new election for DoubleCandidateEntity.
+     */
+    private void lostOwnership() {
+        LOG.debug("LostLeadership method for service Provider {}", clusterSingletonGroupIdentifier);
+        boolean needReleaseLock = false;
+        boolean needCloseProviderInstance = false;
+        try {
+            clusterLock.acquire();
+            needReleaseLock = true;
+            Verify.verify(serviceEntityCandidateReg != null);
+            final List<ListenableFuture<Void>> serviceCloseFutureList = new ArrayList<>();
+            if (hasOwnership) {
+                Verify.verify(asyncCloseEntityCandidateReg != null);
+                for (final ClusterSingletonServiceRegistrationDelegator service : serviceGroup) {
+                    serviceCloseFutureList.add(service.closeServiceInstance());
+                }
+            }
+
+            final ListenableFuture<List<Void>> destroyFuture = Futures.allAsList(serviceCloseFutureList);
+            Futures.addCallback(destroyFuture, newAsyncCloseCallback(clusterLock));
+            /*
+             * We wish to stop all possible EOS activitis before we don't close
+             * a close candidate registration that acts as a guard. So we don't want
+             * to release Semaphore (clusterLock) before we are not fully finished.
+             * Semaphore lock release has to be realized as FutureCallback after a service
+             * instance has fully closed prior to relinquishing service ownership.
+             */
+            needReleaseLock = false;
+        } catch (final Exception e) {
+            LOG.error("Unexpected exception state for service Provider {} in LostLeadership",
+                    clusterSingletonGroupIdentifier, e);
+            needCloseProviderInstance = true;
+        } finally {
+            closeResources(needReleaseLock, needCloseProviderInstance);
+        }
+    }
+
+    /*
+     * Help method for finalization every acquired functionality
+     */
+    private void closeResources(final boolean needReleaseLock, final boolean needCloseProvider) {
+        if (needReleaseLock) {
+            clusterLock.release();
+        }
+        if (needCloseProvider) {
+            final ListenableFuture<List<Void>> closeFutureList = this.closeClusterSingletonGroup();
+            Futures.addCallback(closeFutureList, new FutureCallback<List<Void>>() {
+
+                @Override
+                public void onSuccess(final List<Void> result) {
+                    removeThisGroupFromProvider(null);
+                }
+
+                @Override
+                public void onFailure(final Throwable t) {
+                    removeThisGroupFromProvider(t);
+                }
+            });
+        }
+    }
+
+    /*
+     * Help method for final ClusterSingletonGroup removing
+     */
+    protected final void removeThisGroupFromProvider(@Nullable final Throwable t) {
+        LOG.debug("Removing ClusterSingletonServiceGroup {}", clusterSingletonGroupIdentifier);
+        if (t != null) {
+            LOG.warn("Unexpected problem by closingResources ClusterSingletonServiceGroup {}",
+                    clusterSingletonGroupIdentifier);
+        }
+        allServiceGroups.remove(clusterSingletonGroupIdentifier, this);
+    }
+
+    /*
+     * Help method creates FutureCallback for suspend Future
+     */
+    private FutureCallback<List<Void>> newAsyncCloseCallback(@Nullable final Semaphore semaphore) {
+        final Consumer<Throwable> closeEntityCandidateRegistration = (@Nullable final Throwable t) -> {
+            if (t != null) {
+                LOG.warn("Unexpected error closing service instance {}", clusterSingletonGroupIdentifier, t);
+            } else {
+                LOG.debug("Destroy service Instance {} is success", clusterSingletonGroupIdentifier);
+            }
+            if (asyncCloseEntityCandidateReg != null) {
+                asyncCloseEntityCandidateReg.close();
+                asyncCloseEntityCandidateReg = null;
+            }
+            if (semaphore != null) {
+                semaphore.release();
+            }
+            allServiceGroups.remove(clusterSingletonGroupIdentifier, this);
+        };
+
+        return new FutureCallback<List<Void>>() {
+
+            @Override
+            public void onSuccess(final List<Void> result) {
+                closeEntityCandidateRegistration.accept(null);
+            }
+
+            @Override
+            public void onFailure(final Throwable t) {
+                closeEntityCandidateRegistration.accept(t);
+            }
+        };
+    }
+
+}
@@ -6,10 +6,13 @@
  * and is available at http://www.eclipse.org/legal/epl-v10.html
  */
 
-package org.opendaylight.mdsal.singleton.common.api;
+package org.opendaylight.mdsal.singleton.common.spi;
 
 import com.google.common.base.Preconditions;
 import com.google.common.util.concurrent.ListenableFuture;
+import org.opendaylight.mdsal.singleton.common.api.ClusterSingletonService;
+import org.opendaylight.mdsal.singleton.common.api.ClusterSingletonServiceRegistration;
+import org.opendaylight.mdsal.singleton.common.api.ServiceGroupIdentifier;
 
 /**
  * Package protected help class represent a Delegator for {@link ClusterSingletonService}
diff --git a/singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceGroupImplTest.java b/singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceGroupImplTest.java
new file mode 100644 (file)
index 0000000..13b09c9
--- /dev/null
@@ -0,0 +1,572 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.mdsal.singleton.common.spi;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import com.google.common.util.concurrent.Futures;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.mdsal.eos.common.api.EntityOwnershipChangeState;
+import org.opendaylight.mdsal.eos.common.api.GenericEntityOwnershipCandidateRegistration;
+import org.opendaylight.mdsal.eos.common.api.GenericEntityOwnershipChange;
+import org.opendaylight.mdsal.eos.common.api.GenericEntityOwnershipListener;
+import org.opendaylight.mdsal.eos.common.api.GenericEntityOwnershipService;
+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.common.spi.util.TestEntity;
+import org.opendaylight.mdsal.singleton.common.spi.util.TestInstanceIdentifier;
+
+/**
+ * Testing {@link ClusterSingletonServiceGroupImpl}
+ */
+public class ClusterSingletonServiceGroupImplTest {
+
+    private static final String SERVICE_ENTITY_TYPE = "org.opendaylight.mdsal.ServiceEntityType";
+    private static final String CLOSE_SERVICE_ENTITY_TYPE = "org.opendaylight.mdsal.AsyncServiceCloseEntityType";
+    private static final String SERVICE_IDENTIFIER = "TestServiceIdent";
+    private static final ServiceGroupIdentifier SERVICE_GROUP_IDENT = ServiceGroupIdentifier.create(SERVICE_IDENTIFIER);
+
+    @Mock
+    private ClusterSingletonService mockClusterSingletonService;
+    @Mock
+    private ClusterSingletonService mockClusterSingletonServiceSecond;
+    @Mock
+    private GenericEntityOwnershipCandidateRegistration<?, ?> mockEntityCandReg;
+    @Mock
+    private GenericEntityOwnershipCandidateRegistration<?, ?> mockCloseEntityCandReg;
+    @Mock
+    private GenericEntityOwnershipListener<TestInstanceIdentifier,GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity>> mockEosListener;
+
+    @Mock
+    private GenericEntityOwnershipService<TestInstanceIdentifier,TestEntity, GenericEntityOwnershipListener<TestInstanceIdentifier,GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity>>> mockEosService;
+
+    private ClusterSingletonServiceGroupImpl<TestInstanceIdentifier,TestEntity,GenericEntityOwnershipChange<TestInstanceIdentifier,TestEntity>,
+                                         GenericEntityOwnershipListener<TestInstanceIdentifier, GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity>>,
+                                         GenericEntityOwnershipService<TestInstanceIdentifier, TestEntity, GenericEntityOwnershipListener<TestInstanceIdentifier,
+                                         GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity>>>> singletonServiceGroup;
+
+    private final TestEntity mainEntity = new TestEntity(SERVICE_ENTITY_TYPE, SERVICE_IDENTIFIER);
+    private final TestEntity closeEntity = new TestEntity(CLOSE_SERVICE_ENTITY_TYPE, SERVICE_IDENTIFIER);
+    private final ConcurrentMap<String, ClusterSingletonServiceGroup<?, ?, ?>> map = new ConcurrentHashMap<>();
+
+    /**
+     * Initialization functionality for every Tests in this suite
+     *
+     * @throws Exception - unexpected exception
+     */
+   @Before
+   public void setup() throws Exception {
+       MockitoAnnotations.initMocks(this);
+
+       doReturn(mockEntityCandReg).when(mockEosService).registerCandidate(mainEntity);
+       doReturn(mockCloseEntityCandReg).when(mockEosService).registerCandidate(closeEntity);
+       doNothing().when(mockEntityCandReg).close();
+       doNothing().when(mockCloseEntityCandReg).close();
+       doNothing().when(mockClusterSingletonService).instantiateServiceInstance();
+       doReturn(Futures.immediateFuture(null)).when(mockClusterSingletonService).closeServiceInstance();
+
+        doReturn(SERVICE_GROUP_IDENT).when(mockClusterSingletonService).getIdentifier();
+        doReturn(SERVICE_GROUP_IDENT).when(mockClusterSingletonServiceSecond).getIdentifier();
+
+       singletonServiceGroup = new ClusterSingletonServiceGroupImpl(SERVICE_IDENTIFIER, mainEntity, closeEntity, mockEosService, map);
+    }
+
+    /**
+     * Test NULL ServiceIdent input for new ServiceGroup instance
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void instantiationClusterSingletonServiceGroupNullIdentTest() throws Exception {
+        singletonServiceGroup = new ClusterSingletonServiceGroupImpl(null, mainEntity, closeEntity, mockEosService, map);
+    }
+
+    /**
+     * Test empty ServiceIdent input for new ServiceGroup instance
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void instantiationClusterSingletonServiceGroupEmptyIdentTest() throws Exception {
+        singletonServiceGroup = new ClusterSingletonServiceGroupImpl("", mainEntity, closeEntity, mockEosService, map);
+    }
+
+    /**
+     * Test NULL MainEntity input for new ServiceGroup instance
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test(expected = NullPointerException.class)
+    public void instantiationClusterSingletonServiceGroupNullMainEntityTest() throws Exception {
+        singletonServiceGroup = new ClusterSingletonServiceGroupImpl(SERVICE_IDENTIFIER, null, closeEntity, mockEosService, map);
+    }
+
+    /**
+     * Test NULL MainEntity input for new ServiceGroup instance
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test(expected = NullPointerException.class)
+    public void instantiationClusterSingletonServiceGroupNullCloseEntityTest() throws Exception {
+        singletonServiceGroup = new ClusterSingletonServiceGroupImpl(SERVICE_IDENTIFIER, mainEntity, null, mockEosService, map);
+    }
+
+    /**
+     * Test NULL MainEntity input for new ServiceGroup instance
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test(expected = NullPointerException.class)
+    public void instantiationClusterSingletonServiceGroupNullEOS_Test() throws Exception {
+        singletonServiceGroup = new ClusterSingletonServiceGroupImpl(SERVICE_IDENTIFIER, mainEntity, closeEntity, null, map);
+    }
+
+    /**
+     * Test NULL MainEntity input for new ServiceGroup instance
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test(expected = NullPointerException.class)
+    public void instantiationClusterSingletonServiceGroupNullMapRefTest() throws Exception {
+        singletonServiceGroup = new ClusterSingletonServiceGroupImpl(SERVICE_IDENTIFIER, mainEntity, closeEntity, mockEosService, null);
+    }
+
+    /**
+     * Test GoldPath for initialization ServiceGroup
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void initializationClusterSingletonServiceGroupTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        verify(mockEosService).registerCandidate(mainEntity);
+    }
+
+    /**
+     * Test GoldPath for NO-TO-SLAVE entity Candidate role change
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void initializationSlaveTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToSlave());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService, never()).registerCandidate(closeEntity);
+    }
+
+    /**
+     * Test GoldPath for NO-TO-SLAVE but without MASTER entity Candidate role change
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void initializationNoMasterTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToSlaveNoMaster());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService, never()).registerCandidate(closeEntity);
+    }
+
+    /**
+     * Test GoldPath for InJeopardy entity Candidate role change
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void initializationInJeopardyTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToJeopardy());
+        final ClusterSingletonServiceGroup<?, ?, ?> serviceGroup = map.get(SERVICE_IDENTIFIER);
+        Assert.assertNull(serviceGroup);
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService, never()).registerCandidate(closeEntity);
+    }
+
+    /**
+     * Test GoldPath for registration SingletonService
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void serviceRegistrationClusterSingletonServiceGroupTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+    }
+
+    /**
+     * Test GoldPath for registration SingletonService
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void serviceRegistrationClusterSingletonServiceGroupTwoServiceTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        final ClusterSingletonServiceRegistration reg2 = singletonServiceGroup
+                .registerService(mockClusterSingletonServiceSecond);
+        Assert.assertNotNull(reg2);
+    }
+
+    /**
+     * Test GoldPath for unregistration SingletonService don't call closeServiceInstance
+     * without mastership and don't remove ServiceGroup from map
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void serviceUnregistrationClusterSingletonServiceGroupTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        reg.close();
+        verify(mockClusterSingletonService, never()).closeServiceInstance();
+        final ClusterSingletonServiceGroup<?, ?, ?> serviceGroup = map.get(SERVICE_IDENTIFIER);
+        Assert.assertNull(serviceGroup);
+    }
+
+    /**
+     * Test GoldPath for unregistration SingletonService don't call closeServiceInstance
+     * without mastership and don't remove ServiceGroup from map
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void serviceUnregistrationClusterSingletonServiceGroupTwoServicesTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        final ClusterSingletonServiceRegistration reg2 = singletonServiceGroup
+                .registerService(mockClusterSingletonServiceSecond);
+        Assert.assertNotNull(reg2);
+        reg.close();
+        verify(mockClusterSingletonService, never()).closeServiceInstance();
+        final ClusterSingletonServiceGroup<?, ?, ?> serviceGroup = map.get(SERVICE_IDENTIFIER);
+        Assert.assertNotNull(serviceGroup);
+    }
+
+    /**
+     * Test GoldPath get Slave role for registered main entity
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void getSlaveClusterSingletonServiceGroupTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToSlave());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+    }
+
+    /**
+     * Test GoldPath get Master role for registered main entity
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void tryToTakeLeaderClusterSingletonServiceGroupTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToMaster());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService).registerCandidate(closeEntity);
+    }
+
+    /**
+     * Test GoldPath get Master role for registered close entity
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void takeMasterClusterSingletonServiceGroupTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToMaster());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService).registerCandidate(closeEntity);
+        singletonServiceGroup.ownershipChanged(getDoubleEntityToMaster());
+        verify(mockClusterSingletonService).instantiateServiceInstance();
+    }
+
+    /**
+     * Test GoldPath get Master role for registered entity but initial Slave
+     * role for closeEntity
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void waitToTakeMasterClusterSingletonServiceGroupTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToMaster());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService).registerCandidate(closeEntity);
+        singletonServiceGroup.ownershipChanged(getInitDoubleEntityToSlave());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockClusterSingletonService, never()).closeServiceInstance();
+        final ClusterSingletonServiceGroup<?, ?, ?> serviceGroup = map.get(SERVICE_IDENTIFIER);
+        Assert.assertNotNull(serviceGroup);
+    }
+
+    /**
+     * Test inJeopardy validation during wait phase for Master role for closeEntity
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void inJeopardyInWaitPhaseClusterSingletonServiceGroupTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToMaster());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService).registerCandidate(closeEntity);
+        singletonServiceGroup.ownershipChanged(getEntityToJeopardy());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockClusterSingletonService, never()).closeServiceInstance();
+        final ClusterSingletonServiceGroup<?, ?, ?> serviceGroup = map.get(SERVICE_IDENTIFIER);
+        Assert.assertNull(serviceGroup);
+    }
+
+    /**
+     * Test inJeopardy validation during wait phase for Master role for closeEntity
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void inJeopardyInWaitPhaseClusterSingletonServiceGroupTwoServiceTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        final ClusterSingletonServiceRegistration reg2 = singletonServiceGroup
+                .registerService(mockClusterSingletonServiceSecond);
+        Assert.assertNotNull(reg2);
+        singletonServiceGroup.ownershipChanged(getEntityToMaster());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService).registerCandidate(closeEntity);
+        singletonServiceGroup.ownershipChanged(getEntityToJeopardy());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockClusterSingletonService, never()).closeServiceInstance();
+        final ClusterSingletonServiceGroup<?, ?, ?> serviceGroup = map.get(SERVICE_IDENTIFIER);
+        Assert.assertNull(serviceGroup);
+    }
+
+    /**
+     * Test inJeopardy validation for holding leadership
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void inJeopardyLeaderClusterSingletonServiceGroupTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToMaster());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService).registerCandidate(closeEntity);
+        singletonServiceGroup.ownershipChanged(getDoubleEntityToMaster());
+        verify(mockClusterSingletonService).instantiateServiceInstance();
+        singletonServiceGroup.ownershipChanged(getEntityToJeopardy());
+        verify(mockClusterSingletonService).closeServiceInstance();
+        final ClusterSingletonServiceGroup<?, ?, ?> serviceGroup = map.get(SERVICE_IDENTIFIER);
+        Assert.assertNull(serviceGroup);
+    }
+
+    /**
+     * Test GoldPath for SLAVE-TO-MASTER entity Candidate role change
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void lostLeaderClusterSingletonServiceGroupTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToMaster());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService).registerCandidate(closeEntity);
+        singletonServiceGroup.ownershipChanged(getDoubleEntityToMaster());
+        verify(mockClusterSingletonService).instantiateServiceInstance();
+        singletonServiceGroup.ownershipChanged(getEntityToSlave());
+        verify(mockClusterSingletonService).closeServiceInstance();
+    }
+
+    /**
+     * Test checks validation Error processing for SLAVE-TO-MASTER entity Candidate role change.
+     * Not initialized provider has to close and remove all singletonServices from Group and
+     * Group itself remove too.
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void tryToTakeLeaderForNotInitializedGroupTest() throws Exception {
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNull(reg);
+        final ClusterSingletonServiceGroup<?, ?, ?> serviceGroup = map.get(SERVICE_IDENTIFIER);
+        Assert.assertNull(serviceGroup);
+    }
+
+    /**
+     * Test checks closing procesing for close {@link ClusterSingletonServiceRegistration}
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void checkClosingRegistrationTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToMaster());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService).registerCandidate(closeEntity);
+        singletonServiceGroup.ownershipChanged(getDoubleEntityToMaster());
+        verify(mockClusterSingletonService).instantiateServiceInstance();
+        reg.close();
+        verify(mockClusterSingletonService).closeServiceInstance();
+    }
+
+    /**
+     * Test checks validation Error processing for MASTER-TO-SLAVE closeEntity Candidate role change
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void checkClosingUnexpectedDoubleEntityForMasterOwnershipChangeRegistrationTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToMaster());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService).registerCandidate(closeEntity);
+        singletonServiceGroup.ownershipChanged(getDoubleEntityToMaster());
+        verify(mockClusterSingletonService).instantiateServiceInstance();
+        singletonServiceGroup.ownershipChanged(getDoubleEntityToSlave());
+        verify(mockClusterSingletonService).closeServiceInstance();
+        final ClusterSingletonServiceGroup<?, ?, ?> serviceGroup = map.get(SERVICE_IDENTIFIER);
+        Assert.assertNull(serviceGroup);
+    }
+
+    /**
+     * Test checks validation Error processing for MASTER-TO-SLAVE closeEntity Candidate role change
+     * without closeEntity registration
+     *
+     * @throws Exception - unexpected exception
+     */
+    @Test
+    public void checkClosingUnexpectedDoubleEntityForSlaveOwnershipChangeRegistrationTest() throws Exception {
+        singletonServiceGroup.initializationClusterSingletonGroup();
+        map.putIfAbsent(SERVICE_IDENTIFIER, singletonServiceGroup);
+        verify(mockEosService).registerCandidate(mainEntity);
+        final ClusterSingletonServiceRegistration reg = singletonServiceGroup
+                .registerService(mockClusterSingletonService);
+        Assert.assertNotNull(reg);
+        singletonServiceGroup.ownershipChanged(getEntityToSlave());
+        verify(mockClusterSingletonService, never()).instantiateServiceInstance();
+        verify(mockEosService, never()).registerCandidate(closeEntity);
+        singletonServiceGroup.ownershipChanged(getDoubleEntityToSlave());
+        verify(mockClusterSingletonService, never()).closeServiceInstance();
+        final ClusterSingletonServiceGroup<?, ?, ?> serviceGroup = map.get(SERVICE_IDENTIFIER);
+        Assert.assertNull(serviceGroup);
+    }
+
+    private GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity> getEntityToMaster() {
+        return new GenericEntityOwnershipChange<>(mainEntity, EntityOwnershipChangeState.from(false, true, true));
+    }
+
+    private GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity> getEntityToSlave() {
+        return new GenericEntityOwnershipChange<>(mainEntity, EntityOwnershipChangeState.from(true, false, true));
+    }
+
+    private GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity> getEntityToSlaveNoMaster() {
+        return new GenericEntityOwnershipChange<>(mainEntity, EntityOwnershipChangeState.from(true, false, false));
+    }
+
+    private GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity> getDoubleEntityToMaster() {
+        return new GenericEntityOwnershipChange<>(closeEntity, EntityOwnershipChangeState.from(false, true, true));
+    }
+
+    private GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity> getDoubleEntityToSlave() {
+        return new GenericEntityOwnershipChange<>(closeEntity, EntityOwnershipChangeState.from(true, false, true));
+    }
+
+    private GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity> getInitDoubleEntityToSlave() {
+        return new GenericEntityOwnershipChange<>(closeEntity, EntityOwnershipChangeState.from(false, false, true));
+    }
+
+    private GenericEntityOwnershipChange<TestInstanceIdentifier, TestEntity> getEntityToJeopardy() {
+        return new GenericEntityOwnershipChange<>(mainEntity, EntityOwnershipChangeState.from(false, false, false), true);
+    }
+
+}
diff --git a/singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceRegistrationDelegatorTest.java b/singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/ClusterSingletonServiceRegistrationDelegatorTest.java
new file mode 100644 (file)
index 0000000..fed7785
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.mdsal.singleton.common.spi;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import com.google.common.util.concurrent.Futures;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.mdsal.singleton.common.api.ClusterSingletonService;
+import org.opendaylight.mdsal.singleton.common.api.ServiceGroupIdentifier;
+
+/**
+ * Testing {@link ClusterSingletonServiceRegistrationDelegator}
+ */
+public class ClusterSingletonServiceRegistrationDelegatorTest {
+
+    private static final String SERVICE_IDENTIFIER_NAME = "TestServiceIdent";
+    private static final ServiceGroupIdentifier SERVICE_IDENTIFIER = ServiceGroupIdentifier
+            .create(SERVICE_IDENTIFIER_NAME);
+
+    @Mock
+    private ClusterSingletonServiceGroup<?, ?, ?> mockClusterSingletonServiceGroup;
+    @Mock
+    private ClusterSingletonService mockClusterSingletonService;
+
+    private ClusterSingletonServiceRegistrationDelegator delegator;
+
+    /**
+     * Initialization functionality for every Tests in this suite
+     *
+     * @throws Exception - unexpected setup exception
+     */
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        doNothing().when(mockClusterSingletonServiceGroup)
+                .unregisterService(any(ClusterSingletonServiceRegistrationDelegator.class));
+        doReturn(SERVICE_IDENTIFIER).when(mockClusterSingletonService).getIdentifier();
+        doNothing().when(mockClusterSingletonService).instantiateServiceInstance();
+        doReturn(Futures.immediateFuture(null)).when(mockClusterSingletonService).closeServiceInstance();
+        delegator = new ClusterSingletonServiceRegistrationDelegator(mockClusterSingletonService,
+                mockClusterSingletonServiceGroup);
+    }
+
+    /**
+     * Test create input with {@link ClusterSingletonService} as null
+     */
+    @Test(expected = NullPointerException.class)
+    public void testSetupNullService() {
+        delegator = new ClusterSingletonServiceRegistrationDelegator(null, mockClusterSingletonServiceGroup);
+    }
+
+    /**
+     * Test create input with {@link ClusterSingletonServiceGroupImpl} as null
+     */
+    @Test(expected = NullPointerException.class)
+    public void testSetupNullGroup() {
+        delegator = new ClusterSingletonServiceRegistrationDelegator(mockClusterSingletonService, null);
+    }
+
+    /**
+     * Test a method delegation {@link ClusterSingletonService#instantiateServiceInstance()}
+     */
+    @Test
+    public void testInstatiateServiceDelegMethod() {
+        delegator.instantiateServiceInstance();
+        verify(mockClusterSingletonService).instantiateServiceInstance();
+    }
+
+    /**
+     * Test a method delegation {@link ClusterSingletonService#instantiateServiceInstance()}
+     */
+    @Test
+    public void testCloseServiceDelegMethod() {
+        delegator.closeServiceInstance();
+        verify(mockClusterSingletonService).closeServiceInstance();
+    }
+
+    /**
+     * Test a method delegation {@link ClusterSingletonService#getIdentifier()}
+     */
+    @Test
+    public void testGetServiceIdentifierDelegMethod() {
+        final String serviceIdentifier = delegator.getServiceGroupIdentifier();
+        Assert.assertEquals(SERVICE_IDENTIFIER_NAME, serviceIdentifier);
+        verify(mockClusterSingletonService).getIdentifier();
+    }
+
+    /**
+     * Test a close method delegation to {@link ClusterSingletonServiceGroupImpl#unregisterService(ClusterSingletonService)}
+     *
+     * @throws Exception is from AutoclosableInterface
+     */
+    @Test
+    public void testCloseMethod() throws Exception {
+        delegator.close();
+        verify(mockClusterSingletonServiceGroup).unregisterService(delegator);
+    }
+}
diff --git a/singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/util/TestEntity.java b/singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/util/TestEntity.java
new file mode 100644 (file)
index 0000000..d2d55d7
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.mdsal.singleton.common.spi.util;
+
+import javax.annotation.Nonnull;
+import org.opendaylight.mdsal.eos.common.api.GenericEntity;
+import org.opendaylight.yangtools.yang.common.QName;
+
+/**
+ * Test util class.
+ */
+public class TestEntity extends GenericEntity<TestInstanceIdentifier> {
+    private static final long serialVersionUID = 1L;
+
+    static final QName ENTITY = QName
+            .create("urn:opendaylight:params:xml:ns:yang:mdsal:core:test-entity", "2016-06-06", "entity").intern();
+    static final QName ENTITY_NAME = QName.create(ENTITY, "name").intern();
+
+    /** Constructs an instance.
+     *
+     * @param type the entity type
+     * @param id the entity id.
+     */
+    public TestEntity(@Nonnull final String type, @Nonnull final TestInstanceIdentifier id) {
+        super(type, id);
+    }
+
+    /**
+     * Construct an Entity with an with a name. The general-entity schema is used to construct the
+     * {@link TestInstanceIdentifier}.
+     *
+     * @param type the type of the entity
+     * @param entityName the name of the entity used to construct a general-entity YangInstanceIdentifier
+     */
+    public TestEntity(@Nonnull final String type, @Nonnull final String entityName) {
+        super(type, new TestInstanceIdentifier(entityName));
+    }
+}
diff --git a/singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/util/TestInstanceIdentifier.java b/singleton-service/mdsal-singleton-common-spi/src/test/java/org/opendaylight/mdsal/singleton/common/spi/util/TestInstanceIdentifier.java
new file mode 100644 (file)
index 0000000..feb3227
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.mdsal.singleton.common.spi.util;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import java.util.LinkedList;
+import java.util.List;
+import org.opendaylight.yangtools.concepts.Path;
+
+/**
+ * Test helper class. {@link Path} for testing only
+ */
+public class TestInstanceIdentifier implements Path<TestInstanceIdentifier> {
+
+    static final TestInstanceIdentifier EMPTY_INSTANCE = new TestInstanceIdentifier(ImmutableList.of());
+
+    private final ImmutableList<String> path;
+
+    /**
+     * {@link TestInstanceIdentifier} constructor
+     *
+     * @param path - path
+     */
+    public TestInstanceIdentifier(final Iterable<? extends TestInstanceIdentifier> path) {
+        Preconditions.checkArgument(path != null);
+        final List<String> tiis = new LinkedList<>();
+        for (final TestInstanceIdentifier t : path) {
+            tiis.add(t.toString());
+        }
+        this.path = ImmutableList.copyOf(tiis);
+    }
+
+    TestInstanceIdentifier(final String path) {
+        this.path = ImmutableList.of(path);
+    }
+
+    @Override
+    public boolean contains(final TestInstanceIdentifier other) {
+        return path.contains(other);
+    }
+
+}
index 65137d8cc5e2b9177922030425e85df95b329e43..b965b7125ffc63801893eafe628a200d353cbe04 100644 (file)
@@ -27,6 +27,7 @@
       <module>mdsal-singleton-common-api</module>
       <module>mdsal-singleton-dom-api</module>
       <module>mdsal-singleton-binding-api</module>
+      <module>mdsal-singleton-common-spi</module>
     </modules>