You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 09:25:30 UTC

[sling-org-apache-sling-discovery-base] annotated tag org.apache.sling.discovery.base-1.0.0 created (now 7294a6f)

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

rombert pushed a change to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git.


      at 7294a6f  (tag)
 tagging 73e6df43d630614fe80a7f978c4a893782f3b6a1 (commit)
      by Stefan Egli
      on Mon Oct 26 16:18:39 2015 +0000

- Log -----------------------------------------------------------------
org.apache.sling.discovery.base-1.0.0
-----------------------------------------------------------------------

This annotated tag includes the following new commits:

     new 976d955  SLING-5173 : introducing discovery.base which is the sharable parts of discovery.impl for discovery.oak - eg it includes topology connectors and base classes - plus it also includes many it-kind tests of discovery.impl
     new 973f209  SLING-5173 : fixed getSocketConnectionTimeout to a more proper getSocketConnectTimeout
     new e375668  SLING-5173 : rename commons impl packages to base as they are meant for reuse by discovery.impl and discovery.oak - plus avoid using abstract component class with scr annotations - use abstract getters instead - plus some more fine-tuning of log messages - plus make discovery.impl's Config also implement DiscoveryLiteConfig - plus properly handle binds happening before activate in DiscoveryServiceImpl
     new 117b474  SLING-5173 related : minor wording fix
     new c854b64  SLING-5173 : fixed test logic
     new c2d2fec  SLING-5196 : using maven-bundle-plugin 2.5.3 to remain backwards-compatibility
     new 64c9956  SLING-5173 : using discovery.commons 1.0.0
     new a40c75a  [maven-release-plugin] prepare release org.apache.sling.discovery.base-1.0.0
     new 73e6df4  [maven-release-plugin] copy for tag org.apache.sling.discovery.base-1.0.0

The 9 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


-- 
To stop receiving notification emails like this one, please contact
['"commits@sling.apache.org" <co...@sling.apache.org>'].

[sling-org-apache-sling-discovery-base] 04/09: SLING-5173 related : minor wording fix

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git

commit 117b4743baa7a7651a0135b8862cbcda74cdba2c
Author: Stefan Egli <st...@apache.org>
AuthorDate: Wed Oct 21 10:59:44 2015 +0000

    SLING-5173 related : minor wording fix
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base@1709792 13f79535-47bb-0310-9956-ffa450edef68
---
 .../org/apache/sling/discovery/base/commons/DefaultTopologyView.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/org/apache/sling/discovery/base/commons/DefaultTopologyView.java b/src/main/java/org/apache/sling/discovery/base/commons/DefaultTopologyView.java
index bc97d79..653e6d8 100644
--- a/src/main/java/org/apache/sling/discovery/base/commons/DefaultTopologyView.java
+++ b/src/main/java/org/apache/sling/discovery/base/commons/DefaultTopologyView.java
@@ -237,7 +237,7 @@ public class DefaultTopologyView extends BaseTopologyView {
 
     @Override
     public String toString() {
-        return "TopologyViewImpl [current=" + isCurrent() + ", num=" + instances.size() + ", instances="
+        return "DefaultTopologyView[current=" + isCurrent() + ", num=" + instances.size() + ", instances="
                 + instances + "]";
     }
 

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-discovery-base] 05/09: SLING-5173 : fixed test logic

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git

commit c854b6484e729360d71009bb7df643ee90b6403a
Author: Stefan Egli <st...@apache.org>
AuthorDate: Wed Oct 21 17:10:11 2015 +0000

    SLING-5173 : fixed test logic
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base@1709888 13f79535-47bb-0310-9956-ffa450edef68
---
 .../sling/discovery/base/its/AbstractTopologyEventTest.java | 13 +------------
 1 file changed, 1 insertion(+), 12 deletions(-)

diff --git a/src/test/java/org/apache/sling/discovery/base/its/AbstractTopologyEventTest.java b/src/test/java/org/apache/sling/discovery/base/its/AbstractTopologyEventTest.java
index d8c7792..0280c15 100644
--- a/src/test/java/org/apache/sling/discovery/base/its/AbstractTopologyEventTest.java
+++ b/src/test/java/org/apache/sling/discovery/base/its/AbstractTopologyEventTest.java
@@ -88,23 +88,12 @@ public abstract class AbstractTopologyEventTest {
                 .setConnectorPingTimeout(3 /* heartbeat-timeout */)
                 .setMinEventDelay(3 /*min event delay*/).build();
         AssertingTopologyEventListener l1 = new AssertingTopologyEventListener("instance1.l1");
+        l1.addExpected(Type.TOPOLOGY_INIT);
         instance1.bindTopologyEventListener(l1);
         logger.info("testDelayedInitEvent: instance1 created, no events expected yet. slingId="+instance1.slingId);
         
-        // should not have received any events yet
-        assertEquals(0, l1.getEvents().size());
-        assertEquals(0, l1.getUnexpectedCount());
-
-        // one heartbeat doesn't make a day yet - and is 2sec too early for the init
         instance1.heartbeatsAndCheckView();
         Thread.sleep(1200);
-        logger.info("testDelayedInitEvent: even after 500ms no events expected, as it needs more than 1 heartbeat");
-        // should not have received any events yet
-        assertEquals(0, l1.getEvents().size());
-        assertEquals(0, l1.getUnexpectedCount());
-        
-        // but two are a good start
-        l1.addExpected(Type.TOPOLOGY_INIT);
         instance1.heartbeatsAndCheckView();
         Thread.sleep(1200);
         instance1.heartbeatsAndCheckView();

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-discovery-base] 08/09: [maven-release-plugin] prepare release org.apache.sling.discovery.base-1.0.0

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git

commit a40c75a460e560032b0eaf218c7ed7431aaab64f
Author: Stefan Egli <st...@apache.org>
AuthorDate: Mon Oct 26 16:18:22 2015 +0000

    [maven-release-plugin] prepare release org.apache.sling.discovery.base-1.0.0
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base@1710646 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index 05267ad..378bab9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,15 +29,15 @@
 
     <artifactId>org.apache.sling.discovery.base</artifactId>
     <packaging>bundle</packaging>
-    <version>1.0.0-SNAPSHOT</version>
+    <version>1.0.0</version>
 
     <name>Apache Sling Discovery Base</name>
     <description>Contains Connector and Properties support that some implementations might choose to build upon</description>
 
     <scm>
-        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base</connection>
-        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base</developerConnection>
-        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base</url>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.discovery.base-1.0.0</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.discovery.base-1.0.0</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/tags/org.apache.sling.discovery.base-1.0.0</url>
     </scm>
 
     <build>

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-discovery-base] 09/09: [maven-release-plugin] copy for tag org.apache.sling.discovery.base-1.0.0

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git

commit 73e6df43d630614fe80a7f978c4a893782f3b6a1
Author: Stefan Egli <st...@apache.org>
AuthorDate: Mon Oct 26 16:18:39 2015 +0000

    [maven-release-plugin] copy for tag org.apache.sling.discovery.base-1.0.0
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.discovery.base-1.0.0@1710647 13f79535-47bb-0310-9956-ffa450edef68

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-discovery-base] 03/09: SLING-5173 : rename commons impl packages to base as they are meant for reuse by discovery.impl and discovery.oak - plus avoid using abstract component class with scr annotations - use abstract getters instead - plus some more fine-tuning of log messages - plus make discovery.impl's Config also implement DiscoveryLiteConfig - plus properly handle binds happening before activate in DiscoveryServiceImpl

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git

commit e37566826399bf3faf79c1b16e38fee80b1720c5
Author: Stefan Egli <st...@apache.org>
AuthorDate: Wed Oct 21 08:29:08 2015 +0000

    SLING-5173 : rename commons impl packages to base as they are meant for reuse by discovery.impl and discovery.oak - plus avoid using abstract component class with scr annotations - use abstract getters instead - plus some more fine-tuning of log messages - plus make discovery.impl's Config also implement DiscoveryLiteConfig - plus properly handle binds happening before activate in DiscoveryServiceImpl
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base@1709751 13f79535-47bb-0310-9956-ffa450edef68
---
 .../base/commons/BaseDiscoveryService.java         | 17 ++----
 .../discovery/base/commons/BaseViewChecker.java    | 66 +++++++++-------------
 .../base/commons/DummyDiscoveryService.java        | 15 -----
 .../announcement/AnnouncementRegistryImplTest.java |  2 +-
 .../connectors/ping/ConnectorRegistryImplTest.java |  2 +-
 .../discovery/base/its/setup/VirtualInstance.java  |  2 +-
 .../base/its/setup/VirtualInstanceBuilder.java     |  2 +-
 .../base/its/setup/mock/DummyViewChecker.java      | 47 +++++++++++++++
 8 files changed, 84 insertions(+), 69 deletions(-)

diff --git a/src/main/java/org/apache/sling/discovery/base/commons/BaseDiscoveryService.java b/src/main/java/org/apache/sling/discovery/base/commons/BaseDiscoveryService.java
index cf9563d..40afb95 100644
--- a/src/main/java/org/apache/sling/discovery/base/commons/BaseDiscoveryService.java
+++ b/src/main/java/org/apache/sling/discovery/base/commons/BaseDiscoveryService.java
@@ -47,12 +47,6 @@ public abstract class BaseDiscoveryService implements DiscoveryService {
     
     protected abstract void handleIsolatedFromTopology();
     
-    public abstract void updateProperties();
-
-    public abstract void handlePotentialTopologyChange();
-
-    public abstract void handleTopologyChanging();
-
     protected DefaultTopologyView getOldView() {
         return oldView;
     }
@@ -69,16 +63,17 @@ public abstract class BaseDiscoveryService implements DiscoveryService {
      * @see DiscoveryService#getTopology()
      */
     public TopologyView getTopology() {
-        ClusterViewService clusterViewService = getClusterViewService();
-        if (clusterViewService == null) {
-            throw new IllegalStateException(
-                    "DiscoveryService not yet initialized with IClusterViewService");
-        }
         // create a new topology view
         final DefaultTopologyView topology = new DefaultTopologyView();
 
         LocalClusterView localClusterView = null;
         try {
+            ClusterViewService clusterViewService = getClusterViewService();
+            if (clusterViewService == null) {
+                throw new UndefinedClusterViewException(
+                        Reason.REPOSITORY_EXCEPTION,
+                        "no ClusterViewService available at the moment");
+            }
             localClusterView = clusterViewService.getLocalClusterView();
             topology.setLocalClusterView(localClusterView);
         } catch (UndefinedClusterViewException e) {
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/BaseViewChecker.java b/src/main/java/org/apache/sling/discovery/base/commons/BaseViewChecker.java
index 516e8ec..754bb18 100644
--- a/src/main/java/org/apache/sling/discovery/base/commons/BaseViewChecker.java
+++ b/src/main/java/org/apache/sling/discovery/base/commons/BaseViewChecker.java
@@ -28,7 +28,6 @@ import java.util.UUID;
 
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Deactivate;
-import org.apache.felix.scr.annotations.Reference;
 import org.apache.sling.api.resource.ResourceResolverFactory;
 import org.apache.sling.commons.scheduler.Scheduler;
 import org.apache.sling.discovery.base.connectors.BaseConfig;
@@ -66,27 +65,6 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
     /** the name used for the period job with the scheduler **/
     protected String NAME = "discovery.impl.heartbeat.runner.";
 
-    @Reference
-    protected SlingSettingsService slingSettingsService;
-
-    @Reference
-    protected ResourceResolverFactory resourceResolverFactory;
-
-    @Reference
-    protected ConnectorRegistry connectorRegistry;
-
-    @Reference
-    protected AnnouncementRegistry announcementRegistry;
-
-    @Reference
-    protected Scheduler scheduler;
-
-    @Reference
-    protected BaseConfig connectorConfig;
-
-    /** the discovery service reference is used to get properties updated before heartbeats are sent **/
-    protected BaseDiscoveryService discoveryService;
-
     /** the sling id of the local instance **/
     protected String slingId;
     
@@ -116,6 +94,18 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
     		startupFinished(mode);
     	}
     }
+    
+    protected abstract SlingSettingsService getSlingSettingsService();
+
+    protected abstract ResourceResolverFactory getResourceResolverFactory();
+
+    protected abstract ConnectorRegistry getConnectorRegistry();
+
+    protected abstract AnnouncementRegistry getAnnouncementRegistry();
+
+    protected abstract Scheduler getScheduler();
+
+    protected abstract BaseConfig getConnectorConfig();
 
     public void startupFinished(StartupMode mode) {
     	synchronized(lock) {
@@ -133,7 +123,7 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
     	synchronized(lock) {
     		this.context = context;
 
-	        slingId = slingSettingsService.getSlingId();
+	        slingId = getSlingSettingsService().getSlingId();
 	        NAME = "discovery.connectors.common.runner." + slingId;
 
 	        doActivate();
@@ -143,9 +133,9 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
 
     protected void doActivate() {
         try {
-            final long interval = connectorConfig.getConnectorPingInterval();
+            final long interval = getConnectorConfig().getConnectorPingInterval();
             logger.info("doActivate: starting periodic connectorPing job for "+slingId+" with interval "+interval+" sec.");
-            scheduler.addPeriodicJob(NAME, this,
+            getScheduler().addPeriodicJob(NAME, this,
                     null, interval, false);
         } catch (Exception e) {
             logger.error("doActivate: Could not start connectorPing runner: " + e, e);
@@ -158,7 +148,7 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
         // SLING-3365 : dont synchronize on deactivate
         activated = false;
         logger.info("deactivate: deactivated slingId: {}, this: {}", slingId, this);
-    	scheduler.removeJob(NAME);
+    	getScheduler().removeJob(NAME);
     }
     
     /** for testing only **/
@@ -175,7 +165,7 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
     
     @Override
     public void heartbeatAndCheckView() {
-        logger.info("run: start. [ConnectorPinger of slingId="+slingId+"]");
+        logger.debug("run: start. [for slingId="+slingId+"]");
         synchronized(lock) {
         	if (!activated) {
         		// SLING:2895: avoid heartbeats if not activated
@@ -189,7 +179,7 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
             // check the view
             doCheckView();
         }
-        logger.info("run: end. [ConnectorPinger of slingId="+slingId+"]");
+        logger.debug("run: end. [for slingId="+slingId+"]");
     }
 
     /** Trigger the issuance of the next heartbeat asap instead of at next heartbeat interval **/
@@ -201,7 +191,7 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
             // 'fireJob' checks for a job from the same job-class to already exist
             // 'fireJobAt' though allows to pass a name for the job - which can be made unique, thus does not conflict/already-exist
             logger.info("triggerConnectorPing: firing job to trigger heartbeat");
-            scheduler.fireJobAt(NAME+UUID.randomUUID(), this, null, new Date(System.currentTimeMillis()-1000 /* make sure it gets triggered immediately*/));
+            getScheduler().fireJobAt(NAME+UUID.randomUUID(), this, null, new Date(System.currentTimeMillis()-1000 /* make sure it gets triggered immediately*/));
         } catch (Exception e) {
             logger.info("triggerConnectorPing: Could not trigger heartbeat: " + e);
         }
@@ -216,18 +206,16 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
      * which announce this part of the topology to others)
      */
     protected void issueHeartbeat() {
-        if (discoveryService == null) {
-            logger.error("issueHeartbeat: discoveryService is null");
-        } else {
-            discoveryService.updateProperties();
-        }
-//        issueClusterLocalHeartbeat();
+        updateProperties();
+
         issueConnectorPings();
     }
 
+    protected abstract void updateProperties();
+
     /** Issue a remote heartbeat using the topology connectors **/
     protected void issueConnectorPings() {
-        if (connectorRegistry == null) {
+        if (getConnectorRegistry() == null) {
             logger.error("issueConnectorPings: connectorRegistry is null");
             return;
         }
@@ -238,7 +226,7 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
         if (logger.isDebugEnabled()) {
             logger.debug("issueConnectorPings: pinging outgoing topology connectors (if there is any) for "+slingId);
         }
-        connectorRegistry.pingOutgoingConnectors(forcePing);
+        getConnectorRegistry().pingOutgoingConnectors(forcePing);
         forcePing = false;
     }
 
@@ -247,11 +235,11 @@ public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupL
      */
     protected void doCheckView() {
         // check the remotes first
-        if (announcementRegistry == null) {
+        if (getAnnouncementRegistry() == null) {
             logger.error("announcementRegistry is null");
             return;
         }
-        announcementRegistry.checkExpiredAnnouncements();
+        getAnnouncementRegistry().checkExpiredAnnouncements();
     }
 
     /**
diff --git a/src/test/java/org/apache/sling/discovery/base/commons/DummyDiscoveryService.java b/src/test/java/org/apache/sling/discovery/base/commons/DummyDiscoveryService.java
index ddde4fc..c7928ab 100644
--- a/src/test/java/org/apache/sling/discovery/base/commons/DummyDiscoveryService.java
+++ b/src/test/java/org/apache/sling/discovery/base/commons/DummyDiscoveryService.java
@@ -39,11 +39,6 @@ public class DummyDiscoveryService extends BaseDiscoveryService {
     }
     
     @Override
-    public void updateProperties() {
-        throw new IllegalStateException("updateProperties not yet impl");
-    }
-
-    @Override
     protected ClusterViewService getClusterViewService() {
         return clusterViewService;
     }
@@ -58,14 +53,4 @@ public class DummyDiscoveryService extends BaseDiscoveryService {
         throw new IllegalStateException("handleIsolatedFromTopology not yet impl");
     }
     
-    @Override
-    public void handlePotentialTopologyChange() {
-        throw new IllegalStateException("handlePotentialTopologyChange not yet impl");
-    }
-    
-    @Override
-    public void handleTopologyChanging() {
-        throw new IllegalStateException("handleTopologyChanging not yet impl");
-    }
-    
 }
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImplTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImplTest.java
index 6a38bea..062a56b 100644
--- a/src/test/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImplTest.java
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImplTest.java
@@ -40,7 +40,7 @@ import org.apache.sling.discovery.base.its.setup.mock.MockFactory;
 import org.apache.sling.discovery.base.its.setup.mock.SimpleConnectorConfig;
 import org.apache.sling.discovery.commons.providers.DefaultClusterView;
 import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
-import org.apache.sling.discovery.commons.providers.spi.impl.DummySlingSettingsService;
+import org.apache.sling.discovery.commons.providers.spi.base.DummySlingSettingsService;
 import org.junit.Before;
 import org.junit.Test;
 
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImplTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImplTest.java
index 9972e85..f743418 100644
--- a/src/test/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImplTest.java
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImplTest.java
@@ -31,7 +31,7 @@ import org.apache.sling.discovery.base.its.setup.VirtualInstance;
 import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
 import org.apache.sling.discovery.base.its.setup.mock.MockFactory;
 import org.apache.sling.discovery.base.its.setup.mock.SimpleConnectorConfig;
-import org.apache.sling.discovery.commons.providers.spi.impl.DummySlingSettingsService;
+import org.apache.sling.discovery.commons.providers.spi.base.DummySlingSettingsService;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstance.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstance.java
index a21becb..9e10871 100644
--- a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstance.java
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstance.java
@@ -338,7 +338,7 @@ public class VirtualInstance {
         try {
             stopViewChecker();
         } catch (Throwable e) {
-            throw new Exception("Caught Throwable in stopConnectorPinger: "+e, e);
+            throw new Exception("Caught Throwable in stop(): "+e, e);
         }
 
         if (resourceResolver != null) {
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceBuilder.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceBuilder.java
index feaaf97..fb287d6 100644
--- a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceBuilder.java
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceBuilder.java
@@ -33,7 +33,7 @@ import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegis
 import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
 import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistryImpl;
 import org.apache.sling.discovery.base.its.setup.mock.FailingScheduler;
-import org.apache.sling.discovery.commons.providers.spi.impl.DummySlingSettingsService;
+import org.apache.sling.discovery.commons.providers.spi.base.DummySlingSettingsService;
 import org.apache.sling.settings.SlingSettingsService;
 
 import junitx.util.PrivateAccessor;
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/DummyViewChecker.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/DummyViewChecker.java
index e66ec09..e83556c 100644
--- a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/DummyViewChecker.java
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/DummyViewChecker.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sling.discovery.base.its.setup.mock;
 
+import org.apache.felix.scr.annotations.Reference;
 import org.apache.sling.api.resource.ResourceResolverFactory;
 import org.apache.sling.commons.scheduler.Scheduler;
 import org.apache.sling.discovery.base.commons.BaseViewChecker;
@@ -28,6 +29,18 @@ import org.apache.sling.settings.SlingSettingsService;
 
 public class DummyViewChecker extends BaseViewChecker {
     
+    protected SlingSettingsService slingSettingsService;
+
+    protected ResourceResolverFactory resourceResolverFactory;
+
+    protected ConnectorRegistry connectorRegistry;
+
+    protected AnnouncementRegistry announcementRegistry;
+
+    protected Scheduler scheduler;
+
+    protected BaseConfig connectorConfig;
+
     public static DummyViewChecker testConstructor(
             SlingSettingsService slingSettingsService,
             ResourceResolverFactory resourceResolverFactory,
@@ -45,4 +58,38 @@ public class DummyViewChecker extends BaseViewChecker {
         return pinger;
     }
 
+    @Override
+    protected SlingSettingsService getSlingSettingsService() {
+        return slingSettingsService;
+    }
+
+    @Override
+    protected ResourceResolverFactory getResourceResolverFactory() {
+        return resourceResolverFactory;
+    }
+
+    @Override
+    protected ConnectorRegistry getConnectorRegistry() {
+        return connectorRegistry;
+    }
+
+    @Override
+    protected AnnouncementRegistry getAnnouncementRegistry() {
+        return announcementRegistry;
+    }
+
+    @Override
+    protected Scheduler getScheduler() {
+        return scheduler;
+    }
+
+    @Override
+    protected BaseConfig getConnectorConfig() {
+        return connectorConfig;
+    }
+
+    @Override
+    protected void updateProperties() {
+        // nothing done for the dummyViewChecker
+    }
 }

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-discovery-base] 06/09: SLING-5196 : using maven-bundle-plugin 2.5.3 to remain backwards-compatibility

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git

commit c2d2fec441f98d00007430f435dfefc0989509d6
Author: Stefan Egli <st...@apache.org>
AuthorDate: Mon Oct 26 13:39:12 2015 +0000

    SLING-5196 : using maven-bundle-plugin 2.5.3 to remain backwards-compatibility
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base@1710596 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/pom.xml b/pom.xml
index 764b776..7a33c64 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,13 @@
             <plugin>
                 <groupId>org.apache.felix</groupId>
                 <artifactId>maven-bundle-plugin</artifactId>
+                <!--  with 3.0.0 the following bundle dependency is failing
+                      on AEM 5.6.1 - thus switching back to 2.5.3 here:
+					javax.servlet,version=[2.6,3) - - Cannot be resolved
+					javax.servlet.http,version=[2.6,3) - - Cannot be resolved
+					org.apache.commons.codec.binary,version=[1.6,2) - - Cannot be resolved
+                       -->
+                <version>2.5.3</version>
                 <extensions>true</extensions>
                 <configuration>
                   <instructions>

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-discovery-base] 07/09: SLING-5173 : using discovery.commons 1.0.0

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git

commit 64c995676afad4fcf75f06d37101cd0117b75d0c
Author: Stefan Egli <st...@apache.org>
AuthorDate: Mon Oct 26 16:16:13 2015 +0000

    SLING-5173 : using discovery.commons 1.0.0
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base@1710645 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index 7a33c64..05267ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -151,7 +151,7 @@
  		<dependency>
 			<groupId>org.apache.sling</groupId>
 			<artifactId>org.apache.sling.discovery.commons</artifactId>
-			<version>1.0.0-SNAPSHOT</version>
+			<version>1.0.0</version>
   		</dependency>
         <!-- besides including discovery.commons' normal jar above, 
               for testing a few test helper classes are also reused.
@@ -159,7 +159,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.discovery.commons</artifactId>
-            <version>1.0.0-SNAPSHOT</version>
+            <version>1.0.0</version>
             <scope>test</scope>
             <type>test-jar</type>
         </dependency>

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-discovery-base] 02/09: SLING-5173 : fixed getSocketConnectionTimeout to a more proper getSocketConnectTimeout

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git

commit 973f2095c3c7eca66f084a15ef5f68184265be13
Author: Stefan Egli <st...@apache.org>
AuthorDate: Tue Oct 20 14:44:27 2015 +0000

    SLING-5173 : fixed getSocketConnectionTimeout to a more proper getSocketConnectTimeout
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base@1709617 13f79535-47bb-0310-9956-ffa450edef68
---
 .../java/org/apache/sling/discovery/base/connectors/BaseConfig.java   | 2 +-
 .../sling/discovery/base/connectors/ping/TopologyConnectorClient.java | 4 ++--
 .../sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java    | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/BaseConfig.java b/src/main/java/org/apache/sling/discovery/base/connectors/BaseConfig.java
index fdc0435..cf881c2 100644
--- a/src/main/java/org/apache/sling/discovery/base/connectors/BaseConfig.java
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/BaseConfig.java
@@ -29,7 +29,7 @@ public interface BaseConfig {
      * Returns the socket connect() timeout used by the topology connector, 0 disables the timeout
      * @return the socket connect() timeout used by the topology connector, 0 disables the timeout
      */
-    public int getSocketConnectionTimeout();
+    public int getSocketConnectTimeout();
 
     /**
      * Returns the socket read timeout (SO_TIMEOUT) used by the topology connector, 0 disables the timeout
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java
index 5686ec5..a5db553 100644
--- a/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java
@@ -166,7 +166,7 @@ public class TopologyConnectorClient implements
     	// setting the connection timeout (idle connection, configured in seconds)
     	putRequest.setConfig(RequestConfig.
     			custom().
-    			setConnectTimeout(1000*config.getSocketConnectionTimeout()).
+    			setConnectTimeout(1000*config.getSocketConnectTimeout()).
     			build());
 
         Announcement resultingAnnouncement = null;
@@ -460,7 +460,7 @@ public class TopologyConnectorClient implements
         // setting the connection timeout (idle connection, configured in seconds)
         deleteRequest.setConfig(RequestConfig.
         		custom().
-        		setConnectTimeout(1000*config.getSocketConnectionTimeout()).
+        		setConnectTimeout(1000*config.getSocketConnectTimeout()).
         		build());
 
         try {
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java
index 5bffff4..bf2157e 100644
--- a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java
@@ -45,7 +45,7 @@ public class SimpleConnectorConfig implements ModifiableTestBaseConfig {
     private int minEventDelay;
 
     @Override
-    public int getSocketConnectionTimeout() {
+    public int getSocketConnectTimeout() {
         return connectionTimeout;
     }
     

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-discovery-base] 01/09: SLING-5173 : introducing discovery.base which is the sharable parts of discovery.impl for discovery.oak - eg it includes topology connectors and base classes - plus it also includes many it-kind tests of discovery.impl

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.discovery.base-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-base.git

commit 976d955d1b9d7678e19ff3cddd1b0c7d90b60322
Author: Stefan Egli <st...@apache.org>
AuthorDate: Tue Oct 20 14:12:31 2015 +0000

    SLING-5173 : introducing discovery.base which is the sharable parts of discovery.impl for discovery.oak - eg it includes topology connectors and base classes - plus it also includes many it-kind tests of discovery.impl
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base@1709601 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |  295 ++++
 .../base/commons/BaseDiscoveryService.java         |  104 ++
 .../discovery/base/commons/BaseViewChecker.java    |  335 +++++
 .../discovery/base/commons/ClusterViewHelper.java  |   55 +
 .../discovery/base/commons/ClusterViewService.java |   47 +
 .../base/commons/DefaultTopologyView.java          |  252 ++++
 .../commons/UndefinedClusterViewException.java     |   64 +
 .../sling/discovery/base/commons/ViewChecker.java  |   40 +
 .../sling/discovery/base/commons/package-info.java |   30 +
 .../discovery/base/connectors/BaseConfig.java      |  129 ++
 .../base/connectors/announcement/Announcement.java |  460 ++++++
 .../announcement/AnnouncementFilter.java           |   32 +
 .../announcement/AnnouncementRegistry.java         |   66 +
 .../announcement/AnnouncementRegistryImpl.java     |  593 ++++++++
 .../announcement/CachedAnnouncement.java           |  122 ++
 .../base/connectors/announcement/package-info.java |   30 +
 .../discovery/base/connectors/package-info.java    |   30 +
 .../base/connectors/ping/ConnectorRegistry.java    |   44 +
 .../connectors/ping/ConnectorRegistryImpl.java     |  157 ++
 .../connectors/ping/TopologyConnectorClient.java   |  496 +++++++
 .../ping/TopologyConnectorClientInformation.java   |   63 +
 .../connectors/ping/TopologyConnectorServlet.java  |  360 +++++
 .../connectors/ping/TopologyRequestValidator.java  |  585 ++++++++
 .../base/connectors/ping/package-info.java         |   30 +
 .../connectors/ping/wl/SubnetWhitelistEntry.java   |   47 +
 .../base/connectors/ping/wl/WhitelistEntry.java    |   35 +
 .../base/connectors/ping/wl/WildcardHelper.java    |   52 +
 .../connectors/ping/wl/WildcardWhitelistEntry.java |   45 +
 .../base/connectors/ping/wl/package-info.java      |   30 +
 .../base/commons/DefaultTopologyViewTest.java      |  209 +++
 .../base/commons/DummyDiscoveryService.java        |   71 +
 .../connectors/DummyVirtualInstanceBuilder.java    |   85 ++
 .../base/connectors/LargeTopologyWithHubTest.java  |  123 ++
 .../announcement/AnnouncementRegistryImplTest.java |  391 +++++
 .../connectors/ping/ConnectorRegistryImplTest.java |  118 ++
 .../ping/TopologyConnectorServletTest.java         |  115 ++
 .../ping/TopologyRequestValidatorTest.java         |  180 +++
 .../connectors/ping/wl/WildcardHelperTest.java     |  117 ++
 .../base/its/AbstractClusterLoadTest.java          |  287 ++++
 .../discovery/base/its/AbstractClusterTest.java    | 1540 ++++++++++++++++++++
 .../base/its/AbstractSingleInstanceTest.java       |  293 ++++
 .../base/its/AbstractTopologyEventTest.java        |  252 ++++
 .../sling/discovery/base/its/TopologyTest.java     |  150 ++
 .../base/its/setup/ModifiableTestBaseConfig.java   |   37 +
 .../sling/discovery/base/its/setup/OSGiMock.java   |  124 ++
 .../discovery/base/its/setup/TopologyHelper.java   |  145 ++
 .../discovery/base/its/setup/VirtualConnector.java |   39 +
 .../discovery/base/its/setup/VirtualInstance.java  |  374 +++++
 .../base/its/setup/VirtualInstanceBuilder.java     |  238 +++
 .../base/its/setup/VirtualInstanceHelper.java      |   88 ++
 .../base/its/setup/VirtualRepository.java          |   27 +
 .../base/its/setup/WithholdingAppender.java        |   82 ++
 .../base/its/setup/mock/AcceptsMultiple.java       |   65 +
 .../setup/mock/AcceptsParticularTopologyEvent.java |   50 +
 .../setup/mock/AssertingTopologyEventListener.java |  160 ++
 .../base/its/setup/mock/DummyViewChecker.java      |   48 +
 .../base/its/setup/mock/FailingScheduler.java      |   72 +
 .../discovery/base/its/setup/mock/MockFactory.java |  126 ++
 .../base/its/setup/mock/MockedResource.java        |  297 ++++
 .../its/setup/mock/MockedResourceResolver.java     |  335 +++++
 .../base/its/setup/mock/PropertyProviderImpl.java  |   58 +
 .../its/setup/mock/SimpleClusterViewService.java   |   59 +
 .../base/its/setup/mock/SimpleConnectorConfig.java |  215 +++
 .../base/its/setup/mock/TopologyEventAsserter.java |   25 +
 src/test/resources/log4j.properties                |   26 +
 65 files changed, 11249 insertions(+)

diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..764b776
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,295 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+  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.
+-->
+<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/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>25</version>
+        <relativePath />
+    </parent>
+
+    <artifactId>org.apache.sling.discovery.base</artifactId>
+    <packaging>bundle</packaging>
+    <version>1.0.0-SNAPSHOT</version>
+
+    <name>Apache Sling Discovery Base</name>
+    <description>Contains Connector and Properties support that some implementations might choose to build upon</description>
+
+    <scm>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/discovery/base</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/base</url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                 <artifactId>maven-surefire-plugin</artifactId>
+	  	 <configuration>
+       		 	<redirectTestOutputToFile>false</redirectTestOutputToFile>
+       		 	<argLine>-Xmx2048m</argLine>
+        	 </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                  <instructions>
+		            <Embed-Dependency>
+		              commons-net;inline=org/apache/commons/net/util/SubnetUtils*
+		            </Embed-Dependency>
+		          </instructions>
+		        </configuration>
+            </plugin>
+			<!-- discovery.base exports a few test classes for reuse.
+                 In order for others to use these, the test-jar must be built/installed too.
+                 Note that 'mvn -Dmaven.test.skip=true' does NOT build the test-jar,
+                 however 'mvn -DskipTests' does. -->
+             <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>biz.aQute</groupId>
+            <artifactId>bndlib</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+	        <version>1.6.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.jcr.api</artifactId>
+			<version>2.1.0</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>javax.jcr</groupId>
+			<artifactId>jcr</artifactId>
+			<version>2.0</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.jackrabbit</groupId>
+			<artifactId>jackrabbit-api</artifactId>
+			<version>2.2.4</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.commons.osgi</artifactId>
+			<version>2.1.0</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.settings</artifactId>
+			<version>1.2.2</version>
+            <scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.discovery.api</artifactId>
+			<version>1.0.0</version>
+            <scope>provided</scope>
+		</dependency>
+ 		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.discovery.commons</artifactId>
+			<version>1.0.0-SNAPSHOT</version>
+  		</dependency>
+        <!-- besides including discovery.commons' normal jar above, 
+              for testing a few test helper classes are also reused.
+              in order to achieve that, also adding a test/test-jar dependency: --> 
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.discovery.commons</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+            <scope>test</scope>
+            <type>test-jar</type>
+        </dependency>
+		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.api</artifactId>
+			<version>2.4.0</version>
+            <scope>provided</scope>
+		</dependency>
+        <dependency>
+        	<groupId>org.apache.sling</groupId>
+        	<artifactId>org.apache.sling.commons.scheduler</artifactId>
+        	<version>2.3.4</version>
+            <scope>provided</scope>
+        </dependency>
+		<dependency>
+			<groupId>org.apache.felix</groupId>
+			<artifactId>org.apache.felix.webconsole</artifactId>
+			<version>3.0.0</version>
+            <scope>provided</scope>
+		</dependency>
+	    <dependency>
+	      <groupId>org.eclipse.jetty</groupId>
+	      <artifactId>jetty-servlet</artifactId>
+	      <version>8.1.2.v20120308</version>
+	      <scope>test</scope>
+	    </dependency>
+		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>servlet-api</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient-osgi</artifactId>
+			<version>4.3.5</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.commons.json</artifactId>
+			<version>2.0.6</version>
+            <scope>provided</scope>
+		</dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.launchpad.api</artifactId>
+		    <version>1.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-net</groupId>
+            <artifactId>commons-net</artifactId>
+            <version>3.3</version>
+            <scope>provided</scope>
+        </dependency>
+      <!-- Testing -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit-addons</groupId>
+            <artifactId>junit-addons</artifactId>
+            <version>1.4</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jmock</groupId>
+            <artifactId>jmock-junit4</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.9.5</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+        	<groupId>org.apache.sling</groupId>
+        	<artifactId>org.apache.sling.commons.testing</artifactId>
+        	<version>2.0.16</version>
+        	<scope>test</scope>
+            <exclusions>
+                <!-- slf4j simple implementation logs INFO + higher to stdout (we don't want that behaviour) -->
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-simple</artifactId>
+                </exclusion>
+                <!--  also excluding jcl-over-slf4j as we need a newer vesion of this which is compatible with slf4j 1.6 -->
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>jcl-over-slf4j</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        
+        <!-- using log4j under slf4j to allow fine-grained logging config (see src/test/resources/log4j.properties) -->
+        <dependency>
+        	<groupId>org.slf4j</groupId>
+        	<artifactId>slf4j-log4j12</artifactId>
+        	<version>1.7.5</version>
+        	<scope>test</scope>
+        </dependency>
+        <dependency>
+        	<groupId>org.apache.sling</groupId>
+        	<artifactId>org.apache.sling.jcr.resource</artifactId>
+        	<version>2.3.8</version>
+        	<scope>test</scope>
+        </dependency>
+	    <dependency>
+			<groupId>log4j</groupId>
+			<artifactId>log4j</artifactId>
+			<version>1.2.13</version>
+			<scope>test</scope>
+	    </dependency>
+        <dependency>
+        	<groupId>org.apache.sling</groupId>
+        	<artifactId>org.apache.sling.commons.threads</artifactId>
+        	<version>3.1.0</version>
+        	<type>bundle</type>
+        	<scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.tools</artifactId>
+            <version>1.0.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.sling</groupId>
+          <artifactId>org.apache.sling.testing.sling-mock</artifactId>
+          <version>1.2.0</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/BaseDiscoveryService.java b/src/main/java/org/apache/sling/discovery/base/commons/BaseDiscoveryService.java
new file mode 100644
index 0000000..cf9563d
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/BaseDiscoveryService.java
@@ -0,0 +1,104 @@
+/*
+ * 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.sling.discovery.base.commons;
+
+import java.util.Collection;
+
+import org.apache.sling.discovery.DiscoveryService;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException.Reason;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Abstract base class for DiscoveryService implementations which uses the 
+ * ClusterViewService plus Topology Connectors to calculate
+ * the current TopologyView
+ */
+public abstract class BaseDiscoveryService implements DiscoveryService {
+
+    private final static Logger logger = LoggerFactory.getLogger(BaseDiscoveryService.class);
+
+    /** the old view previously valid and sent to the TopologyEventListeners **/
+    private DefaultTopologyView oldView;
+
+    protected abstract ClusterViewService getClusterViewService();
+    
+    protected abstract AnnouncementRegistry getAnnouncementRegistry();
+    
+    protected abstract void handleIsolatedFromTopology();
+    
+    public abstract void updateProperties();
+
+    public abstract void handlePotentialTopologyChange();
+
+    public abstract void handleTopologyChanging();
+
+    protected DefaultTopologyView getOldView() {
+        return oldView;
+    }
+    
+    protected void setOldView(DefaultTopologyView view) {
+        if (view==null) {
+            throw new IllegalArgumentException("view must not be null");
+        }
+        logger.debug("setOldView: oldView is now: {}", oldView);
+        oldView = view;
+    }
+    
+    /**
+     * @see DiscoveryService#getTopology()
+     */
+    public TopologyView getTopology() {
+        ClusterViewService clusterViewService = getClusterViewService();
+        if (clusterViewService == null) {
+            throw new IllegalStateException(
+                    "DiscoveryService not yet initialized with IClusterViewService");
+        }
+        // create a new topology view
+        final DefaultTopologyView topology = new DefaultTopologyView();
+
+        LocalClusterView localClusterView = null;
+        try {
+            localClusterView = clusterViewService.getLocalClusterView();
+            topology.setLocalClusterView(localClusterView);
+        } catch (UndefinedClusterViewException e) {
+            // SLING-5030 : when we're cut off from the local cluster we also
+            // treat it as being cut off from the entire topology, ie we don't
+            // update the announcements but just return
+            // the previous oldView marked as !current
+            logger.info("getTopology: undefined cluster view: "+e.getReason()+"] "+e);
+            oldView.setNotCurrent();
+            if (e.getReason()==Reason.ISOLATED_FROM_TOPOLOGY) {
+                handleIsolatedFromTopology();
+            }
+            return oldView;
+        }
+
+        Collection<InstanceDescription> attachedInstances = getAnnouncementRegistry()
+                .listInstances(localClusterView);
+        topology.addInstances(attachedInstances);
+
+        return topology;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/BaseViewChecker.java b/src/main/java/org/apache/sling/discovery/base/commons/BaseViewChecker.java
new file mode 100644
index 0000000..516e8ec
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/BaseViewChecker.java
@@ -0,0 +1,335 @@
+/*
+ * 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.sling.discovery.base.commons;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+import org.apache.sling.launchpad.api.StartupListener;
+import org.apache.sling.launchpad.api.StartupMode;
+import org.apache.sling.settings.SlingSettingsService;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The heartbeat handler is responsible and capable of issuing both local and
+ * remote heartbeats and registers a periodic job with the scheduler for doing so.
+ * <p>
+ * Local heartbeats are stored in the repository. Remote heartbeats are POSTs to
+ * remote TopologyConnectorServlets.
+ */
+public abstract class BaseViewChecker implements ViewChecker, Runnable, StartupListener {
+
+    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** Endpoint service registration property from RFC 189 */
+    private static final String REG_PROPERTY_ENDPOINTS = "osgi.http.service.endpoints";
+
+    protected static final String PROPERTY_ID_ENDPOINTS = "endpoints";
+
+    protected static final String PROPERTY_ID_SLING_HOME_PATH = "slingHomePath";
+
+    protected static final String PROPERTY_ID_RUNTIME = "runtimeId";
+
+    /** the name used for the period job with the scheduler **/
+    protected String NAME = "discovery.impl.heartbeat.runner.";
+
+    @Reference
+    protected SlingSettingsService slingSettingsService;
+
+    @Reference
+    protected ResourceResolverFactory resourceResolverFactory;
+
+    @Reference
+    protected ConnectorRegistry connectorRegistry;
+
+    @Reference
+    protected AnnouncementRegistry announcementRegistry;
+
+    @Reference
+    protected Scheduler scheduler;
+
+    @Reference
+    protected BaseConfig connectorConfig;
+
+    /** the discovery service reference is used to get properties updated before heartbeats are sent **/
+    protected BaseDiscoveryService discoveryService;
+
+    /** the sling id of the local instance **/
+    protected String slingId;
+    
+    /** SLING-2901: the runtimeId is a unique id, set on activation, used for robust duplicate sling.id detection **/
+    protected String runtimeId;
+
+    /** lock object for synchronizing the run method **/
+    protected final Object lock = new Object();
+
+    /** SLING-2895: avoid heartbeats after deactivation **/
+    protected volatile boolean activated = false;
+
+    /** keep a reference to the component context **/
+    protected ComponentContext context;
+
+    /** SLING-2968 : start issuing remote heartbeats only after startup finished **/
+    protected boolean startupFinished = false;
+
+    /** SLING-3382 : force ping instructs the servlet to start the backoff from scratch again **/
+    private boolean forcePing;
+
+    /** SLING-4765 : store endpoints to /clusterInstances for more verbose duplicate slingId/ghost detection **/
+    protected final Map<Long, String[]> endpoints = new HashMap<Long, String[]>();
+
+    public void inform(StartupMode mode, boolean finished) {
+    	if (finished) {
+    		startupFinished(mode);
+    	}
+    }
+
+    public void startupFinished(StartupMode mode) {
+    	synchronized(lock) {
+    		startupFinished = true;
+    		issueHeartbeat();
+    	}
+    }
+
+    public void startupProgress(float ratio) {
+    	// we dont care
+    }
+
+    @Activate
+    protected void activate(ComponentContext context) {
+    	synchronized(lock) {
+    		this.context = context;
+
+	        slingId = slingSettingsService.getSlingId();
+	        NAME = "discovery.connectors.common.runner." + slingId;
+
+	        doActivate();
+	        activated = true;
+    	}
+    }
+
+    protected void doActivate() {
+        try {
+            final long interval = connectorConfig.getConnectorPingInterval();
+            logger.info("doActivate: starting periodic connectorPing job for "+slingId+" with interval "+interval+" sec.");
+            scheduler.addPeriodicJob(NAME, this,
+                    null, interval, false);
+        } catch (Exception e) {
+            logger.error("doActivate: Could not start connectorPing runner: " + e, e);
+        }
+        logger.info("doActivate: activated with slingId: {}, this: {}", slingId, this);
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        // SLING-3365 : dont synchronize on deactivate
+        activated = false;
+        logger.info("deactivate: deactivated slingId: {}, this: {}", slingId, this);
+    	scheduler.removeJob(NAME);
+    }
+    
+    /** for testing only **/
+    @Override
+    public void checkView() {
+        synchronized(lock) {
+            doCheckView();
+        }
+    }
+    
+    public void run() {
+        heartbeatAndCheckView();
+    }
+    
+    @Override
+    public void heartbeatAndCheckView() {
+        logger.info("run: start. [ConnectorPinger of slingId="+slingId+"]");
+        synchronized(lock) {
+        	if (!activated) {
+        		// SLING:2895: avoid heartbeats if not activated
+        	    logger.debug("run: not activated yet");
+        		return;
+        	}
+
+            // issue a heartbeat
+            issueHeartbeat();
+
+            // check the view
+            doCheckView();
+        }
+        logger.info("run: end. [ConnectorPinger of slingId="+slingId+"]");
+    }
+
+    /** Trigger the issuance of the next heartbeat asap instead of at next heartbeat interval **/
+    public void triggerAsyncConnectorPing() {
+        forcePing = true;
+        try {
+            // then fire a job immediately
+            // use 'fireJobAt' here, instead of 'fireJob' to make sure the job can always be triggered
+            // 'fireJob' checks for a job from the same job-class to already exist
+            // 'fireJobAt' though allows to pass a name for the job - which can be made unique, thus does not conflict/already-exist
+            logger.info("triggerConnectorPing: firing job to trigger heartbeat");
+            scheduler.fireJobAt(NAME+UUID.randomUUID(), this, null, new Date(System.currentTimeMillis()-1000 /* make sure it gets triggered immediately*/));
+        } catch (Exception e) {
+            logger.info("triggerConnectorPing: Could not trigger heartbeat: " + e);
+        }
+    }
+    
+    /**
+     * Issue a heartbeat.
+     * <p>
+     * This action consists of first updating the local properties,
+     * then issuing a cluster-local heartbeat (within the repository)
+     * and then a remote heartbeat (to all the topology connectors
+     * which announce this part of the topology to others)
+     */
+    protected void issueHeartbeat() {
+        if (discoveryService == null) {
+            logger.error("issueHeartbeat: discoveryService is null");
+        } else {
+            discoveryService.updateProperties();
+        }
+//        issueClusterLocalHeartbeat();
+        issueConnectorPings();
+    }
+
+    /** Issue a remote heartbeat using the topology connectors **/
+    protected void issueConnectorPings() {
+        if (connectorRegistry == null) {
+            logger.error("issueConnectorPings: connectorRegistry is null");
+            return;
+        }
+        if (!startupFinished) {
+        	logger.debug("issueConnectorPings: not issuing remote heartbeat yet, startup not yet finished");
+        	return;
+        }
+        if (logger.isDebugEnabled()) {
+            logger.debug("issueConnectorPings: pinging outgoing topology connectors (if there is any) for "+slingId);
+        }
+        connectorRegistry.pingOutgoingConnectors(forcePing);
+        forcePing = false;
+    }
+
+    /** Check whether the established view matches the reality, ie matches the
+     * heartbeats
+     */
+    protected void doCheckView() {
+        // check the remotes first
+        if (announcementRegistry == null) {
+            logger.error("announcementRegistry is null");
+            return;
+        }
+        announcementRegistry.checkExpiredAnnouncements();
+    }
+
+    /**
+     * Bind a http service
+     */
+    protected void bindHttpService(final ServiceReference reference) {
+        final String[] endpointUrls = toStringArray(reference.getProperty(REG_PROPERTY_ENDPOINTS));
+        if ( endpointUrls != null ) {
+            synchronized ( lock ) {
+                this.endpoints.put((Long)reference.getProperty(Constants.SERVICE_ID), endpointUrls);
+            }
+        }
+    }
+
+    /**
+     * Unbind a http service
+     */
+    protected void unbindHttpService(final ServiceReference reference) {
+        synchronized ( lock ) {
+            if ( this.endpoints.remove(reference.getProperty(Constants.SERVICE_ID)) != null ) {
+            }
+        }
+    }
+    
+    private String[] toStringArray(final Object propValue) {
+        if (propValue == null) {
+            // no value at all
+            return null;
+
+        } else if (propValue instanceof String) {
+            // single string
+            return new String[] { (String) propValue };
+
+        } else if (propValue instanceof String[]) {
+            // String[]
+            return (String[]) propValue;
+
+        } else if (propValue.getClass().isArray()) {
+            // other array
+            Object[] valueArray = (Object[]) propValue;
+            List<String> values = new ArrayList<String>(valueArray.length);
+            for (Object value : valueArray) {
+                if (value != null) {
+                    values.add(value.toString());
+                }
+            }
+            return values.toArray(new String[values.size()]);
+
+        } else if (propValue instanceof Collection<?>) {
+            // collection
+            Collection<?> valueCollection = (Collection<?>) propValue;
+            List<String> valueList = new ArrayList<String>(valueCollection.size());
+            for (Object value : valueCollection) {
+                if (value != null) {
+                    valueList.add(value.toString());
+                }
+            }
+            return valueList.toArray(new String[valueList.size()]);
+        }
+
+        return null;
+    }
+    
+    protected String getEndpointsAsString() {
+        final StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        for(final String[] points : endpoints.values()) {
+            for(final String point : points) {
+                if ( first ) {
+                    first = false;
+                } else {
+                    sb.append(",");
+                }
+                sb.append(point);
+            }
+        }
+        return sb.toString();
+        
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewHelper.java b/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewHelper.java
new file mode 100644
index 0000000..26e4f28
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewHelper.java
@@ -0,0 +1,55 @@
+/*
+ * 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.sling.discovery.base.commons;
+
+import java.util.Collection;
+
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+
+/**
+ * Contains some static helper methods around ClusterView
+ */
+public class ClusterViewHelper {
+
+    /** checks whether the cluster view contains a particular sling id **/
+    public static boolean contains(ClusterView clusterView, String slingId) throws UndefinedClusterViewException {
+        InstanceDescription found = null;
+        for (InstanceDescription i : clusterView.getInstances()) {
+            if (i.getSlingId().equals(slingId)) {
+                if (found!=null) {
+                    throw new IllegalStateException("multiple instances with slingId found: "+slingId);
+                }
+                found = i;
+            }
+        }
+        return found!=null;
+    }
+
+    /** checks whether the cluster contains any of the provided instances **/
+    public static boolean containsAny(ClusterView clusterView, Collection<InstanceDescription> listInstances) 
+            throws UndefinedClusterViewException {
+        for (InstanceDescription i : listInstances) {
+            if (contains(clusterView, i.getSlingId())) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewService.java b/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewService.java
new file mode 100644
index 0000000..4552e81
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/ClusterViewService.java
@@ -0,0 +1,47 @@
+/*
+ * 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.sling.discovery.base.commons;
+
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+
+/**
+ * The ClusterViewService is capable of determining the 
+ * ClusterView of the local cluster (ie of the instances
+ * that are all hooked to the same underlying repository).
+ */
+public interface ClusterViewService {
+
+    /** the sling id of the local instance **/
+    String getSlingId();
+
+    /**
+     * Returns the current, local cluster view - throwing an
+     * UndefinedClusterViewException if it cannot determine
+     * a clusterView at the moment.
+     * @return the current cluster view - never returns null 
+     * (it rather throws an UndefinedClusterViewException that
+     * contains more details about why exactly the clusterView
+     * is undefined at the moment)
+     * @throws UndefinedClusterViewException thrown when
+     * the ClusterView cannot be determined at the moment
+     * (also contains more details as to why exactly)
+     */
+    LocalClusterView getLocalClusterView() throws UndefinedClusterViewException;
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/DefaultTopologyView.java b/src/main/java/org/apache/sling/discovery/base/commons/DefaultTopologyView.java
new file mode 100644
index 0000000..bc97d79
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/DefaultTopologyView.java
@@ -0,0 +1,252 @@
+/*
+ * 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.sling.discovery.base.commons;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.InstanceFilter;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.commons.providers.BaseTopologyView;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default Implementation of the topology view interface
+ */
+public class DefaultTopologyView extends BaseTopologyView {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** the instances that are part of this topology **/
+    private final Set<InstanceDescription> instances = new HashSet<InstanceDescription>();
+
+    private String localClusterSyncTokenId;
+
+    /** Create a new empty topology **/
+    public DefaultTopologyView() {
+        // nothing to be initialized then
+    }
+
+    /** Create a new topology filled with the given list of instances **/
+    public DefaultTopologyView(final Collection<InstanceDescription> instances) {
+        if (instances != null) {
+            this.instances.addAll(instances);
+        }
+    }
+
+    /**
+     * Compare this topology with the given one and determine how they compare
+     * @param other the other topology against which to compare
+     * @return the type describing how these two compare
+     * @see Type
+     */
+    public Type compareTopology(final DefaultTopologyView other) {
+        if (other == null) {
+            throw new IllegalArgumentException("other must not be null");
+        }
+        if (this.instances.size() != other.instances.size()) {
+        	logger.debug("compareTopology: different number of instances");
+            return Type.TOPOLOGY_CHANGED;
+        }
+        boolean propertiesChanged = false;
+        for(final InstanceDescription instance : this.instances) {
+
+            final Iterator<InstanceDescription> it2 = other.instances.iterator();
+            InstanceDescription matchingInstance = null;
+            while (it2.hasNext()) {
+                final InstanceDescription otherInstance = it2.next();
+                if (instance.getSlingId().equals(otherInstance.getSlingId())) {
+                    matchingInstance = otherInstance;
+                    break;
+                }
+            }
+            if (matchingInstance == null) {
+            	if (logger.isDebugEnabled()) {
+	            	logger.debug("compareTopology: no matching instance found for {}", instance);
+            	}
+                return Type.TOPOLOGY_CHANGED;
+            }
+            if (!instance.getClusterView().getId()
+                    .equals(matchingInstance.getClusterView().getId())) {
+            	logger.debug("compareTopology: cluster view id does not match");
+                return Type.TOPOLOGY_CHANGED;
+            }
+            if (!instance.isLeader()==matchingInstance.isLeader()) {
+                logger.debug("compareTopology: leaders differ");
+                return Type.TOPOLOGY_CHANGED;
+            }
+            if (!instance.getProperties().equals(
+                    matchingInstance.getProperties())) {
+                propertiesChanged = true;
+            }
+        }
+        if (propertiesChanged) {
+            return Type.PROPERTIES_CHANGED;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null || !(obj instanceof DefaultTopologyView)) {
+            return false;
+        }
+        DefaultTopologyView other = (DefaultTopologyView) obj;
+        if (this.isCurrent() != other.isCurrent()) {
+            return false;
+        }
+        Type diff = compareTopology(other);
+        return diff == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int code = 0;
+        for (Iterator<InstanceDescription> it = instances.iterator(); it
+                .hasNext();) {
+            InstanceDescription instance = it.next();
+            code += instance.hashCode();
+        }
+        return code;
+    }
+
+    /**
+     * @see org.apache.sling.discovery.TopologyView#getLocalInstance()
+     */
+    public InstanceDescription getLocalInstance() {
+        for (Iterator<InstanceDescription> it = instances.iterator(); it
+                .hasNext();) {
+            InstanceDescription instance = it.next();
+            if (instance.isLocal()) {
+                return instance;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @see org.apache.sling.discovery.TopologyView#getInstances()
+     */
+    public Set<InstanceDescription> getInstances() {
+        return Collections.unmodifiableSet(instances);
+    }
+
+    /**
+     * Adds the instances of the given ClusterView to this topology
+     */
+    public void setLocalClusterView(final LocalClusterView localClusterView) {
+        if (localClusterView == null) {
+            throw new IllegalArgumentException("localClusterView must not be null");
+        }
+        final List<InstanceDescription> instances = localClusterView.getInstances();
+        addInstances(instances);
+        
+        this.localClusterSyncTokenId = localClusterView.getLocalClusterSyncTokenId();
+    }
+    
+    /**
+     * Adds the given instances to this topology
+     */
+    public void addInstances(final Collection<InstanceDescription> instances) {
+        if (instances == null) {
+            return;
+        }
+        outerLoop: for (Iterator<InstanceDescription> it = instances.iterator(); it
+                .hasNext();) {
+            InstanceDescription instanceDescription = it.next();
+            for (Iterator<InstanceDescription> it2 = this.instances.iterator(); it2.hasNext();) {
+                InstanceDescription existingInstance = it2.next();
+                if (existingInstance.getSlingId().equals(instanceDescription.getSlingId())) {
+                    // SLING-3726:
+                    // while 'normal duplicate instances' are filtered out here correctly,
+                    // 'hidden duplicate instances' that are added via this instanceDescription's
+                    // cluster, are not caught.
+                    // there is, however, no simple fix for this. Since the reason is 
+                    // inconsistent state information in /var/discovery/impl - either
+                    // due to stale-announcements (SLING-4139) - or by some manualy
+                    // copying of data from one cluster to the next (which will also
+                    // be cleaned up by SLING-4139 though)
+                    // so the fix for avoiding duplicate instances is really SLING-4139
+                    logger.info("addInstance: cannot add same instance twice: "
+                            + instanceDescription);
+                    continue outerLoop;
+                }
+            }
+            this.instances.add(instanceDescription);
+        }
+    }
+
+    /**
+     * @see org.apache.sling.discovery.TopologyView#findInstances(org.apache.sling.discovery.InstanceFilter)
+     */
+    public Set<InstanceDescription> findInstances(final InstanceFilter picker) {
+        if (picker == null) {
+            throw new IllegalArgumentException("picker must not be null");
+        }
+        Set<InstanceDescription> result = new HashSet<InstanceDescription>();
+        for (Iterator<InstanceDescription> it = instances.iterator(); it
+                .hasNext();) {
+            InstanceDescription instance = it.next();
+            if (picker.accept(instance)) {
+                result.add(instance);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * @see org.apache.sling.discovery.TopologyView#getClusterViews()
+     */
+    public Set<ClusterView> getClusterViews() {
+        Set<ClusterView> result = new HashSet<ClusterView>();
+        for (Iterator<InstanceDescription> it = instances.iterator(); it
+                .hasNext();) {
+            InstanceDescription instance = it.next();
+            ClusterView cluster = instance.getClusterView();
+            if (cluster != null) {
+                result.add(cluster);
+            }
+        }
+        return new HashSet<ClusterView>(result);
+    }
+
+    @Override
+    public String toString() {
+        return "TopologyViewImpl [current=" + isCurrent() + ", num=" + instances.size() + ", instances="
+                + instances + "]";
+    }
+
+    @Override
+    public String getLocalClusterSyncTokenId() {
+        if (localClusterSyncTokenId==null) {
+            throw new IllegalStateException("no syncToken set");
+        } else {
+            return localClusterSyncTokenId;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/UndefinedClusterViewException.java b/src/main/java/org/apache/sling/discovery/base/commons/UndefinedClusterViewException.java
new file mode 100644
index 0000000..9a300aa
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/UndefinedClusterViewException.java
@@ -0,0 +1,64 @@
+/*
+ * 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.sling.discovery.base.commons;
+
+/**
+ * This exception is thrown when the ClusterViewService
+ * does not have a cluster view that is valid. 
+ * That can either be because it cannot access the repository
+ * (login or other repository exception) or that there is
+ * no established view yet at all (not yet voted case) - 
+ * or that there is an established view but it doesn't include 
+ * the local instance (isolated case)
+ */
+@SuppressWarnings("serial")
+public class UndefinedClusterViewException extends Exception {
+
+    public static enum Reason {
+        /** used when the local instance is isolated from the topology
+         * (which is noticed by an established view that does not include
+         * the local instance)
+         */
+        ISOLATED_FROM_TOPOLOGY,
+        
+        /** used when there is no established view yet
+         * (happens on a fresh installation)
+         */
+        NO_ESTABLISHED_VIEW,
+        
+        /** used when we couldn't reach the repository **/
+        REPOSITORY_EXCEPTION
+    }
+
+    private final Reason reason;
+    
+    public UndefinedClusterViewException(Reason reason) {
+        super();
+        this.reason = reason;
+    }
+
+    public UndefinedClusterViewException(Reason reason, String msg) {
+        super(msg);
+        this.reason = reason;
+    }
+    
+    public Reason getReason() {
+        return reason;
+    }
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/ViewChecker.java b/src/main/java/org/apache/sling/discovery/base/commons/ViewChecker.java
new file mode 100644
index 0000000..47e123e
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/ViewChecker.java
@@ -0,0 +1,40 @@
+/*
+ * 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.sling.discovery.base.commons;
+
+/**
+ * A ViewChecker is capable of issuing a heartbeat and
+ * subsequently checking the resulting, current view 
+ * in the local cluster.
+ * <p>
+ * This is mostly used as hooks for testing
+ */
+public interface ViewChecker {
+
+    /**
+     * Check the view (without issuing a heartbeat)
+     */
+    public void checkView();
+    
+    /**
+     * Issue a heartbeat and check the view
+     */
+    public void heartbeatAndCheckView();
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/commons/package-info.java b/src/main/java/org/apache/sling/discovery/base/commons/package-info.java
new file mode 100644
index 0000000..7923ff4
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/commons/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides some common classes for discovery implementors that 
+ * choose to extend from discovery.base
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+package org.apache.sling.discovery.base.commons;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/BaseConfig.java b/src/main/java/org/apache/sling/discovery/base/connectors/BaseConfig.java
new file mode 100644
index 0000000..fdc0435
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/BaseConfig.java
@@ -0,0 +1,129 @@
+/*
+ * 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.sling.discovery.base.connectors;
+
+import java.net.URL;
+
+/**
+ * Configuration for discovery.base
+ */
+public interface BaseConfig {
+
+    /**
+     * Returns the socket connect() timeout used by the topology connector, 0 disables the timeout
+     * @return the socket connect() timeout used by the topology connector, 0 disables the timeout
+     */
+    public int getSocketConnectionTimeout();
+
+    /**
+     * Returns the socket read timeout (SO_TIMEOUT) used by the topology connector, 0 disables the timeout
+     * @return the socket read timeout (SO_TIMEOUT) used by the topology connector, 0 disables the timeout
+     */
+    public int getSoTimeout();
+    
+    /**
+     * Returns the URLs to which to open a topology connector - or null/empty if no topology connector
+     * is configured (default is null)
+     * @return the URLs to which to open a topology connector - or null/empty if no topology connector
+     * is configured
+     */
+    public URL[] getTopologyConnectorURLs();
+
+    /**
+     * Returns a comma separated list of hostnames and/or ip addresses which are allowed as
+     * remote hosts to open connections to the topology connector servlet
+     * @return a comma separated list of hostnames and/or ip addresses which are allowed as
+     * remote hosts to open connections to the topology connector servlet
+     */
+    public String[] getTopologyConnectorWhitelist();
+
+    /**
+     * Returns the resource path where cluster instance informations are stored.
+     * @return the resource path where cluster instance informations are stored
+     */
+    public String getClusterInstancesPath();
+
+
+    /**
+     * @return true if hmac is enabled.
+     */
+    public boolean isHmacEnabled();
+
+    /**
+     * @return the shared key
+     */
+    public String getSharedKey();
+
+    /**
+     * @return the interval of the shared key for hmac.
+     */
+    public long getKeyInterval();
+
+    /**
+     * @return true if encryption is enabled.
+     */
+    public boolean isEncryptionEnabled();
+    
+    /**
+     * @return true if requests on the topology connector should be gzipped
+     * (which only works if the server accepts that.. ie discovery.impl 1.0.4+)
+     */
+    public boolean isGzipConnectorRequestsEnabled();
+    
+    /**
+     * @return true if the auto-stopping of local-loop topology connectors is enabled.
+     */
+    public boolean isAutoStopLocalLoopEnabled();
+
+    /**
+     * Returns the backoff factor to be used for standby (loop) connectors
+     * @return the backoff factor to be used for standby (loop) connectors
+     */
+    public int getBackoffStandbyFactor();
+
+    /**
+     * Returns the (maximum) backoff factor to be used for stable connectors
+     * @return the (maximum) backoff factor to be used for stable connectors
+     */
+    public int getBackoffStableFactor();
+
+    /**
+     * Returns the backoff interval for standby (loop) connectors in seconds
+     * @return the backoff interval for standby (loop) connectors in seconds
+     */
+    public long getBackoffStandbyInterval();
+
+    /**
+     * Returns the interval (in seconds) in which connectors are pinged
+     * @return the interval (in seconds) in which connectors are pinged
+     */
+    public long getConnectorPingInterval();
+
+    /**
+     * Returns the timeout (in seconds) after which a connector ping is considered invalid/timed out
+     * @return the timeout (in seconds) after which a connector ping is considered invalid/timed out
+     */
+    public long getConnectorPingTimeout();
+
+    /**
+     * The minEventDelay to apply to the ViewStateManager
+     */
+    public int getMinEventDelay();
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/Announcement.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/Announcement.java
new file mode 100644
index 0000000..c4345ae
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/Announcement.java
@@ -0,0 +1,460 @@
+/*
+ * 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.sling.discovery.base.connectors.announcement;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.commons.json.JSONArray;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.commons.json.JSONObject;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.commons.providers.DefaultClusterView;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+import org.apache.sling.discovery.commons.providers.NonLocalInstanceDescription;
+
+/**
+ * An announcement is the information exchanged by the topology connector and
+ * contains all clusters and instances which both the topology connector client
+ * and servlet see (in their part before joining the two worlds).
+ * <p>
+ * An announcement is exchanged in json format and carries a timeout.
+ */
+public class Announcement {
+
+    /** the protocol version this announcement currently represents. Mismatching protocol versions are
+     * used to detect incompatible topology connectors
+     */
+    private final static int PROTOCOL_VERSION = 1;
+
+    /** the sling id of the owner of this announcement. the owner is where this announcement comes from **/
+    private final String ownerId;
+
+    /** announcement protocol version **/
+    private final int protocolVersion;
+
+    /** the local cluster view **/
+    private ClusterView localCluster;
+
+    /** the incoming instances **/
+    private List<Announcement> incomings = new LinkedList<Announcement>();
+
+    /** whether or not this annoucement was inherited (response of a connect) or incoming (the connect) **/
+    private boolean inherited = false;
+
+    /** some information about the server where this announcement came from **/
+    private String serverInfo;
+
+    /** whether or not this announcement represents a loop detected in the topology connectors **/
+    private boolean loop = false;
+
+    /** SLING-3382: Sets the backoffInterval which the connector servlets passes back to the client to use as the next heartbeatInterval **/
+    private long backoffInterval = -1;
+
+    /** SLING-3382: the resetBackoff flag is sent from client to server and indicates that the client wants to start from (backoff) scratch **/
+    private boolean resetBackoff = false;
+
+    public Announcement(final String ownerId) {
+        this(ownerId, PROTOCOL_VERSION);
+    }
+
+    public Announcement(final String ownerId, int protocolVersion) {
+        if (ownerId==null || ownerId.length()==0) {
+            throw new IllegalArgumentException("ownerId must not be null or empty");
+        }
+        this.ownerId = ownerId;
+        this.protocolVersion = protocolVersion;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder incomingList = new StringBuilder();
+        for (Iterator<Announcement> it = incomings.iterator(); it.hasNext();) {
+            Announcement anIncomingAnnouncement = it.next();
+            if (incomingList.length()!=0) {
+                incomingList.append(", ");
+            }
+            incomingList.append(anIncomingAnnouncement);
+        }
+        return "Announcement[ownerId="+getOwnerId()+
+                ", protocolVersion="+protocolVersion+
+                ", inherited="+isInherited()+
+                ", loop="+loop+
+                ", incomings="+incomingList+"]";
+    }
+
+    /** check whether this is announcement contains the valid protocol version **/
+    public boolean isCorrectVersion() {
+        return (protocolVersion==PROTOCOL_VERSION);
+    }
+
+    /** check whether this is a valid announcement, containing the minimal information **/
+    public boolean isValid() {
+        if (ownerId==null || ownerId.length()==0) {
+            return false;
+        }
+        if (loop) {
+            return true;
+        }
+        if (!isCorrectVersion()) {
+            return false;
+        }
+        if (localCluster==null) {
+            return false;
+        }
+        try{
+            List<InstanceDescription> instances = localCluster.getInstances();
+            if (instances==null || instances.size()==0) {
+                return false;
+            }
+            boolean isOwnerMemberOfLocalCluster = false;
+            for (Iterator<InstanceDescription> it = instances.iterator(); it.hasNext();) {
+                InstanceDescription instanceDescription = it.next();
+                if (instanceDescription.getSlingId().equals(ownerId)) {
+                    isOwnerMemberOfLocalCluster = true;
+                }
+            }
+            if (!isOwnerMemberOfLocalCluster) {
+                return false;
+            }
+        } catch(Exception ise) {
+            return false;
+        }
+        return true;
+    }
+
+    /** set the inherited flag - if true this means this announcement is the response of a topology connect **/
+    public void setInherited(final boolean inherited) {
+        this.inherited = inherited;
+    }
+
+    /** Returns the inherited flag - if true this means that this announcement is the response of a topology connect **/
+    public boolean isInherited() {
+        return inherited;
+    }
+
+    /** Sets the loop falg - set true when this announcement should represent a loop detected in the topology connectors **/
+    public void setLoop(final boolean loop) {
+        this.loop = loop;
+    }
+    
+    /** Sets the backoffInterval which the connector servlets passes back to the client to use as the next heartbeatInterval **/
+    public void setBackoffInterval(long backoffInterval) {
+        this.backoffInterval = backoffInterval;
+    }
+    
+    /** Gets the backoffInterval which the connector servlets passes back to the client to use as the next heartbeatInterval **/
+    public long getBackoffInterval() {
+        return this.backoffInterval;
+    }
+    
+    /** sets the resetBackoff flag **/
+    public void setResetBackoff(boolean resetBackoff) {
+        this.resetBackoff = resetBackoff;
+    }
+    
+    /** gets the resetBackoff flag **/
+    public boolean getResetBackoff() {
+        return resetBackoff;
+    }
+
+    /** Returns the loop flag - set when this announcement represents a loop detected in the topology connectors **/
+    public boolean isLoop() {
+        return loop;
+    }
+
+    /** Returns the protocolVersion of this announcement **/
+    public int getProtocolVersion() {
+        return protocolVersion;
+    }
+
+    /** sets the information about the server where this announcement came from **/
+    public void setServerInfo(final String serverInfo) {
+        this.serverInfo = serverInfo;
+    }
+
+    /** the information about the server where this announcement came from **/
+    public String getServerInfo() {
+        return serverInfo;
+    }
+
+    /**
+     * Returns the slingid of the owner of this announcement.
+     * <p>
+     * The owner is the instance which initiated the topology connection
+     */
+    public String getOwnerId() {
+        return ownerId;
+    }
+
+    /** Convert this announcement into a json object **/
+    public JSONObject asJSONObject() throws JSONException {
+        return asJSONObject(false);
+    }
+    
+    /** Convert this announcement into a json object **/
+    private JSONObject asJSONObject(boolean filterTimes) throws JSONException {
+        JSONObject announcement = new JSONObject();
+        announcement.put("ownerId", ownerId);
+        announcement.put("protocolVersion", protocolVersion);
+        // SLING-3389: leaving the 'created' property in the announcement
+        // for backwards compatibility!
+        if (!filterTimes) {
+            announcement.put("created", System.currentTimeMillis());
+        }
+        announcement.put("inherited", inherited);
+        if (loop) {
+            announcement.put("loop", loop);
+        }
+        if (serverInfo != null) {
+            announcement.put("serverInfo", serverInfo);
+        }
+        if (localCluster!=null) {
+            announcement.put("localClusterView", asJSON(localCluster));
+        }
+        if (!filterTimes && backoffInterval>0) {
+            announcement.put("backoffInterval", backoffInterval);
+        }
+        if (resetBackoff) {
+            announcement.put("resetBackoff", resetBackoff);
+        }
+        JSONArray incomingAnnouncements = new JSONArray();
+        for (Iterator<Announcement> it = incomings.iterator(); it.hasNext();) {
+            Announcement incoming = it.next();
+            incomingAnnouncements.put(incoming.asJSONObject(filterTimes));
+        }
+        announcement.put("topologyAnnouncements", incomingAnnouncements);
+        return announcement;
+    }
+
+    /** Create an announcement form json **/
+    public static Announcement fromJSON(final String topologyAnnouncementJSON)
+            throws JSONException {
+        JSONObject announcement = new JSONObject(topologyAnnouncementJSON);
+        final String ownerId = announcement.getString("ownerId");
+        final int protocolVersion;
+        if (!announcement.has("protocolVersion")) {
+            protocolVersion = -1;
+        } else {
+            protocolVersion = announcement.getInt("protocolVersion");
+        }
+        final Announcement result = new Announcement(ownerId, protocolVersion);
+        if (announcement.has("backoffInterval")) {
+            long backoffInterval = announcement.getLong("backoffInterval");
+            result.backoffInterval = backoffInterval;
+        }
+        if (announcement.has("resetBackoff")) {
+            boolean resetBackoff = announcement.getBoolean("resetBackoff");
+            result.resetBackoff = resetBackoff;
+        }
+        if (announcement.has("loop") && announcement.getBoolean("loop")) {
+            result.setLoop(true);
+            return result;
+        }
+        final String localClusterViewJSON = announcement
+                .getString("localClusterView");
+        final ClusterView localClusterView = asClusterView(localClusterViewJSON);
+        final JSONArray subAnnouncements = announcement
+                .getJSONArray("topologyAnnouncements");
+
+        if (announcement.has("inherited")) {
+            final Boolean inherited = announcement.getBoolean("inherited");
+            result.inherited = inherited;
+        }
+        if (announcement.has("serverInfo")) {
+            String serverInfo = announcement.getString("serverInfo");
+            result.serverInfo = serverInfo;
+        }
+        result.setLocalCluster(localClusterView);
+        for (int i = 0; i < subAnnouncements.length(); i++) {
+            String subAnnouncementJSON = subAnnouncements.getString(i);
+            result.addIncomingTopologyAnnouncement(fromJSON(subAnnouncementJSON));
+        }
+        return result;
+    }
+
+    /** create a clusterview from json **/
+    private static ClusterView asClusterView(final String localClusterViewJSON)
+            throws JSONException {
+        JSONObject obj = new JSONObject(localClusterViewJSON);
+        DefaultClusterView clusterView = new DefaultClusterView(
+                obj.getString("id"));
+        JSONArray instancesObj = obj.getJSONArray("instances");
+
+        for (int i = 0; i < instancesObj.length(); i++) {
+            JSONObject anInstance = instancesObj.getJSONObject(i);
+            clusterView.addInstanceDescription(asInstance(anInstance));
+        }
+
+        return clusterView;
+    }
+
+    /** convert a clusterview into json **/
+    private static JSONObject asJSON(final ClusterView clusterView)
+            throws JSONException {
+        JSONObject obj = new JSONObject();
+        obj.put("id", clusterView.getId());
+        JSONArray instancesObj = new JSONArray();
+        List<InstanceDescription> instances = clusterView.getInstances();
+        for (Iterator<InstanceDescription> it = instances.iterator(); it
+                .hasNext();) {
+            InstanceDescription instanceDescription = it.next();
+            instancesObj.put(asJSON(instanceDescription));
+        }
+        obj.put("instances", instancesObj);
+        return obj;
+    }
+
+    /** create an instancedescription from json **/
+    private static DefaultInstanceDescription asInstance(
+            final JSONObject anInstance) throws JSONException {
+        final boolean isLeader = anInstance.getBoolean("isLeader");
+        final String slingId = anInstance.getString("slingId");
+
+        final JSONObject propertiesObj = anInstance.getJSONObject("properties");
+        Iterator<String> it = propertiesObj.keys();
+        Map<String, String> properties = new HashMap<String, String>();
+        while (it.hasNext()) {
+            String key = it.next();
+            properties.put(key, propertiesObj.getString(key));
+        }
+
+        NonLocalInstanceDescription instance = new NonLocalInstanceDescription(
+                null, isLeader, slingId, properties);
+        return instance;
+    }
+
+    /** convert an instance description into a json object **/
+    private static JSONObject asJSON(final InstanceDescription instanceDescription)
+            throws JSONException {
+        JSONObject obj = new JSONObject();
+        obj.put("slingId", instanceDescription.getSlingId());
+        obj.put("isLeader", instanceDescription.isLeader());
+        ClusterView cluster = instanceDescription.getClusterView();
+        if (cluster != null) {
+            obj.put("cluster", cluster.getId());
+        }
+        JSONObject propertiesObj = new JSONObject();
+        Map<String, String> propertiesMap = instanceDescription.getProperties();
+        for (Iterator<Entry<String, String>> it = propertiesMap.entrySet()
+                .iterator(); it.hasNext();) {
+            Entry<String, String> entry = it.next();
+            propertiesObj.put(entry.getKey(), entry.getValue());
+        }
+        obj.put("properties", propertiesObj);
+        return obj;
+    }
+
+    /** sets the local clusterview **/
+    public void setLocalCluster(ClusterView localCluster) {
+        this.localCluster = localCluster;
+    }
+
+    /** adds an incoming announcement to this announcement **/
+    public void addIncomingTopologyAnnouncement(
+            Announcement incomingTopologyAnnouncement) {
+        incomings.add(incomingTopologyAnnouncement);
+    }
+
+    /** Convert this announcement into json **/
+    public String asJSON() throws JSONException {
+        return asJSONObject().toString();
+    }
+
+    /** the key which is unique to this announcement **/
+    public String getPrimaryKey() {
+        return ownerId;
+    }
+
+    /** Returns the list of instances that are contained in this announcement **/
+    public Collection<InstanceDescription> listInstances() {
+        Collection<InstanceDescription> instances = new LinkedList<InstanceDescription>();
+        instances.addAll(localCluster.getInstances());
+
+        for (Iterator<Announcement> it = incomings.iterator(); it.hasNext();) {
+            Announcement incomingAnnouncement = it.next();
+            instances.addAll(incomingAnnouncement.listInstances());
+        }
+        return instances;
+    }
+
+    /**
+     * Persists this announcement using the given 'announcements' resource,
+     * under which a node with the primary key is created
+     **/
+    public void persistTo(Resource announcementsResource)
+            throws PersistenceException, JSONException {
+        Resource announcementChildResource = announcementsResource.getChild(getPrimaryKey());
+        
+        // SLING-2967 used to introduce 'resetting the created time' here
+        // in order to become machine-clock independent.
+        // With introduction of SLING-3389, where we dont store any
+        // announcement-heartbeat-dates anymore at all, this resetting here
+        // became unnecessary.
+        
+        final String announcementJson = asJSON();
+		if (announcementChildResource==null) {
+            final ResourceResolver resourceResolver = announcementsResource.getResourceResolver();
+            Map<String, Object> properties = new HashMap<String, Object>();
+            properties.put("topologyAnnouncement", announcementJson);
+            resourceResolver.create(announcementsResource, getPrimaryKey(), properties);
+        } else {
+            final ModifiableValueMap announcementChildMap = announcementChildResource.adaptTo(ModifiableValueMap.class);
+            announcementChildMap.put("topologyAnnouncement", announcementJson);
+        }
+    }
+
+	/**
+     * Remove all announcements that match the given owner Id
+     */
+    public void removeInherited(final String ownerId) {
+        for (Iterator<Announcement> it = incomings.iterator(); it.hasNext();) {
+            Announcement anIncomingAnnouncement = it.next();
+            if (anIncomingAnnouncement.isInherited()
+                    && anIncomingAnnouncement.getOwnerId().equals(ownerId)) {
+                // then filter this
+                it.remove();
+            }
+
+        }
+    }
+
+    /**
+     * Compare this Announcement with another one, ignoring the 'created'
+     * property - which gets added to the JSON object automatically due
+     * to SLING-3389 wire-backwards-compatibility - and backoffInterval
+     * introduced as part of SLING-3382
+     */
+    public boolean correspondsTo(Announcement announcement) throws JSONException {
+        final JSONObject myJson = asJSONObject(true);
+        final JSONObject otherJson = announcement.asJSONObject(true);
+        return myJson.toString().equals(otherJson.toString());
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementFilter.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementFilter.java
new file mode 100644
index 0000000..f1664a2
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementFilter.java
@@ -0,0 +1,32 @@
+/*
+ * 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.sling.discovery.base.connectors.announcement;
+
+/**
+ * Filter used during announcement processing internally 
+ **/
+public interface AnnouncementFilter {
+
+    /**
+     * Check if the provided announcement, which was received by the provided
+     * slingId can be accepted or not.
+     **/
+    boolean accept(String receivingSlingId, Announcement announcement);
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistry.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistry.java
new file mode 100644
index 0000000..fb8b2c1
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistry.java
@@ -0,0 +1,66 @@
+/*
+ * 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.sling.discovery.base.connectors.announcement;
+
+import java.util.Collection;
+
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+
+/**
+ * The announcement registry keeps track of all the announcement that this
+ * instance either received by a joined topology connector or that a topology
+ * connector inherited from the counterpart (the topology connector servlet)
+ */
+public interface AnnouncementRegistry {
+
+    /** 
+     * Register the given announcement - and returns the backoff interval (in seconds)
+     * for stable connectors
+     * - or -1 if the registration was not successful (likely indicating a loop) 
+     * @return the backoff interval (in seconds) for stable connectors
+     * - or -1 if the registration was not successful (likely indicating a loop) 
+     */
+    long registerAnnouncement(Announcement topologyAnnouncement);
+    
+    /** list all announcements that were received by instances in the local cluster **/
+    Collection<Announcement> listAnnouncementsInSameCluster(ClusterView localClusterView);
+    
+    /** list all announcements that were received (incoming or inherited) by this instance **/
+    Collection<Announcement> listLocalAnnouncements();
+    
+    /** list all announcements that this instance received (incoming) **/ 
+    Collection<CachedAnnouncement> listLocalIncomingAnnouncements();
+    
+    /** Check for expired announcements and remove any if applicable **/
+    void checkExpiredAnnouncements();
+
+    /** Returns the list of instances contained in all non-expired announcements of this registry **/
+    Collection<InstanceDescription> listInstances(ClusterView localClusterView);
+
+    /** Add all registered announcements to the given target announcement that are accepted by the given filter **/
+    void addAllExcept(Announcement target, ClusterView localClusterView, AnnouncementFilter filter);
+
+    /** Unregister the announcement owned by the given slingId **/
+    void unregisterAnnouncement(String ownerId);
+
+    /** Whether or not the given owner has an active (ie not expired) announcement registered **/
+    boolean hasActiveAnnouncement(String ownerId);
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImpl.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImpl.java
new file mode 100644
index 0000000..bb109a7
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImpl.java
@@ -0,0 +1,593 @@
+/*
+ * 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.sling.discovery.base.connectors.announcement;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.commons.providers.util.ResourceHelper;
+import org.apache.sling.settings.SlingSettingsService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default implementation of the AnnouncementRegistry which
+ * handles JSON-backed announcements and does so by storing
+ * them in a local like /var/discovery/impl/clusterNodes/$slingId/announcement.
+ */
+@Component
+@Service(value = AnnouncementRegistry.class)
+public class AnnouncementRegistryImpl implements AnnouncementRegistry {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    @Reference
+    private ResourceResolverFactory resourceResolverFactory;
+
+    @Reference
+    private SlingSettingsService settingsService;
+
+    private String slingId;
+    
+    @Reference
+    private BaseConfig config;
+    
+    public static AnnouncementRegistryImpl testConstructorAndActivate(ResourceResolverFactory resourceResolverFactory,
+            SlingSettingsService slingSettingsService, BaseConfig config) {
+        AnnouncementRegistryImpl registry = testConstructor(resourceResolverFactory, slingSettingsService, config);
+        registry.activate();
+        return registry;
+    }
+    
+    public static AnnouncementRegistryImpl testConstructor(ResourceResolverFactory resourceResolverFactory,
+            SlingSettingsService slingSettingsService, BaseConfig config) {
+        AnnouncementRegistryImpl registry = new AnnouncementRegistryImpl();
+        registry.resourceResolverFactory = resourceResolverFactory;
+        registry.settingsService = slingSettingsService;
+        registry.config = config;
+        return registry;
+    }
+    
+    @Activate
+    protected void activate() {
+        slingId = settingsService.getSlingId();
+    }
+    
+    private final Map<String,CachedAnnouncement> ownAnnouncementsCache = 
+            new HashMap<String,CachedAnnouncement>();
+
+    public synchronized void unregisterAnnouncement(final String ownerId) {
+        if (ownerId==null || ownerId.length()==0) {
+            throw new IllegalArgumentException("ownerId must not be null or empty");
+        }
+        // remove from the cache - even if there's an error afterwards
+        ownAnnouncementsCache.remove(ownerId);
+        
+        if (resourceResolverFactory == null) {
+            logger.error("unregisterAnnouncement: resourceResolverFactory is null");
+            return;
+        }
+        ResourceResolver resourceResolver = null;
+        try {
+            resourceResolver = resourceResolverFactory
+                    .getAdministrativeResourceResolver(null);
+
+            final String path = config.getClusterInstancesPath()
+                    + "/"
+                    + slingId
+                    + "/announcements/" + ownerId;
+            final Resource announcementsResource = resourceResolver.getResource(path);
+            if (announcementsResource!=null) {
+                resourceResolver.delete(announcementsResource);
+                resourceResolver.commit();
+            }
+
+        } catch (LoginException e) {
+            logger.error(
+                    "unregisterAnnouncement: could not log in administratively: "
+                            + e, e);
+            throw new RuntimeException("Could not log in to repository (" + e
+                    + ")", e);
+        } catch (PersistenceException e) {
+            logger.error("unregisterAnnouncement: got a PersistenceException: "
+                    + e, e);
+            throw new RuntimeException(
+                    "Exception while talking to repository (" + e + ")", e);
+        } finally {
+            if (resourceResolver != null) {
+                resourceResolver.close();
+            }
+        }
+    }
+
+    public synchronized Collection<Announcement> listLocalAnnouncements() {
+        return fillWithCachedAnnouncements(new LinkedList<Announcement>());
+    }
+    
+    public synchronized Collection<CachedAnnouncement> listLocalIncomingAnnouncements() {
+        Collection<CachedAnnouncement> result = new LinkedList<CachedAnnouncement>(ownAnnouncementsCache.values());
+        for (Iterator<CachedAnnouncement> it = result.iterator(); it.hasNext();) {
+            CachedAnnouncement cachedAnnouncement = it.next();
+            if (cachedAnnouncement.getAnnouncement().isInherited()) {
+                it.remove();
+                continue;
+            }
+            if (cachedAnnouncement.hasExpired()) {
+                it.remove();
+                continue;
+            }
+        }
+        return result;
+    }
+    
+    private final InstanceDescription getLocalInstanceDescription(final ClusterView localClusterView) {
+        for (Iterator<InstanceDescription> it = localClusterView.getInstances().iterator(); it
+                .hasNext();) {
+            InstanceDescription id = it.next();
+            if (id.isLocal()) {
+                return id;
+            }
+        }
+        return null;
+    }
+
+    public synchronized Collection<Announcement> listAnnouncementsInSameCluster(final ClusterView localClusterView) {
+        logger.debug("listAnnouncementsInSameCluster: start. localClusterView: {}", localClusterView);
+        if (localClusterView==null) {
+            throw new IllegalArgumentException("clusterView must not be null");
+        }
+        ResourceResolver resourceResolver = null;
+        final Collection<Announcement> incomingAnnouncements = new LinkedList<Announcement>();
+        final InstanceDescription localInstance = getLocalInstanceDescription(localClusterView);
+        try {
+            resourceResolver = resourceResolverFactory
+                    .getAdministrativeResourceResolver(null);
+
+            Resource clusterInstancesResource = ResourceHelper
+                    .getOrCreateResource(
+                            resourceResolver,
+                            config.getClusterInstancesPath());
+
+            Iterator<Resource> it0 = clusterInstancesResource.getChildren()
+                    .iterator();
+            while (it0.hasNext()) {
+                Resource aClusterInstanceResource = it0.next();
+                final String instanceId = aClusterInstanceResource.getName();
+                logger.debug("listAnnouncementsInSameCluster: handling clusterInstance: {}", instanceId);
+                if (localInstance!=null && localInstance.getSlingId().equals(instanceId)) {
+                    // this is the local instance then - which we serve from the cache only
+                    logger.debug("listAnnouncementsInSameCluster: matched localInstance, filling with cache: {}", instanceId);
+                    fillWithCachedAnnouncements(incomingAnnouncements);
+                    continue;
+                }
+                
+                //TODO: add ClusterView.contains(instanceSlingId) for convenience to next api change
+                if (!contains(localClusterView, instanceId)) {
+                    logger.debug("listAnnouncementsInSameCluster: instance is not in my view, ignoring: {}", instanceId);
+                    // then the instance is not in my view, hence ignore its announcements
+                    // (corresponds to earlier expiry-handling)
+                    continue;
+                }
+                final Resource announcementsResource = aClusterInstanceResource
+                        .getChild("announcements");
+                if (announcementsResource == null) {
+                    logger.debug("listAnnouncementsInSameCluster: instance has no announcements: {}", instanceId);
+                    continue;
+                }
+                logger.debug("listAnnouncementsInSameCluster: instance has announcements: {}", instanceId);
+                Iterator<Resource> it = announcementsResource.getChildren()
+                        .iterator();
+                Announcement topologyAnnouncement;
+                while (it.hasNext()) {
+                    Resource anAnnouncement = it.next();
+                    topologyAnnouncement = Announcement
+                            .fromJSON(anAnnouncement
+                                    .adaptTo(ValueMap.class).get(
+                                            "topologyAnnouncement",
+                                            String.class));
+                    logger.debug("listAnnouncementsInSameCluster: found announcement: {}", topologyAnnouncement);
+                    incomingAnnouncements.add(topologyAnnouncement);
+                    // SLING-3389: no longer check for expired announcements - 
+                    // instead make use of the fact that this instance
+                    // has a clusterView and that every live instance
+                    // is responsible of cleaning up expired announcements
+                    // with the repository
+                }
+            }
+            // since SLING-3389 this method does only read operations, hence
+            // no commit necessary anymore - close happens in below finally block
+        } catch (LoginException e) {
+            logger.error(
+                    "listAnnouncementsInSameCluster: could not log in administratively: " + e, e);
+            throw new RuntimeException("Could not log in to repository (" + e
+                    + ")", e);
+        } catch (PersistenceException e) {
+            logger.error("listAnnouncementsInSameCluster: got a PersistenceException: " + e, e);
+            throw new RuntimeException(
+                    "Exception while talking to repository (" + e + ")", e);
+        } catch (JSONException e) {
+            logger.error("listAnnouncementsInSameCluster: got a JSONException: " + e, e);
+            throw new RuntimeException("Exception while converting json (" + e
+                    + ")", e);
+        } finally {
+            if (resourceResolver != null) {
+                resourceResolver.close();
+            }
+        }
+    	if (logger.isDebugEnabled()) {
+    		logger.debug("listAnnouncementsInSameCluster: result: "+incomingAnnouncements.size());
+    	}
+        return incomingAnnouncements;
+    }
+    
+    private final Collection<Announcement> fillWithCachedAnnouncements(
+            final Collection<Announcement> incomingAnnouncements) {
+        for (Iterator<Entry<String, CachedAnnouncement>> it = ownAnnouncementsCache.entrySet().iterator(); it
+                .hasNext();) {
+            final Entry<String, CachedAnnouncement> entry = it.next();
+            if (entry.getValue().hasExpired()) {
+                // filter this one out then
+                continue;
+            }
+            incomingAnnouncements.add(entry.getValue().getAnnouncement());
+        }
+        return incomingAnnouncements;
+    }
+
+    private final boolean contains(final ClusterView clusterView, final String instanceId) {
+        for (Iterator<InstanceDescription> it = clusterView.getInstances().iterator(); it
+                .hasNext();) {
+            InstanceDescription instance = it.next();
+            if (instance.getSlingId().equals(instanceId)) {
+                // fine, then the instance is in the view
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public synchronized boolean hasActiveAnnouncement(final String ownerId) {
+        if (ownerId==null || ownerId.length()==0) {
+            throw new IllegalArgumentException("ownerId must not be null or empty: "+ownerId);
+        }
+        final CachedAnnouncement cachedAnnouncement = ownAnnouncementsCache.get(ownerId);
+        if (cachedAnnouncement==null) {
+            return false;
+        }
+        
+        return !cachedAnnouncement.hasExpired();
+    }
+
+    public synchronized long registerAnnouncement(final Announcement topologyAnnouncement) {
+        if (topologyAnnouncement==null) {
+            throw new IllegalArgumentException("topologyAnnouncement must not be null");
+        }
+        if (!topologyAnnouncement.isValid()) {
+            logger.warn("topologyAnnouncement is not valid");
+            return -1;
+        }
+        if (resourceResolverFactory == null) {
+            logger.error("registerAnnouncement: resourceResolverFactory is null");
+            return -1;
+        }
+        
+        final CachedAnnouncement cachedAnnouncement = 
+                ownAnnouncementsCache.get(topologyAnnouncement.getOwnerId());
+        if (cachedAnnouncement!=null) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("registerAnnouncement: got existing cached announcement for ownerId="+topologyAnnouncement.getOwnerId());
+            }
+            try{
+                if (topologyAnnouncement.correspondsTo(cachedAnnouncement.getAnnouncement())) {
+                    // then nothing has changed with this announcement, so just update
+                    // the heartbeat and fine is.
+                    // this should actually be the normal case for a stable connector
+                    logger.debug("registerAnnouncement: nothing has changed, only updating heartbeat in-memory.");
+                    return cachedAnnouncement.registerPing(topologyAnnouncement, config);
+                }
+                logger.debug("registerAnnouncement: incoming announcement differs from existing one!");
+                
+            } catch(JSONException e) {
+                logger.error("registerAnnouncement: got JSONException while converting incoming announcement to JSON: "+e, e);
+            }
+            // otherwise the repository and the cache require to be updated
+            // resetting the cache therefore at this point already
+            ownAnnouncementsCache.remove(topologyAnnouncement.getOwnerId());
+        } else {
+            logger.debug("registerAnnouncement: no cached announcement yet for ownerId="+topologyAnnouncement.getOwnerId());
+        }
+
+        logger.debug("registerAnnouncement: getting the list of all local announcements");
+        final Collection<Announcement> announcements = new LinkedList<Announcement>();
+        fillWithCachedAnnouncements(announcements);
+        if (logger.isDebugEnabled()) {
+            logger.debug("registerAnnouncement: list returned: "+(announcements==null ? "null" : announcements.size()));
+        }
+        for (Iterator<Announcement> it1 = announcements.iterator(); it1
+                .hasNext();) {
+            Announcement announcement = it1.next();
+            if (announcement.getOwnerId().equals(
+                    topologyAnnouncement.getOwnerId())) {
+                // then this is from the same owner - skip this
+                continue;
+            }
+            // analyse to see if any of the instances in the announcement
+            // include the new owner
+            Collection<InstanceDescription> attachedInstances = announcement
+                    .listInstances();
+            for (Iterator<InstanceDescription> it2 = attachedInstances
+                    .iterator(); it2.hasNext();) {
+                InstanceDescription instanceDescription = it2.next();
+                if (topologyAnnouncement.getOwnerId().equals(
+                        instanceDescription.getSlingId())) {
+                    logger.info("registerAnnouncement: already have this instance attached: "
+                            + instanceDescription.getSlingId());
+                    return -1;
+                }
+            }
+        }
+
+        ResourceResolver resourceResolver = null;
+        try {
+            resourceResolver = resourceResolverFactory
+                    .getAdministrativeResourceResolver(null);
+
+            final Resource announcementsResource = ResourceHelper
+                    .getOrCreateResource(
+                            resourceResolver,
+                            config.getClusterInstancesPath()
+                                    + "/"
+                                    + slingId
+                                    + "/announcements");
+
+            topologyAnnouncement.persistTo(announcementsResource);
+            resourceResolver.commit();
+            ownAnnouncementsCache.put(topologyAnnouncement.getOwnerId(), 
+                    new CachedAnnouncement(topologyAnnouncement, config));
+        } catch (LoginException e) {
+            logger.error(
+                    "registerAnnouncement: could not log in administratively: "
+                            + e, e);
+            throw new RuntimeException("Could not log in to repository (" + e
+                    + ")", e);
+        } catch (PersistenceException e) {
+            logger.error("registerAnnouncement: got a PersistenceException: "
+                    + e, e);
+            throw new RuntimeException(
+                    "Exception while talking to repository (" + e + ")", e);
+        } catch (JSONException e) {
+            logger.error("registerAnnouncement: got a JSONException: " + e, e);
+            throw new RuntimeException("Exception while converting json (" + e
+                    + ")", e);
+        } finally {
+            if (resourceResolver != null) {
+                resourceResolver.close();
+            }
+        }
+        return 0;
+    }
+
+    public synchronized void addAllExcept(final Announcement target, final ClusterView clusterView, 
+            final AnnouncementFilter filter) {
+        ResourceResolver resourceResolver = null;
+        try {
+            resourceResolver = resourceResolverFactory
+                    .getAdministrativeResourceResolver(null);
+
+            final Resource clusterInstancesResource = ResourceHelper
+                    .getOrCreateResource(
+                            resourceResolver,
+                            config.getClusterInstancesPath());
+
+            final Iterator<Resource> it0 = clusterInstancesResource.getChildren()
+                    .iterator();
+            Resource announcementsResource;
+            while (it0.hasNext()) {
+                final Resource aClusterInstanceResource = it0.next();
+                final String instanceId = aClusterInstanceResource.getName();
+                //TODO: add ClusterView.contains(instanceSlingId) for convenience to next api change
+                if (!contains(clusterView, instanceId)) {
+                    // then the instance is not in my view, hence dont propagate
+                    // its announcements
+                    // (corresponds to earlier expiry-handling)
+                    continue;
+                }
+                announcementsResource = aClusterInstanceResource
+                        .getChild("announcements");
+                if (announcementsResource == null) {
+                    continue;
+                }
+                Iterator<Resource> it = announcementsResource.getChildren()
+                        .iterator();
+                while (it.hasNext()) {
+                    Resource anAnnouncement = it.next();
+                	if (logger.isDebugEnabled()) {
+	                    logger.debug("addAllExcept: anAnnouncement="
+	                            + anAnnouncement);
+                	}
+                    Announcement topologyAnnouncement;
+                    topologyAnnouncement = Announcement.fromJSON(anAnnouncement
+                            .adaptTo(ValueMap.class).get(
+                                    "topologyAnnouncement", String.class));
+                    if (filter != null && !filter.accept(aClusterInstanceResource.getName(), topologyAnnouncement)) {
+                        continue;
+                    }
+                    target.addIncomingTopologyAnnouncement(topologyAnnouncement);
+                }
+            }
+            // even before SLING-3389 this method only did read operations,
+            // hence no commit was ever necessary. The close happens in the finally block
+        } catch (LoginException e) {
+            logger.error(
+                    "handleEvent: could not log in administratively: " + e, e);
+            throw new RuntimeException("Could not log in to repository (" + e
+                    + ")", e);
+        } catch (PersistenceException e) {
+            logger.error("handleEvent: got a PersistenceException: " + e, e);
+            throw new RuntimeException(
+                    "Exception while talking to repository (" + e + ")", e);
+        } catch (JSONException e) {
+            logger.error("handleEvent: got a JSONException: " + e, e);
+            throw new RuntimeException("Exception while converting json (" + e
+                    + ")", e);
+        } finally {
+            if (resourceResolver != null) {
+                resourceResolver.close();
+            }
+        }
+    }
+
+    public synchronized void checkExpiredAnnouncements() {
+        for (Iterator<Entry<String, CachedAnnouncement>> it = 
+                ownAnnouncementsCache.entrySet().iterator(); it.hasNext();) {
+            final Entry<String, CachedAnnouncement> entry = it.next();
+            if (entry.getValue().hasExpired()) {
+                // then we have an expiry
+                it.remove();
+                
+                final String instanceId = entry.getKey();
+                logger.info("checkExpiredAnnouncements: topology connector of "+instanceId+
+                        " (to me="+slingId+
+                        ", inherited="+entry.getValue().getAnnouncement().isInherited()+") has expired.");
+                deleteAnnouncementsOf(instanceId);
+            }
+        }
+        //SLING-4139 : also make sure there are no stale announcements
+        //             in the repository (from a crash or any other action).
+        //             The ownAnnouncementsCache is the authorative set
+        //             of announcements that are registered to this
+        //             instance's registry - and the repository must not
+        //             contain any additional announcements
+        ResourceResolver resourceResolver = null;
+        try {
+            resourceResolver = resourceResolverFactory
+                    .getAdministrativeResourceResolver(null);
+            final Resource announcementsResource = ResourceHelper
+                    .getOrCreateResource(
+                            resourceResolver,
+                            config.getClusterInstancesPath()
+                                    + "/"
+                                    + slingId
+                                    + "/announcements");
+            final Iterator<Resource> it = announcementsResource.getChildren().iterator();
+            while(it.hasNext()) {
+            	final Resource res = it.next();
+            	final String ownerId = res.getName();
+            	// ownerId is the slingId of the owner of the announcement (ie of the peer of the connector).
+            	// let's check if the we have that owner's announcement in the cache
+            	
+            	if (ownAnnouncementsCache.containsKey(ownerId)) {
+            		// fine then, we'll leave this announcement untouched
+            		continue;
+            	}
+            	// otherwise this announcement is likely from an earlier incarnation
+            	// of this instance - hence stale - hence we must remove it now
+            	//  (SLING-4139)
+            	ResourceHelper.deleteResource(resourceResolver, 
+            			res.getPath());
+            }
+            resourceResolver.commit();
+            resourceResolver.close();
+            resourceResolver = null;
+        } catch (LoginException e) {
+            logger.error(
+                    "checkExpiredAnnouncements: could not log in administratively when checking "
+                    + "for expired announcements of slingId="+slingId+": " + e, e);
+        } catch (PersistenceException e) {
+            logger.error(
+                    "checkExpiredAnnouncements: got PersistenceException when checking "
+                    + "for expired announcements of slingId="+slingId+": " + e, e);
+        } finally {
+            if (resourceResolver!=null) {
+                resourceResolver.revert();
+                resourceResolver.close();
+                resourceResolver = null;
+            }
+        }
+    }
+
+    private final void deleteAnnouncementsOf(final String instanceId) {
+        ResourceResolver resourceResolver = null;
+        try {
+            resourceResolver = resourceResolverFactory
+                    .getAdministrativeResourceResolver(null);
+            ResourceHelper.deleteResource(resourceResolver, 
+                    config.getClusterInstancesPath()
+                                + "/"
+                                + slingId
+                                + "/announcements/"
+                                + instanceId);
+            resourceResolver.commit();
+            resourceResolver.close();
+            resourceResolver = null;
+        } catch (LoginException e) {
+            logger.error(
+                    "deleteAnnouncementsOf: could not log in administratively when deleting "
+                    + "announcements of instanceId="+instanceId+": " + e, e);
+        } catch (PersistenceException e) {
+            logger.error(
+                    "deleteAnnouncementsOf: got PersistenceException when deleting "
+                    + "announcements of instanceId="+instanceId+": " + e, e);
+        } finally {
+            if (resourceResolver!=null) {
+                resourceResolver.revert();
+                resourceResolver.close();
+                resourceResolver = null;
+            }
+        }
+    }
+
+    public synchronized Collection<InstanceDescription> listInstances(final ClusterView localClusterView) {
+        logger.debug("listInstances: start. localClusterView: {}", localClusterView);
+        final Collection<InstanceDescription> instances = new LinkedList<InstanceDescription>();
+
+        final Collection<Announcement> announcements = listAnnouncementsInSameCluster(localClusterView);
+        if (announcements == null) {
+            logger.debug("listInstances: no announcement found. end. instances: {}", instances);
+            return instances;
+        }
+
+        for (Iterator<Announcement> it = announcements.iterator(); it.hasNext();) {
+            final Announcement announcement = it.next();
+            logger.debug("listInstances: adding announcement: {}", announcement);
+            instances.addAll(announcement.listInstances());
+        }
+        logger.debug("listInstances: announcements added. end. instances: {}", instances);
+        return instances;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/CachedAnnouncement.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/CachedAnnouncement.java
new file mode 100644
index 0000000..fc946ef
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/CachedAnnouncement.java
@@ -0,0 +1,122 @@
+/*
+ * 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.sling.discovery.base.connectors.announcement;
+
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * With SLING-3389 the Announcement itself doesn't use the created
+ * (ie timeout) field anymore (it still has it currently for backwards
+ * compatibility on the wire-level) - hence that's why there's this
+ * small in-memory wrapper object which contains an Announcement and 
+ * carries a lastHeartbeat property.
+ */
+public class CachedAnnouncement {
+    
+    private final static Logger logger = LoggerFactory.getLogger(CachedAnnouncement.class);
+
+    private long lastPing = System.currentTimeMillis();
+
+    private final Announcement announcement;
+    
+    private long firstPing = System.currentTimeMillis();
+
+    private long backoffIntervalSeconds = -1;
+
+    private final BaseConfig config;
+    
+    CachedAnnouncement(final Announcement announcement, final BaseConfig config) {
+        this.announcement = announcement;
+        this.config = config;
+    }
+    
+    private long getConfiguredConnectorTimeout() {
+        return config.getConnectorPingTimeout();
+    }
+    
+    private long getConfiguredConnectorInterval() {
+        return config.getConnectorPingInterval();
+    }
+
+    public final boolean hasExpired() {
+        final long now = System.currentTimeMillis();
+        final long diff = now-lastPing;
+        if (diff<1000*getEffectiveHeartbeatTimeout()) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+    
+    public final long getLastPing() {
+        return lastPing;
+    }
+    
+    /** Returns the second until the next heartbeat is expected, otherwise the timeout will hit **/
+    public final long getSecondsUntilTimeout() {
+        final long now = System.currentTimeMillis();
+        final long diff = now-lastPing;
+        final long left = 1000*getEffectiveHeartbeatTimeout() - diff;
+        return left/1000;
+    }
+    
+    
+    private final long getEffectiveHeartbeatTimeout() {
+        final long configuredGoodwill = getConfiguredConnectorTimeout() - getConfiguredConnectorInterval();
+        return Math.max(getConfiguredConnectorTimeout(), backoffIntervalSeconds + configuredGoodwill);
+    }
+
+    /** Registers a heartbeat event, and returns the new resulting backoff interval -
+     * or 0 if no backoff is applicable yet.
+     * @param incomingAnnouncement 
+     * @return the new resulting backoff interval -
+     * or 0 if no backoff is applicable yet.
+     */
+    final long registerPing(Announcement incomingAnnouncement, BaseConfig config) {
+        lastPing = System.currentTimeMillis();
+        if (incomingAnnouncement.isInherited()) {
+            // then we are the client, we inherited this announcement from the server
+            // hence we have no power to do any backoff instructions towards the server
+            // (since the server decides about backoff-ing). hence returning 0 here
+            // but taking note of what the server instructed us in terms of backoff
+            backoffIntervalSeconds = incomingAnnouncement.getBackoffInterval();
+            logger.debug("registerPing: inherited announcement - hence returning 0");
+            return 0;
+        }
+        if (incomingAnnouncement.getResetBackoff()) {
+            // on resetBackoff we reset the firstHeartbeat and start 
+            // from 0 again
+            firstPing = lastPing;
+            logger.debug("registerPing: got a resetBackoff - hence returning 0");
+            return 0;
+        }
+        final long stableSince = lastPing - firstPing;
+        final long numStableTimeouts = stableSince / (1000 * config.getConnectorPingTimeout());
+        final long backoffFactor = Math.min(numStableTimeouts, config.getBackoffStableFactor());
+        backoffIntervalSeconds = backoffFactor * config.getConnectorPingInterval();
+        return backoffIntervalSeconds;
+    }
+
+    public final Announcement getAnnouncement() {
+        return announcement;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/announcement/package-info.java b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/package-info.java
new file mode 100644
index 0000000..42eab68
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/announcement/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides topology announcement implementations for discovery
+ * implementors that choose to extend from discovery.base
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+package org.apache.sling.discovery.base.connectors.announcement;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/package-info.java b/src/main/java/org/apache/sling/discovery/base/connectors/package-info.java
new file mode 100644
index 0000000..2fa7bf8
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides topology connector related classes for discovery
+ * implementors that choose to extend from discovery.base
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+package org.apache.sling.discovery.base.connectors;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistry.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistry.java
new file mode 100644
index 0000000..ba81211
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistry.java
@@ -0,0 +1,44 @@
+/*
+ * 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.sling.discovery.base.connectors.ping;
+
+import java.net.URL;
+import java.util.Collection;
+
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+
+/**
+ * Registry for topology connector clients
+ */
+public interface ConnectorRegistry {
+
+    /** Register an outgoing topology connector using the provided endpoint url **/
+    TopologyConnectorClientInformation registerOutgoingConnector(
+            ClusterViewService clusterViewService, URL topologyConnectorEndpoint);
+
+    /** Lists all outgoing topology connectors **/
+    Collection<TopologyConnectorClientInformation> listOutgoingConnectors();
+
+    /** ping all outgoing topology connectors **/
+    void pingOutgoingConnectors(boolean force);
+
+    /** Unregister an outgoing topology connector identified by the given (connector) id **/
+    boolean unregisterOutgoingConnector(String id);
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImpl.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImpl.java
new file mode 100644
index 0000000..f46a6e4
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImpl.java
@@ -0,0 +1,157 @@
+/*
+ * 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.sling.discovery.base.connectors.ping;
+
+import java.net.InetAddress;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default implementation of the ConnectorRegistry which
+ * keeps a list of outgoing connectors and is capable of
+ * pinging them.
+ */
+@Component
+@Service(value = ConnectorRegistry.class)
+public class ConnectorRegistryImpl implements ConnectorRegistry {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** A map of id-> topology connector clients currently registered/activate **/
+    private final Map<String, TopologyConnectorClient> outgoingClientsMap = new HashMap<String, TopologyConnectorClient>();
+
+    @Reference
+    private AnnouncementRegistry announcementRegistry;
+
+    @Reference
+    private BaseConfig config;
+
+    /** the local port is added to the announcement as the serverInfo object **/
+    private String port = "";
+
+    public static ConnectorRegistry testConstructor(AnnouncementRegistry announcementRegistry,
+            BaseConfig config) {
+        ConnectorRegistryImpl registry = new ConnectorRegistryImpl();
+        registry.announcementRegistry = announcementRegistry;
+        registry.config = config;
+        // Note that port is not set - but that is only for information purpose
+        // and not useful for testing
+        return registry;
+    }
+    
+    @Activate
+    protected void activate(final ComponentContext cc) {
+        port = cc.getBundleContext().getProperty("org.osgi.service.http.port");
+    }
+    
+    @Deactivate
+    protected void deactivate() {
+        synchronized (outgoingClientsMap) {
+            for (Iterator<TopologyConnectorClient> it = outgoingClientsMap.values().iterator(); it.hasNext();) {
+                final TopologyConnectorClient client = it.next();
+                client.disconnect();
+                it.remove();
+            }
+        }
+    }
+    
+    public TopologyConnectorClientInformation registerOutgoingConnector(
+            final ClusterViewService clusterViewService, final URL connectorUrl) {
+        if (announcementRegistry == null) {
+            logger.error("registerOutgoingConnection: announcementRegistry is null");
+            return null;
+        }
+        TopologyConnectorClient client;
+        synchronized (outgoingClientsMap) {
+            for (Iterator<Entry<String, TopologyConnectorClient>> it = outgoingClientsMap
+                    .entrySet().iterator(); it.hasNext();) {
+                Entry<String, TopologyConnectorClient> entry = it.next();
+                if (entry.getValue().getConnectorUrl().toExternalForm().equals(connectorUrl.toExternalForm())) {
+                    it.remove();
+                    logger.info("registerOutgoingConnection: re-registering connector: "+connectorUrl);
+                }
+            }
+            String serverInfo;
+            try {
+                serverInfo = InetAddress.getLocalHost().getCanonicalHostName()
+                        + ":" + port;
+            } catch (Exception e) {
+                serverInfo = "localhost:" + port;
+            }
+            client = new TopologyConnectorClient(clusterViewService,
+                    announcementRegistry, config, connectorUrl,
+                    serverInfo);
+            outgoingClientsMap.put(client.getId(), client);
+        }
+        client.ping(false);
+        return client;
+    }
+
+    public Collection<TopologyConnectorClientInformation> listOutgoingConnectors() {
+        final List<TopologyConnectorClientInformation> result = new ArrayList<TopologyConnectorClientInformation>();
+        synchronized (outgoingClientsMap) {
+            result.addAll(outgoingClientsMap.values());
+        }
+        return result;
+    }
+
+    public boolean unregisterOutgoingConnector(final String id) {
+        if (id == null || id.length() == 0) {
+            throw new IllegalArgumentException("id must not be null");
+        }
+        synchronized (outgoingClientsMap) {
+            TopologyConnectorClient client = outgoingClientsMap.remove(id);
+            if (client != null) {
+                client.disconnect();
+            }
+            return client != null;
+        }
+    }
+
+    public void pingOutgoingConnectors(boolean force) {
+        List<TopologyConnectorClient> outgoingTemplatesClone;
+        synchronized (outgoingClientsMap) {
+            outgoingTemplatesClone = new ArrayList<TopologyConnectorClient>(
+                    outgoingClientsMap.values());
+        }
+        for (Iterator<TopologyConnectorClient> it = outgoingTemplatesClone
+                .iterator(); it.hasNext();) {
+            it.next().ping(force);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java
new file mode 100644
index 0000000..5686ec5
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClient.java
@@ -0,0 +1,496 @@
+/*
+ * 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.sling.discovery.base.connectors.ping;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.UUID;
+import java.util.zip.GZIPOutputStream;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.Header;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.config.SocketConfig;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.Announcement;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementFilter;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A topology connector client is used for sending (pinging) a remote topology
+ * connector servlet and exchanging announcements with it
+ */
+public class TopologyConnectorClient implements
+        TopologyConnectorClientInformation {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    /** the endpoint url **/
+    private final URL connectorUrl;
+
+    /** the cluster view service **/
+    private final ClusterViewService clusterViewService;
+
+    /** the config service to user **/
+    private final BaseConfig config;
+
+    /** the id of this connection **/
+    private final UUID id;
+
+    /** the announcement registry **/
+    private final AnnouncementRegistry announcementRegistry;
+
+    /** the last inherited announcement **/
+    private Announcement lastInheritedAnnouncement;
+
+    /** the time when the last announcement was inherited - for webconsole use only **/
+    private long lastPingedAt;
+    
+    /** the information about this server **/
+    private final String serverInfo;
+    
+    /** the status code of the last post **/
+    private int lastStatusCode = -1;
+    
+    /** SLING-3316: whether or not this connector was auto-stopped **/
+    private boolean autoStopped = false;
+    
+    /** more details about connection failures **/
+    private String statusDetails = null;
+    
+    /** SLING-2882: whether or not to suppress ping warnings **/
+    private boolean suppressPingWarnings_ = false;
+
+    private TopologyRequestValidator requestValidator;
+
+    /** value of Content-Encoding of the last request **/
+    private String lastRequestEncoding;
+
+    /** value of Content-Encoding of the last repsonse **/
+    private String lastResponseEncoding;
+
+    /** SLING-3382: unix-time at which point the backoff-period ends and pings can be sent again **/
+    private long backoffPeriodEnd = -1;
+    
+    TopologyConnectorClient(final ClusterViewService clusterViewService,
+            final AnnouncementRegistry announcementRegistry, final BaseConfig config,
+            final URL connectorUrl, final String serverInfo) {
+        if (clusterViewService == null) {
+            throw new IllegalArgumentException(
+                    "clusterViewService must not be null");
+        }
+        if (announcementRegistry == null) {
+            throw new IllegalArgumentException(
+                    "announcementRegistry must not be null");
+        }
+        if (config == null) {
+            throw new IllegalArgumentException("config must not be null");
+        }
+        if (connectorUrl == null) {
+            throw new IllegalArgumentException("connectorUrl must not be null");
+        }
+        this.requestValidator = new TopologyRequestValidator(config);
+        this.clusterViewService = clusterViewService;
+        this.announcementRegistry = announcementRegistry;
+        this.config = config;
+        this.connectorUrl = connectorUrl;
+        this.serverInfo = serverInfo;
+        this.id = UUID.randomUUID();
+    }
+
+    /** ping the server and pass the announcements between the two **/
+    void ping(final boolean force) {
+    	if (autoStopped) {
+    		// then we suppress any further pings!
+    		logger.debug("ping: autoStopped=true, hence suppressing any further pings.");
+    		return;
+    	}
+    	if (force) {
+    	    backoffPeriodEnd = -1;
+    	} else if (backoffPeriodEnd>0) {
+    	    if (System.currentTimeMillis()<backoffPeriodEnd) {
+    	        logger.debug("ping: not issueing a heartbeat due to backoff instruction from peer.");
+    	        return;
+    	    } else {
+                logger.debug("ping: backoff period ended, issuing another ping now.");
+    	    }
+    	}
+        final String uri = connectorUrl.toString()+"."+clusterViewService.getSlingId()+".json";
+    	if (logger.isDebugEnabled()) {
+    		logger.debug("ping: connectorUrl=" + connectorUrl + ", complete uri=" + uri);
+    	}
+    	final HttpClientContext clientContext = HttpClientContext.create();
+    	final CloseableHttpClient httpClient = createHttpClient();
+    	final HttpPut putRequest = new HttpPut(uri);
+
+    	// setting the connection timeout (idle connection, configured in seconds)
+    	putRequest.setConfig(RequestConfig.
+    			custom().
+    			setConnectTimeout(1000*config.getSocketConnectionTimeout()).
+    			build());
+
+        Announcement resultingAnnouncement = null;
+        try {
+            String userInfo = connectorUrl.getUserInfo();
+            if (userInfo != null) {
+                Credentials c = new UsernamePasswordCredentials(userInfo);
+            	clientContext.getCredentialsProvider().setCredentials(
+                        new AuthScope(putRequest.getURI().getHost(), putRequest
+                                .getURI().getPort()), c);
+            }
+
+            Announcement topologyAnnouncement = new Announcement(
+                    clusterViewService.getSlingId());
+            topologyAnnouncement.setServerInfo(serverInfo);
+            final ClusterView clusterView;
+            try {
+                clusterView = clusterViewService
+                        .getLocalClusterView();
+            } catch (UndefinedClusterViewException e) {
+                // SLING-5030 : then we cannot ping
+                logger.warn("ping: no clusterView available at the moment, cannot ping others now: "+e);
+                return;
+            }
+            topologyAnnouncement.setLocalCluster(clusterView);
+            if (force) {
+                logger.debug("ping: sending a resetBackoff");
+                topologyAnnouncement.setResetBackoff(true);
+            }
+            announcementRegistry.addAllExcept(topologyAnnouncement, clusterView, new AnnouncementFilter() {
+                
+                public boolean accept(final String receivingSlingId, final Announcement announcement) {
+                    // filter out announcements that are of old cluster instances
+                    // which I dont really have in my cluster view at the moment
+                    final Iterator<InstanceDescription> it = 
+                            clusterView.getInstances().iterator();
+                    while(it.hasNext()) {
+                        final InstanceDescription instance = it.next();
+                        if (instance.getSlingId().equals(receivingSlingId)) {
+                            // then I have the receiving instance in my cluster view
+                            // all fine then
+                            return true;
+                        }
+                    }
+                    // looks like I dont have the receiving instance in my cluster view
+                    // then I should also not propagate that announcement anywhere
+                    return false;
+                }
+            });
+            final String p = requestValidator.encodeMessage(topologyAnnouncement.asJSON());
+            
+            if (logger.isDebugEnabled()) {
+                logger.debug("ping: topologyAnnouncement json is: " + p);
+            }
+            requestValidator.trustMessage(putRequest, p);
+            if (config.isGzipConnectorRequestsEnabled()) {
+                // tell the server that the content is gzipped:
+                putRequest.addHeader("Content-Encoding", "gzip");
+                // and gzip the body:
+                final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                final GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
+                gzipOut.write(p.getBytes("UTF-8"));
+                gzipOut.close();
+                final byte[] gzippedEncodedJson = baos.toByteArray();
+                putRequest.setEntity(new ByteArrayEntity(gzippedEncodedJson, ContentType.APPLICATION_JSON));
+                lastRequestEncoding = "gzip";
+            } else {
+                // otherwise plaintext:
+            	final StringEntity plaintext = new StringEntity(p, "UTF-8");
+            	plaintext.setContentType(ContentType.APPLICATION_JSON.getMimeType());
+            	putRequest.setEntity(plaintext);
+                lastRequestEncoding = "plaintext";
+            }
+            // independent of request-gzipping, we do accept the response to be gzipped,
+            // so indicate this to the server:
+            putRequest.addHeader("Accept-Encoding", "gzip");
+            final CloseableHttpResponse response = httpClient.execute(putRequest, clientContext);
+        	if (logger.isDebugEnabled()) {
+	            logger.debug("ping: done. code=" + response.getStatusLine().getStatusCode() + " - "
+	                    + response.getStatusLine().getReasonPhrase());
+        	}
+            lastStatusCode = response.getStatusLine().getStatusCode();
+            lastResponseEncoding = null;
+            if (response.getStatusLine().getStatusCode()==HttpServletResponse.SC_OK) {
+                final Header contentEncoding = response.getFirstHeader("Content-Encoding");
+                if (contentEncoding!=null && contentEncoding.getValue()!=null &&
+                        contentEncoding.getValue().contains("gzip")) {
+                    lastResponseEncoding = "gzip";
+                } else {
+                    lastResponseEncoding = "plaintext";
+                }
+                final String responseBody = requestValidator.decodeMessage(putRequest.getURI().getPath(), response); // limiting to 16MB, should be way enough
+            	if (logger.isDebugEnabled()) {
+            		logger.debug("ping: response body=" + responseBody);
+            	}
+                if (responseBody!=null && responseBody.length()>0) {
+                    Announcement inheritedAnnouncement = Announcement
+                            .fromJSON(responseBody);
+                    final long backoffInterval = inheritedAnnouncement.getBackoffInterval();
+                    if (backoffInterval>0) {
+                        // then reset the backoffPeriodEnd:
+                        
+                        /* minus 1 sec to avoid slipping the interval by a few millis */
+                        this.backoffPeriodEnd = System.currentTimeMillis() + (1000 * backoffInterval) - 1000;
+                        logger.debug("ping: servlet instructed to backoff: backoffInterval="+backoffInterval+", resulting in period end of "+new Date(backoffPeriodEnd));
+                    } else {
+                        logger.debug("ping: servlet did not instruct any backoff-ing at this stage");
+                        this.backoffPeriodEnd = -1;
+                    }
+                    if (inheritedAnnouncement.isLoop()) {
+                    	if (logger.isDebugEnabled()) {
+	                        logger.debug("ping: connector response indicated a loop detected. not registering this announcement from "+
+	                                    inheritedAnnouncement.getOwnerId());
+                    	}
+                    	if (inheritedAnnouncement.getOwnerId().equals(clusterViewService.getSlingId())) {
+                    		// SLING-3316 : local-loop detected. Check config to see if we should stop this connector
+                    		
+                    	    if (config.isAutoStopLocalLoopEnabled()) {
+                    			inheritedAnnouncement = null; // results in connected -> false and representsloop -> true
+                    			autoStopped = true; // results in isAutoStopped -> true
+                    		}
+                    	}
+                    } else {
+                        inheritedAnnouncement.setInherited(true);
+                        if (announcementRegistry
+                                .registerAnnouncement(inheritedAnnouncement)==-1) {
+                        	if (logger.isDebugEnabled()) {
+	                            logger.debug("ping: connector response is from an instance which I already see in my topology"
+	                                    + inheritedAnnouncement);
+                        	}
+                            statusDetails = "receiving side is seeing me via another path (connector or cluster) already (loop)";
+                            return;
+                        }
+                    }
+                    resultingAnnouncement = inheritedAnnouncement;
+                    statusDetails = null;
+                } else {
+                    statusDetails = "no response body received";
+                }
+            } else {
+                statusDetails = "got HTTP Status-Code: "+lastStatusCode;
+            }
+        	// SLING-2882 : reset suppressPingWarnings_ flag in success case
+    		suppressPingWarnings_ = false;
+        } catch (IOException e) {
+        	// SLING-2882 : set/check the suppressPingWarnings_ flag
+        	if (suppressPingWarnings_) {
+        		if (logger.isDebugEnabled()) {
+        			logger.debug("ping: got IOException: " + e + ", uri=" + uri);
+        		}
+        	} else {
+        		suppressPingWarnings_ = true;
+    			logger.warn("ping: got IOException [suppressing further warns]: " + e + ", uri=" + uri);
+        	}
+            statusDetails = e.toString();
+        } catch (JSONException e) {
+            logger.warn("ping: got JSONException: " + e);
+            statusDetails = e.toString();
+        } catch (RuntimeException re) {
+            logger.warn("ping: got RuntimeException: " + re, re);
+            statusDetails = re.toString();
+        } finally {
+            putRequest.releaseConnection();
+            lastInheritedAnnouncement = resultingAnnouncement;
+            lastPingedAt = System.currentTimeMillis();
+            try {
+				httpClient.close();
+			} catch (IOException e) {
+				logger.error("disconnect: could not close httpClient: "+e, e);
+			}
+        }
+    }
+
+	private CloseableHttpClient createHttpClient() {
+		final HttpClientBuilder builder = HttpClientBuilder.create();
+    	// setting the SoTimeout (which is configured in seconds)
+    	builder.setDefaultSocketConfig(SocketConfig.
+    			custom().
+    			setSoTimeout(1000*config.getSoTimeout()).
+    			build());
+		builder.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
+
+    	return builder.build();
+	}
+
+    public int getStatusCode() {
+        return lastStatusCode;
+    }
+    
+    public URL getConnectorUrl() {
+        return connectorUrl;
+    }
+    
+    public boolean representsLoop() {
+    	if (autoStopped) {
+    		return true;
+    	}
+        if (lastInheritedAnnouncement == null) {
+            return false;
+        } else {
+            return lastInheritedAnnouncement.isLoop();
+        }
+    }
+
+    public boolean isConnected() {
+    	if (autoStopped) {
+    		return false;
+    	}
+        if (lastInheritedAnnouncement == null) {
+            return false;
+        } else {
+            return announcementRegistry.hasActiveAnnouncement(lastInheritedAnnouncement.getOwnerId());
+        }
+    }
+    
+    public String getStatusDetails() {
+        if (autoStopped) {
+            return "auto-stopped";
+        }
+        if (lastInheritedAnnouncement == null) {
+            return statusDetails;
+        } else {
+            if (announcementRegistry.hasActiveAnnouncement(lastInheritedAnnouncement.getOwnerId())) {
+                // still active - so no status details
+                return null;
+            } else {
+                return "received announcement has expired (it was last renewed "+new Date(lastPingedAt)+") - consider increasing heartbeat timeout";
+            }
+        }
+    }
+    
+    public long getLastPingSent() {
+        return lastPingedAt;
+    }
+    
+    public int getNextPingDue() {
+        final long absDue;
+        if (backoffPeriodEnd>0) {
+            absDue = backoffPeriodEnd;
+        } else {
+            absDue = lastPingedAt + 1000*config.getConnectorPingInterval();
+        }
+        final int relDue = (int) ((absDue - System.currentTimeMillis()) / 1000);
+        if (relDue<0) {
+            return -1;
+        } else {
+            return relDue;
+        }
+    }
+    
+    public boolean isAutoStopped() {
+    	return autoStopped;
+    }
+    
+    public String getLastRequestEncoding() {
+        return lastRequestEncoding==null ? "" : lastRequestEncoding;
+    }
+
+    public String getLastResponseEncoding() {
+        return lastResponseEncoding==null ? "" : lastResponseEncoding;
+    }
+    
+    public String getRemoteSlingId() {
+        if (lastInheritedAnnouncement == null) {
+            return null;
+        } else {
+            return lastInheritedAnnouncement.getOwnerId();
+        }
+    }
+
+    public String getId() {
+        return id.toString();
+    }
+
+    /** Disconnect this connector **/
+    public void disconnect() {
+        final String uri = connectorUrl.toString()+"."+clusterViewService.getSlingId()+".json";
+    	if (logger.isDebugEnabled()) {
+    		logger.debug("disconnect: connectorUrl=" + connectorUrl + ", complete uri="+uri);
+    	}
+
+        if (lastInheritedAnnouncement != null) {
+            announcementRegistry
+                    .unregisterAnnouncement(lastInheritedAnnouncement
+                            .getOwnerId());
+        }
+
+        final HttpClientContext clientContext = HttpClientContext.create();
+        final CloseableHttpClient httpClient = createHttpClient();
+        final HttpDelete deleteRequest = new HttpDelete(uri);
+        // setting the connection timeout (idle connection, configured in seconds)
+        deleteRequest.setConfig(RequestConfig.
+        		custom().
+        		setConnectTimeout(1000*config.getSocketConnectionTimeout()).
+        		build());
+
+        try {
+            String userInfo = connectorUrl.getUserInfo();
+            if (userInfo != null) {
+                Credentials c = new UsernamePasswordCredentials(userInfo);
+                clientContext.getCredentialsProvider().setCredentials(
+                        new AuthScope(deleteRequest.getURI().getHost(), deleteRequest
+                                .getURI().getPort()), c);
+            }
+
+            requestValidator.trustMessage(deleteRequest, null);
+            final CloseableHttpResponse response = httpClient.execute(deleteRequest, clientContext);
+        	if (logger.isDebugEnabled()) {
+	            logger.debug("disconnect: done. code=" + response.getStatusLine().getStatusCode()
+	                    + " - " + response.getStatusLine().getReasonPhrase());
+        	}
+            // ignoring the actual statuscode though as there's little we can
+            // do about it after this point
+        } catch (IOException e) {
+            logger.warn("disconnect: got IOException: " + e);
+        } catch (RuntimeException re) {
+            logger.error("disconnect: got RuntimeException: " + re, re);
+        } finally {
+            deleteRequest.releaseConnection();
+            try {
+				httpClient.close();
+			} catch (IOException e) {
+				logger.error("disconnect: could not close httpClient: "+e, e);
+			}
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java
new file mode 100644
index 0000000..e410c73
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorClientInformation.java
@@ -0,0 +1,63 @@
+/*
+ * 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.sling.discovery.base.connectors.ping;
+
+import java.net.URL;
+
+/**
+ * provides information about a topology connector client
+ */
+public interface TopologyConnectorClientInformation {
+
+    /** the endpoint url where this connector is connecting to **/
+    URL getConnectorUrl();
+
+    /** return the http status code of the last post to the servlet, -1 if no post was ever done **/
+    int getStatusCode();
+
+    /** SLING-3316 : whether or not this connector was auto-stopped **/
+    boolean isAutoStopped();
+    
+    /** whether or not this connector was able to successfully connect **/
+    boolean isConnected();
+    
+    /** provides more details about connection failures **/
+    String getStatusDetails();
+
+    /** whether or not the counterpart of this connector has detected a loop in the topology connectors **/
+    boolean representsLoop();
+    
+    /** the sling id of the remote end **/
+    String getRemoteSlingId();
+
+    /** the unique id of this connector **/
+    String getId();
+
+    /** the Content-Encoding of the last request **/
+    String getLastRequestEncoding();
+
+    /** the Content-Encoding of the last response **/
+    String getLastResponseEncoding();
+
+    /** the unix-millis when the last heartbeat was sent **/
+    long getLastPingSent();
+
+    /** the seconds until the next heartbeat is due **/
+    int getNextPingDue();
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java
new file mode 100644
index 0000000..4211cf3
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServlet.java
@@ -0,0 +1,360 @@
+/*
+ * 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.sling.discovery.base.connectors.ping;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.zip.GZIPOutputStream;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.base.commons.ClusterViewHelper;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.Announcement;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementFilter;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.wl.SubnetWhitelistEntry;
+import org.apache.sling.discovery.base.connectors.ping.wl.WhitelistEntry;
+import org.apache.sling.discovery.base.connectors.ping.wl.WildcardWhitelistEntry;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Servlet which receives topology announcements at
+ * /libs/sling/topology/connector*
+ * without authorization (authorization is handled either via
+ * hmac-signature with a shared key or via a flexible whitelist)
+ */
+@SuppressWarnings("serial")
+@Component(immediate = true)
+@Service(value=TopologyConnectorServlet.class)
+public class TopologyConnectorServlet extends HttpServlet {
+
+    /** 
+     * prefix under which the topology connector servlet is registered -
+     * the URL will consist of this prefix + "connector.slingId.json" 
+     */
+    private static final String TOPOLOGY_CONNECTOR_PREFIX = "/libs/sling/topology";
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    @Reference
+    private AnnouncementRegistry announcementRegistry;
+
+    @Reference
+    private ClusterViewService clusterViewService;
+
+    @Reference
+    private HttpService httpService;
+    
+    @Reference
+    private BaseConfig config;
+
+    /** 
+     * This list contains WhitelistEntry (ips/hostnames, cidr, wildcards),
+     * each filtering some hostname/addresses that are allowed to connect to this servlet.
+     **/
+    private final List<WhitelistEntry> whitelist = new ArrayList<WhitelistEntry>();
+    
+    /** Set of plaintext whitelist entries - for faster lookups **/
+    private final Set<String> plaintextWhitelist = new HashSet<String>();
+
+    private TopologyRequestValidator requestValidator;
+
+    @Activate
+    protected void activate(final ComponentContext context) {
+        whitelist.clear();
+        if (!config.isHmacEnabled()) {
+            String[] whitelistConfig = config.getTopologyConnectorWhitelist();
+            initWhitelist(whitelistConfig);
+        }
+        requestValidator = new TopologyRequestValidator(config);
+        
+        try {
+            httpService.registerServlet(TopologyConnectorServlet.TOPOLOGY_CONNECTOR_PREFIX, 
+                    this, null, null);
+            logger.info("activate: connector servlet registered at "+
+                    TopologyConnectorServlet.TOPOLOGY_CONNECTOR_PREFIX);
+        } catch (ServletException e) {
+            logger.error("activate: ServletException while registering topology connector servlet: "+e, e);
+        } catch (NamespaceException e) {
+            logger.error("activate: NamespaceException while registering topology connector servlet: "+e, e);
+        }
+    }
+    
+    @Deactivate
+    protected void deactivate() {
+        httpService.unregister(TOPOLOGY_CONNECTOR_PREFIX);
+    }
+
+    void initWhitelist(String[] whitelistConfig) {
+        if (whitelistConfig==null) {
+            return;
+        }
+        for (int i = 0; i < whitelistConfig.length; i++) {
+            String aWhitelistEntry = whitelistConfig[i];
+            
+            WhitelistEntry whitelistEntry = null;
+            if (aWhitelistEntry.contains(".") && aWhitelistEntry.contains("/")) {
+                // then this is a CIDR notation
+                try{
+                    whitelistEntry = new SubnetWhitelistEntry(aWhitelistEntry);
+                } catch(Exception e) {
+                    logger.error("activate: wrongly formatted CIDR subnet definition. Expected eg '1.2.3.4/24'. ignoring: "+aWhitelistEntry);
+                    continue;
+                }
+            } else if (aWhitelistEntry.contains(".") && aWhitelistEntry.contains(" ")) {
+                // then this is a IP/subnet-mask notation
+                try{
+                    final StringTokenizer st = new StringTokenizer(aWhitelistEntry, " ");
+                    final String ip = st.nextToken();
+                    if (st.hasMoreTokens()) {
+                        final String mask = st.nextToken();
+                        if (st.hasMoreTokens()) {
+                            logger.error("activate: wrongly formatted ip subnet definition. Expected '10.1.2.3 255.0.0.0'. Ignoring: "+aWhitelistEntry);
+                            continue;
+                        }
+                        whitelistEntry = new SubnetWhitelistEntry(ip, mask);
+                    }
+                } catch(Exception e) {
+                    logger.error("activate: wrongly formatted ip subnet definition. Expected '10.1.2.3 255.0.0.0'. Ignoring: "+aWhitelistEntry);
+                    continue;
+                }
+            }
+            if (whitelistEntry==null) {
+                if (aWhitelistEntry.contains("*") || aWhitelistEntry.contains("?")) {
+                    whitelistEntry = new WildcardWhitelistEntry(aWhitelistEntry);
+                } else {
+                    plaintextWhitelist.add(aWhitelistEntry);
+                }
+            }
+            logger.info("activate: adding whitelist entry: " + aWhitelistEntry);
+            if (whitelistEntry!=null) {
+                whitelist.add(whitelistEntry);
+            }
+        }
+    }
+
+    @Override
+    protected void doDelete(HttpServletRequest request, HttpServletResponse response)
+            throws ServletException, IOException {
+
+        if (!isWhitelisted(request)) {
+            // in theory it would be 403==forbidden, but that would reveal that
+            // a resource would exist there in the first place
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+
+        final String[] pathInfo = request.getPathInfo().split("\\.");
+        final String extension = pathInfo.length==3 ? pathInfo[2] : "";
+        if (!"json".equals(extension)) {
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+        final String selector = pathInfo.length==3 ? pathInfo[1] : "";
+
+        announcementRegistry.unregisterAnnouncement(selector);
+    }
+    
+    @Override
+    protected void doPut(HttpServletRequest request, HttpServletResponse response)
+            throws ServletException, IOException {
+
+        if (!isWhitelisted(request)) {
+            // in theory it would be 403==forbidden, but that would reveal that
+            // a resource would exist there in the first place
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+
+        final String[] pathInfo = request.getPathInfo().split("\\.");
+        final String extension = pathInfo.length==3 ? pathInfo[2] : "";
+        if (!"json".equals(extension)) {
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+        
+        final String selector = pathInfo.length==3 ? pathInfo[1] : "";
+
+        String topologyAnnouncementJSON = requestValidator.decodeMessage(request);
+    	if (logger.isDebugEnabled()) {
+	        logger.debug("doPost: incoming topology announcement is: "
+	                + topologyAnnouncementJSON);
+    	}
+        final Announcement incomingTopologyAnnouncement;
+        try {
+            incomingTopologyAnnouncement = Announcement
+                    .fromJSON(topologyAnnouncementJSON);
+
+            if (!incomingTopologyAnnouncement.getOwnerId().equals(selector)) {
+                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+                return;
+            }
+
+            String slingId = clusterViewService.getSlingId();
+            if (slingId==null) {
+            	response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            	logger.info("doPut: no slingId available. Service not ready as expected at the moment.");
+            	return;
+            }
+			incomingTopologyAnnouncement.removeInherited(slingId);
+
+            final Announcement replyAnnouncement = new Announcement(
+                    slingId);
+
+            long backoffInterval = -1;
+            ClusterView clusterView = clusterViewService.getLocalClusterView();
+            if (!incomingTopologyAnnouncement.isCorrectVersion()) {
+                logger.warn("doPost: rejecting an announcement from an incompatible connector protocol version: "
+                        + incomingTopologyAnnouncement);
+                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+                return;
+            } else if (ClusterViewHelper.contains(clusterView, incomingTopologyAnnouncement
+                    .getOwnerId())) {
+            	if (logger.isDebugEnabled()) {
+	                logger.debug("doPost: rejecting an announcement from an instance that is part of my cluster: "
+	                        + incomingTopologyAnnouncement);
+            	}
+                // marking as 'loop'
+                replyAnnouncement.setLoop(true);
+                backoffInterval = config.getBackoffStandbyInterval();
+            } else if (ClusterViewHelper.containsAny(clusterView, incomingTopologyAnnouncement
+                    .listInstances())) {
+            	if (logger.isDebugEnabled()) {
+	                logger.debug("doPost: rejecting an announcement as it contains instance(s) that is/are part of my cluster: "
+	                        + incomingTopologyAnnouncement);
+            	}
+                // marking as 'loop'
+                replyAnnouncement.setLoop(true);
+                backoffInterval = config.getBackoffStandbyInterval();
+            } else {
+                backoffInterval = announcementRegistry
+                        .registerAnnouncement(incomingTopologyAnnouncement);
+                if (logger.isDebugEnabled()) {
+                    logger.debug("doPost: backoffInterval after registration: "+backoffInterval);
+                }
+                if (backoffInterval==-1) {
+                	if (logger.isDebugEnabled()) {
+    	                logger.debug("doPost: rejecting an announcement from an instance that I already see in my topology: "
+    	                        + incomingTopologyAnnouncement);
+                	}
+                    // marking as 'loop'
+                    replyAnnouncement.setLoop(true);
+                    backoffInterval = config.getBackoffStandbyInterval();
+                } else {
+                    // normal, successful case: replying with the part of the topology which this instance sees
+                    replyAnnouncement.setLocalCluster(clusterView);
+                    announcementRegistry.addAllExcept(replyAnnouncement, clusterView,
+                            new AnnouncementFilter() {
+    
+                                public boolean accept(final String receivingSlingId, Announcement announcement) {
+                                    if (announcement.getPrimaryKey().equals(
+                                            incomingTopologyAnnouncement
+                                                    .getPrimaryKey())) {
+                                        return false;
+                                    }
+                                    return true;
+                                }
+                            });
+                }
+            }
+            if (backoffInterval>0) {
+                replyAnnouncement.setBackoffInterval(backoffInterval);
+                if (logger.isDebugEnabled()) {
+                    logger.debug("doPost: backoffInterval for client set to "+replyAnnouncement.getBackoffInterval());
+                }
+            }
+            final String p = requestValidator.encodeMessage(replyAnnouncement.asJSON());
+            requestValidator.trustMessage(response, request, p);
+            // gzip the response if the client accepts this
+            final String acceptEncodingHeader = request.getHeader("Accept-Encoding");
+            if (acceptEncodingHeader!=null && acceptEncodingHeader.contains("gzip")) {
+                // tell the client that the content is gzipped:
+                response.setHeader("Content-Encoding", "gzip");
+                
+                // then gzip the body
+                final GZIPOutputStream gzipOut = new GZIPOutputStream(response.getOutputStream());
+                gzipOut.write(p.getBytes("UTF-8"));
+                gzipOut.close();
+            } else {
+                // otherwise plaintext
+                final PrintWriter pw = response.getWriter();
+                pw.print(p);
+                pw.flush();
+            }
+        } catch (JSONException e) {
+            logger.error("doPost: Got a JSONException: " + e, e);
+            response.sendError(500);
+        } catch (UndefinedClusterViewException e) {
+            logger.warn("doPost: no clusterView available at the moment - cannot handle connectors now: "+e);
+            response.sendError(503); // "please retry, but atm I can't help since I'm isolated"
+        }
+
+    }
+    
+    /** Checks if the provided request's remote server is whitelisted **/
+    boolean isWhitelisted(final HttpServletRequest request) {
+        if (config.isHmacEnabled()) {
+            final boolean isTrusted = requestValidator.isTrusted(request);
+            if (!isTrusted) {
+                logger.info("isWhitelisted: rejecting distrusted " + request.getRemoteAddr()
+                        + ", " + request.getRemoteHost());
+            }
+            return isTrusted;
+        }
+        
+        if (plaintextWhitelist.contains(request.getRemoteHost()) ||
+                plaintextWhitelist.contains(request.getRemoteAddr())) {
+            return true;
+        }
+
+        for (Iterator<WhitelistEntry> it = whitelist.iterator(); it.hasNext();) {
+            WhitelistEntry whitelistEntry = it.next();
+            if (whitelistEntry.accepts(request)) {
+                return true;
+            }
+        }
+        logger.info("isWhitelisted: rejecting " + request.getRemoteAddr()
+                + ", " + request.getRemoteHost());
+        return false;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java
new file mode 100644
index 0000000..fee273c
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidator.java
@@ -0,0 +1,585 @@
+/*
+ * 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 SF 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.sling.discovery.base.connectors.ping;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.security.spec.KeySpec;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.zip.GZIPInputStream;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.sling.commons.json.JSONArray;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.commons.json.JSONObject;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+
+/**
+ * Request Validator helper.
+ */
+public class TopologyRequestValidator {
+
+    public static final String SIG_HEADER = "X-SlingTopologyTrust";
+
+    public static final String HASH_HEADER = "X-SlingTopologyHash";
+
+    /**
+     * Maximum number of keys to keep in memory.
+     */
+    private static final int MAXKEYS = 5;
+
+    /**
+     * Minimum number of keys to keep in memory.
+     */
+    private static final int MINKEYS = 3;
+
+    /**
+     * true if trust information should be in request headers.
+     */
+    private boolean trustEnabled;
+
+    /**
+     * true if encryption of the message payload should be encrypted.
+     */
+    private boolean encryptionEnabled;
+
+    /**
+     * map of hmac keys, keyed by key number.
+     */
+    private Map<Integer, Key> keys = new ConcurrentHashMap<Integer, Key>();
+
+    /**
+     * The shared key.
+     */
+    private String sharedKey;
+
+    /**
+     * TTL of each shared key generation.
+     */
+    private long interval;
+
+    /**
+     * If true, everything is deactivated.
+     */
+    private boolean deactivated;
+
+    private SecureRandom random = new SecureRandom();
+
+    /**
+     * Create a TopologyRequestValidator.
+     *
+     * @param config the configuation object
+     */
+    public TopologyRequestValidator(BaseConfig config) {
+        trustEnabled = false;
+        encryptionEnabled = false;
+        if (config.isHmacEnabled()) {
+            trustEnabled = true;
+            sharedKey = config.getSharedKey();
+            interval = config.getKeyInterval();
+            encryptionEnabled = config.isEncryptionEnabled();
+        }
+        deactivated = false;
+    }
+
+    /**
+     * Encodes a request returning the encoded body
+     *
+     * @param body
+     * @return the encoded body.
+     * @throws IOException
+     */
+    public String encodeMessage(String body) throws IOException {
+        checkActive();
+        if (encryptionEnabled) {
+            try {
+                JSONObject json = new JSONObject();
+                json.put("payload", new JSONArray(encrypt(body)));
+                return json.toString();
+            } catch (InvalidKeyException e) {
+                e.printStackTrace();
+                throw new IOException("Unable to Encrypt Message " + e.getMessage());
+            } catch (IllegalBlockSizeException e) {
+                throw new IOException("Unable to Encrypt Message " + e.getMessage());
+            } catch (BadPaddingException e) {
+                throw new IOException("Unable to Encrypt Message " + e.getMessage());
+            } catch (UnsupportedEncodingException e) {
+                throw new IOException("Unable to Encrypt Message " + e.getMessage());
+            } catch (NoSuchAlgorithmException e) {
+                throw new IOException("Unable to Encrypt Message " + e.getMessage());
+            } catch (NoSuchPaddingException e) {
+                throw new IOException("Unable to Encrypt Message " + e.getMessage());
+            } catch (JSONException e) {
+                throw new IOException("Unable to Encrypt Message " + e.getMessage());
+            } catch (InvalidKeySpecException e) {
+                throw new IOException("Unable to Encrypt Message " + e.getMessage());
+            } catch (InvalidParameterSpecException e) {
+                throw new IOException("Unable to Encrypt Message " + e.getMessage());
+            }
+
+        }
+        return body;
+    }
+
+    /**
+     * Decode a message sent from the client.
+     *
+     * @param request the request object for the message.
+     * @return the message in clear text.
+     * @throws IOException if there is a problem decoding the message or the
+     *             message is invalid.
+     */
+    public String decodeMessage(HttpServletRequest request) throws IOException {
+        checkActive();
+        return decodeMessage("request:", request.getRequestURI(), getRequestBody(request),
+            request.getHeader(HASH_HEADER));
+    }
+
+    /**
+     * Decode a response from the server.
+     *
+     * @param response the response.
+     * @return the message in clear text.
+     * @throws IOException if there was a problem decoding the message.
+     */
+    public String decodeMessage(String uri, HttpResponse response) throws IOException {
+        checkActive();
+        return decodeMessage("response:", uri, getResponseBody(response),
+            getResponseHeader(response, HASH_HEADER));
+    }
+
+    /**
+     * Decode a message
+     *
+     * @param prefix the prefix to indicate if the message is a request or
+     *            response message.
+     * @param url the url associated with the message.
+     * @param body the body of the message.
+     * @param requestHash a hash of the message.
+     * @return the message in clear text
+     * @throws IOException if the message can't be decrypted.
+     */
+    private String decodeMessage(String prefix, String url, String body, String requestHash)
+            throws IOException {
+        if (trustEnabled) {
+            String bodyHash = hash(prefix + url + ":" + body);
+            if (bodyHash.equals(requestHash)) {
+                if (encryptionEnabled) {
+                    try {
+                        JSONObject json = new JSONObject(body);
+                        if (json.has("payload")) {
+                            return decrypt(json.getJSONArray("payload"));
+                        }
+                    } catch (JSONException e) {
+                        throw new IOException("Encrypted Message is in the correct json format");
+                    } catch (InvalidKeyException e) {
+                        throw new IOException("Encrypted Message is in the correct json format");
+                    } catch (IllegalBlockSizeException e) {
+                        throw new IOException("Encrypted Message is in the correct json format");
+                    } catch (BadPaddingException e) {
+                        throw new IOException("Encrypted Message is in the correct json format");
+                    } catch (NoSuchAlgorithmException e) {
+                        throw new IOException("Encrypted Message is in the correct json format");
+                    } catch (NoSuchPaddingException e) {
+                        throw new IOException("Encrypted Message is in the correct json format");
+                    } catch (InvalidAlgorithmParameterException e) {
+                        throw new IOException("Encrypted Message is in the correct json format");
+                    } catch (InvalidKeySpecException e) {
+                        throw new IOException("Encrypted Message is in the correct json format");
+                    }
+
+                }
+            }
+            throw new IOException("Message is not valid, hash does not match message");
+        }
+        return body;
+    }
+
+    /**
+     * Is the request from the client trusted, based on the signature headers.
+     *
+     * @param request the request.
+     * @return true if trusted, or true if this component is disabled.
+     */
+    public boolean isTrusted(HttpServletRequest request) {
+        checkActive();
+        if (trustEnabled) {
+            return checkTrustHeader(request.getHeader(HASH_HEADER),
+                request.getHeader(SIG_HEADER));
+        }
+        return false;
+    }
+
+    /**
+     * Is the response from the server to be trusted by the client.
+     *
+     * @param response the response
+     * @return true if trusted, or true if this component is disabled.
+     */
+    public boolean isTrusted(HttpResponse response) {
+        checkActive();
+        if (trustEnabled) {
+            return checkTrustHeader(getResponseHeader(response, HASH_HEADER),
+                getResponseHeader(response, SIG_HEADER));
+        }
+        return false;
+    }
+
+    /**
+     * Trust a message on the client before sending, only if trust is enabled.
+     *
+     * @param method the method which will have headers set after the call.
+     * @param body the body.
+     */
+    public void trustMessage(HttpUriRequest method, String body) {
+        checkActive();
+        if (trustEnabled) {
+            String bodyHash = hash("request:" + method.getURI().getPath() + ":" + body);
+            method.setHeader(HASH_HEADER, bodyHash);
+            method.setHeader(SIG_HEADER, createTrustHeader(bodyHash));
+        }
+    }
+
+    /**
+     * Trust a response message sent from the server to the client.
+     *
+     * @param response the response.
+     * @param request the request,
+     * @param body body of the response.
+     */
+    public void trustMessage(HttpServletResponse response, HttpServletRequest request, String body) {
+        checkActive();
+        if (trustEnabled) {
+            String bodyHash = hash("response:" + request.getRequestURI() + ":" + body);
+            response.setHeader(HASH_HEADER, bodyHash);
+            response.setHeader(SIG_HEADER, createTrustHeader(bodyHash));
+        }
+    }
+
+    /**
+     * @param body
+     * @return a hash of body base64 encoded.
+     */
+    private String hash(String toHash) {
+        try {
+            MessageDigest m = MessageDigest.getInstance("SHA-256");
+            return new String(Base64.encodeBase64(m.digest(toHash.getBytes("UTF-8"))), "UTF-8");
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Generate a signature of the bodyHash and encode it so that it contains
+     * the key number used to generate the signature.
+     *
+     * @param bodyHash a hash
+     * @return the signature.
+     */
+    private String createTrustHeader(String bodyHash) {
+        try {
+            int keyNo = getCurrentKey();
+            return keyNo + "/" + hmac(keyNo, bodyHash);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        } catch (InvalidKeyException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        } catch (IllegalStateException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Check that the signature is a signature of the body hash.
+     *
+     * @param bodyHash the body hash.
+     * @param signature the signature.
+     * @return true if the signature can be trusted.
+     */
+    private boolean checkTrustHeader(String bodyHash, String signature) {
+        try {
+            if (bodyHash == null || signature == null ) {
+                return false;
+            }
+            String[] parts = signature.split("/", 2);
+            int keyNo = Integer.parseInt(parts[0]);
+            return hmac(keyNo, bodyHash).equals(parts[1]);
+        } catch (ArrayIndexOutOfBoundsException e) {
+            return false;
+        } catch (IllegalArgumentException e) {
+            return false;
+        } catch (InvalidKeyException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        } catch (IllegalStateException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Get a Mac instance for the key number.
+     *
+     * @param keyNo the key number.
+     * @return the mac instance.
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeyException
+     * @throws UnsupportedEncodingException
+     */
+    private Mac getMac(int keyNo) throws NoSuchAlgorithmException, InvalidKeyException,
+            UnsupportedEncodingException {
+        Mac m = Mac.getInstance("HmacSHA256");
+        m.init(getKey(keyNo));
+        return m;
+    }
+
+    /**
+     * Perform a HMAC on the body using the key specified.
+     *
+     * @param keyNo the key number.
+     * @param bodyHash a hash of the body.
+     * @return the hmac signature.
+     * @throws InvalidKeyException
+     * @throws UnsupportedEncodingException
+     * @throws IllegalStateException
+     * @throws NoSuchAlgorithmException
+     */
+    private String hmac(int keyNo, String bodyHash) throws InvalidKeyException,
+            UnsupportedEncodingException, IllegalStateException, NoSuchAlgorithmException {
+        return new String(Base64.encodeBase64(getMac(keyNo).doFinal(bodyHash.getBytes("UTF-8"))),
+            "UTF-8");
+    }
+
+    /**
+     * Decrypt the body.
+     *
+     * @param jsonArray the encrypted payload
+     * @return the decrypted payload.
+     * @throws IllegalBlockSizeException
+     * @throws BadPaddingException
+     * @throws UnsupportedEncodingException
+     * @throws InvalidKeyException
+     * @throws NoSuchAlgorithmException
+     * @throws NoSuchPaddingException
+     * @throws InvalidKeySpecException
+     * @throws InvalidAlgorithmParameterException
+     * @throws JSONException
+     */
+    private String decrypt(JSONArray jsonArray) throws IllegalBlockSizeException,
+            BadPaddingException, UnsupportedEncodingException, InvalidKeyException,
+            NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeySpecException, JSONException {
+        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+        cipher.init(Cipher.DECRYPT_MODE, getCiperKey(Base64.decodeBase64(jsonArray.getString(0).getBytes("UTF-8"))), new IvParameterSpec(Base64.decodeBase64(jsonArray.getString(1).getBytes("UTF-8"))));
+        return new String(cipher.doFinal(Base64.decodeBase64(jsonArray.getString(2).getBytes("UTF-8"))));
+    }
+
+    /**
+     * Encrypt a payload with the numbed key/
+     *
+     * @param payload the payload.
+     * @param keyNo the key number.
+     * @return an encrypted version.
+     * @throws IllegalBlockSizeException
+     * @throws BadPaddingException
+     * @throws UnsupportedEncodingException
+     * @throws InvalidKeyException
+     * @throws NoSuchAlgorithmException
+     * @throws NoSuchPaddingException
+     * @throws InvalidKeySpecException
+     * @throws InvalidParameterSpecException
+     */
+    private List<String> encrypt(String payload) throws IllegalBlockSizeException,
+            BadPaddingException, UnsupportedEncodingException, InvalidKeyException,
+            NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeySpecException, InvalidParameterSpecException {
+        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+        byte[] salt = new byte[9];
+        random.nextBytes(salt);
+        cipher.init(Cipher.ENCRYPT_MODE, getCiperKey(salt));
+        AlgorithmParameters params = cipher.getParameters();
+        List<String> encrypted = new ArrayList<String>();
+        encrypted.add(new String(Base64.encodeBase64(salt)));
+        encrypted.add(new String(Base64.encodeBase64(params.getParameterSpec(IvParameterSpec.class).getIV())));
+        encrypted.add(new String(Base64.encodeBase64(cipher.doFinal(payload.getBytes("UTF-8")))));
+        return encrypted;
+    }
+
+    /**
+     * @param salt number of the key.
+     * @return the CupherKey.
+     * @throws UnsupportedEncodingException
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeySpecException
+     */
+    private Key getCiperKey(byte[] salt) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException {
+        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+        // hashing the password 65K times takes 151ms, hashing 256 times takes 2ms.
+        // Since the salt has 2^^72 values, 256 times is probably good enough.
+        KeySpec spec = new PBEKeySpec(sharedKey.toCharArray(), salt, 256, 128);
+        SecretKey tmp = factory.generateSecret(spec);
+        SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES");
+        return key;
+    }
+
+    /**
+     * @param keyNo number of the key.
+     * @return the HMac key.
+     * @throws UnsupportedEncodingException
+     */
+    private Key getKey(int keyNo) throws UnsupportedEncodingException {
+        if(Math.abs(keyNo - getCurrentKey()) > 1 ) {
+            throw new IllegalArgumentException("Key has expired");
+        }
+        if (keys.containsKey(keyNo)) {
+            return keys.get(keyNo);
+        }
+        trimKeys();
+        SecretKeySpec key = new SecretKeySpec(hash(sharedKey + keyNo).getBytes("UTF-8"),
+            "HmacSHA256");
+        keys.put(keyNo, key);
+        return key;
+    }
+
+    private int getCurrentKey() {
+        return (int) (System.currentTimeMillis() / interval);
+    }
+
+    /**
+     * dump olf keys.
+     */
+    private void trimKeys() {
+        if (keys.size() > MAXKEYS) {
+            List<Integer> keysKeys = new ArrayList<Integer>(keys.keySet());
+            Collections.sort(keysKeys);
+            for (Integer k : keysKeys) {
+                if (keys.size() < MINKEYS) {
+                    break;
+                }
+                keys.remove(k);
+            }
+        }
+
+    }
+
+    /**
+     * Get the value of a response header.
+     *
+     * @param response the response
+     * @param name the name of the response header.
+     * @return the value of the response header, null if none.
+     */
+    private String getResponseHeader(HttpResponse response, String name) {
+        Header h = response.getFirstHeader(name);
+        if (h == null) {
+            return null;
+        }
+        return h.getValue();
+    }
+
+    /**
+     * Get the request body.
+     *
+     * @param request the request.
+     * @return the body as a string.
+     * @throws IOException
+     */
+    private String getRequestBody(HttpServletRequest request) throws IOException {
+        final String contentEncoding = request.getHeader("Content-Encoding");
+        if (contentEncoding!=null && contentEncoding.contains("gzip")) {
+            // then treat the request body as gzip:
+            final GZIPInputStream gzipIn = new GZIPInputStream(request.getInputStream());
+            final String gunzippedEncodedJson = IOUtils.toString(gzipIn);
+            gzipIn.close();
+            return gunzippedEncodedJson;
+        } else {
+            // otherwise assume plain-text:
+            return IOUtils.toString(request.getReader());
+        }
+    }
+
+    /**
+     * @param response the response
+     * @return the body of the response from the server.
+     * @throws IOException
+     */
+    private String getResponseBody(HttpResponse response) throws IOException {
+        final Header contentEncoding = response.getFirstHeader("Content-Encoding");
+        if (contentEncoding!=null && contentEncoding.getValue()!=null &&
+                contentEncoding.getValue().contains("gzip")) {
+            // then the server sent gzip - treat it so:
+            final GZIPInputStream gzipIn = new GZIPInputStream(response.getEntity().getContent());
+            final String gunzippedEncodedJson = IOUtils.toString(gzipIn);
+            gzipIn.close();
+            return gunzippedEncodedJson;
+        } else {
+        	// otherwise the server sent plaintext:
+        	return IOUtils.toString(response.getEntity().getContent(), "UTF-8");
+        }
+    }
+
+    /**
+     * throw an exception if not active.
+     */
+    private void checkActive() {
+        if (deactivated) {
+            throw new IllegalStateException(this.getClass().getName() + " is not active");
+        }
+        if ((trustEnabled || encryptionEnabled) && sharedKey == null) {
+            throw new IllegalStateException(this.getClass().getName()
+                + " Shared Key must be set if encryption or signing is enabled.");
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java
new file mode 100644
index 0000000..c1aa7e0
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides topology connector implementations for discovery
+ * implementors that choose to extend from discovery.base
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+package org.apache.sling.discovery.base.connectors.ping;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java
new file mode 100644
index 0000000..7dae377
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/SubnetWhitelistEntry.java
@@ -0,0 +1,47 @@
+/*
+ * 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.sling.discovery.base.connectors.ping.wl;
+
+import javax.servlet.ServletRequest;
+
+import org.apache.commons.net.util.SubnetUtils;
+import org.apache.commons.net.util.SubnetUtils.SubnetInfo;
+
+/**
+ * Implementation of a WhitelistEntry which accepts
+ * cidr and ip mask notations.
+ */
+public class SubnetWhitelistEntry implements WhitelistEntry {
+
+    private final SubnetInfo subnetInfo;
+    
+    public SubnetWhitelistEntry(String cidrNotation) {
+        subnetInfo = new SubnetUtils(cidrNotation).getInfo();
+    }
+    
+    public SubnetWhitelistEntry(String ip, String subnetMask) {
+        subnetInfo = new SubnetUtils(ip, subnetMask).getInfo();
+    }
+    
+    public boolean accepts(ServletRequest request) {
+        final String remoteAddr = request.getRemoteAddr();
+        return subnetInfo.isInRange(remoteAddr);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java
new file mode 100644
index 0000000..af18554
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WhitelistEntry.java
@@ -0,0 +1,35 @@
+/*
+ * 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.sling.discovery.base.connectors.ping.wl;
+
+import javax.servlet.ServletRequest;
+
+/**
+ * A WhitelistEntry is capable of accepting certain requests
+ * depending on a configuration.
+ */
+public interface WhitelistEntry {
+
+    /**
+     * @param request the incoming request which should be accepted or rejected
+     * @return true if the request is accepted by this WhitelistEntry
+     */
+    public boolean accepts(ServletRequest request);
+    
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java
new file mode 100644
index 0000000..a402860
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelper.java
@@ -0,0 +1,52 @@
+/*
+ * 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.sling.discovery.base.connectors.ping.wl;
+
+import java.util.regex.Pattern;
+
+/** Helper class for wildcards **/
+public class WildcardHelper {
+
+    /** converts a string containing wildcards (* and ?) into a valid regex **/
+    public static String wildcardAsRegex(String patternWithWildcards) {
+        if (patternWithWildcards==null) {
+            throw new IllegalArgumentException("patternWithWildcards must not be null");
+        }
+        return "\\Q"+patternWithWildcards.replace("?", "\\E.\\Q").replace("*", "\\E.*\\Q")+"\\E";
+    }
+
+    /**
+     * Compare a given string (comparee) against a pattern that contains wildcards
+     * and return true if it matches.
+     * @param comparee the string which should be tested against a pattern containing wildcards
+     * @param patternWithWildcards the pattern containing wildcards (* and ?)
+     * @return true if the comparee string matches against the pattern containing wildcards
+     */
+    public static boolean matchesWildcard(String comparee, String patternWithWildcards) {
+        if (comparee==null) {
+            throw new IllegalArgumentException("comparee must not be null");
+        }
+        if (patternWithWildcards==null) {
+            throw new IllegalArgumentException("patternWithEWildcards must not be null");
+        }
+        final String regex = wildcardAsRegex(patternWithWildcards);
+        return Pattern.matches(regex, comparee);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java
new file mode 100644
index 0000000..396d8a4
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardWhitelistEntry.java
@@ -0,0 +1,45 @@
+/*
+ * 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.sling.discovery.base.connectors.ping.wl;
+
+import javax.servlet.ServletRequest;
+
+/**
+ * Implementation of a WhitelistEntry which can accept
+ * wildcards (* and ?) in both IP and hostname
+ */
+public class WildcardWhitelistEntry implements WhitelistEntry {
+
+    private final String hostOrAddressWithWildcard;
+    
+    public WildcardWhitelistEntry(String hostOrAddressWithWildcard) {
+        this.hostOrAddressWithWildcard = hostOrAddressWithWildcard;
+    }
+    
+    public boolean accepts(ServletRequest request) {
+        if (WildcardHelper.matchesWildcard(request.getRemoteAddr(), hostOrAddressWithWildcard)) {
+            return true;
+        }
+        if (WildcardHelper.matchesWildcard(request.getRemoteHost(), hostOrAddressWithWildcard)) {
+            return true;
+        }
+        return false;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java
new file mode 100644
index 0000000..091715c
--- /dev/null
+++ b/src/main/java/org/apache/sling/discovery/base/connectors/ping/wl/package-info.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides whitelist-related classes for discovery
+ * implementors that choose to extend from discovery.base
+ *
+ * @version 1.0.0
+ */
+@Version("1.0.0")
+package org.apache.sling.discovery.base.connectors.ping.wl;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/test/java/org/apache/sling/discovery/base/commons/DefaultTopologyViewTest.java b/src/test/java/org/apache/sling/discovery/base/commons/DefaultTopologyViewTest.java
new file mode 100644
index 0000000..27446f3
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/commons/DefaultTopologyViewTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.sling.discovery.base.commons;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Iterator;
+import java.util.Set;
+import java.util.UUID;
+
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.InstanceFilter;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.base.its.setup.TopologyHelper;
+import org.apache.sling.discovery.commons.providers.DefaultClusterView;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+import org.junit.Test;
+
+import junitx.util.PrivateAccessor;
+
+public class DefaultTopologyViewTest {
+
+    @Test
+    public void testForcedLeaderChangeCompare() throws Exception {
+        // create view 1 with first instance the leader
+        final String slingId1 = UUID.randomUUID().toString();
+        final DefaultTopologyView view1 = TopologyHelper.createTopologyView(UUID
+                .randomUUID().toString(), slingId1);
+        final DefaultInstanceDescription id2 = TopologyHelper.addInstanceDescription(view1, TopologyHelper
+                .createInstanceDescription(view1.getClusterViews().iterator()
+                        .next()));
+        final String slingId2 = id2.getSlingId();
+        final DefaultInstanceDescription id3 = TopologyHelper.addInstanceDescription(view1, TopologyHelper
+                .createInstanceDescription(view1.getClusterViews().iterator()
+                        .next()));
+        final String slingId3 = id3.getSlingId();
+        
+        // now create view 2 with exactly the same instances as above, but the second instance the leader
+        DefaultTopologyView view2 = TopologyHelper.cloneTopologyView(view1, slingId2);
+        // make sure we've chosen a new leader:
+        assertNotEquals(view1.getClusterViews().iterator().next().getLeader().getSlingId(),
+                view2.getClusterViews().iterator().next().getLeader().getSlingId());
+        // and now test the compare method which should catch the leader change
+        assertTrue(view1.compareTopology(view2)==Type.TOPOLOGY_CHANGED);
+        
+        // same thing now with view3 which takes slingId3 as the leader
+        DefaultTopologyView view3 = TopologyHelper.cloneTopologyView(view1, slingId3);
+        // make sure we've chosen a new leader:
+        assertNotEquals(view1.getClusterViews().iterator().next().getLeader().getSlingId(),
+                view3.getClusterViews().iterator().next().getLeader().getSlingId());
+        // and now test the compare method which should catch the leader change
+        assertTrue(view1.compareTopology(view3)==Type.TOPOLOGY_CHANGED);
+    }
+    
+    @Test
+    public void testCompare() throws Exception {
+
+        DefaultTopologyView newView = TopologyHelper.createTopologyView(UUID
+                .randomUUID().toString(), UUID.randomUUID().toString());
+
+        try {
+            newView.compareTopology(null);
+            fail("Should complain about null");
+        } catch (Exception e) {
+            // ok
+        }
+
+        DefaultTopologyView oldView = TopologyHelper
+                .cloneTopologyView(newView);
+        assertNull(newView.compareTopology(oldView));
+
+        DefaultInstanceDescription id = TopologyHelper
+                .createInstanceDescription(newView.getClusterViews().iterator()
+                        .next());
+        TopologyHelper.addInstanceDescription(newView, id);
+        assertEquals(Type.TOPOLOGY_CHANGED, newView.compareTopology(oldView));
+
+        assertEquals(2, newView.getInstances().size());
+        // addInstanceDescription now no longer throws an exception if you add
+        // the same
+        // instance twice. this provides greater stability
+        TopologyHelper.addInstanceDescription(newView, id);
+        assertEquals(2, newView.getInstances().size());
+        // try{
+        // TopologyTestHelper.addInstanceDescription(newView, id);
+        // fail("should not be able to add twice");
+        // } catch(Exception e) {
+        // // ok
+        // }
+
+        oldView = TopologyHelper.cloneTopologyView(newView);
+        assertNull(newView.compareTopology(oldView));
+
+        DefaultInstanceDescription instance = (DefaultInstanceDescription) newView.getInstances().iterator().next();
+        instance.setProperty("a", "b");
+        assertEquals(Type.PROPERTIES_CHANGED, newView.compareTopology(oldView));
+        oldView = TopologyHelper.cloneTopologyView(newView);
+        assertNull(newView.compareTopology(oldView));
+
+        instance.setProperty("a", "B");
+        assertEquals(Type.PROPERTIES_CHANGED, newView.compareTopology(oldView));
+        oldView = TopologyHelper.cloneTopologyView(newView);
+        assertNull(newView.compareTopology(oldView));
+
+        instance.setProperty("a", "B");
+        assertNull(newView.compareTopology(oldView));
+        
+        // now change the properties of the first instance but modify the second instance' cluster
+        Iterator<InstanceDescription> it = newView.getInstances().iterator();
+        DefaultInstanceDescription firstInstance = (DefaultInstanceDescription) it.next();
+        assertNotNull(firstInstance);
+        DefaultInstanceDescription secondInstance = (DefaultInstanceDescription) it.next();
+        assertNotNull(secondInstance);
+        firstInstance.setProperty("c", "d");
+        DefaultClusterView cluster = new DefaultClusterView(UUID.randomUUID().toString());
+        PrivateAccessor.setField(secondInstance, "clusterView", null);
+        cluster.addInstanceDescription(secondInstance);
+        assertEquals(Type.TOPOLOGY_CHANGED, newView.compareTopology(oldView));
+    }
+
+    @Test
+    public void testFind() throws Exception {
+        DefaultTopologyView newView = TopologyHelper.createTopologyView(UUID
+                .randomUUID().toString(), UUID.randomUUID().toString());
+        TopologyHelper.createAndAddInstanceDescription(newView, newView
+                .getClusterViews().iterator().next());
+
+        try {
+            newView.findInstances(null);
+            fail("should complain");
+        } catch (IllegalArgumentException iae) {
+            // ok
+        }
+
+        final DefaultInstanceDescription id = TopologyHelper
+                .createAndAddInstanceDescription(newView, newView
+                        .getClusterViews().iterator().next());
+        TopologyHelper.createAndAddInstanceDescription(newView, newView
+                .getClusterViews().iterator().next());
+        assertEquals(4, newView.findInstances(new InstanceFilter() {
+
+            public boolean accept(InstanceDescription instance) {
+                return true;
+            }
+        }).size());
+        assertEquals(1, newView.findInstances(new InstanceFilter() {
+
+            public boolean accept(InstanceDescription instance) {
+                return instance.getSlingId().equals(id.getSlingId());
+            }
+        }).size());
+        assertEquals(1, newView.findInstances(new InstanceFilter() {
+
+            public boolean accept(InstanceDescription instance) {
+                return instance.isLeader();
+            }
+        }).size());
+        assertEquals(1, newView.findInstances(new InstanceFilter() {
+            boolean first = true;
+
+            public boolean accept(InstanceDescription instance) {
+                if (!first) {
+                    return false;
+                }
+                first = false;
+                return true;
+            }
+        }).size());
+    }
+
+    @Test
+    public void testGetInstances() throws Exception {
+        DefaultTopologyView newView = TopologyHelper.createTopologyView(UUID
+                .randomUUID().toString(), UUID.randomUUID().toString());
+
+        Set<InstanceDescription> instances = newView.getInstances();
+        assertNotNull(instances);
+
+        try {
+            instances.remove(instances.iterator().next());
+            fail("list should not be modifiable");
+        } catch (Exception e) {
+            // ok
+        }
+
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/commons/DummyDiscoveryService.java b/src/test/java/org/apache/sling/discovery/base/commons/DummyDiscoveryService.java
new file mode 100644
index 0000000..ddde4fc
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/commons/DummyDiscoveryService.java
@@ -0,0 +1,71 @@
+/*
+ * 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.sling.discovery.base.commons;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+
+public class DummyDiscoveryService extends BaseDiscoveryService {
+
+    private final ClusterViewService clusterViewService;
+    private final AnnouncementRegistry topologyRegistry;
+
+    public DummyDiscoveryService(String slingId,
+            ClusterViewService clusterViewService,
+            AnnouncementRegistry topologyRegistry,
+            ResourceResolverFactory resourceResolverFactory, BaseConfig config, 
+            ConnectorRegistry connectorRegistry, Scheduler scheduler) {
+        this.clusterViewService = clusterViewService;
+        this.topologyRegistry = topologyRegistry;
+    }
+    
+    @Override
+    public void updateProperties() {
+        throw new IllegalStateException("updateProperties not yet impl");
+    }
+
+    @Override
+    protected ClusterViewService getClusterViewService() {
+        return clusterViewService;
+    }
+
+    @Override
+    protected AnnouncementRegistry getAnnouncementRegistry() {
+        return topologyRegistry;
+    }
+
+    @Override
+    protected void handleIsolatedFromTopology() {
+        throw new IllegalStateException("handleIsolatedFromTopology not yet impl");
+    }
+    
+    @Override
+    public void handlePotentialTopologyChange() {
+        throw new IllegalStateException("handlePotentialTopologyChange not yet impl");
+    }
+    
+    @Override
+    public void handleTopologyChanging() {
+        throw new IllegalStateException("handleTopologyChanging not yet impl");
+    }
+    
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/DummyVirtualInstanceBuilder.java b/src/test/java/org/apache/sling/discovery/base/connectors/DummyVirtualInstanceBuilder.java
new file mode 100644
index 0000000..0761613
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/DummyVirtualInstanceBuilder.java
@@ -0,0 +1,85 @@
+/*
+ * 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.sling.discovery.base.connectors;
+
+import org.apache.sling.discovery.base.commons.BaseDiscoveryService;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.DummyDiscoveryService;
+import org.apache.sling.discovery.base.commons.ViewChecker;
+import org.apache.sling.discovery.base.its.setup.ModifiableTestBaseConfig;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.mock.DummyViewChecker;
+import org.apache.sling.discovery.base.its.setup.mock.MockFactory;
+import org.apache.sling.discovery.base.its.setup.mock.SimpleClusterViewService;
+import org.apache.sling.discovery.base.its.setup.mock.SimpleConnectorConfig;
+
+public class DummyVirtualInstanceBuilder extends VirtualInstanceBuilder {
+
+    private ModifiableTestBaseConfig connectorConfig;
+
+    public DummyVirtualInstanceBuilder() {
+    }
+
+    @Override
+    public VirtualInstanceBuilder createNewRepository() throws Exception {
+        this.factory = MockFactory.mockResourceResolverFactory();
+        return this;
+    }
+
+    @Override
+    public VirtualInstanceBuilder setPath(String string) {
+        // nothing to do now
+        return this;
+    }
+    
+    @Override
+    public Object[] getAdditionalServices(VirtualInstance instance) throws Exception {
+        return null;
+    }
+    
+    protected ClusterViewService createClusterViewService() {
+        return new SimpleClusterViewService(getSlingId());
+    }
+
+    protected ViewChecker createViewChecker() throws Exception {
+        return DummyViewChecker.testConstructor(getSlingSettingsService(), getResourceResolverFactory(), getConnectorRegistry(), getAnnouncementRegistry(), getScheduler(), getConnectorConfig());
+    }
+
+    protected BaseDiscoveryService createDiscoveryService() throws Exception {
+        return new DummyDiscoveryService(getSlingId(), getClusterViewService(), getAnnouncementRegistry(), getResourceResolverFactory(), getConnectorConfig(), getConnectorRegistry(), getScheduler());
+    }
+
+    @Override
+    public ModifiableTestBaseConfig getConnectorConfig() {
+        if (connectorConfig==null) {
+            connectorConfig = createConnectorConfig();
+        }
+        return connectorConfig;
+    }
+
+    private ModifiableTestBaseConfig createConnectorConfig() {
+        return new SimpleConnectorConfig();
+    }
+    
+    @Override
+    protected void resetRepo() {
+        // does nothing
+    }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/LargeTopologyWithHubTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/LargeTopologyWithHubTest.java
new file mode 100644
index 0000000..c98c30f
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/LargeTopologyWithHubTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.sling.discovery.base.connectors;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sling.commons.testing.junit.Retry;
+import org.apache.sling.commons.testing.junit.RetryRule;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.its.setup.TopologyHelper;
+import org.apache.sling.discovery.base.its.setup.VirtualConnector;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.testing.tools.sling.TimeoutsProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LargeTopologyWithHubTest {
+
+    private static final Logger logger = LoggerFactory.getLogger(LargeTopologyWithHubTest.class);
+
+    private static List<VirtualInstance> instances;
+    private static VirtualInstance hub;
+    private static List<String> slingIds;
+    private static final int TEST_SIZE = 50;
+    
+    @Rule
+    public final RetryRule retryRule = new RetryRule();
+
+    private VirtualInstanceBuilder newBuilder() {
+        return new DummyVirtualInstanceBuilder();
+    }
+    
+    @Before
+    public void setup() throws Throwable {
+        instances = new LinkedList<VirtualInstance>();
+        final int defaultHeartbeatTimeout = 3600; // 1 hour should be enough, really
+        final int heartbeatTimeout = TimeoutsProvider.getInstance().getTimeout(defaultHeartbeatTimeout);
+        VirtualInstanceBuilder hubBuilder = newBuilder()
+                .newRepository("/var/discovery/impl/", true)
+                .setDebugName("hub")
+                .setConnectorPingInterval(5)
+                .setConnectorPingTimeout(heartbeatTimeout);
+        hub = hubBuilder.build();
+        instances.add(hub);
+        hub.getConfig().setViewCheckTimeout(heartbeatTimeout);
+//        hub.installVotingOnHeartbeatHandler();
+        hub.heartbeatsAndCheckView();
+        hub.heartbeatsAndCheckView();
+        assertNotNull(hub.getClusterViewService().getLocalClusterView());
+        hub.startViewChecker(1);
+        hub.dumpRepo();
+        
+        slingIds = new LinkedList<String>();
+        slingIds.add(hub.getSlingId());
+        logger.info("setUp: using heartbeatTimeout of "+heartbeatTimeout+"sec "
+                + "(default: "+defaultHeartbeatTimeout+")");
+        for(int i=0; i<TEST_SIZE; i++) {
+            logger.info("setUp: creating instance"+i);
+            VirtualInstanceBuilder builder2 = newBuilder()
+                    .newRepository("/var/discovery/impl/", false)
+                    .setDebugName("instance"+i)
+                    .setConnectorPingInterval(5)
+                    .setConnectorPingTimeout(heartbeatTimeout);
+            VirtualInstance instance = builder2.build();
+            instances.add(instance);
+            instance.getConfig().setViewCheckTimeout(heartbeatTimeout);
+//            instance.installVotingOnHeartbeatHandler();
+            instance.heartbeatsAndCheckView();
+            instance.heartbeatsAndCheckView();
+            ClusterView clusterView = instance.getClusterViewService().getLocalClusterView();
+            assertNotNull(clusterView);
+            new VirtualConnector(instance, hub);
+            slingIds.add(instance.getSlingId());
+        }
+    }
+    
+    @After
+    public void tearDown() throws Exception {
+        for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+            final VirtualInstance instance = it.next();
+            instance.stop();
+        }
+    }
+    
+    @Test
+    @Retry(timeoutMsec=30000, intervalMsec=500)
+    public void testLargeTopologyWithHub() throws Exception {
+        hub.dumpRepo();
+        final TopologyView tv = hub.getDiscoveryService().getTopology();
+        assertNotNull(tv);
+        logger.info(
+                "testLargeTopologyWithHub: checking if all connectors are registered, TopologyView has {} Instances", 
+                tv.getInstances().size());
+        TopologyHelper.assertTopologyConsistsOf(tv, slingIds.toArray(new String[slingIds.size()]));
+        logger.info("testLargeTopologyWithHub: test passed");
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImplTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImplTest.java
new file mode 100644
index 0000000..6a38bea
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/announcement/AnnouncementRegistryImplTest.java
@@ -0,0 +1,391 @@
+/*
+ * 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.sling.discovery.base.connectors.announcement;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.jcr.Session;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.testing.jcr.RepositoryProvider;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.its.setup.TopologyHelper;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceHelper;
+import org.apache.sling.discovery.base.its.setup.mock.MockFactory;
+import org.apache.sling.discovery.base.its.setup.mock.SimpleConnectorConfig;
+import org.apache.sling.discovery.commons.providers.DefaultClusterView;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+import org.apache.sling.discovery.commons.providers.spi.impl.DummySlingSettingsService;
+import org.junit.Before;
+import org.junit.Test;
+
+public class AnnouncementRegistryImplTest {
+
+    private AnnouncementRegistryImpl registry;
+    private String slingId;
+    private ResourceResolverFactory resourceResolverFactory;
+    private BaseConfig config;
+
+    @Before
+    public void setup() throws Exception {
+        resourceResolverFactory = MockFactory
+                .mockResourceResolverFactory();
+        config = new SimpleConnectorConfig() {
+            public long getConnectorPingTimeout() {
+                // 10s for tests that also run on apache jenkins
+                return 10;
+            };
+        };
+        slingId = UUID.randomUUID().toString();
+        Session l = RepositoryProvider.instance().getRepository()
+                .loginAdministrative(null);
+        try {
+            l.removeItem("/var");
+            l.save();
+            l.logout();
+        } catch (Exception e) {
+            l.refresh(false);
+            l.logout();
+        }
+        registry = AnnouncementRegistryImpl.testConstructorAndActivate(
+                resourceResolverFactory, new DummySlingSettingsService(slingId), config);
+    }
+
+    @Test
+    public void testRegisterUnregister() throws Exception {
+        try{
+            registry.registerAnnouncement(null);
+            fail("should complain");
+        } catch(IllegalArgumentException iae) {
+            // ok
+        }
+        try{
+            registry.unregisterAnnouncement(null);
+            fail("should complain");
+        } catch(IllegalArgumentException iae) {
+            // ok
+        }
+        try{
+            registry.unregisterAnnouncement("");
+            fail("should complain");
+        } catch(IllegalArgumentException iae) {
+            // ok
+        }
+        
+        try{
+            new Announcement(null);
+            fail("should complain");
+        } catch(IllegalArgumentException iae) {
+            // ok
+        }
+        try{
+            new Announcement("");
+            fail("should complain"); 
+        } catch(IllegalArgumentException iae) {
+            // ok
+        }
+        
+        Announcement ann = new Announcement(slingId);
+        assertFalse(ann.isValid());
+        assertFalse(registry.registerAnnouncement(ann)!=-1);
+        
+        DefaultClusterView localCluster = new DefaultClusterView(UUID.randomUUID().toString());
+        ann.setLocalCluster(localCluster);
+        assertFalse(ann.isValid());
+        assertFalse(registry.registerAnnouncement(ann)!=-1);
+
+        try{
+            registry.listInstances(localCluster);
+            fail("doing getInstances() on an empty cluster should throw an illegalstateexception");
+        } catch(IllegalStateException ise) {
+            // ok
+        }
+        
+        DefaultInstanceDescription instance = TopologyHelper.createInstanceDescription(ann.getOwnerId(), true, localCluster);
+        assertEquals(instance.getSlingId(), ann.getOwnerId());
+        assertTrue(ann.isValid());
+        assertTrue(registry.registerAnnouncement(ann)!=-1);
+        
+        assertEquals(1, registry.listInstances(localCluster).size());
+        
+        registry.checkExpiredAnnouncements();
+        assertEquals(1, registry.listInstances(localCluster).size());
+        
+        registry.unregisterAnnouncement(ann.getOwnerId());
+        assertEquals(0, registry.listInstances(localCluster).size());
+        assertTrue(ann.isValid());
+        assertTrue(registry.registerAnnouncement(ann)!=-1);
+        assertEquals(1, registry.listInstances(localCluster).size());
+
+        Thread.sleep(10500);
+        assertEquals(0, registry.listInstances(localCluster).size());
+    
+    }
+    
+    @Test
+    public void testLists() throws Exception {
+        try{
+            registry.listAnnouncementsInSameCluster(null);
+            fail("should complain");
+        } catch(IllegalArgumentException iae) {
+            // ok
+        }
+        try{
+            registry.listAnnouncementsInSameCluster(null);
+            fail("should complain");
+        } catch(IllegalArgumentException iae) {
+            // ok
+        }
+        assertEquals(0, registry.listLocalAnnouncements().size());
+        assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+        DefaultClusterView localCluster = new DefaultClusterView(UUID.randomUUID().toString());
+        DefaultInstanceDescription instance = TopologyHelper.createInstanceDescription(slingId, true, localCluster);        
+        assertEquals(0, registry.listAnnouncementsInSameCluster(localCluster).size());
+        assertEquals(0, registry.listLocalAnnouncements().size());
+        assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+        
+        Announcement ann = new Announcement(slingId);
+        ann.setLocalCluster(localCluster);
+        ann.setInherited(true);
+        registry.registerAnnouncement(ann);
+        assertEquals(1, registry.listAnnouncementsInSameCluster(localCluster).size());
+        assertEquals(1, registry.listLocalAnnouncements().size());
+        assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+        ann.setInherited(true);
+        assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+        assertTrue(registry.hasActiveAnnouncement(slingId));
+        assertFalse(registry.hasActiveAnnouncement(UUID.randomUUID().toString()));
+        registry.unregisterAnnouncement(slingId);
+        assertEquals(0, registry.listAnnouncementsInSameCluster(localCluster).size());
+        assertEquals(0, registry.listLocalAnnouncements().size());
+        assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+        assertFalse(registry.hasActiveAnnouncement(slingId));
+        assertFalse(registry.hasActiveAnnouncement(UUID.randomUUID().toString()));
+        ann.setInherited(false);
+        registry.registerAnnouncement(ann);
+        assertEquals(1, registry.listAnnouncementsInSameCluster(localCluster).size());
+        assertEquals(1, registry.listLocalAnnouncements().size());
+        assertEquals(1, registry.listLocalIncomingAnnouncements().size());
+        assertTrue(registry.hasActiveAnnouncement(slingId));
+        assertFalse(registry.hasActiveAnnouncement(UUID.randomUUID().toString()));
+        registry.unregisterAnnouncement(slingId);
+        assertEquals(0, registry.listAnnouncementsInSameCluster(localCluster).size());
+        assertEquals(0, registry.listLocalAnnouncements().size());
+        assertEquals(0, registry.listLocalIncomingAnnouncements().size());
+        assertFalse(registry.hasActiveAnnouncement(slingId));
+        assertFalse(registry.hasActiveAnnouncement(UUID.randomUUID().toString()));
+        
+        assertEquals(1, ann.listInstances().size());
+        registry.addAllExcept(ann, localCluster, new AnnouncementFilter() {
+            
+            public boolean accept(String receivingSlingId, Announcement announcement) {
+                assertNotNull(receivingSlingId);
+                assertNotNull(announcement);
+                return true;
+            }
+        });
+        assertEquals(1, ann.listInstances().size());
+        registry.registerAnnouncement(createAnnouncement(createCluster(3), 1, false));
+        assertEquals(1, registry.listAnnouncementsInSameCluster(localCluster).size());
+        assertEquals(3, registry.listInstances(localCluster).size());
+        registry.addAllExcept(ann, localCluster, new AnnouncementFilter() {
+            
+            public boolean accept(String receivingSlingId, Announcement announcement) {
+                assertNotNull(receivingSlingId);
+                assertNotNull(announcement);
+                return true;
+            }
+        });
+        assertEquals(4, ann.listInstances().size());
+        registry.registerAnnouncement(ann);
+        assertEquals(2, registry.listAnnouncementsInSameCluster(localCluster).size());
+    }
+    
+    private ClusterView createCluster(int numInstances) {
+        DefaultClusterView localCluster = new DefaultClusterView(UUID.randomUUID().toString());
+        for (int i = 0; i < numInstances; i++) {
+            DefaultInstanceDescription instance = TopologyHelper.createInstanceDescription(UUID.randomUUID().toString(), (i==0 ? true : false), localCluster);        
+        }
+        return localCluster;
+    }
+    
+    private ClusterView createCluster(String... instanceIds) {
+        DefaultClusterView localCluster = new DefaultClusterView(UUID.randomUUID().toString());
+        for (int i = 0; i < instanceIds.length; i++) {
+            DefaultInstanceDescription instance = TopologyHelper.createInstanceDescription(instanceIds[i], (i==0 ? true : false), localCluster);        
+        }
+        return localCluster;
+    }
+
+    private Announcement createAnnouncement(ClusterView remoteCluster, int ownerIndex, boolean inherited) {
+        List<InstanceDescription> instances = remoteCluster.getInstances();
+        Announcement ann = new Announcement(instances.get(ownerIndex).getSlingId());
+        ann.setInherited(inherited);
+        ann.setLocalCluster(remoteCluster);
+        return ann;
+    }
+    
+    @Test
+    public void testExpiry() throws InterruptedException, NoSuchFieldException {
+        ClusterView cluster1 = createCluster(4);
+        ClusterView cluster2 = createCluster(3);
+        ClusterView cluster3 = createCluster(5);
+        
+        ClusterView myCluster = createCluster(slingId);
+        
+        Announcement ann1 = createAnnouncement(cluster1, 0, true);
+        Announcement ann2 = createAnnouncement(cluster2, 1, true);
+        Announcement ann3 = createAnnouncement(cluster3, 1, false);
+        
+        assertTrue(registry.registerAnnouncement(ann1)!=-1);
+        assertTrue(registry.registerAnnouncement(ann2)!=-1);
+        assertTrue(registry.registerAnnouncement(ann3)!=-1);
+        assertTrue(registry.hasActiveAnnouncement(cluster1.getInstances().get(0).getSlingId()));
+        assertTrue(registry.hasActiveAnnouncement(cluster2.getInstances().get(1).getSlingId()));
+        assertTrue(registry.hasActiveAnnouncement(cluster3.getInstances().get(1).getSlingId()));
+        assertEquals(3, registry.listAnnouncementsInSameCluster(myCluster).size());
+        assertEquals(3, registry.listLocalAnnouncements().size());
+        assertEquals(1, registry.listLocalIncomingAnnouncements().size());
+
+        
+        {
+            Announcement testAnn = createAnnouncement(myCluster, 0, false);
+            assertEquals(1, testAnn.listInstances().size());
+            registry.addAllExcept(testAnn, myCluster, null);
+            assertEquals(13, testAnn.listInstances().size());
+        }
+
+        
+        Thread.sleep(10500);
+        {
+            Announcement testAnn = createAnnouncement(myCluster, 0, false);
+            assertEquals(1, testAnn.listInstances().size());
+            registry.addAllExcept(testAnn, myCluster, null);
+            assertEquals(13, testAnn.listInstances().size());
+        }
+        assertTrue(registry.registerAnnouncement(ann3)!=-1);
+        {
+            Announcement testAnn = createAnnouncement(myCluster, 0, false);
+            assertEquals(1, testAnn.listInstances().size());
+            registry.addAllExcept(testAnn, myCluster, null);
+            assertEquals(13, testAnn.listInstances().size());
+        }
+        
+        registry.checkExpiredAnnouncements();
+        
+        assertEquals(1, registry.listAnnouncementsInSameCluster(myCluster).size());
+        assertEquals(1, registry.listLocalAnnouncements().size());
+        assertEquals(1, registry.listLocalIncomingAnnouncements().size());
+        assertFalse(registry.hasActiveAnnouncement(cluster1.getInstances().get(0).getSlingId()));
+        assertFalse(registry.hasActiveAnnouncement(cluster2.getInstances().get(1).getSlingId()));
+        assertTrue(registry.hasActiveAnnouncement(cluster3.getInstances().get(1).getSlingId()));
+        {
+            Announcement testAnn = createAnnouncement(myCluster, 0, false);
+            assertEquals(1, testAnn.listInstances().size());
+            registry.addAllExcept(testAnn, myCluster, null);
+            assertEquals(6, testAnn.listInstances().size());
+        }
+        
+    }
+    
+    @Test
+    public void testCluster() throws Exception {
+        ClusterView cluster1 = createCluster(2);
+        ClusterView cluster2 = createCluster(4);
+        ClusterView cluster3 = createCluster(7);
+        
+        Announcement ann1 = createAnnouncement(cluster1, 1, true);
+        Announcement ann2 = createAnnouncement(cluster2, 2, true);
+        Announcement ann3 = createAnnouncement(cluster3, 3, false);
+        
+        final String instance1 = UUID.randomUUID().toString();
+        final String instance2 = UUID.randomUUID().toString();
+        final String instance3 = UUID.randomUUID().toString();
+        ClusterView myCluster = createCluster(instance1, instance2, instance3);
+
+        AnnouncementRegistryImpl registry1 = AnnouncementRegistryImpl.testConstructorAndActivate(
+                resourceResolverFactory, new DummySlingSettingsService(instance1), config);
+        AnnouncementRegistryImpl registry2 = AnnouncementRegistryImpl.testConstructorAndActivate(
+                resourceResolverFactory, new DummySlingSettingsService(instance2), config);
+        AnnouncementRegistryImpl registry3 = AnnouncementRegistryImpl.testConstructorAndActivate(
+                resourceResolverFactory, new DummySlingSettingsService(instance3), config);
+
+        assertTrue(registry1.registerAnnouncement(ann1)!=-1);
+        assertTrue(registry2.registerAnnouncement(ann2)!=-1);
+        assertTrue(registry3.registerAnnouncement(ann3)!=-1);
+        
+        assertTrue(registry1.hasActiveAnnouncement(cluster1.getInstances().get(1).getSlingId()));
+        assertTrue(registry2.hasActiveAnnouncement(cluster2.getInstances().get(2).getSlingId()));
+        assertTrue(registry3.hasActiveAnnouncement(cluster3.getInstances().get(3).getSlingId()));
+
+        assertEquals(3, registry1.listAnnouncementsInSameCluster(myCluster).size());
+        assertEquals(1, registry1.listLocalAnnouncements().size());
+        assertEquals(0, registry1.listLocalIncomingAnnouncements().size());
+        assertAnnouncements(registry1, myCluster, 4, 16);
+        
+        assertEquals(3, registry2.listAnnouncementsInSameCluster(myCluster).size());
+        assertEquals(1, registry2.listLocalAnnouncements().size());
+        assertEquals(0, registry2.listLocalIncomingAnnouncements().size());
+        assertAnnouncements(registry2, myCluster, 4, 16);
+
+        assertEquals(3, registry3.listAnnouncementsInSameCluster(myCluster).size());
+        assertEquals(1, registry3.listLocalAnnouncements().size());
+        assertEquals(1, registry3.listLocalIncomingAnnouncements().size());
+        assertAnnouncements(registry3, myCluster, 4, 16);
+        
+        myCluster = createCluster(instance1, instance2);
+        
+        VirtualInstanceHelper.dumpRepo(resourceResolverFactory);
+
+        assertEquals(2, registry1.listAnnouncementsInSameCluster(myCluster).size());
+        assertEquals(1, registry1.listLocalAnnouncements().size());
+        assertEquals(0, registry1.listLocalIncomingAnnouncements().size());
+        assertAnnouncements(registry1, myCluster, 3, 8);
+        
+        assertEquals(2, registry2.listAnnouncementsInSameCluster(myCluster).size());
+        assertEquals(1, registry2.listLocalAnnouncements().size());
+        assertEquals(0, registry2.listLocalIncomingAnnouncements().size());
+        assertAnnouncements(registry2, myCluster, 3, 8);
+        
+        Thread.sleep(10500);
+        assertAnnouncements(registry1, myCluster, 3, 8);
+        assertAnnouncements(registry2, myCluster, 3, 8);
+        registry1.checkExpiredAnnouncements();
+        registry2.checkExpiredAnnouncements();
+        assertAnnouncements(registry1, myCluster, 1, 2);
+        assertAnnouncements(registry2, myCluster, 1, 2);
+    }
+
+    private void assertAnnouncements(AnnouncementRegistryImpl registry,
+            ClusterView myCluster, int expectedNumAnnouncements, int expectedNumInstances) {
+        Announcement ann = createAnnouncement(myCluster, 0, false);
+        registry.addAllExcept(ann, myCluster, null);
+        assertEquals(expectedNumInstances, ann.listInstances().size());
+    }
+    
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImplTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImplTest.java
new file mode 100644
index 0000000..9972e85
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/ping/ConnectorRegistryImplTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.sling.discovery.base.connectors.ping;
+
+import static org.junit.Assert.fail;
+
+import java.net.URL;
+import java.util.UUID;
+
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.DummyVirtualInstanceBuilder;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistryImpl;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.mock.MockFactory;
+import org.apache.sling.discovery.base.its.setup.mock.SimpleConnectorConfig;
+import org.apache.sling.discovery.commons.providers.spi.impl.DummySlingSettingsService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ConnectorRegistryImplTest {
+
+    private VirtualInstance i;
+
+    public VirtualInstanceBuilder newBuilder() {
+        return new DummyVirtualInstanceBuilder();
+    }
+    
+    @Before
+    public void setup() throws Exception {
+        VirtualInstanceBuilder builder = newBuilder()
+                .newRepository("/var/discovery/impl/", true)
+                .setDebugName("i")
+                .setConnectorPingInterval(20)
+                .setConnectorPingTimeout(20);
+        i = builder.build();
+    }
+    
+    @After
+    public void teardown() throws Exception {
+        if (i!=null) {
+            try {
+                i.stopViewChecker();
+            } catch (Throwable e) {
+                e.printStackTrace();
+                i.stop();
+                throw new RuntimeException(e);
+            }
+            i.stop();
+        }
+    }
+    
+    @Test
+    public void testRegisterUnregister() throws Exception {
+        BaseConfig config = new SimpleConnectorConfig() {
+            @Override
+            public long getConnectorPingTimeout() {
+                return 20000;
+            }
+        };
+        AnnouncementRegistryImpl announcementRegistry = AnnouncementRegistryImpl.testConstructorAndActivate(
+                MockFactory.mockResourceResolverFactory(), new DummySlingSettingsService(UUID.randomUUID().toString()), config);
+
+        ConnectorRegistry c = ConnectorRegistryImpl.testConstructor(
+                announcementRegistry, config);
+
+        final URL url = new URL("http://localhost:1234/connector");
+        final ClusterViewService cvs = i.getClusterViewService();
+        try {
+            c.registerOutgoingConnector(null, url);
+            fail("should have complained");
+        } catch (IllegalArgumentException e) {
+            // ok
+        }
+        try {
+            c.registerOutgoingConnector(cvs, null);
+            fail("should have complained");
+        } catch (IllegalArgumentException e) {
+            // ok
+        }
+        TopologyConnectorClientInformation client = c
+                .registerOutgoingConnector(cvs, url);
+        try {
+            // should not be able to register same url twice
+            client = c.registerOutgoingConnector(cvs, url);
+            // ok - no longer complains - SLING-3446
+        } catch (IllegalStateException e) {
+            fail("should no longer be thrown"); // SLING-3446
+        }
+
+        try {
+            c.unregisterOutgoingConnector(null);
+            fail("should have complained");
+        } catch (IllegalArgumentException e) {
+            // ok
+        }
+
+        c.unregisterOutgoingConnector(client.getId());
+    }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServletTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServletTest.java
new file mode 100644
index 0000000..8bf4c5a
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyConnectorServletTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.sling.discovery.base.connectors.ping;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+import junitx.util.PrivateAccessor;
+
+public class TopologyConnectorServletTest {
+
+    private TopologyConnectorServlet servlet;
+    
+    private HttpServletRequest getRequest(String host, String addr) {
+        HttpServletRequest result = mock(HttpServletRequest.class);
+        when(result.getRemoteAddr()).thenReturn(addr);
+        when(result.getRemoteHost()).thenReturn(host);
+        return result;
+    }
+    
+    @Before
+    public void setUp() throws Exception {
+        servlet = new TopologyConnectorServlet();
+        BaseConfig config = mock(BaseConfig.class);
+        PrivateAccessor.setField(servlet, "config", config);
+    }
+    
+    @Test
+    public void testNull() throws Exception {
+        servlet.initWhitelist(null); // should work fine
+        servlet.initWhitelist(new String[0]); // should also work fine
+    }
+    
+    @Test
+    public void testPlaintextWhitelist_enabled() throws Exception {
+        servlet.initWhitelist(new String[] {"foo", "bar"});
+        assertTrue(servlet.isWhitelisted(getRequest("foo", "x")));
+        assertTrue(servlet.isWhitelisted(getRequest("bar", "x")));
+        assertTrue(servlet.isWhitelisted(getRequest("y", "foo")));
+        assertTrue(servlet.isWhitelisted(getRequest("y", "bar")));
+    }
+    
+    @Test
+    public void testPlaintextWhitelist_disabled() throws Exception {
+        servlet.initWhitelist(new String[] {});
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "x")));
+        assertFalse(servlet.isWhitelisted(getRequest("bar", "x")));
+        assertFalse(servlet.isWhitelisted(getRequest("y", "foo")));
+        assertFalse(servlet.isWhitelisted(getRequest("y", "bar")));
+    }
+    
+    @Test
+    public void testWildcardWhitelist() throws Exception {
+        servlet.initWhitelist(new String[] {"foo*", "b?r", "test"});
+        assertTrue(servlet.isWhitelisted(getRequest("foo", "x")));
+        assertTrue(servlet.isWhitelisted(getRequest("fooo", "x")));
+        assertTrue(servlet.isWhitelisted(getRequest("foooo", "x")));
+        assertTrue(servlet.isWhitelisted(getRequest("x", "foo")));
+        assertTrue(servlet.isWhitelisted(getRequest("x", "fooo")));
+        assertTrue(servlet.isWhitelisted(getRequest("x", "foooo")));
+        assertTrue(servlet.isWhitelisted(getRequest("bur", "x")));
+        assertTrue(servlet.isWhitelisted(getRequest("x", "bur")));
+        assertTrue(servlet.isWhitelisted(getRequest("x", "test")));
+        assertFalse(servlet.isWhitelisted(getRequest("fo", "x")));
+        assertFalse(servlet.isWhitelisted(getRequest("x", "testy")));
+    }
+    
+    @Test
+    public void testSubnetMaskWhitelist() throws Exception {
+        servlet.initWhitelist(new String[] {"1.2.3.4/24", "2.3.4.1/30", "3.4.5.6/31"});
+        
+        assertTrue(servlet.isWhitelisted(getRequest("foo", "1.2.3.4")));
+        assertFalse(servlet.isWhitelisted(getRequest("1.2.3.4", "1.2.4.3")));
+        assertTrue(servlet.isWhitelisted(getRequest("foo", "1.2.3.1")));
+        assertTrue(servlet.isWhitelisted(getRequest("foo", "1.2.3.254")));
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "1.2.4.5")));
+
+        assertTrue(servlet.isWhitelisted(getRequest("foo", "2.3.4.1")));
+        assertTrue(servlet.isWhitelisted(getRequest("foo", "2.3.4.2")));
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "2.3.4.3")));
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "2.3.4.4")));
+
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.1")));
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.2")));
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.3")));
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.4")));
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.5")));
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.6")));
+        assertFalse(servlet.isWhitelisted(getRequest("foo", "3.4.5.7")));
+    }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidatorTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidatorTest.java
new file mode 100644
index 0000000..accac53
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/ping/TopologyRequestValidatorTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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 SF 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.sling.discovery.base.connectors.ping;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.message.BasicHeader;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.its.setup.mock.SimpleConnectorConfig;
+import org.hamcrest.Description;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.api.Action;
+import org.jmock.api.Invocation;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TopologyRequestValidatorTest {
+    
+    private TopologyRequestValidator topologyRequestValidator;
+    private Mockery context = new JUnit4Mockery();
+
+
+    @Before
+    public void before() throws SecurityException, IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
+        BaseConfig config= new SimpleConnectorConfig();
+        setPrivate(config, "sharedKey", "testKey");
+        setPrivate(config, "hmacEnabled", true);
+        setPrivate(config, "encryptionEnabled", true);
+        setPrivate(config, "keyInterval", 3600*100*4);
+        topologyRequestValidator = new TopologyRequestValidator(config);
+    }
+    
+    private void setPrivate(Object o, String field, Object value) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
+        Field f = o.getClass().getDeclaredField(field);
+        if ( !f.isAccessible()) {
+            f.setAccessible(true);
+        }
+        f.set(o, value);
+    }
+
+    @Test
+    public void testTrustRequest() throws IOException {
+        final HttpPut method = new HttpPut("/TestUri");
+        String clearMessage = "TestMessage";
+        final String message = topologyRequestValidator.encodeMessage(clearMessage);
+        Assert.assertNotNull(message);
+        Assert.assertNotEquals(message, clearMessage);
+        topologyRequestValidator.trustMessage(method, message);
+        
+        Assert.assertNotNull(method.getFirstHeader(TopologyRequestValidator.HASH_HEADER));
+        Assert.assertNotNull(method.getFirstHeader(TopologyRequestValidator.HASH_HEADER).getValue());
+        Assert.assertTrue(method.getFirstHeader(TopologyRequestValidator.HASH_HEADER).getValue().length() > 0);
+        Assert.assertNotNull(method.getFirstHeader(TopologyRequestValidator.SIG_HEADER));
+        Assert.assertNotNull(method.getFirstHeader(TopologyRequestValidator.SIG_HEADER).getValue());
+        Assert.assertTrue(method.getFirstHeader(TopologyRequestValidator.SIG_HEADER).getValue().length() > 0);
+        final HttpServletRequest request = context.mock(HttpServletRequest.class);
+        context.checking(new Expectations() {
+            {
+                allowing(request).getHeader(with(TopologyRequestValidator.HASH_HEADER));
+                will(returnValue(method.getFirstHeader(TopologyRequestValidator.HASH_HEADER).getValue()));
+                
+                allowing(request).getHeader(with(TopologyRequestValidator.SIG_HEADER));
+                will(returnValue(method.getFirstHeader(TopologyRequestValidator.SIG_HEADER).getValue()));
+                
+                allowing(request).getHeader(with("Content-Encoding"));
+                will(returnValue(""));
+
+                allowing(request).getRequestURI();
+                will(returnValue(method.getURI().getPath()));
+                
+                allowing(request).getReader();
+                will(returnValue(new BufferedReader(new StringReader(message))));
+            }
+        });
+        
+        Assert.assertTrue(topologyRequestValidator.isTrusted(request));
+        Assert.assertEquals(clearMessage, topologyRequestValidator.decodeMessage(request));
+    }
+    
+    
+    
+    @Test
+    public void testTrustResponse() throws IOException {
+        final HttpServletRequest request = context.mock(HttpServletRequest.class);
+        context.checking(new Expectations() {
+            {
+                allowing(request).getRequestURI();
+                will(returnValue("/Test/Uri2"));
+            }
+        });
+
+        final HttpServletResponse response = context.mock(HttpServletResponse.class);
+        final Map<Object, Object> headers = new HashMap<Object, Object>();
+        context.checking(new Expectations() {
+            {
+                allowing(response).setHeader(with(any(String.class)), with(any(String.class)));
+                will(new Action(){
+
+                    public void describeTo(Description desc) {
+                        desc.appendText("Setting header ");
+                    }
+
+                    public Object invoke(Invocation invocation) throws Throwable {
+                        headers.put(invocation.getParameter(0), invocation.getParameter(1));
+                        return null;
+                    }
+                    
+                });
+            }
+        });
+
+        String clearMessage =  "TestMessage2";
+        final String message = topologyRequestValidator.encodeMessage(clearMessage);
+        topologyRequestValidator.trustMessage(response, request, message);
+        
+        final HttpEntity responseEntity = context.mock(HttpEntity.class);
+        context.checking(new Expectations() {
+        	{
+        		allowing(responseEntity).getContent();
+        		will(returnValue(new ByteArrayInputStream(message.getBytes())));
+        	}
+        });
+        
+        final HttpResponse resp = context.mock(HttpResponse.class);
+        context.checking(new Expectations(){
+            {
+                allowing(resp).getFirstHeader(with(any(String.class)));
+                will(new Action() {
+                    public void describeTo(Description desc) {
+                        desc.appendText("Getting (first) header ");
+                    }
+
+                    public Object invoke(Invocation invocation) throws Throwable {
+                        return new BasicHeader((String)invocation.getParameter(0), (String)headers.get(invocation.getParameter(0)));
+                    }
+                    
+                });
+                
+                allowing(resp).getEntity();
+                will(returnValue(responseEntity));
+            } 
+        });
+        topologyRequestValidator.isTrusted(resp);
+        topologyRequestValidator.decodeMessage("/Test/Uri2", resp);
+        
+    }
+    
+    
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelperTest.java b/src/test/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelperTest.java
new file mode 100644
index 0000000..6931b14
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/connectors/ping/wl/WildcardHelperTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.sling.discovery.base.connectors.ping.wl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.UUID;
+
+import org.apache.commons.net.util.SubnetUtils;
+import org.junit.Test;
+
+public class WildcardHelperTest {
+
+    @Test
+    public void testNullValues() {
+        
+        SubnetUtils s = new SubnetUtils("1.2.3.4/10");
+        s = new SubnetUtils("1.2.3.4", "255.255.0.0");
+        
+        try{
+            WildcardHelper.wildcardAsRegex(null);
+            fail("should complain");
+        } catch(IllegalArgumentException iae) {
+            // ok
+        }
+        try{
+            WildcardHelper.matchesWildcard(null, "foo");
+            fail("should complain");
+        } catch(IllegalArgumentException iae) {
+            // ok
+        }
+        try{
+            WildcardHelper.matchesWildcard("foo", null);
+            fail("should complain");
+        } catch(IllegalArgumentException iae) {
+            // ok
+        }
+    }
+    
+    @Test
+    public void testEmptyValue() {
+        final String expected = "\\Q\\E";
+        assertEquals(expected, WildcardHelper.wildcardAsRegex(""));
+    }
+    
+    @Test
+    public void testWithoutWildcards() {
+        for(int i=0; i<1000; i++) {
+            String randomString = UUID.randomUUID().toString();
+            assertTrue(WildcardHelper.matchesWildcard(randomString, randomString));
+        }
+    }
+    
+    @Test
+    public void testWildcards() {
+        assertTrue(WildcardHelper.matchesWildcard("", "*"));
+        assertTrue(WildcardHelper.matchesWildcard("fooo", "*"));
+        assertTrue(WildcardHelper.matchesWildcard("fooo", "**"));
+        assertTrue(WildcardHelper.matchesWildcard("fooo", "**?"));
+        assertTrue(WildcardHelper.matchesWildcard("fooo", "????"));
+        assertFalse(WildcardHelper.matchesWildcard("fooo", "???"));
+        assertFalse(WildcardHelper.matchesWildcard("fooo", "?????"));
+
+        for(int i=0; i<100; i++) {
+            String randomString = UUID.randomUUID().toString();
+            assertTrue(WildcardHelper.matchesWildcard(randomString, "*"));
+            assertTrue(WildcardHelper.matchesWildcard(randomString, randomString.substring(0, randomString.length()-1)+"?"));
+            assertTrue(WildcardHelper.matchesWildcard(randomString, randomString.substring(0, randomString.length()-1)+"*"));
+            assertTrue(WildcardHelper.matchesWildcard(randomString, randomString.substring(0, randomString.length()-2)+"??"));
+            assertTrue(WildcardHelper.matchesWildcard(randomString, randomString.substring(0, randomString.length()-2)+"*"));
+            assertTrue(WildcardHelper.matchesWildcard(randomString, "?" + randomString.substring(1, randomString.length())));
+            assertTrue(WildcardHelper.matchesWildcard(randomString, "*" + randomString.substring(1, randomString.length())));
+            assertTrue(WildcardHelper.matchesWildcard(randomString, "??" + randomString.substring(2, randomString.length())));
+            assertTrue(WildcardHelper.matchesWildcard(randomString, "*" + randomString.substring(2, randomString.length())));
+        }
+        
+        assertTrue(WildcardHelper.matchesWildcard("fooo", "f*"));
+        assertTrue(WildcardHelper.matchesWildcard("fooo", "fo*"));
+        assertTrue(WildcardHelper.matchesWildcard("fooo", "foo*"));
+        assertTrue(WildcardHelper.matchesWildcard("fooo", "fooo*"));
+        assertFalse(WildcardHelper.matchesWildcard("fooo", "fooo?"));
+        assertFalse(WildcardHelper.matchesWildcard("fooo", "foooo*"));
+        
+        assertTrue(WildcardHelper.matchesWildcard("fooo", "*"));
+        assertFalse(WildcardHelper.matchesWildcard("fooo", "?"));
+        assertFalse(WildcardHelper.matchesWildcard("fooo", "f?"));
+
+        assertTrue(WildcardHelper.matchesWildcard("fo", "f?"));
+
+        assertTrue(WildcardHelper.matchesWildcard("foooba", "f*b?"));
+        assertFalse(WildcardHelper.matchesWildcard("fooobar", "f*b?"));
+        assertFalse(WildcardHelper.matchesWildcard("foooba", "f*b?r"));
+        assertTrue(WildcardHelper.matchesWildcard("fooobar", "f*b?r"));
+        assertTrue(WildcardHelper.matchesWildcard("foooba", "f???b?"));
+        assertFalse(WildcardHelper.matchesWildcard("foooba", "f??b?"));
+        assertFalse(WildcardHelper.matchesWildcard("foooba", "f??b?"));
+    }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterLoadTest.java b/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterLoadTest.java
new file mode 100644
index 0000000..1e934fd
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterLoadTest.java
@@ -0,0 +1,287 @@
+/*
+ * 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 SF 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.sling.discovery.base.its;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Random;
+
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.WithholdingAppender;
+import org.apache.sling.testing.tools.retry.RetryLoop;
+import org.junit.After;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Migrated from org.apache.sling.discovery.impl.cluster.ClusterLoadTest
+ */
+public abstract class AbstractClusterLoadTest {
+
+    // wait up to 120 sec - in 1sec wait-intervals
+    private static final int INSTANCE_VIEW_TIMEOUT_SECONDS = 120;
+    private static final int INSTANCE_VIEW_POLL_INTERVAL_MILLIS = 500;
+
+    private final Random random = new Random();
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    List<VirtualInstance> instances = new LinkedList<VirtualInstance>();
+
+    @After
+    public void tearDown() throws Exception {
+    	if (instances==null || instances.size()==0) {
+    		return;
+    	}
+    	for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+    	    VirtualInstance i = it.next();
+			i.stop();
+			it.remove();
+		}
+    }
+    
+    public abstract VirtualInstanceBuilder newBuilder();
+    
+
+    @Test
+    public void testFramework() throws Exception {
+        logger.info("testFramework: building 1st instance..");
+        VirtualInstanceBuilder builder = newBuilder()
+                .newRepository("/var/discovery/impl/ClusterLoadTest/testFramework/", true)
+                .setDebugName("firstInstance")
+                .setConnectorPingTimeout(3)
+                .setConnectorPingInterval(20)
+                .setMinEventDelay(0);
+        VirtualInstance firstInstance = builder.build();
+		instances.add(firstInstance);
+    	Thread.sleep(2000);
+    	// without any heartbeat action, the discovery service reports its local instance
+    	// in so called 'isolated' mode - lets test for that
+    	try{
+    	    firstInstance.getClusterViewService().getLocalClusterView();
+    	    fail("should complain");
+    	} catch(UndefinedClusterViewException e) {
+    	    // SLING-5030:
+    	}
+        firstInstance.startViewChecker(1);
+        Thread.sleep(4000);
+        // after a heartbeat and letting it settle, the discovery service must have
+        // established a view - test for that
+        firstInstance.dumpRepo();
+        firstInstance.assertEstablishedView();
+
+        VirtualInstanceBuilder builder2 = newBuilder()
+                .useRepositoryOf(builder)
+                .setDebugName("secondInstance")
+                .setConnectorPingTimeout(3)
+                .setConnectorPingInterval(20)
+                .setMinEventDelay(0);
+        firstInstance.dumpRepo();
+        logger.info("testFramework: building 2nd instance..");
+        VirtualInstance secondInstance = builder2.build();
+        instances.add(secondInstance);
+        secondInstance.startViewChecker(1);
+        Thread.sleep(4000);
+        firstInstance.dumpRepo();
+        assertEquals(firstInstance.getClusterViewService().getLocalClusterView().getInstances().size(), 2);
+        assertEquals(secondInstance.getClusterViewService().getLocalClusterView().getInstances().size(), 2);
+    }
+
+    @Test
+    public void testTwoInstances() throws Throwable {
+    	doTest(2, 5);
+    }
+
+    @Test
+    public void testThreeInstances() throws Throwable {
+    	doTest(3, 6);
+    }
+
+    @Test
+    public void testFourInstances() throws Throwable {
+    	doTest(4, 7);
+    }
+
+    @Test
+    public void testFiveInstances() throws Throwable {
+    	doTest(5, 8);
+    }
+
+    @Test
+    public void testSixInstances() throws Throwable {
+    	doTest(6, 9);
+    }
+
+    @Test
+    public void testSevenInstances() throws Throwable {
+    	doTest(7, 10);
+    }
+
+    private void doTest(final int size, final int loopCnt) throws Throwable {
+        WithholdingAppender withholdingAppender = null;
+        boolean failure = true;
+        try{
+            logger.info("doTest("+size+","+loopCnt+"): muting log output...");
+            withholdingAppender = WithholdingAppender.install();
+            doDoTest(size, loopCnt);
+            failure = false;
+        } finally {
+            if (withholdingAppender!=null) {
+                if (failure) {
+                    logger.info("doTest("+size+","+loopCnt+"): writing muted log output due to failure...");
+                }
+                withholdingAppender.release(failure);
+                if (!failure) {
+                    logger.info("doTest("+size+","+loopCnt+"): not writing muted log output due to success...");
+                }
+            }
+            logger.info("doTest("+size+","+loopCnt+"): unmuted log output.");
+        }
+    }
+    
+	private void doDoTest(final int size, final int loopCnt) throws Throwable {
+		if (size<2) {
+			fail("can only test 2 or more instances");
+		}
+        VirtualInstanceBuilder builder = newBuilder()
+                .newRepository("/var/discovery/impl/ClusterLoadTest/doTest-"+size+"-"+loopCnt+"/", true)
+                .setDebugName("firstInstance")
+                .setConnectorPingTimeout(3)
+                .setConnectorPingInterval(20)
+                .setMinEventDelay(0);
+		VirtualInstance firstInstance = builder.build();
+		firstInstance.startViewChecker(1);
+		instances.add(firstInstance);
+		for(int i=1; i<size; i++) {
+		    VirtualInstanceBuilder builder2 = newBuilder()
+		            .useRepositoryOf(builder)
+		            .setDebugName("subsequentInstance-"+i)
+	                .setConnectorPingTimeout(3)
+	                .setMinEventDelay(0)
+	                .setConnectorPingInterval(20);
+			VirtualInstance subsequentInstance = builder2.build();
+			instances.add(subsequentInstance);
+			subsequentInstance.startViewChecker(1);
+		}
+
+		for(int i=0; i<loopCnt; i++) {
+			logger.info("=====================");
+			logger.info(" START of LOOP "+i);
+			logger.info("=====================");
+
+			// count how many instances had heartbeats running in the first place
+			int aliveCnt = 0;
+			for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+			    VirtualInstance instance = it.next();
+				if (instance.isViewCheckerRunning()) {
+					aliveCnt++;
+				}
+			}
+			logger.info("=====================");
+			logger.info(" original aliveCnt "+aliveCnt);
+			logger.info("=====================");
+			if (aliveCnt==0) {
+				// if no one is sending heartbeats, all instances go back to isolated mode
+				aliveCnt=1;
+			}
+
+            final int aliveCntFinal = aliveCnt;
+
+			for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+			    VirtualInstance instance = it.next();
+				try {
+                    instance.dumpRepo();
+                } catch (Exception e) {
+                    logger.error("Failed dumping repo for instance " + instance.getSlingId(), e);
+                }
+			}
+
+			// then verify that each instance sees that many instances
+			for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+                final VirtualInstance instance = it.next();
+				if (!instance.isViewCheckerRunning()) {
+					// if the heartbeat is not running, this instance is considered dead
+					// hence we're not doing any assert here (as the count is only
+					// valid if heartbeat/checkView is running and that would void the test)
+				} else {
+                    new RetryLoop(new ConditionImplementation(instance, aliveCntFinal), INSTANCE_VIEW_TIMEOUT_SECONDS,
+                            INSTANCE_VIEW_POLL_INTERVAL_MILLIS);
+				}
+			}
+
+			// start/stop heartbeats accordingly
+			logger.info("Starting/Stopping heartbeats with count="+instances.size());
+			for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+			    VirtualInstance instance = it.next();
+				if (random.nextBoolean()) {
+					logger.info("Starting heartbeats with "+instance.slingId);
+					instance.startViewChecker(1);
+					logger.info("Started heartbeats with "+instance.slingId);
+				} else {
+					logger.info("Stopping heartbeats with "+instance.slingId);
+					instance.stopViewChecker();
+					logger.info("Stopped heartbeats with "+instance.slingId);
+				}
+			}
+
+		}
+	}
+
+    class ConditionImplementation implements RetryLoop.Condition {
+
+        private final int expectedAliveCount;
+        private final VirtualInstance instance;
+
+        private ConditionImplementation(VirtualInstance instance, int expectedAliveCount) {
+            this.expectedAliveCount = expectedAliveCount;
+            this.instance = instance;
+        }
+
+        public boolean isTrue() throws Exception {
+            boolean result = false;
+            int actualAliveCount = -1;
+            try{
+                actualAliveCount = instance.getClusterViewService().getLocalClusterView().getInstances().size();
+                result = expectedAliveCount == actualAliveCount;
+            } catch(UndefinedClusterViewException e) {
+                logger.info("no view at the moment: "+e);
+                return false;
+            } catch(Exception e) {
+                logger.error("isTrue: got exception: "+e, e);
+                throw e;
+            }
+            if (!result) {
+                logger.info("isTrue: expected="+expectedAliveCount+", actual="+actualAliveCount+", result="+result);
+            }
+            return result;
+        }
+
+        public String getDescription() {
+            return "Waiting for instance with " + instance.getSlingId() + " to see " + expectedAliveCount
+                    + " instances";
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterTest.java b/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterTest.java
new file mode 100644
index 0000000..08ead70
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/AbstractClusterTest.java
@@ -0,0 +1,1540 @@
+/*
+ * 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.sling.discovery.base.its;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Semaphore;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.commons.ClusterViewHelper;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.connectors.announcement.Announcement;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementFilter;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.mock.AcceptsMultiple;
+import org.apache.sling.discovery.base.its.setup.mock.AssertingTopologyEventListener;
+import org.apache.sling.discovery.base.its.setup.mock.PropertyProviderImpl;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractClusterTest {
+	
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+    
+    private class SimpleClusterView {
+    	
+    	private VirtualInstance[] instances;
+
+    	SimpleClusterView(VirtualInstance... instances) {
+    		this.instances = instances;
+    	}
+    	
+    	@Override
+    	public String toString() {
+    	    String instanceSlingIds = "";
+    	    for(int i=0; i<instances.length; i++) {
+    	        instanceSlingIds = instanceSlingIds + instances[i].slingId + ",";
+    	    }
+            return "an expected cluster with "+instances.length+" instances: "+instanceSlingIds;
+    	}
+    }
+
+    VirtualInstance instance1;
+    VirtualInstance instance2;
+    VirtualInstance instance3;
+
+    private String property1Value;
+
+    protected String property2Value;
+
+    private String property1Name;
+
+    private String property2Name;
+    VirtualInstance instance4;
+    VirtualInstance instance5;
+    VirtualInstance instance1Restarted;
+    private Level logLevel;
+
+    protected abstract VirtualInstanceBuilder newBuilder();
+    
+    @Before
+    public void setup() throws Exception {
+        final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+        logLevel = discoveryLogger.getLevel();
+        discoveryLogger.setLevel(Level.TRACE);
+        logger.debug("here we are");
+        instance1 = newBuilder().setDebugName("firstInstance").newRepository("/var/discovery/impl/", true).build();
+        instance2 = newBuilder().setDebugName("secondInstance").useRepositoryOf(instance1).build();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (instance5 != null) {
+            instance5.stop();
+        }
+        if (instance4 != null) {
+            instance4.stop();
+        }
+        if (instance3 != null) {
+            instance3.stop();
+        }
+        if (instance3 != null) {
+            instance3.stop();
+        }
+        if (instance2 != null) {
+        	instance2.stop();
+        }
+        if (instance1 != null) {
+            instance1.stop();
+        }
+        if (instance1Restarted != null) {
+            instance1Restarted.stop();
+        }
+        instance1Restarted = null;
+        instance1 = null;
+        instance2 = null;
+        instance3 = null;
+        instance4 = null;
+        instance5 = null;
+        final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+        discoveryLogger.setLevel(logLevel);
+    }
+    
+    /** test leader behaviour with ascending slingIds, SLING-3253 **/
+    @Test
+    public void testLeaderAsc() throws Throwable {
+        logger.info("testLeaderAsc: start");
+    	doTestLeader("000", "111");
+        logger.info("testLeaderAsc: end");
+    }
+
+    /** test leader behaviour with descending slingIds, SLING-3253 **/
+    @Test
+    public void testLeaderDesc() throws Throwable {
+        logger.info("testLeaderDesc: start");
+    	doTestLeader("111", "000");
+        logger.info("testLeaderDesc: end");
+    }
+
+    private void doTestLeader(String slingId1, String slingId2) throws Throwable {
+        logger.info("doTestLeader("+slingId1+","+slingId2+"): start");
+    	// stop 1 and 2 and create them with a lower heartbeat timeout
+    	instance2.stopViewChecker();
+    	instance1.stopViewChecker();
+        instance2.stop();
+        instance1.stop();
+        instance1 = newBuilder().setDebugName("firstInstance")
+                .newRepository("/var/discovery/impl/", true)
+                .setConnectorPingTimeout(30)
+                .setMinEventDelay(1)
+                .setSlingId(slingId1).build();
+        // sleep so that the two dont have the same startup time, and thus leaderElectionId is lower for instance1
+        logger.info("doTestLeader: 1st sleep 200ms");
+        Thread.sleep(200);
+        instance2 = newBuilder().setDebugName("secondInstance")
+                .useRepositoryOf(instance1)
+                .setConnectorPingTimeout(30)
+                .setMinEventDelay(1)
+                .setSlingId(slingId2).build();
+        assertNotNull(instance1);
+        assertNotNull(instance2);
+
+        // the two instances are still isolated - hence they throw an exception
+        try{
+            instance1.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+        try{
+            instance2.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+
+        // let the sync/voting happen
+        for(int m=0; m<4; m++) {
+            instance1.heartbeatsAndCheckView();
+            instance2.heartbeatsAndCheckView();
+            logger.info("doTestLeader: sleep 500ms");
+            Thread.sleep(500);
+        }
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        
+        // now they must be in the same cluster, so in a cluster of size 1
+        assertEquals(2, instance1.getClusterViewService().getLocalClusterView().getInstances().size());
+        assertEquals(2, instance2.getClusterViewService().getLocalClusterView().getInstances().size());
+        
+        // the first instance should be the leader - since it was started first
+        assertTrue(instance1.getLocalInstanceDescription().isLeader());
+        assertFalse(instance2.getLocalInstanceDescription().isLeader());
+        logger.info("doTestLeader("+slingId1+","+slingId2+"): end");
+    }
+
+    /**
+     * Tests stale announcement reported in SLING-4139:
+     * An instance which crashes but had announcements, never cleans up those announcements.
+     * Thus on a restart, those announcements are still there, even if the connector
+     * would no longer be in use (or point somewhere else etc).
+     * That has various effects, one of them tested in this method: peers in the same cluster,
+     * after the crashed/stopped instance restarts, will assume those stale announcements
+     * as being correct and include them in the topology - hence reporting stale instances
+     * (which can be old instances or even duplicates).
+     */
+    @Test
+    public void testStaleAnnouncementsVisibleToClusterPeers4139() throws Throwable {
+        logger.info("testStaleAnnouncementsVisibleToClusterPeers4139: start");
+    	final String instance1SlingId = prepare4139();
+        
+        // remove topology connector from instance3 to instance1
+        // -> corresponds to stop pinging
+        // (nothing to assert additionally here)
+        
+        // start instance 1
+        instance1Restarted = newBuilder().setDebugName("firstInstance")
+                .useRepositoryOf(instance2)
+                .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+                .setMinEventDelay(1)
+                .setSlingId(instance1SlingId).build();
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+        
+        // facts: connector 3->1 does not exist actively anymore,
+        //        instance 1+2 should build a cluster, 
+        //        instance 3 should be isolated
+        logger.info("instance1Restarted.dump: "+instance1Restarted.slingId);
+        instance1Restarted.dumpRepo();
+        
+        logger.info("instance2.dump: "+instance2.slingId);
+        instance2.dumpRepo();
+
+        logger.info("instance3.dump: "+instance3.slingId);
+        instance3.dumpRepo();
+
+        assertTopology(instance1Restarted, new SimpleClusterView(instance1Restarted, instance2));
+        assertTopology(instance3, new SimpleClusterView(instance3));
+        assertTopology(instance2, new SimpleClusterView(instance1Restarted, instance2));
+        instance1Restarted.stop();
+        logger.info("testStaleAnnouncementsVisibleToClusterPeers4139: end");
+    }
+    
+    /**
+     * Tests a situation where a connector was done to instance1, which eventually
+     * crashed, then the connector is done to instance2. Meanwhile instance1 
+     * got restarted and this test assures that the instance3 is not reported
+     * twice in the topology. Did not happen before 4139, but should never afterwards neither
+     */
+    @Test
+    public void testDuplicateInstanceIn2Clusters4139() throws Throwable {
+        logger.info("testDuplicateInstanceIn2Clusters4139: start");
+        final String instance1SlingId = prepare4139();
+        
+        // remove topology connector from instance3 to instance1
+        // -> corresponds to stop pinging
+        // (nothing to assert additionally here)
+        // instead, now start a connector from instance3 to instance2
+        pingConnector(instance3, instance2);
+        
+        // start instance 1
+        instance1Restarted = newBuilder().setDebugName("firstInstance")
+                .useRepositoryOf(instance2)
+                .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+                .setMinEventDelay(1)
+                .setSlingId(instance1SlingId).build();
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+        pingConnector(instance3, instance2);
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+        pingConnector(instance3, instance2);
+        logger.info("iteration 0");
+        logger.info("instance1Restarted.slingId: "+instance1Restarted.slingId);
+        logger.info("instance2.slingId: "+instance2.slingId);
+        logger.info("instance3.slingId: "+instance3.slingId);
+        instance1Restarted.dumpRepo();
+        assertSameTopology(new SimpleClusterView(instance1Restarted, instance2), new SimpleClusterView(instance3));
+        
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+        pingConnector(instance3, instance2);
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3);
+        pingConnector(instance3, instance2);
+        logger.info("iteration 1");
+        logger.info("instance1Restarted.slingId: "+instance1Restarted.slingId);
+        logger.info("instance2.slingId: "+instance2.slingId);
+        logger.info("instance3.slingId: "+instance3.slingId);
+        instance1Restarted.dumpRepo();
+        assertSameTopology(new SimpleClusterView(instance1Restarted, instance2), new SimpleClusterView(instance3));
+        instance1Restarted.stop();
+
+        logger.info("testDuplicateInstanceIn2Clusters4139: end");
+    }
+    
+/*    ok, this test should do the following:
+         * cluster A with instance 1 and instance 2
+         * cluster B with instance 3 and instance 4
+         * cluster C with instance 5
+         
+         * initially, instance3 is pinging instance1, and instance 5 is pinging instance1 as well (MAC hub)
+          * that should result in instance3 and 5 to inherit the rest from instance1
+         * then simulate load balancer switching from instance1 to instance2 - hence pings go to instance2 
+         * 
+         */
+    @Test
+    public void testConnectorSwitching4139() throws Throwable {
+        final int MIN_EVENT_DELAY = 1;
+
+        tearDown(); // reset any setup that was done - we start with a different setup than the default one
+        final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+        logLevel = discoveryLogger.getLevel();
+        discoveryLogger.setLevel(Level.DEBUG);
+        
+        instance1 = newBuilder().setDebugName("instance1")
+                .newRepository("/var/discovery/clusterA/", true)
+                .setConnectorPingTimeout(10 /* sec */)
+                .setConnectorPingInterval(999)
+                .setMinEventDelay(MIN_EVENT_DELAY).build();
+        instance2 = newBuilder().setDebugName("instance2")
+                .useRepositoryOf(instance1)
+                .setConnectorPingTimeout(10 /* sec */)
+                .setConnectorPingInterval(999)
+                .setMinEventDelay(MIN_EVENT_DELAY).build();
+        // now launch the remote instance
+        instance3 = newBuilder().setDebugName("instance3")
+                .newRepository("/var/discovery/clusterB/", false)
+                .setConnectorPingTimeout(10 /* sec */)
+                .setConnectorPingInterval(999)
+                .setMinEventDelay(MIN_EVENT_DELAY).build();
+        instance4 = newBuilder().setDebugName("instance4")
+                .useRepositoryOf(instance3)
+                .setConnectorPingTimeout(10 /* sec */)
+                .setConnectorPingInterval(999)
+                .setMinEventDelay(MIN_EVENT_DELAY).build();
+        instance5 = newBuilder().setDebugName("instance5")
+                .newRepository("/var/discovery/clusterC/", false)
+                .setConnectorPingTimeout(10 /* sec */)
+                .setConnectorPingInterval(999)
+                .setMinEventDelay(MIN_EVENT_DELAY).build();
+
+        // join the instances to form a cluster by sending out heartbeats
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+        Thread.sleep(500);
+
+        assertSameTopology(new SimpleClusterView(instance1, instance2));
+        assertSameTopology(new SimpleClusterView(instance3, instance4));
+        assertSameTopology(new SimpleClusterView(instance5));
+        
+        // create a topology connector from instance3 to instance1
+        // -> corresponds to starting to ping
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+        pingConnector(instance3, instance1);
+        pingConnector(instance5, instance1);
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+        pingConnector(instance3, instance1);
+        pingConnector(instance5, instance1);
+        Thread.sleep(500);
+        
+        // make asserts on the topology
+        logger.info("testConnectorSwitching4139: instance1.slingId="+instance1.slingId);
+        logger.info("testConnectorSwitching4139: instance2.slingId="+instance2.slingId);
+        logger.info("testConnectorSwitching4139: instance3.slingId="+instance3.slingId);
+        logger.info("testConnectorSwitching4139: instance4.slingId="+instance4.slingId);
+        logger.info("testConnectorSwitching4139: instance5.slingId="+instance5.slingId);
+        instance1.dumpRepo();
+        
+        assertSameTopology(new SimpleClusterView(instance1, instance2), 
+                new SimpleClusterView(instance3, instance4), 
+                new SimpleClusterView(instance5));
+        
+        // simulate a crash of instance1, resulting in load-balancer to switch the pings
+        boolean success = false;
+        for(int i=0; i<25; i++) {
+            // loop for max 25 times, min 20 times
+            runHeartbeatOnceWith(instance2, instance3, instance4, instance5);
+            final boolean ping1 = pingConnector(instance3, instance2);
+            final boolean ping2 = pingConnector(instance5, instance2);
+            if (ping1 && ping2) {
+                // both pings were fine - hence break
+                success = true;
+                logger.info("testConnectorSwitching4139: successfully switched all pings to instance2 after "+i+" rounds.");
+                if (i<20) {
+                    logger.info("testConnectorSwitching4139: min loop cnt not yet reached: i="+i);
+                    Thread.sleep(1000); // 20x1000ms = 20sec max - (vs 10sec timeout) - should be enough for timing out
+                    continue;
+                }
+                break;
+            }
+            logger.info("testConnectorSwitching4139: looping cos ping1="+ping1+", ping2="+ping2);
+            Thread.sleep(1000); // 25x1000ms = 25sec max - (vs 10sec timeout)
+            
+        }
+        assertTrue(success);
+        // one final heartbeat
+        runHeartbeatOnceWith(instance2, instance3, instance4, instance5);
+        assertTrue(pingConnector(instance3, instance2));
+        assertTrue(pingConnector(instance5, instance2));
+
+        instance2.dumpRepo();
+
+        assertSameTopology(new SimpleClusterView(instance2), 
+                new SimpleClusterView(instance3, instance4), 
+                new SimpleClusterView(instance5));
+
+        // restart instance1, crash instance4
+        instance4.stopViewChecker();
+        instance1Restarted = newBuilder().setDebugName("instance1")
+                .useRepositoryOf(instance2)
+                .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+                .setMinEventDelay(1)
+                .setSlingId(instance1.getSlingId()).build();
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance5);
+        assertTrue(pingConnector(instance3, instance2));
+        assertTrue(pingConnector(instance5, instance2));
+        success = false;
+        for(int i=0; i<25; i++) {
+            runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance5);
+            instance1.getViewChecker().checkView();
+            assertTrue(pingConnector(instance3, instance2));
+            assertTrue(pingConnector(instance5, instance2));
+            final TopologyView topology = instance3.getDiscoveryService().getTopology();
+            InstanceDescription i3 = null;
+            for (Iterator<InstanceDescription> it = topology.getInstances().iterator(); it.hasNext();) {
+                final InstanceDescription id = it.next();
+                if (id.getSlingId().equals(instance3.slingId)) {
+                    i3 = id;
+                    break;
+                }
+            }
+            assertNotNull(i3);
+            assertEquals(instance3.slingId, i3.getSlingId());
+            final ClusterView i3Cluster = i3.getClusterView();
+            final int i3ClusterSize = i3Cluster.getInstances().size();
+            if (i3ClusterSize==1) {
+                if (i<20) {
+                    logger.info("testConnectorSwitching4139: [2] min loop cnt not yet reached: i="+i);
+                    Thread.sleep(500); // 20x500ms = 10sec max - (vs 5sec timeout) - should be enough for timing out
+                    continue;
+                }
+                success = true;
+                break;
+            }
+            logger.info("testConnectorSwitching4139: i3ClusterSize: "+i3ClusterSize);
+            Thread.sleep(500);
+        }
+
+        logger.info("testConnectorSwitching4139: instance1Restarted.slingId="+instance1Restarted.slingId);
+        logger.info("testConnectorSwitching4139: instance2.slingId="+instance2.slingId);
+        logger.info("testConnectorSwitching4139: instance3.slingId="+instance3.slingId);
+        logger.info("testConnectorSwitching4139: instance4.slingId="+instance4.slingId);
+        logger.info("testConnectorSwitching4139: instance5.slingId="+instance5.slingId);
+        instance1Restarted.dumpRepo();
+        assertTrue(success);
+
+        assertSameTopology(new SimpleClusterView(instance1Restarted, instance2), 
+                new SimpleClusterView(instance3), 
+                new SimpleClusterView(instance5));
+        instance1Restarted.stop();
+
+    }
+
+    @Test
+    public void testDuplicateInstance3726() throws Throwable {
+        logger.info("testDuplicateInstance3726: start");
+        final int MIN_EVENT_DELAY = 1;
+
+        tearDown(); // reset any setup that was done - we start with a different setup than the default one
+        final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+        logLevel = discoveryLogger.getLevel();
+        discoveryLogger.setLevel(Level.DEBUG);
+        
+        instance1 = newBuilder().setDebugName("instance1")
+                .newRepository("/var/discovery/clusterA/", true)
+                .setConnectorPingTimeout(15 /* sec */)
+                .setMinEventDelay(MIN_EVENT_DELAY).build();
+        instance2 = newBuilder().setDebugName("instance2")
+                .useRepositoryOf(instance1)
+                .setConnectorPingTimeout(15 /* sec */)
+                .setMinEventDelay(MIN_EVENT_DELAY).build();
+        // now launch the remote instance
+        instance3 = newBuilder().setDebugName("instance3")
+                .newRepository("/var/discovery/clusterB/", false)
+                .setConnectorPingTimeout(15 /* sec */)
+                .setMinEventDelay(MIN_EVENT_DELAY).build();
+        instance5 = newBuilder().setDebugName("instance5")
+                .newRepository("/var/discovery/clusterC/", false)
+                .setConnectorPingTimeout(15 /* sec */)
+                .setMinEventDelay(MIN_EVENT_DELAY).build();
+
+        // join the instances to form a cluster by sending out heartbeats
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance5);
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance5);
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance5);
+        Thread.sleep(500);
+
+        assertSameTopology(new SimpleClusterView(instance1, instance2));
+        assertSameTopology(new SimpleClusterView(instance3));
+        assertSameTopology(new SimpleClusterView(instance5));
+        
+        // create a topology connector from instance3 to instance1
+        // -> corresponds to starting to ping
+        pingConnector(instance3, instance1);
+        pingConnector(instance5, instance1);
+        pingConnector(instance3, instance1);
+        pingConnector(instance5, instance1);
+        
+        // make asserts on the topology
+        logger.info("testDuplicateInstance3726: instance1.slingId="+instance1.slingId);
+        logger.info("testDuplicateInstance3726: instance2.slingId="+instance2.slingId);
+        logger.info("testDuplicateInstance3726: instance3.slingId="+instance3.slingId);
+        logger.info("testDuplicateInstance3726: instance5.slingId="+instance5.slingId);
+        instance1.dumpRepo();
+        
+        assertSameTopology(new SimpleClusterView(instance1, instance2), 
+                new SimpleClusterView(instance3/*, instance4*/), 
+                new SimpleClusterView(instance5));
+        
+        // simulate a crash of instance1, resulting in load-balancer to switch the pings
+        instance1.stopViewChecker();
+        boolean success = false;
+        for(int i=0; i<25; i++) {
+            // loop for max 25 times, min 20 times
+            runHeartbeatOnceWith(instance2, instance3, /*instance4, */instance5);
+            final boolean ping1 = pingConnector(instance3, instance2);
+            final boolean ping2 = pingConnector(instance5, instance2);
+            if (ping1 && ping2) {
+                // both pings were fine - hence break
+                success = true;
+                logger.info("testDuplicateInstance3726: successfully switched all pings to instance2 after "+i+" rounds.");
+                if (i<20) {
+                    logger.info("testDuplicateInstance3726: min loop cnt not yet reached: i="+i);
+                    Thread.sleep(1000); // 20x1000ms = 20sec max - (vs 15sec timeout) - should be enough for timing out
+                    continue;
+                }
+                break;
+            }
+            logger.info("testDuplicateInstance3726: looping");
+            Thread.sleep(1000); // 25x1000ms = 25sec max - (vs 15sec timeout)
+            
+        }
+        assertTrue(success);
+        // one final heartbeat
+        runHeartbeatOnceWith(instance2, instance3, instance5);
+        assertTrue(pingConnector(instance3, instance2));
+        assertTrue(pingConnector(instance5, instance2));
+
+        instance2.dumpRepo();
+
+        assertSameTopology(new SimpleClusterView(instance2), 
+                new SimpleClusterView(instance3), 
+                new SimpleClusterView(instance5));
+
+        // restart instance1, start instance4
+        instance1Restarted = newBuilder().setDebugName("instance1")
+                .useRepositoryOf(instance2)
+                .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+                .setMinEventDelay(1)
+                .setSlingId(instance1.getSlingId()).build();
+        instance4 = newBuilder().setDebugName("instance4")
+                .useRepositoryOf(instance3)
+                .setConnectorPingTimeout(30 /* sec */)
+                .setMinEventDelay(MIN_EVENT_DELAY).build();
+        for(int i=0; i<3; i++) {
+            runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4, instance5);
+            assertTrue(pingConnector(instance3, instance2));
+            assertTrue(pingConnector(instance5, instance2));
+        }
+
+        instance1Restarted.dumpRepo();
+        logger.info("testDuplicateInstance3726: instance1Restarted.slingId="+instance1Restarted.slingId);
+        logger.info("testDuplicateInstance3726: instance2.slingId="+instance2.slingId);
+        logger.info("testDuplicateInstance3726: instance3.slingId="+instance3.slingId);
+        logger.info("testDuplicateInstance3726: instance4.slingId="+instance4.slingId);
+        logger.info("testDuplicateInstance3726: instance5.slingId="+instance5.slingId);
+        assertTrue(success);
+
+        assertSameTopology(new SimpleClusterView(instance1Restarted, instance2), 
+                new SimpleClusterView(instance3, instance4), 
+                new SimpleClusterView(instance5));
+        instance1Restarted.stop();
+        logger.info("testDuplicateInstance3726: end");
+    }
+
+    private void assertSameTopology(SimpleClusterView... clusters) throws UndefinedClusterViewException {
+        if (clusters==null) {
+            return;
+        }
+        for(int i=0; i<clusters.length; i++) { // go through all clusters 
+            final SimpleClusterView aCluster = clusters[i];
+            assertSameClusterIds(aCluster.instances);
+            for(int j=0; j<aCluster.instances.length; j++) { // and all instances therein
+                final VirtualInstance anInstance = aCluster.instances[j];
+                assertTopology(anInstance, clusters); // an verify that they all see the same
+                for(int k=0; k<clusters.length; k++) {
+                    final SimpleClusterView otherCluster = clusters[k];
+                    if (aCluster==otherCluster) {
+                        continue; // then ignore this one
+                    }
+                    for(int m=0; m<otherCluster.instances.length; m++) {
+                        assertNotSameClusterIds(anInstance, otherCluster.instances[m]);
+                    }
+                }
+            }
+        }
+    }
+
+    private void runHeartbeatOnceWith(VirtualInstance... instances) {
+        if (instances==null) {
+            return;
+        }
+        for(int i=0; i<instances.length; i++) {
+            instances[i].heartbeatsAndCheckView();
+        }
+    }
+
+    /**
+     * Tests a situation where a connector was done to instance1, which eventually
+     * crashed, then the connector is done to instance4 (which is in a separate, 3rd cluster). 
+     * Meanwhile instance1 got restarted and this test assures that the instance3 is not reported
+     * twice in the topology. This used to happen prior to SLING-4139
+     */
+    @Test
+    public void testStaleInstanceIn3Clusters4139() throws Throwable {
+        logger.info("testStaleInstanceIn3Clusters4139: start");
+        final String instance1SlingId = prepare4139();
+        
+        // remove topology connector from instance3 to instance1
+        // -> corresponds to stop pinging
+        // (nothing to assert additionally here)
+        
+        // start instance4 in a separate cluster
+        instance4 = newBuilder().setDebugName("remoteInstance4")
+                .newRepository("/var/discovery/implremote4/", false)
+                .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+                .setMinEventDelay(1).build();
+        try{
+            instance4.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+        
+        // instead, now start a connector from instance3 to instance2
+        instance4.heartbeatsAndCheckView();
+        instance4.heartbeatsAndCheckView();
+        pingConnector(instance3, instance4);
+        
+        // start instance 1
+        instance1Restarted = newBuilder().setDebugName("firstInstance")
+                .useRepositoryOf(instance2)
+                .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+                .setMinEventDelay(1)
+                .setSlingId(instance1SlingId).build();
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4);
+        pingConnector(instance3, instance4);
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4);
+        pingConnector(instance3, instance4);
+        logger.info("iteration 0");
+        logger.info("instance1Restarted.slingId: "+instance1Restarted.slingId);
+        logger.info("instance2.slingId: "+instance2.slingId);
+        logger.info("instance3.slingId: "+instance3.slingId);
+        logger.info("instance4.slingId: "+instance4.slingId);
+        instance1Restarted.dumpRepo();
+        assertSameTopology(
+                new SimpleClusterView(instance3),
+                new SimpleClusterView(instance4));
+        assertSameTopology(new SimpleClusterView(instance1Restarted, instance2));
+        
+        Thread.sleep(100);
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4);
+        pingConnector(instance3, instance4);
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4);
+        pingConnector(instance3, instance4);
+        logger.info("iteration 1");
+        logger.info("instance1Restarted.slingId: "+instance1Restarted.slingId);
+        logger.info("instance2.slingId: "+instance2.slingId);
+        logger.info("instance3.slingId: "+instance3.slingId);
+        logger.info("instance4.slingId: "+instance4.slingId);
+        instance1Restarted.dumpRepo();
+        assertSameTopology(new SimpleClusterView(instance1Restarted, instance2));
+        assertSameTopology(
+                new SimpleClusterView(instance3),
+                new SimpleClusterView(instance4));
+
+        Thread.sleep(100);
+        runHeartbeatOnceWith(instance1Restarted, instance2, instance3, instance4);
+        pingConnector(instance3, instance4);
+        
+        // now the situation should be as follows:
+        logger.info("iteration 2");
+        logger.info("instance1Restarted.slingId: "+instance1Restarted.slingId);
+        logger.info("instance2.slingId: "+instance2.slingId);
+        logger.info("instance3.slingId: "+instance3.slingId);
+        logger.info("instance4.slingId: "+instance4.slingId);
+        instance1Restarted.dumpRepo();
+        assertSameTopology(new SimpleClusterView(instance1Restarted, instance2));
+        assertSameTopology(
+                new SimpleClusterView(instance3),
+                new SimpleClusterView(instance4));
+        instance1Restarted.stop();
+
+        logger.info("testStaleInstanceIn3Clusters4139: end");
+    }
+    
+    /**
+     * Preparation steps for SLING-4139 tests:
+     * Creates two clusters: A: with instance1 and 2, B with instance 3
+     * instance 3 creates a connector to instance 1
+     * then instance 1 is killed (crashes)
+     * @return the slingId of the original (crashed) instance1
+     */
+	private String prepare4139() throws Throwable, Exception,
+			InterruptedException {
+	    tearDown(); // stop anything running..
+        instance1 = newBuilder().setDebugName("firstInstance")
+                .newRepository("/var/discovery/impl/", true)
+                .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+                .setMinEventDelay(1).build();
+        instance2 = newBuilder().setDebugName("secondInstance")
+                .useRepositoryOf(instance1)
+                .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+                .setMinEventDelay(1).build();
+        // join the two instances to form a cluster by sending out heartbeats
+        runHeartbeatOnceWith(instance1, instance2);
+        Thread.sleep(100);
+        runHeartbeatOnceWith(instance1, instance2);
+        Thread.sleep(100);
+        runHeartbeatOnceWith(instance1, instance2);
+        assertSameClusterIds(instance1, instance2);
+        
+        // now launch the remote instance
+        instance3 = newBuilder().setDebugName("remoteInstance")
+                .newRepository("/var/discovery/implremote/", false)
+                .setConnectorPingTimeout(Integer.MAX_VALUE /* no timeout */)
+                .setMinEventDelay(1).build();
+        assertSameClusterIds(instance1, instance2);
+        try{
+            instance3.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException ue) {
+            // ok
+        }
+        assertEquals(0, instance1.getAnnouncementRegistry().listLocalAnnouncements().size());
+        assertEquals(0, instance1.getAnnouncementRegistry().listLocalIncomingAnnouncements().size());
+        assertEquals(0, instance2.getAnnouncementRegistry().listLocalAnnouncements().size());
+        assertEquals(0, instance2.getAnnouncementRegistry().listLocalIncomingAnnouncements().size());
+        assertEquals(0, instance3.getAnnouncementRegistry().listLocalAnnouncements().size());
+        assertEquals(0, instance3.getAnnouncementRegistry().listLocalIncomingAnnouncements().size());
+        
+        // create a topology connector from instance3 to instance1
+        // -> corresponds to starting to ping
+        instance3.heartbeatsAndCheckView();
+        instance3.heartbeatsAndCheckView();
+        pingConnector(instance3, instance1);
+        // make asserts on the topology
+        instance1.dumpRepo();
+        assertSameTopology(new SimpleClusterView(instance1, instance2), new SimpleClusterView(instance3));
+        
+        // kill instance 1
+        logger.info("instance1.slingId="+instance1.slingId);
+        logger.info("instance2.slingId="+instance2.slingId);
+        logger.info("instance3.slingId="+instance3.slingId);
+        final String instance1SlingId = instance1.slingId;
+        instance1.stopViewChecker(); // and have instance3 no longer pinging instance1
+        instance1.stop(); // otherwise it will have itself still registered with the observation manager and fiddle with future events..
+        instance1 = null; // set to null to early fail if anyone still assumes (original) instance1 is up form now on
+        instance2.getConfig().setViewCheckTimeout(1); // set instance2's heartbeatTimeout to 1 sec to time out instance1 quickly!
+        instance3.getConfig().setViewCheckTimeout(1); // set instance3's heartbeatTimeout to 1 sec to time out instance1 quickly!
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance2, instance3);
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance2, instance3);
+        Thread.sleep(500);
+        runHeartbeatOnceWith(instance2, instance3);
+        // instance 2 should now be alone - in fact, 3 should be alone as well
+        instance2.dumpRepo();
+        assertTopology(instance2, new SimpleClusterView(instance2));
+        assertTopology(instance3, new SimpleClusterView(instance3));
+        instance2.getConfig().setViewCheckTimeout(Integer.MAX_VALUE /* no timeout */); // set instance2's heartbeatTimeout back to Integer.MAX_VALUE /* no timeout */
+        instance3.getConfig().setViewCheckTimeout(Integer.MAX_VALUE /* no timeout */); // set instance3's heartbeatTimeout back to Integer.MAX_VALUE /* no timeout */
+		return instance1SlingId;
+	}
+    
+    private void assertNotSameClusterIds(VirtualInstance... instances) throws UndefinedClusterViewException {
+    	if (instances==null) {
+    		fail("must not pass empty set of instances here");
+    	}
+    	if (instances.length<=1) {
+    		fail("must not pass 0 or 1 instance only");
+    	}
+        final String clusterId1 = instances[0].getClusterViewService()
+                .getLocalClusterView().getId();
+        for(int i=1; i<instances.length; i++) {
+	        final String otherClusterId = instances[i].getClusterViewService()
+	                .getLocalClusterView().getId();
+	        // cluster ids must NOT be the same
+	        assertNotEquals(clusterId1, otherClusterId);
+        }
+        if (instances.length>2) {
+        	final VirtualInstance[] subset = new VirtualInstance[instances.length-1];
+        	System.arraycopy(instances, 0, subset, 1, instances.length-1);
+        	assertNotSameClusterIds(subset);
+        }
+	}
+
+	private void assertSameClusterIds(VirtualInstance... instances) throws UndefinedClusterViewException {
+    	if (instances==null) {
+            // then there is nothing to compare
+            return;
+    	}
+    	if (instances.length==1) {
+    	    // then there is nothing to compare
+    	    return;
+    	}
+        final String clusterId1 = instances[0].getClusterViewService()
+                .getLocalClusterView().getId();
+        for(int i=1; i<instances.length; i++) {
+	        final String otherClusterId = instances[i].getClusterViewService()
+	                .getLocalClusterView().getId();
+	        // cluster ids must be the same
+	        if (!clusterId1.equals(otherClusterId)) {
+	            logger.error("assertSameClusterIds: instances[0]: "+instances[0]);
+	            logger.error("assertSameClusterIds: instances["+i+"]: "+instances[i]);
+	            fail("mismatch in clusterIds: expected to equal: clusterId1="+clusterId1+", otherClusterId="+otherClusterId);
+	        }
+        }
+	}
+
+	private void assertTopology(VirtualInstance instance, SimpleClusterView... assertedClusterViews) {
+    	final TopologyView topology = instance.getDiscoveryService().getTopology();
+    	logger.info("assertTopology: instance "+instance.slingId+" sees topology: "+topology+", expected: "+assertedClusterViews);
+    	assertNotNull(topology);
+    	if (assertedClusterViews.length!=topology.getClusterViews().size()) {
+            dumpFailureDetails(topology, assertedClusterViews);
+    	    fail("instance "+instance.slingId+ " expected "+assertedClusterViews.length+", got: "+topology.getClusterViews().size());
+    	}
+    	final Set<ClusterView> actualClusters = new HashSet<ClusterView>(topology.getClusterViews());
+    	for(int i=0; i<assertedClusterViews.length; i++) {
+    		final SimpleClusterView assertedClusterView = assertedClusterViews[i];
+    		boolean foundMatch = false;
+    		for (Iterator<ClusterView> it = actualClusters.iterator(); it
+					.hasNext();) {
+				final ClusterView actualClusterView = it.next();
+				if (matches(assertedClusterView, actualClusterView)) {
+					it.remove();
+					foundMatch = true;
+					break;
+				}
+			}
+    		if (!foundMatch) {
+    		    dumpFailureDetails(topology, assertedClusterViews);
+    			fail("instance "+instance.slingId+ " could not find a match in the topology with instance="+instance.slingId+" and clusterViews="+assertedClusterViews.length);
+    		}
+    	}
+    	assertEquals("not all asserted clusterviews are in the actual view with instance="+instance+" and clusterViews="+assertedClusterViews, actualClusters.size(), 0);
+	}
+
+    private void dumpFailureDetails(TopologyView topology, SimpleClusterView... assertedClusterViews) {
+        logger.error("assertTopology: expected: "+assertedClusterViews.length);
+        for(int j=0; j<assertedClusterViews.length; j++) {
+            logger.error("assertTopology:  ["+j+"]: "+assertedClusterViews[j].toString());
+        }
+        final Set<ClusterView> clusterViews = topology.getClusterViews();
+        final Set<InstanceDescription> instances = topology.getInstances();
+        logger.error("assertTopology: actual: "+clusterViews.size()+" clusters with a total of "+instances.size()+" instances");
+        for (Iterator<ClusterView> it = clusterViews.iterator(); it.hasNext();) {
+            final ClusterView aCluster = it.next();
+            logger.error("assertTopology:  a cluster: "+aCluster.getId());
+            for (Iterator<InstanceDescription> it2 = aCluster.getInstances().iterator(); it2.hasNext();) {
+                final InstanceDescription id = it2.next();
+                logger.error("assertTopology:   - an instance "+id.getSlingId());
+            }
+        }
+        logger.error("assertTopology: list of all instances: "+instances.size());
+        for (Iterator<InstanceDescription> it = instances.iterator(); it.hasNext();) {
+            final InstanceDescription id = it.next();
+            logger.error("assertTopology: - an instance: "+id.getSlingId());
+        }
+    }
+
+	private boolean matches(SimpleClusterView assertedClusterView,
+			ClusterView actualClusterView) {
+		assertNotNull(assertedClusterView);
+		assertNotNull(actualClusterView);
+		if (assertedClusterView.instances.length!=actualClusterView.getInstances().size()) {
+			return false;
+		}
+		final Set<InstanceDescription> actualInstances = new HashSet<InstanceDescription>(actualClusterView.getInstances());
+		outerLoop:for(int i=0; i<assertedClusterView.instances.length; i++) {
+			final VirtualInstance assertedInstance = assertedClusterView.instances[i];
+			for (Iterator<InstanceDescription> it = actualInstances.iterator(); it
+					.hasNext();) {
+				final InstanceDescription anActualInstance = it.next();
+				if (assertedInstance.slingId.equals(anActualInstance.getSlingId())) {
+					continue outerLoop;
+				}
+			}
+			return false;
+		}
+		return true;
+	}
+
+	private boolean pingConnector(final VirtualInstance from, final VirtualInstance to) throws UndefinedClusterViewException {
+	    final Announcement fromAnnouncement = createFromAnnouncement(from);
+	    Announcement replyAnnouncement = null;
+	    try{
+            replyAnnouncement = ping(to, fromAnnouncement);
+	    } catch(AssertionError e) {
+	        logger.warn("pingConnector: ping failed, assertionError: "+e);
+	        return false;
+	    } catch (UndefinedClusterViewException e) {
+            logger.warn("pingConnector: ping failed, currently the cluster view is undefined: "+e);
+            return false;
+        }
+        registerReplyAnnouncement(from, replyAnnouncement);
+        return true;
+    }
+
+	private void registerReplyAnnouncement(VirtualInstance from,
+			Announcement inheritedAnnouncement) {
+		final AnnouncementRegistry announcementRegistry = from.getAnnouncementRegistry();
+        if (inheritedAnnouncement.isLoop()) {
+        	fail("loop detected");
+        	// we dont currently support loops here in the junit tests
+        	return;
+        } else {
+            inheritedAnnouncement.setInherited(true);
+            if (announcementRegistry
+                    .registerAnnouncement(inheritedAnnouncement)==-1) {
+                logger.info("ping: connector response is from an instance which I already see in my topology"
+                        + inheritedAnnouncement);
+                return;
+            }
+        }
+//        resultingAnnouncement = inheritedAnnouncement;
+//        statusDetails = null;
+	}
+
+	private Announcement ping(VirtualInstance to, final Announcement incomingTopologyAnnouncement) 
+	        throws UndefinedClusterViewException {
+		final String slingId = to.slingId;
+		final ClusterViewService clusterViewService = to.getClusterViewService();
+		final AnnouncementRegistry announcementRegistry = to.getAnnouncementRegistry();
+		
+		incomingTopologyAnnouncement.removeInherited(slingId);
+
+        final Announcement replyAnnouncement = new Announcement(
+                slingId);
+
+        long backoffInterval = -1;
+        final ClusterView clusterView = clusterViewService.getLocalClusterView();
+        if (!incomingTopologyAnnouncement.isCorrectVersion()) {
+        	fail("incorrect version");
+            return null; // never reached
+        } else if (ClusterViewHelper.contains(clusterView, incomingTopologyAnnouncement
+                .getOwnerId())) {
+        	fail("loop=true");
+            return null; // never reached
+        } else if (ClusterViewHelper.containsAny(clusterView, incomingTopologyAnnouncement
+                .listInstances())) {
+        	fail("incoming announcement contains instances that are part of my cluster");
+            return null; // never reached
+        } else {
+            backoffInterval = announcementRegistry
+                    .registerAnnouncement(incomingTopologyAnnouncement);
+            if (backoffInterval==-1) {
+            	fail("rejecting an announcement from an instance that I already see in my topology: ");
+                return null; // never reached
+            } else {
+                // normal, successful case: replying with the part of the topology which this instance sees
+                replyAnnouncement.setLocalCluster(clusterView);
+                announcementRegistry.addAllExcept(replyAnnouncement, clusterView,
+                        new AnnouncementFilter() {
+
+                            public boolean accept(final String receivingSlingId, Announcement announcement) {
+                                if (announcement.getPrimaryKey().equals(
+                                        incomingTopologyAnnouncement
+                                                .getPrimaryKey())) {
+                                    return false;
+                                }
+                                return true;
+                            }
+                        });
+                return replyAnnouncement;
+            }
+        }
+	}
+
+	private Announcement createFromAnnouncement(final VirtualInstance from) throws UndefinedClusterViewException {
+		// TODO: refactor TopologyConnectorClient to avoid duplicating code from there (ping())
+		Announcement topologyAnnouncement = new Announcement(from.slingId);
+        topologyAnnouncement.setServerInfo(from.slingId);
+        final ClusterView clusterView = from.getClusterViewService().getLocalClusterView();
+        topologyAnnouncement.setLocalCluster(clusterView);
+        from.getAnnouncementRegistry().addAllExcept(topologyAnnouncement, clusterView, new AnnouncementFilter() {
+            
+            public boolean accept(final String receivingSlingId, final Announcement announcement) {
+                // filter out announcements that are of old cluster instances
+                // which I dont really have in my cluster view at the moment
+                final Iterator<InstanceDescription> it = 
+                        clusterView.getInstances().iterator();
+                while(it.hasNext()) {
+                    final InstanceDescription instance = it.next();
+                    if (instance.getSlingId().equals(receivingSlingId)) {
+                        // then I have the receiving instance in my cluster view
+                        // all fine then
+                        return true;
+                    }
+                }
+                // looks like I dont have the receiving instance in my cluster view
+                // then I should also not propagate that announcement anywhere
+                return false;
+            }
+        });
+        return topologyAnnouncement;
+	}
+
+	@Test
+    public void testStableClusterId() throws Throwable {
+        logger.info("testStableClusterId: start");
+    	// stop 1 and 2 and create them with a lower heartbeat timeout
+    	instance2.stopViewChecker();
+    	instance1.stopViewChecker();
+        instance2.stop();
+        instance1.stop();
+	// SLING-4302 : first set the heartbeatTimeout to 100 sec - large enough to work on all CI instances
+        instance1 = newBuilder().setDebugName("firstInstance")
+                .newRepository("/var/discovery/impl/", true)
+                .setConnectorPingTimeout(100)
+                .setMinEventDelay(1).build();
+        instance2 = newBuilder().setDebugName("secondInstance")
+                .useRepositoryOf(instance1)
+                .setConnectorPingTimeout(100)
+                .setMinEventDelay(1).build();
+        assertNotNull(instance1);
+        assertNotNull(instance2);
+
+        try{
+            instance1.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+        try{
+            instance2.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+
+        // let the sync/voting happen
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        
+        String newClusterId1 = instance1.getClusterViewService()
+                .getLocalClusterView().getId();
+        String newClusterId2 = instance2.getClusterViewService()
+                .getLocalClusterView().getId();
+        // both cluster ids must be the same
+        assertEquals(newClusterId1, newClusterId1);
+        
+        instance1.dumpRepo();
+        assertEquals(2, instance1.getClusterViewService().getLocalClusterView().getInstances().size());
+        assertEquals(2, instance2.getClusterViewService().getLocalClusterView().getInstances().size());
+        
+        // let instance2 'die' by now longer doing heartbeats
+	// SLING-4302 : then set the heartbeatTimeouts back to 1 sec to have them properly time out with the sleeps applied below
+        instance2.getConfig().setViewCheckTimeout(1);
+        instance1.getConfig().setViewCheckTimeout(1);
+        instance2.stopViewChecker(); // would actually not be necessary as it was never started.. this test only runs heartbeats manually
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        // the cluster should now have size 1
+        assertEquals(1, instance1.getClusterViewService().getLocalClusterView().getInstances().size());
+        // the instance 2 should be in isolated mode as it is no longer in the established view
+        // hence null
+        try{
+            instance2.getViewChecker().checkView();
+            instance2.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+
+        // but the cluster id must have remained stable
+        instance1.dumpRepo();
+        String actualClusterId = instance1.getClusterViewService()
+                .getLocalClusterView().getId();
+        logger.info("expected cluster id: "+newClusterId1);
+        logger.info("actual   cluster id: "+actualClusterId);
+		assertEquals(newClusterId1, actualClusterId);
+        logger.info("testStableClusterId: end");
+    }
+    
+    @Test
+    public void testClusterView() throws Exception {
+        logger.info("testClusterView: start");
+        assertNotNull(instance1);
+        assertNotNull(instance2);
+        assertNull(instance3);
+        instance3 = newBuilder().setDebugName("thirdInstance")
+                .useRepositoryOf(instance1)
+                .build();
+        assertNotNull(instance3);
+
+        assertEquals(instance1.getSlingId(), instance1.getClusterViewService()
+                .getSlingId());
+        assertEquals(instance2.getSlingId(), instance2.getClusterViewService()
+                .getSlingId());
+        assertEquals(instance3.getSlingId(), instance3.getClusterViewService()
+                .getSlingId());
+
+        try{
+            instance1.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+        try{
+            instance2.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+        try{
+            instance3.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+
+        instance1.dumpRepo();
+
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        instance3.heartbeatsAndCheckView();
+
+        instance1.dumpRepo();
+        logger.info("testClusterView: 1st 2s sleep");
+        Thread.sleep(2000);
+
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        instance3.heartbeatsAndCheckView();
+        logger.info("testClusterView: 2nd 2s sleep");
+        Thread.sleep(2000);
+
+        instance1.dumpRepo();
+        String clusterId1 = instance1.getClusterViewService().getLocalClusterView()
+                .getId();
+        logger.info("clusterId1=" + clusterId1);
+        String clusterId2 = instance2.getClusterViewService().getLocalClusterView()
+                .getId();
+        logger.info("clusterId2=" + clusterId2);
+        String clusterId3 = instance3.getClusterViewService().getLocalClusterView()
+                .getId();
+        logger.info("clusterId3=" + clusterId3);
+        assertEquals(clusterId1, clusterId2);
+        assertEquals(clusterId1, clusterId3);
+
+        assertEquals(3, instance1.getClusterViewService().getLocalClusterView()
+                .getInstances().size());
+        assertEquals(3, instance2.getClusterViewService().getLocalClusterView()
+                .getInstances().size());
+        assertEquals(3, instance3.getClusterViewService().getLocalClusterView()
+                .getInstances().size());
+        logger.info("testClusterView: end");
+    }
+
+    @Test
+    public void testAdditionalInstance() throws Throwable {
+        logger.info("testAdditionalInstance: start");
+        assertNotNull(instance1);
+        assertNotNull(instance2);
+
+        assertEquals(instance1.getSlingId(), instance1.getClusterViewService()
+                .getSlingId());
+        assertEquals(instance2.getSlingId(), instance2.getClusterViewService()
+                .getSlingId());
+
+        try{
+            instance1.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+        try{
+            instance2.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+
+        instance1.dumpRepo();
+        logger.info("testAdditionalInstance: 1st 2s sleep");
+        Thread.sleep(2000);
+
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        logger.info("testAdditionalInstance: 2nd 2s sleep");
+        Thread.sleep(2000);
+
+        instance1.dumpRepo();
+        String clusterId1 = instance1.getClusterViewService().getLocalClusterView()
+                .getId();
+        logger.info("clusterId1=" + clusterId1);
+        String clusterId2 = instance2.getClusterViewService().getLocalClusterView()
+                .getId();
+        logger.info("clusterId2=" + clusterId2);
+        assertEquals(clusterId1, clusterId2);
+
+        assertEquals(2, instance1.getClusterViewService().getLocalClusterView()
+                .getInstances().size());
+        assertEquals(2, instance2.getClusterViewService().getLocalClusterView()
+                .getInstances().size());
+
+        AssertingTopologyEventListener assertingTopologyEventListener = new AssertingTopologyEventListener();
+        assertingTopologyEventListener.addExpected(Type.TOPOLOGY_INIT);
+        assertEquals(1, assertingTopologyEventListener.getRemainingExpectedCount());
+        instance1.bindTopologyEventListener(assertingTopologyEventListener);
+        Thread.sleep(500); // SLING-4755: async event sending requires some minimal wait time nowadays
+        assertEquals(0, assertingTopologyEventListener.getRemainingExpectedCount());
+
+        // startup instance 3
+        AcceptsMultiple acceptsMultiple = new AcceptsMultiple(
+                Type.TOPOLOGY_CHANGING, Type.TOPOLOGY_CHANGED);
+        assertingTopologyEventListener.addExpected(acceptsMultiple);
+        assertingTopologyEventListener.addExpected(acceptsMultiple);
+        instance3 = newBuilder().setDebugName("thirdInstance")
+                .useRepositoryOf(instance1)
+                .build();
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        instance3.heartbeatsAndCheckView();
+        logger.info("testAdditionalInstance: 3rd 2s sleep");
+        Thread.sleep(2000);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        instance3.heartbeatsAndCheckView();
+        logger.info("testAdditionalInstance: 4th 2s sleep");
+        Thread.sleep(2000);
+        assertEquals(1, acceptsMultiple.getEventCnt(Type.TOPOLOGY_CHANGING));
+        assertEquals(1, acceptsMultiple.getEventCnt(Type.TOPOLOGY_CHANGED));
+        logger.info("testAdditionalInstance: end");
+    }
+
+    @Test
+    public void testPropertyProviders() throws Throwable {
+        logger.info("testPropertyProviders: start");
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        assertNull(instance3);
+        instance3 = newBuilder().setDebugName("thirdInstance")
+                .useRepositoryOf(instance1)
+                .build();
+        instance3.heartbeatsAndCheckView();
+        logger.info("testPropertyProviders: 1st 2s sleep");
+        Thread.sleep(2000);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        instance3.heartbeatsAndCheckView();
+        logger.info("testPropertyProviders: 2nd 2s sleep");
+        Thread.sleep(2000);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        instance3.heartbeatsAndCheckView();
+        logger.info("testPropertyProviders: 3rd 2s sleep");
+        Thread.sleep(2000);
+
+        property1Value = UUID.randomUUID().toString();
+        property1Name = UUID.randomUUID().toString();
+        PropertyProviderImpl pp1 = new PropertyProviderImpl();
+        pp1.setProperty(property1Name, property1Value);
+        instance1.bindPropertyProvider(pp1, property1Name);
+
+        property2Value = UUID.randomUUID().toString();
+        property2Name = UUID.randomUUID().toString();
+        PropertyProviderImpl pp2 = new PropertyProviderImpl();
+        pp2.setProperty(property2Name, property2Value);
+        instance2.bindPropertyProvider(pp2, property2Name);
+
+        assertPropertyValues();
+
+        property1Value = UUID.randomUUID().toString();
+        pp1.setProperty(property1Name, property1Value);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+
+        assertPropertyValues();
+        assertNull(instance1.getClusterViewService().getLocalClusterView()
+                .getInstances().get(0)
+                .getProperty(UUID.randomUUID().toString()));
+        assertNull(instance2.getClusterViewService().getLocalClusterView()
+                .getInstances().get(0)
+                .getProperty(UUID.randomUUID().toString()));
+        logger.info("testPropertyProviders: end");
+    }
+
+    private void assertPropertyValues() throws UndefinedClusterViewException {
+        assertPropertyValues(instance1.getSlingId(), property1Name,
+                property1Value);
+        assertPropertyValues(instance2.getSlingId(), property2Name,
+                property2Value);
+    }
+
+    private void assertPropertyValues(String slingId, String name, String value) throws UndefinedClusterViewException {
+        assertEquals(value, getInstance(instance1, slingId).getProperty(name));
+        assertEquals(value, getInstance(instance2, slingId).getProperty(name));
+    }
+
+    private InstanceDescription getInstance(VirtualInstance instance, String slingId) throws UndefinedClusterViewException {
+        Iterator<InstanceDescription> it = instance.getClusterViewService()
+                .getLocalClusterView().getInstances().iterator();
+        while (it.hasNext()) {
+            InstanceDescription id = it.next();
+            if (id.getSlingId().equals(slingId)) {
+                return id;
+            }
+        }
+        throw new IllegalStateException("instance not found: instance="
+                + instance + ", slingId=" + slingId);
+    }
+    
+    class LongRunningListener implements TopologyEventListener {
+        
+        String failMsg = null;
+        
+        boolean initReceived = false;
+        int noninitReceived;
+
+        private Semaphore changedSemaphore = new Semaphore(0);
+        
+        public void assertNoFail() {
+            if (failMsg!=null) {
+                fail(failMsg);
+            }
+        }
+        
+        public Semaphore getChangedSemaphore() {
+            return changedSemaphore;
+        }
+        
+        public void handleTopologyEvent(TopologyEvent event) {
+            if (failMsg!=null) {
+                failMsg += "/ Already failed, got another event; "+event;
+                return;
+            }
+            if (!initReceived) {
+                if (event.getType()!=Type.TOPOLOGY_INIT) {
+                    failMsg = "Expected TOPOLOGY_INIT first, got: "+event.getType();
+                    return;
+                }
+                initReceived = true;
+                return;
+            }
+            if (event.getType()==Type.TOPOLOGY_CHANGED) {
+                try {
+                    changedSemaphore.acquire();
+                } catch (InterruptedException e) {
+                    throw new Error("don't interrupt me pls: "+e);
+                }
+            }
+            noninitReceived++;
+        }
+    }
+    
+    /**
+     * Test plan:
+     *  * have a discoveryservice with two listeners registered
+     *  * one of them (the 'first' one) is long running
+     *  * during one of the topology changes, when the first
+     *    one is hit, deactivate the discovery service
+     *  * that deactivation used to block (SLING-4755) due
+     *    to synchronized(lock) which was blocked by the
+     *    long running listener. With having asynchronous
+     *    event sending this should no longer be the case
+     *  * also, once asserted that deactivation finished,
+     *    and that the first listener is still busy, make
+     *    sure that once the first listener finishes, that
+     *    the second listener still gets the event
+     * @throws Throwable 
+     */
+    @Test
+    public void testLongRunningListener() throws Throwable {
+        // let the instance1 become alone, instance2 is idle
+        instance1.getConfig().setViewCheckTimeout(2);
+        instance2.getConfig().setViewCheckTimeout(2);
+        logger.info("testLongRunningListener : letting instance2 remain silent from now on");
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(1500);
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(1500);
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(1500);
+        instance1.heartbeatsAndCheckView();
+        logger.info("testLongRunningListener : instance 2 should now be considered dead");
+//        instance1.dumpRepo();
+        
+        LongRunningListener longRunningListener1 = new LongRunningListener();
+        AssertingTopologyEventListener fastListener2 = new AssertingTopologyEventListener();
+        fastListener2.addExpected(Type.TOPOLOGY_INIT);
+        longRunningListener1.assertNoFail();
+        assertEquals(1, fastListener2.getRemainingExpectedCount());
+        logger.info("testLongRunningListener : binding longRunningListener1 ...");
+        instance1.bindTopologyEventListener(longRunningListener1);
+        logger.info("testLongRunningListener : binding fastListener2 ...");
+        instance1.bindTopologyEventListener(fastListener2);
+        logger.info("testLongRunningListener : waiting a bit for longRunningListener1 to receive the TOPOLOGY_INIT event");
+        Thread.sleep(2500); // SLING-4755: async event sending requires some minimal wait time nowadays
+        assertEquals(0, fastListener2.getRemainingExpectedCount());
+        assertTrue(longRunningListener1.initReceived);
+        
+        // after INIT, now do an actual change where listener1 will do a long-running handling
+        fastListener2.addExpected(Type.TOPOLOGY_CHANGING);
+        fastListener2.addExpected(Type.TOPOLOGY_CHANGED);
+        instance1.getConfig().setViewCheckTimeout(10);
+        instance2.getConfig().setViewCheckTimeout(10);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        
+        instance1.dumpRepo();
+        longRunningListener1.assertNoFail();
+        // nothing unexpected should arrive at listener2:
+        assertEquals(0, fastListener2.getUnexpectedCount());
+        // however, listener2 should only get one (CHANGING) event, cos the CHANGED event is still blocked
+        assertEquals(1, fastListener2.getRemainingExpectedCount());
+        // and also listener2 should only get CHANGING, the CHANGED is blocked via changedSemaphore
+        assertEquals(1, longRunningListener1.noninitReceived);
+        assertTrue(longRunningListener1.getChangedSemaphore().hasQueuedThreads());
+        Thread.sleep(2000);
+        // even after a 2sec sleep things should be unchanged:
+        assertEquals(0, fastListener2.getUnexpectedCount());
+        assertEquals(1, fastListener2.getRemainingExpectedCount());
+        assertEquals(1, longRunningListener1.noninitReceived);
+        assertTrue(longRunningListener1.getChangedSemaphore().hasQueuedThreads());
+        
+        // now let's simulate SLING-4755: deactivation while longRunningListener1 does long processing
+        // - which is simulated by waiting on changedSemaphore.
+        final List<Exception> asyncException = new LinkedList<Exception>();
+        Thread th = new Thread(new Runnable() {
+
+            public void run() {
+                try {
+                    instance1.stop();
+                } catch (Exception e) {
+                    synchronized(asyncException) {
+                        asyncException.add(e);
+                    }
+                }
+            }
+            
+        });
+        th.start();
+        logger.info("Waiting max 4 sec...");
+        th.join(4000);
+        logger.info("Done waiting max 4 sec...");
+        if (th.isAlive()) {
+            logger.warn("Thread still alive: "+th.isAlive());
+            // release before issuing fail as otherwise test will block forever
+            longRunningListener1.getChangedSemaphore().release();
+            fail("Thread was still alive");
+        }
+        logger.info("Thread was no longer alive: "+th.isAlive());
+        synchronized(asyncException) {
+            logger.info("Async exceptions: "+asyncException.size());
+            if (asyncException.size()!=0) {
+                // release before issuing fail as otherwise test will block forever
+                longRunningListener1.getChangedSemaphore().release();
+                fail("async exceptions: "+asyncException.size()+", first: "+asyncException.get(0));
+            }
+        }
+        
+        // now the test consists of
+        // a) the fact that we reached this place without unlocking the changedSemaphore
+        // b) when we now unlock the changedSemaphore the remaining events should flush through
+        longRunningListener1.getChangedSemaphore().release();
+        Thread.sleep(500);// shouldn't take long and then things should have flushed:
+        assertEquals(0, fastListener2.getUnexpectedCount());
+        assertEquals(0, fastListener2.getRemainingExpectedCount());
+        assertEquals(2, longRunningListener1.noninitReceived);
+        assertFalse(longRunningListener1.getChangedSemaphore().hasQueuedThreads());
+    }
+
+    
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/AbstractSingleInstanceTest.java b/src/test/java/org/apache/sling/discovery/base/its/AbstractSingleInstanceTest.java
new file mode 100644
index 0000000..52a60b8
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/AbstractSingleInstanceTest.java
@@ -0,0 +1,293 @@
+/*
+ * 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.sling.discovery.base.its;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.mock.AssertingTopologyEventListener;
+import org.apache.sling.discovery.base.its.setup.mock.PropertyProviderImpl;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractSingleInstanceTest {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    VirtualInstance instance;
+
+    String propertyValue;
+
+    private Level logLevel;
+    
+    protected abstract VirtualInstanceBuilder newBuilder();
+
+    @Before
+    public void setup() throws Exception {
+        final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+        logLevel = discoveryLogger.getLevel();
+        discoveryLogger.setLevel(Level.DEBUG);
+        logger.info("setup: creating new standalone instance");
+        instance = newBuilder().setDebugName("standaloneInstance")
+                .newRepository("/var/discovery/impl/", true)
+                .setConnectorPingTimeout(20)
+                .setConnectorPingInterval(999)/*long enough heartbeat interval to prevent them to disturb the explicit heartbeats during the test*/
+                .setMinEventDelay(3).build();
+        logger.info("setup: creating new standalone instance done.");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+        discoveryLogger.setLevel(logLevel);
+        logger.info("tearDown: stopping standalone instance");
+        if (instance!=null) {
+            instance.stop();
+            instance = null;
+        }
+        logger.info("tearDown: stopping standalone instance done");
+    }
+
+    @Test
+    public void testGetters() throws UndefinedClusterViewException, InterruptedException {
+        logger.info("testGetters: start");
+        assertNotNull(instance);
+        logger.info("sling id=" + instance.getSlingId());
+        try{
+            instance.getClusterViewService().getLocalClusterView();
+            fail("should complain"); // SLING-5030
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+
+        instance.heartbeatsAndCheckView();
+        // wait 100ms for the vote to happen
+        Thread.sleep(100);
+        
+        assertNotNull(instance.getClusterViewService().getLocalClusterView());
+        ClusterView cv = instance.getClusterViewService().getLocalClusterView();
+        logger.info("cluster view: id=" + cv.getId());
+        assertNotNull(cv.getId());
+        assertNotSame(cv.getId(), "");
+
+        List<InstanceDescription> instances = cv.getInstances();
+        assertNotNull(instances);
+        assertTrue(instances.size() == 1);
+
+        InstanceDescription myInstance = instances.get(0);
+        assertNotNull(myInstance);
+        assertTrue(myInstance.getClusterView() == cv);
+        logger.info("instance id: " + myInstance.getSlingId());
+        assertEquals(instance.getSlingId(), myInstance.getSlingId());
+
+        Map<String, String> properties = myInstance.getProperties();
+        assertNotNull(properties);
+
+        assertNull(myInstance.getProperty("foo"));
+
+        assertTrue(myInstance.isLeader());
+
+        assertTrue(myInstance.isLocal());
+        logger.info("testGetters: end");
+    }
+
+    @Test
+    public void testPropertyProviders() throws Throwable {
+        logger.info("testPropertyProviders: start");
+        final String propertyName = UUID.randomUUID().toString();
+        propertyValue = UUID.randomUUID().toString();
+        PropertyProviderImpl pp = new PropertyProviderImpl();
+        pp.setProperty(propertyName, propertyValue);
+        instance.bindPropertyProvider(pp, propertyName);
+
+        instance.heartbeatsAndCheckView();
+        // wait 100ms for the vote to happen
+        Thread.sleep(100);
+        assertEquals(propertyValue,
+                instance.getClusterViewService().getLocalClusterView()
+                        .getInstances().get(0).getProperty(propertyName));
+
+        propertyValue = UUID.randomUUID().toString();
+        pp.setProperty(propertyName, propertyValue);
+        instance.heartbeatsAndCheckView();
+
+        assertEquals(propertyValue,
+                instance.getClusterViewService().getLocalClusterView()
+                        .getInstances().get(0).getProperty(propertyName));
+        assertNull(instance.getClusterViewService().getLocalClusterView()
+                .getInstances().get(0)
+                .getProperty(UUID.randomUUID().toString()));
+        logger.info("testPropertyProviders: end");
+    }
+    
+    @Test
+    public void testInvalidProperties() throws Throwable {
+        logger.info("testInvalidProperties: start");
+        
+        instance.heartbeatsAndCheckView();
+        instance.heartbeatsAndCheckView();
+        
+        final String propertyValue = UUID.randomUUID().toString();
+        doTestProperty(UUID.randomUUID().toString(), propertyValue, propertyValue);
+
+        doTestProperty("", propertyValue, null);
+        doTestProperty("-", propertyValue, propertyValue);
+        doTestProperty("_", propertyValue, propertyValue);
+        doTestProperty("jcr:" + UUID.randomUUID().toString(), propertyValue, null);
+        doTestProperty("var/" + UUID.randomUUID().toString(), propertyValue, null);
+        doTestProperty(UUID.randomUUID().toString() + "@test", propertyValue, null);
+        doTestProperty(UUID.randomUUID().toString() + "!test", propertyValue, null);
+        logger.info("testInvalidProperties: end");
+    }
+
+	private void doTestProperty(final String propertyName,
+			final String propertyValue,
+			final String expectedPropertyValue) throws Throwable {
+		PropertyProviderImpl pp = new PropertyProviderImpl();
+        pp.setProperty(propertyName, propertyValue);
+        instance.bindPropertyProvider(pp, propertyName);
+        assertEquals(expectedPropertyValue,
+                instance.getClusterViewService().getLocalClusterView()
+                        .getInstances().get(0).getProperty(propertyName));
+	}
+    
+    @Test
+    public void testTopologyEventListeners() throws Throwable {
+        logger.info("testTopologyEventListeners: start");
+        instance.heartbeatsAndCheckView();
+        logger.info("testTopologyEventListeners: 1st sleep 2s");
+        Thread.sleep(2000);
+        instance.heartbeatsAndCheckView();
+        logger.info("testTopologyEventListeners: 2nd sleep 2s");
+        Thread.sleep(2000);
+
+        AssertingTopologyEventListener assertingTopologyEventListener = new AssertingTopologyEventListener();
+        assertingTopologyEventListener.addExpected(Type.TOPOLOGY_INIT);
+        logger.info("testTopologyEventListeners: binding the event listener");
+        instance.bindTopologyEventListener(assertingTopologyEventListener);
+        Thread.sleep(500); // SLING-4755: async event sending requires some minimal wait time nowadays
+        assertEquals(0, assertingTopologyEventListener.getRemainingExpectedCount());
+
+        final String propertyName = UUID.randomUUID().toString();
+        propertyValue = UUID.randomUUID().toString();
+        PropertyProviderImpl pp = new PropertyProviderImpl();
+        pp.setProperty(propertyName, propertyValue);
+
+        assertingTopologyEventListener.addExpected(Type.PROPERTIES_CHANGED);
+
+        assertEquals(1, assertingTopologyEventListener.getRemainingExpectedCount());
+        assertEquals(0, pp.getGetCnt());
+        instance.bindPropertyProvider(pp, propertyName);
+        logger.info("testTopologyEventListeners: 3rd sleep 1.5s");
+        Thread.sleep(1500);
+        logger.info("testTopologyEventListeners: dumping due to failure: ");
+        assertingTopologyEventListener.dump();
+        assertEquals(0, assertingTopologyEventListener.getRemainingExpectedCount());
+        // we can only assume that the getProperty was called at least once - it
+        // could be called multiple times though..
+        assertTrue(pp.getGetCnt() > 0);
+
+        assertingTopologyEventListener.addExpected(Type.PROPERTIES_CHANGED);
+
+        assertEquals(1, assertingTopologyEventListener.getRemainingExpectedCount());
+        pp.setGetCnt(0);
+        propertyValue = UUID.randomUUID().toString();
+        pp.setProperty(propertyName, propertyValue);
+        assertEquals(0, pp.getGetCnt());
+        instance.heartbeatsAndCheckView();
+        logger.info("testTopologyEventListeners: 4th sleep 2s");
+        Thread.sleep(2000);
+        assertEquals(0, assertingTopologyEventListener.getRemainingExpectedCount());
+        assertEquals(1, pp.getGetCnt());
+
+        // a heartbeat repeat should not result in another call though
+        instance.heartbeatsAndCheckView();
+        logger.info("testTopologyEventListeners: 5th sleep 2s");
+        Thread.sleep(2000);
+        assertEquals(0, assertingTopologyEventListener.getRemainingExpectedCount());
+        assertEquals(2, pp.getGetCnt());
+        logger.info("testTopologyEventListeners: done");
+    }
+
+    @Test
+    public void testBootstrap() throws Throwable {
+        logger.info("testBootstrap: start");
+        try{
+            instance.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // SLING-5030 : isolated mode is gone, replaced with exception
+            // ok
+        }
+
+        // SLING-3750 : with delaying the init event, we now should NOT get any events
+        // before we let the view establish (which happens via heartbeats below)
+        AssertingTopologyEventListener ada = new AssertingTopologyEventListener();
+        instance.bindTopologyEventListener(ada);
+        assertEquals(0, ada.getEvents().size());
+        assertEquals(0, ada.getUnexpectedCount());
+
+        try{
+            instance.getClusterViewService().getLocalClusterView();
+            fail("should complain");
+        } catch(UndefinedClusterViewException e) {
+            // ok
+        }
+        
+        ada.addExpected(Type.TOPOLOGY_INIT);
+        instance.heartbeatsAndCheckView();
+        Thread.sleep(1000);
+        instance.heartbeatsAndCheckView();
+        Thread.sleep(1000);
+        instance.dumpRepo();
+        ada.dump();
+        assertEquals(0, ada.getUnexpectedCount());
+        assertEquals(1, ada.getEvents().size());
+        TopologyEvent initEvent = ada.getEvents().remove(0);
+        assertNotNull(initEvent);
+        assertNotNull(initEvent.getNewView());
+        assertNotNull(initEvent.getNewView().getClusterViews());
+
+        // after the view was established though, we expect it to be a normal
+        // EstablishedInstanceDescription
+        instance.assertEstablishedView();
+        logger.info("testBootstrap: end");
+    }
+    
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/AbstractTopologyEventTest.java b/src/test/java/org/apache/sling/discovery/base/its/AbstractTopologyEventTest.java
new file mode 100644
index 0000000..d8c7792
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/AbstractTopologyEventTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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.sling.discovery.base.its;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.apache.sling.discovery.base.its.setup.mock.AssertingTopologyEventListener;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Test class covering correct sending of TopologyEvents
+ * in various scenarios (which are not covered in other tests already).
+ */
+public abstract class AbstractTopologyEventTest {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private VirtualInstance instance1;
+    private VirtualInstance instance2;
+
+    private Level logLevel;
+    
+    @Before
+    public void setup() throws Exception {
+        final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+        logLevel = discoveryLogger.getLevel();
+        discoveryLogger.setLevel(Level.DEBUG);
+    }
+    
+    @After
+    public void tearDown() throws Throwable {
+        if (instance1!=null) {
+            instance1.stopViewChecker();
+            instance1.stop();
+            instance1 = null;
+        }
+        if (instance2!=null) {
+            instance2.stopViewChecker();
+            instance2.stop();
+            instance2 = null;
+        }
+        final org.apache.log4j.Logger discoveryLogger = LogManager.getRootLogger().getLogger("org.apache.sling.discovery");
+        discoveryLogger.setLevel(logLevel);
+    }
+    
+    public abstract VirtualInstanceBuilder newBuilder();
+    
+    /**
+     * Tests the fact that the INIT event is delayed until voting has succeeded
+     * (which is the default with SLIGN-5030 and SLING-4959
+     * @throws Throwable 
+     */
+    @Test
+    public void testDelayedInitEvent() throws Throwable {
+        logger.info("testDelayedInitEvent: start");
+        instance1 = newBuilder().setDebugName("firstInstanceA")
+                .newRepository("/var/discovery/impl/", true)
+                .setConnectorPingTimeout(3 /* heartbeat-timeout */)
+                .setMinEventDelay(3 /*min event delay*/).build();
+        AssertingTopologyEventListener l1 = new AssertingTopologyEventListener("instance1.l1");
+        instance1.bindTopologyEventListener(l1);
+        logger.info("testDelayedInitEvent: instance1 created, no events expected yet. slingId="+instance1.slingId);
+        
+        // should not have received any events yet
+        assertEquals(0, l1.getEvents().size());
+        assertEquals(0, l1.getUnexpectedCount());
+
+        // one heartbeat doesn't make a day yet - and is 2sec too early for the init
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(1200);
+        logger.info("testDelayedInitEvent: even after 500ms no events expected, as it needs more than 1 heartbeat");
+        // should not have received any events yet
+        assertEquals(0, l1.getEvents().size());
+        assertEquals(0, l1.getUnexpectedCount());
+        
+        // but two are a good start
+        l1.addExpected(Type.TOPOLOGY_INIT);
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(1200);
+        instance1.heartbeatsAndCheckView();
+        Thread.sleep(1200);
+        logger.info("testDelayedInitEvent: 2nd/3rd heartbeat sent - now expecting a TOPOLOGY_INIT");
+        instance1.dumpRepo();
+        assertEquals(1, l1.getEvents().size()); // one event
+        assertEquals(0, l1.getRemainingExpectedCount()); // the expected one
+        assertEquals(0, l1.getUnexpectedCount());
+        
+        logger.info("testDelayedInitEvent: creating instance2");
+        instance2 = newBuilder().setDebugName("secondInstanceB")
+                .useRepositoryOf(instance1)
+                .setConnectorPingTimeout(20)
+                .setMinEventDelay(3).build();
+        logger.info("testDelayedInitEvent: instance2 created with slingId="+instance2.slingId);
+        AssertingTopologyEventListener l2 = new AssertingTopologyEventListener("instance2.l2");
+        instance2.bindTopologyEventListener(l2);
+        logger.info("testDelayedInitEvent: listener instance2.l2 added - it should not get any events though");
+        AssertingTopologyEventListener l1Two = new AssertingTopologyEventListener("instance1.l1Two");
+        l1Two.addExpected(Type.TOPOLOGY_INIT);
+        logger.info("testDelayedInitEvent: listener instance1.l1Two added - it expects an INIT now");
+        instance1.bindTopologyEventListener(l1Two);
+        
+        Thread.sleep(500); // SLING-4755: async event sending requires some minimal wait time nowadays
+
+        // just because instance2 is started doesn't kick off any events yet 
+        // since instance2 didn't send heartbeats yet
+        assertEquals(1, l1.getEvents().size()); // one event
+        assertEquals(0, l1.getRemainingExpectedCount()); // the expected one
+        assertEquals(0, l1.getUnexpectedCount());
+        assertEquals(0, l2.getEvents().size());
+        assertEquals(0, l2.getUnexpectedCount());
+        assertEquals(1, l1Two.getEvents().size());
+        assertEquals(0, l1Two.getRemainingExpectedCount()); // the expected one
+        assertEquals(0, l1Two.getUnexpectedCount());
+        
+        
+        // the second & third heartbeat though triggers the voting etc
+        logger.info("testDelayedInitEvent: two more heartbeats should trigger events");
+        l1.addExpected(Type.TOPOLOGY_CHANGING);
+        l1Two.addExpected(Type.TOPOLOGY_CHANGING);
+        Thread.sleep(500);
+        l2.addExpected(Type.TOPOLOGY_INIT);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        Thread.sleep(500);
+        instance1.heartbeatsAndCheckView();
+        instance2.heartbeatsAndCheckView();
+        logger.info("testDelayedInitEvent: instance1: "+instance1.slingId);
+        logger.info("testDelayedInitEvent: instance2: "+instance2.slingId);
+        instance1.dumpRepo();
+        assertEquals(0, l1.getUnexpectedCount());
+        assertEquals(2, l1.getEvents().size());
+        assertEquals(0, l2.getUnexpectedCount());
+        assertEquals(1, l2.getEvents().size());
+        assertEquals(0, l1Two.getUnexpectedCount());
+        assertEquals(2, l1Two.getEvents().size());
+
+        // wait until CHANGED is sent - which is 3 sec after CHANGING
+        l1.addExpected(Type.TOPOLOGY_CHANGED);
+        l1Two.addExpected(Type.TOPOLOGY_CHANGED);
+        Thread.sleep(4000);
+        assertEquals(0, l1.getUnexpectedCount());
+        assertEquals(3, l1.getEvents().size()); // one event
+        assertEquals(0, l2.getUnexpectedCount());
+        assertEquals(1, l2.getEvents().size());
+        assertEquals(0, l1Two.getUnexpectedCount());
+        assertEquals(3, l1Two.getEvents().size());
+        logger.info("testDelayedInitEvent: end");
+    }
+    
+    @Test
+    public void testGetDuringDelay() throws Throwable {
+        instance1 = newBuilder().setDebugName("firstInstanceA")
+                .newRepository("/var/discovery/impl/", true)
+                .setConnectorPingTimeout(20 /* heartbeat-timeout */)
+                .setMinEventDelay(6 /* min event delay */).build();
+        AssertingTopologyEventListener l1 = new AssertingTopologyEventListener("instance1.l1");
+        l1.addExpected(TopologyEvent.Type.TOPOLOGY_INIT);
+        instance1.bindTopologyEventListener(l1);
+        
+        TopologyView earlyTopo = instance1.getDiscoveryService().getTopology();
+        assertNotNull(earlyTopo);
+        assertFalse(earlyTopo.isCurrent());
+        assertEquals(1, earlyTopo.getInstances().size());
+        
+        for(int i=0; i<4; i++) {
+            instance1.heartbeatsAndCheckView();
+            Thread.sleep(125);
+        }
+        TopologyView secondTopo = instance1.getDiscoveryService().getTopology();
+        assertEquals(1, secondTopo.getInstances().size());
+        assertEquals(instance1.getSlingId(), secondTopo.getInstances().iterator().next().getSlingId());
+        assertTrue(secondTopo.isCurrent());
+        instance1.dumpRepo();
+        
+        assertEarlyAndFirstClusterViewIdMatches(earlyTopo, secondTopo);
+
+        Thread.sleep(500);
+        // should have gotten the INIT, hence 0 remaining expected events
+        assertEquals(0, l1.getRemainingExpectedCount());
+        assertEquals(0, l1.getUnexpectedCount());
+        
+        l1.addExpected(TopologyEvent.Type.TOPOLOGY_CHANGING);
+        instance2 = newBuilder().setDebugName("secondInstanceB")
+                .useRepositoryOf(instance1)
+                .setConnectorPingTimeout(20)
+                .setMinEventDelay(1).build();
+        AssertingTopologyEventListener l2 = new AssertingTopologyEventListener("instance2.l1");
+        l2.addExpected(TopologyEvent.Type.TOPOLOGY_INIT);
+        instance2.bindTopologyEventListener(l2);
+
+        for(int i=0; i<4; i++) {
+            instance2.heartbeatsAndCheckView();
+            instance1.heartbeatsAndCheckView();
+            Thread.sleep(750);
+        }
+        
+        assertEquals(0, l1.getUnexpectedCount());
+        TopologyView topo2 = instance2.getDiscoveryService().getTopology();
+        assertTrue(topo2.isCurrent());
+        assertEquals(2, topo2.getInstances().size());
+        TopologyView topo1 = instance1.getDiscoveryService().getTopology();
+        assertTrue(topo1.isCurrent());
+        assertEquals(2, topo1.getInstances().size());
+        
+        l1.addExpected(TopologyEvent.Type.TOPOLOGY_CHANGED);
+        Thread.sleep(5000);
+        assertEquals(0, l1.getRemainingExpectedCount());
+        assertEquals(0, l1.getUnexpectedCount());
+        assertEquals(0, l2.getRemainingExpectedCount());
+        assertEquals(0, l2.getUnexpectedCount());
+        assertTrue(instance2.getDiscoveryService().getTopology().isCurrent());
+        assertEquals(2, instance2.getDiscoveryService().getTopology().getInstances().size());
+        assertTrue(instance1.getDiscoveryService().getTopology().isCurrent());
+        assertEquals(2, instance1.getDiscoveryService().getTopology().getInstances().size());
+    }
+
+    public abstract void assertEarlyAndFirstClusterViewIdMatches(TopologyView earlyTopo, TopologyView secondTopo);
+    
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/its/TopologyTest.java b/src/test/java/org/apache/sling/discovery/base/its/TopologyTest.java
new file mode 100644
index 0000000..2af8402
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/TopologyTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.sling.discovery.base.its;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.base.connectors.DummyVirtualInstanceBuilder;
+import org.apache.sling.discovery.base.connectors.announcement.Announcement;
+import org.apache.sling.discovery.base.its.setup.TopologyHelper;
+import org.apache.sling.discovery.base.its.setup.VirtualConnector;
+import org.apache.sling.discovery.base.its.setup.VirtualInstance;
+import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
+import org.junit.After;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TopologyTest {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final List<VirtualInstance> instances = new LinkedList<VirtualInstance>();
+    
+    private VirtualInstanceBuilder newBuilder() {
+        return new DummyVirtualInstanceBuilder();
+    }
+    
+    @After
+    public void tearDown() throws Exception {
+        for (Iterator<VirtualInstance> it = instances.iterator(); it.hasNext();) {
+            final VirtualInstance instance = it.next();
+            instance.stop();
+        }
+    }
+    
+    @Test
+    public void testTwoNodes() throws Throwable {
+        VirtualInstanceBuilder builder1 = newBuilder()
+                .newRepository("/var/discovery/impl/", true)
+                .setDebugName("instance1")
+                .setConnectorPingInterval(20)
+                .setConnectorPingTimeout(200);
+        VirtualInstance instance1 = builder1.build();
+        instances.add(instance1);
+        VirtualInstanceBuilder builder2 = newBuilder()
+                .useRepositoryOf(builder1)
+                .setDebugName("instance2")
+                .setConnectorPingInterval(20)
+                .setConnectorPingTimeout(200);
+        VirtualInstance instance2 = builder2.build();
+        instances.add(instance2);
+        instance1.getConfig().setViewCheckTimeout(8);
+        instance1.getConfig().setViewCheckInterval(1);
+        instance2.getConfig().setViewCheckTimeout(2);
+        instance2.getConfig().setViewCheckInterval(1);
+        
+        for(int i=0; i<5; i++) {
+            instance1.heartbeatsAndCheckView();
+            instance2.heartbeatsAndCheckView();
+            Thread.sleep(500);
+        }
+        
+        Set<InstanceDescription> instances1 = instance1.getDiscoveryService().getTopology().getInstances();
+        Set<InstanceDescription> instances2 = instance2.getDiscoveryService().getTopology().getInstances();
+        
+        assertEquals(1, instances1.size());
+        assertEquals(1, instances2.size());
+        assertEquals(instance1.getSlingId(), instances1.iterator().next().getSlingId());
+        assertEquals(instance2.getSlingId(), instances2.iterator().next().getSlingId());
+        
+        new VirtualConnector(instance1, instance2);
+        
+        // check instance 1's announcements
+        Collection<Announcement> instance1LocalAnnouncements = 
+                instance1.getAnnouncementRegistry().listLocalAnnouncements();
+        assertEquals(1, instance1LocalAnnouncements.size());
+        Announcement instance1LocalAnnouncement = instance1LocalAnnouncements.iterator().next();
+        assertEquals(instance2.getSlingId(), instance1LocalAnnouncement.getOwnerId());
+        assertEquals(true, instance1LocalAnnouncement.isInherited());
+
+        // check instance 2's announcements
+        Collection<Announcement> instance2LocalAnnouncements = 
+                instance2.getAnnouncementRegistry().listLocalAnnouncements();
+        assertEquals(1, instance2LocalAnnouncements.size());
+        Announcement instance2LocalAnnouncement = instance2LocalAnnouncements.iterator().next();
+        assertEquals(instance1.getSlingId(), instance2LocalAnnouncement.getOwnerId());
+        assertEquals(false, instance2LocalAnnouncement.isInherited());
+        
+        // check topology
+        TopologyHelper.assertTopologyConsistsOf(instance1.getDiscoveryService().getTopology(), instance1.getSlingId(), instance2.getSlingId());
+        TopologyHelper.assertTopologyConsistsOf(instance2.getDiscoveryService().getTopology(), instance1.getSlingId(), instance2.getSlingId());
+
+        instance1LocalAnnouncements = 
+                instance1.getAnnouncementRegistry().listLocalAnnouncements();
+        assertEquals(1, instance1LocalAnnouncements.size());
+        instance2LocalAnnouncements = 
+                instance2.getAnnouncementRegistry().listLocalAnnouncements();
+        assertEquals(1, instance2LocalAnnouncements.size());
+
+        Thread.sleep(2200); // sleep of 2.2sec ensures instance2's heartbeat timeout (which is 2sec) hits
+        
+        instance1LocalAnnouncements = 
+                instance1.getAnnouncementRegistry().listLocalAnnouncements();
+        assertEquals(1, instance1LocalAnnouncements.size());
+        instance2LocalAnnouncements = 
+                instance2.getAnnouncementRegistry().listLocalAnnouncements();
+        assertEquals(0, instance2LocalAnnouncements.size());
+
+        logger.info("testTwoNodes: instance1: "+instance1.getSlingId());
+        instance1.dumpRepo();
+        logger.info("testTwoNodes: instance2: "+instance2.getSlingId());
+        instance2.dumpRepo();
+        TopologyHelper.assertTopologyConsistsOf(instance1.getDiscoveryService().getTopology(), instance1.getSlingId(), instance2.getSlingId());
+        TopologyHelper.assertTopologyConsistsOf(instance2.getDiscoveryService().getTopology(), instance2.getSlingId());
+        
+        Thread.sleep(6000); // another sleep 6s (2.2+6 = 8.2sec) ensures instance1's heartbeat timeout (which is 8sec) hits as well
+        instance1LocalAnnouncements = 
+                instance1.getAnnouncementRegistry().listLocalAnnouncements();
+        assertEquals(0, instance1LocalAnnouncements.size());
+        instance2LocalAnnouncements = 
+                instance2.getAnnouncementRegistry().listLocalAnnouncements();
+        assertEquals(0, instance2LocalAnnouncements.size());
+
+        TopologyHelper.assertTopologyConsistsOf(instance1.getDiscoveryService().getTopology(), instance1.getSlingId());
+        TopologyHelper.assertTopologyConsistsOf(instance2.getDiscoveryService().getTopology(), instance2.getSlingId());
+    }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/ModifiableTestBaseConfig.java b/src/test/java/org/apache/sling/discovery/base/its/setup/ModifiableTestBaseConfig.java
new file mode 100644
index 0000000..56766dc
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/ModifiableTestBaseConfig.java
@@ -0,0 +1,37 @@
+/*
+ * 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.sling.discovery.base.its.setup;
+
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+
+/**
+ * test extension of the BaseConfig that allows setting some
+ * parameters in test classes
+ */
+public interface ModifiableTestBaseConfig extends BaseConfig {
+
+    void addTopologyConnectorWhitelistEntry(String string);
+
+    void setMinEventDelay(int minEventDelay);
+
+    void setViewCheckTimeout(int viewCheckTimeout);
+
+    void setViewCheckInterval(int viewCheckInterval);
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/OSGiMock.java b/src/test/java/org/apache/sling/discovery/base/its/setup/OSGiMock.java
new file mode 100644
index 0000000..537600a
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/OSGiMock.java
@@ -0,0 +1,124 @@
+/*
+ * 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.sling.discovery.base.its.setup;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sling.discovery.base.its.setup.mock.MockFactory;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class OSGiMock {
+
+    private static final Logger logger = LoggerFactory.getLogger(OSGiMock.class);
+
+    private final List<Object> services = new LinkedList<Object>();
+
+    public void addService(Object service) {
+        if (service==null) {
+            throw new IllegalArgumentException("service must not be null");
+        }
+        services.add(service);
+    }
+    
+    public void activateAll() throws Exception {
+        for (@SuppressWarnings("rawtypes")
+        Iterator it = services.iterator(); it.hasNext();) {
+            Object aService = it.next();
+
+            activate(aService);
+        }
+    }
+
+	public static void activate(Object aService) throws IllegalAccessException,
+			InvocationTargetException {
+	    Class<?> clazz = aService.getClass();
+	    while (clazz != null) {
+	        Method[] methods = clazz.getDeclaredMethods();
+	        for (int i = 0; i < methods.length; i++) {
+	            Method method = methods[i];
+	            if (method.getName().equals("activate")) {
+	                method.setAccessible(true);
+	                if ( method.getParameterTypes().length == 0 ) {
+	                    logger.info("activate: activating "+aService+"...");
+	                    method.invoke(aService, null);
+	                    logger.info("activate: activating "+aService+" done.");
+	                } else if (method.getParameterTypes().length==1 && (method.getParameterTypes()[0]==ComponentContext.class)){
+	                    logger.info("activate: activating "+aService+"...");
+	                    method.invoke(aService, MockFactory.mockComponentContext());
+	                    logger.info("activate: activating "+aService+" done.");
+	                } else if (method.getParameterTypes().length==1 && (method.getParameterTypes()[0]==BundleContext.class)){
+	                    logger.info("activate: activating "+aService+"...");
+	                    method.invoke(aService, MockFactory.mockBundleContext());
+	                    logger.info("activate: activating "+aService+" done.");
+	                } else {
+	                    throw new IllegalStateException("unsupported activate variant: "+method);
+	                }
+	                return;
+	            }
+	        }
+	        clazz = clazz.getSuperclass();
+	    }
+	}
+
+	public void deactivateAll() throws Exception {
+        for (@SuppressWarnings("rawtypes")
+        Iterator it = services.iterator(); it.hasNext();) {
+            Object aService = it.next();
+
+            deactivate(aService);
+        }
+	}
+
+	public static void deactivate(Object aService) throws IllegalAccessException,
+			InvocationTargetException {
+        Class<?> clazz = aService.getClass();
+        while (clazz != null) {
+    		Method[] methods = clazz.getDeclaredMethods();
+    		for (int i = 0; i < methods.length; i++) {
+    		    Method method = methods[i];
+    		    if (method.getName().equals("deactivate")) {
+    		        method.setAccessible(true);
+    		        if ( method.getParameterTypes().length == 0 ) {
+    		            method.invoke(aService, null);
+    		        } else {
+    		            method.invoke(aService, MockFactory.mockComponentContext());
+    		        }
+    		        return;
+    		    }
+    		}
+            clazz = clazz.getSuperclass();
+        }
+	}
+
+    public void addServices(Object[] additionalServices) {
+        if (additionalServices==null) {
+            return;
+        }
+        for (Object additionalService : additionalServices) {
+            addService(additionalService);
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/TopologyHelper.java b/src/test/java/org/apache/sling/discovery/base/its/setup/TopologyHelper.java
new file mode 100644
index 0000000..c7d9995
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/TopologyHelper.java
@@ -0,0 +1,145 @@
+/*
+ * 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.sling.discovery.base.its.setup;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Set;
+import java.util.UUID;
+
+import org.apache.sling.discovery.ClusterView;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.TopologyView;
+import org.apache.sling.discovery.base.commons.DefaultTopologyView;
+import org.apache.sling.discovery.commons.providers.DefaultClusterView;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+
+import junitx.util.PrivateAccessor;
+
+public class TopologyHelper {
+
+    public static DefaultInstanceDescription createInstanceDescription(
+            ClusterView clusterView) {
+        return createInstanceDescription(UUID.randomUUID().toString(), false, clusterView);
+    }
+    
+    public static DefaultInstanceDescription createInstanceDescription(
+            String instanceId, boolean isLocal, ClusterView clusterView) {
+        if (!(clusterView instanceof DefaultClusterView)) {
+            throw new IllegalArgumentException(
+                    "Must pass a clusterView of type "
+                            + DefaultClusterView.class);
+        }
+        DefaultInstanceDescription i = new DefaultInstanceDescription(
+                (DefaultClusterView) clusterView, false, isLocal, instanceId, new HashMap<String, String>());
+        return i;
+    }
+
+    public static DefaultTopologyView createTopologyView(String clusterViewId,
+            String slingId) {
+        DefaultTopologyView t = new DefaultTopologyView();
+        DefaultClusterView c = new DefaultClusterView(clusterViewId);
+        DefaultInstanceDescription i = new DefaultInstanceDescription(
+                c, true, false, slingId, new HashMap<String, String>());
+        Collection<InstanceDescription> instances = new LinkedList<InstanceDescription>();
+        instances.add(i);
+        t.addInstances(instances);
+        return t;
+    }
+
+    public static DefaultTopologyView cloneTopologyView(DefaultTopologyView original) {
+        DefaultTopologyView t = new DefaultTopologyView();
+        Iterator<ClusterView> it = original.getClusterViews().iterator();
+        while (it.hasNext()) {
+            DefaultClusterView c = (DefaultClusterView) it.next();
+            t.addInstances(clone(c).getInstances());
+        }
+        return t;
+    }
+
+    public static DefaultClusterView clone(DefaultClusterView original) {
+        DefaultClusterView c = new DefaultClusterView(original.getId());
+        Iterator<InstanceDescription> it = original.getInstances().iterator();
+        while (it.hasNext()) {
+            DefaultInstanceDescription id = (DefaultInstanceDescription) it
+                    .next();
+            c.addInstanceDescription(cloneWOClusterView(id));
+        }
+        return c;
+    }
+
+    public static DefaultInstanceDescription cloneWOClusterView(
+            DefaultInstanceDescription original) {
+        DefaultInstanceDescription id = new DefaultInstanceDescription(
+                null, original.isLeader(), original.isLocal(),
+                original.getSlingId(), new HashMap<String, String>(
+                        original.getProperties()));
+        return id;
+    }
+
+    public static DefaultInstanceDescription createAndAddInstanceDescription(
+            DefaultTopologyView newView, ClusterView clusterView) {
+        DefaultInstanceDescription i = createInstanceDescription(clusterView);
+        return addInstanceDescription(newView, i);
+    }
+
+    public static DefaultInstanceDescription addInstanceDescription(
+            DefaultTopologyView newView, DefaultInstanceDescription i) {
+        Collection<InstanceDescription> instances = new LinkedList<InstanceDescription>();
+        instances.add(i);
+        newView.addInstances(instances);
+        return i;
+    }
+
+    public static DefaultTopologyView cloneTopologyView(DefaultTopologyView view,
+            String newLeader) throws NoSuchFieldException {
+        final DefaultTopologyView clone = cloneTopologyView(view);
+        final DefaultClusterView cluster = (DefaultClusterView) clone.getClusterViews().iterator().next();
+        for (Iterator it = cluster.getInstances().iterator(); it.hasNext();) {
+            DefaultInstanceDescription id = (DefaultInstanceDescription) it.next();
+            PrivateAccessor.setField(id, "isLeader", id.getSlingId().equals(newLeader));
+        }
+        return clone;
+    }
+
+    public static void assertTopologyConsistsOf(TopologyView topology, String... slingIds) {
+        assertNotNull(topology);
+        assertEquals(slingIds.length, topology.getInstances().size());
+        for(int i=0; i<slingIds.length; i++) {
+            final String aSlingId = slingIds[i];
+            final Set<?> instances = topology.getInstances();
+            boolean found = false;
+            for (Iterator<?> it = instances.iterator(); it.hasNext();) {
+                InstanceDescription anInstance = (InstanceDescription) it.next();
+                if (anInstance.getSlingId().equals(aSlingId)) {
+                    found = true;
+                    break;
+                }
+            }
+            assertTrue(found);
+        }
+    }
+    
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualConnector.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualConnector.java
new file mode 100644
index 0000000..e6de0ef
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualConnector.java
@@ -0,0 +1,39 @@
+/*
+ * 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.sling.discovery.base.its.setup;
+
+import org.apache.sling.discovery.base.connectors.ping.TopologyConnectorClientInformation;
+
+public class VirtualConnector {
+    @SuppressWarnings("unused")
+    private final VirtualInstance from;
+    @SuppressWarnings("unused")
+    private final VirtualInstance to;
+    private final int jettyPort;
+    @SuppressWarnings("unused")
+    private final TopologyConnectorClientInformation connectorInfo;
+
+    public VirtualConnector(VirtualInstance from, VirtualInstance to) throws Throwable {
+        this.from = from;
+        this.to = to;
+        to.startJetty();
+        this.jettyPort = to.getJettyPort();
+        this.connectorInfo = from.connectTo("http://localhost:"+jettyPort+"/system/console/topology/connector");
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstance.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstance.java
new file mode 100644
index 0000000..a21becb
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstance.java
@@ -0,0 +1,374 @@
+/*
+ * 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.sling.discovery.base.its.setup;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.InvocationTargetException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Date;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.jcr.Session;
+import javax.servlet.Servlet;
+
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.testing.jcr.RepositoryProvider;
+import org.apache.sling.discovery.InstanceDescription;
+import org.apache.sling.discovery.PropertyProvider;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.apache.sling.discovery.base.commons.BaseDiscoveryService;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.ViewChecker;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+import org.apache.sling.discovery.base.connectors.ping.TopologyConnectorClientInformation;
+import org.apache.sling.discovery.base.connectors.ping.TopologyConnectorServlet;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.http.HttpService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import junitx.util.PrivateAccessor;
+
+public class VirtualInstance {
+
+    protected final static Logger logger = LoggerFactory.getLogger(VirtualInstance.class);
+
+    public final String slingId;
+
+    ClusterViewService clusterViewService;
+
+    private final ResourceResolverFactory resourceResolverFactory;
+
+    private final OSGiMock osgiMock;
+
+    private final BaseDiscoveryService discoveryService;
+
+    private final AnnouncementRegistry announcementRegistry;
+
+    private final ConnectorRegistry connectorRegistry;
+
+    protected final String debugName;
+
+    private ResourceResolver resourceResolver;
+
+    private int serviceId = 999;
+
+    private ViewCheckerRunner viewCheckerRunner = null;
+
+    private ServletContextHandler servletContext;
+
+    private Server jettyServer;
+
+    private ModifiableTestBaseConfig config;
+
+    private ViewChecker viewChecker;
+
+    private final VirtualInstanceBuilder builder;
+
+    private class ViewCheckerRunner implements Runnable {
+
+    	private final int intervalInSeconds;
+
+    	private boolean stopped_ = false;
+
+		public ViewCheckerRunner(int intervalInSeconds) {
+    		this.intervalInSeconds = intervalInSeconds;
+    	}
+
+		public synchronized void stop() {
+			logger.info("Stopping Instance ["+slingId+"]");
+			stopped_ = true;
+		}
+
+		public void run() {
+			while(true) {
+				synchronized(this) {
+					if (stopped_) {
+						logger.info("Instance ["+slingId+"] stopps.");
+						return;
+					}
+				}
+				try{
+				    heartbeatsAndCheckView();
+				} catch(Exception e) {
+				    logger.error("run: ping connector for slingId="+slingId+" threw exception: "+e, e);
+				}
+				try {
+					Thread.sleep(intervalInSeconds*1000);
+				} catch (InterruptedException e) {
+					e.printStackTrace();
+					return;
+				}
+			}
+		}
+
+    }
+    
+    public VirtualInstance(VirtualInstanceBuilder builder) throws Exception {
+        this.builder = builder;
+    	this.slingId = builder.getSlingId();
+        this.debugName = builder.getDebugName();
+        logger.info("<init>: starting slingId="+slingId+", debugName="+debugName);
+
+        osgiMock = new OSGiMock();
+
+        this.resourceResolverFactory = builder.getResourceResolverFactory();
+
+        config = builder.getConnectorConfig();
+        config.addTopologyConnectorWhitelistEntry("127.0.0.1");
+        config.setMinEventDelay(builder.getMinEventDelay());
+
+        clusterViewService = builder.getClusterViewService();
+        announcementRegistry = builder.getAnnouncementRegistry();
+        connectorRegistry = builder.getConnectorRegistry();
+        viewChecker = builder.getViewChecker();
+		discoveryService = builder.getDiscoverService();
+
+        osgiMock.addService(clusterViewService);
+        osgiMock.addService(announcementRegistry);
+        osgiMock.addService(connectorRegistry);
+        osgiMock.addService(viewChecker);
+        osgiMock.addService(discoveryService);
+        osgiMock.addServices(builder.getAdditionalServices(this));
+
+        resourceResolver = resourceResolverFactory
+                .getAdministrativeResourceResolver(null);
+
+        if (builder.isResetRepo()) {
+            //SLING-4587 : do resetRepo before creating the observationListener
+            // otherwise it will get tons of events from the deletion of /var
+            // which the previous test could have left over.
+            // Doing it before addEventListener should prevent that.
+            builder.resetRepo();
+        }
+
+        osgiMock.activateAll();
+    }
+    
+    @Override
+    public String toString() {
+        return "a [Test]Instance[slingId="+slingId+", debugName="+debugName+"]";
+    }
+
+    public void bindPropertyProvider(PropertyProvider propertyProvider,
+            String... propertyNames) throws Throwable {
+        Map<String, Object> props = new HashMap<String, Object>();
+        props.put(Constants.SERVICE_ID, (long) serviceId++);
+        props.put(PropertyProvider.PROPERTY_PROPERTIES, propertyNames);
+
+        PrivateAccessor.invoke(discoveryService, "bindPropertyProvider",
+                new Class[] { PropertyProvider.class, Map.class },
+                new Object[] { propertyProvider, props });
+    }
+
+    public String getSlingId() {
+        return slingId;
+    }
+
+    public ClusterViewService getClusterViewService() {
+        return clusterViewService;
+    }
+
+    public BaseDiscoveryService getDiscoveryService() {
+        return discoveryService;
+    }
+
+    public AnnouncementRegistry getAnnouncementRegistry() {
+        return announcementRegistry;
+    }
+
+    public synchronized void startJetty() throws Throwable {
+        if (jettyServer!=null) {
+            return;
+        }
+        servletContext = new ServletContextHandler(ServletContextHandler.NO_SECURITY);
+        servletContext.setContextPath("/");
+
+        TopologyConnectorServlet servlet = new TopologyConnectorServlet();
+        PrivateAccessor.setField(servlet, "config", config);
+        PrivateAccessor.setField(servlet, "clusterViewService", clusterViewService);
+        PrivateAccessor.setField(servlet, "announcementRegistry", announcementRegistry);
+
+        Mockery context = new JUnit4Mockery();
+        final HttpService httpService = context.mock(HttpService.class);
+        context.checking(new Expectations() {
+            {
+                allowing(httpService).registerServlet(with(any(String.class)),
+                        with(any(Servlet.class)),
+                        with(any(Dictionary.class)),
+                        with(any(HttpContext.class)));
+            }
+        });
+        PrivateAccessor.setField(servlet, "httpService", httpService);
+        ComponentContext cc = null;
+        PrivateAccessor.invoke(servlet, "activate", new Class[] {ComponentContext.class}, new Object[] {cc});
+
+        ServletHolder holder =
+                new ServletHolder(servlet);
+
+        servletContext.addServlet(holder, "/system/console/topology/*");
+
+        jettyServer = new Server();
+        jettyServer.setHandler(servletContext);
+        Connector connector=new SelectChannelConnector();
+        jettyServer.setConnectors(new Connector[]{connector});
+        jettyServer.start();
+    }
+
+    public synchronized int getJettyPort() {
+        if (jettyServer==null) {
+            throw new IllegalStateException("jettyServer not started");
+        }
+        final Connector[] connectors = jettyServer.getConnectors();
+        return connectors[0].getLocalPort();
+    }
+
+    public TopologyConnectorClientInformation connectTo(String url) throws MalformedURLException {
+        return connectorRegistry.registerOutgoingConnector(clusterViewService, new URL(url));
+    }
+
+    public InstanceDescription getLocalInstanceDescription() throws UndefinedClusterViewException {
+    	final Iterator<InstanceDescription> it = getClusterViewService().getLocalClusterView().getInstances().iterator();
+    	while(it.hasNext()) {
+    		final InstanceDescription id = it.next();
+    		if (slingId.equals(id.getSlingId())) {
+    			return id;
+    		}
+    	}
+    	fail("no local instanceDescription found");
+    	// never called:
+    	return null;
+    }
+
+    public void heartbeatsAndCheckView() {
+    	logger.info("Instance ["+slingId+"] issues a pulse now "+new Date());
+        viewChecker.heartbeatAndCheckView();
+    }
+
+    public void startViewChecker(int intervalInSeconds) throws IllegalAccessException, InvocationTargetException {
+    	logger.info("startViewChecker: intervalInSeconds="+intervalInSeconds);
+    	if (viewCheckerRunner!=null) {
+    		logger.info("startViewChecker: stopping first...");
+    		viewCheckerRunner.stop();
+    		logger.info("startViewChecker: stopped.");
+    	}
+		logger.info("startViewChecker: activating...");
+    	try{
+    		OSGiMock.activate(viewChecker);
+    	} catch(Error er) {
+    		er.printStackTrace(System.out);
+    		throw er;
+    	} catch(RuntimeException re) {
+    		re.printStackTrace(System.out);
+    	}
+		logger.info("startViewChecker: initializing...");
+    	viewCheckerRunner = new ViewCheckerRunner(intervalInSeconds);
+    	Thread th = new Thread(viewCheckerRunner, "Test-ViewCheckerRunner ["+debugName+"]");
+    	th.setDaemon(true);
+		logger.info("startViewChecker: starting thread...");
+    	th.start();
+		logger.info("startViewChecker: done.");
+    }
+
+	public boolean isViewCheckerRunning() {
+		return (viewCheckerRunner!=null);
+	}
+
+    public void stopViewChecker() throws Throwable {
+    	if (viewCheckerRunner!=null) {
+    		viewCheckerRunner.stop();
+    		viewCheckerRunner = null;
+    	}
+        try{
+            OSGiMock.deactivate(viewChecker);
+        } catch(Error er) {
+            er.printStackTrace(System.out);
+            throw er;
+        } catch(RuntimeException re) {
+            re.printStackTrace(System.out);
+            throw re;
+        }
+    }
+
+    public void dumpRepo() throws Exception {
+        VirtualInstanceHelper.dumpRepo(resourceResolverFactory);
+    }
+    
+    public ResourceResolverFactory getResourceResolverFactory() {
+        return resourceResolverFactory;
+    }
+
+    public void stop() throws Exception {
+        logger.info("stop: stopping slingId="+slingId+", debugName="+debugName);
+        try {
+            stopViewChecker();
+        } catch (Throwable e) {
+            throw new Exception("Caught Throwable in stopConnectorPinger: "+e, e);
+        }
+
+        if (resourceResolver != null) {
+            resourceResolver.close();
+        }
+        osgiMock.deactivateAll();
+        logger.info("stop: stopped slingId="+slingId+", debugName="+debugName);
+    }
+
+    public void bindTopologyEventListener(TopologyEventListener eventListener)
+            throws Throwable {
+        PrivateAccessor.invoke(discoveryService, "bindTopologyEventListener",
+                new Class[] { TopologyEventListener.class },
+                new Object[] { eventListener });
+    }
+
+    public ModifiableTestBaseConfig getConfig() {
+        return config;
+    }
+
+    public ViewChecker getViewChecker() {
+        return viewChecker;
+    }
+
+    public void assertEstablishedView() {
+        assertTrue(getDiscoveryService().getTopology().isCurrent());
+    }
+
+    public VirtualInstanceBuilder getBuilder() {
+        return builder;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceBuilder.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceBuilder.java
new file mode 100644
index 0000000..feaaf97
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceBuilder.java
@@ -0,0 +1,238 @@
+/*
+ * 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.sling.discovery.base.its.setup;
+
+import java.util.UUID;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.commons.scheduler.impl.QuartzScheduler;
+import org.apache.sling.commons.threads.ThreadPoolManager;
+import org.apache.sling.commons.threads.impl.DefaultThreadPoolManager;
+import org.apache.sling.discovery.base.commons.BaseDiscoveryService;
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.ViewChecker;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistryImpl;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistryImpl;
+import org.apache.sling.discovery.base.its.setup.mock.FailingScheduler;
+import org.apache.sling.discovery.commons.providers.spi.impl.DummySlingSettingsService;
+import org.apache.sling.settings.SlingSettingsService;
+
+import junitx.util.PrivateAccessor;
+
+public abstract class VirtualInstanceBuilder {
+
+    private static Scheduler singletonScheduler = null;
+    
+    public static Scheduler getSingletonScheduler() throws Exception {
+        if (singletonScheduler!=null) {
+            return singletonScheduler;
+        }
+        final Scheduler newscheduler = new QuartzScheduler();
+        final ThreadPoolManager tpm = new DefaultThreadPoolManager(null, null);
+        try {
+            PrivateAccessor.invoke(newscheduler, "bindThreadPoolManager",
+                    new Class[] { ThreadPoolManager.class },
+                    new Object[] { tpm });
+        } catch (Throwable e1) {
+            org.junit.Assert.fail(e1.toString());
+        }
+        OSGiMock.activate(newscheduler);
+        singletonScheduler = newscheduler;
+        return singletonScheduler;
+    }
+
+    private String debugName;
+    protected ResourceResolverFactory factory;
+    private boolean resetRepo;
+    private String slingId = UUID.randomUUID().toString();
+    private ClusterViewService clusterViewService;
+    protected ViewChecker viewChecker;
+    private AnnouncementRegistry announcementRegistry;
+    private ConnectorRegistry connectorRegistry;
+    private Scheduler scheduler;
+    private BaseDiscoveryService discoveryService;
+    private SlingSettingsService slingSettingsService;
+    protected boolean ownRepository;
+    private int minEventDelay = 1;
+    protected VirtualInstanceBuilder hookedToBuilder;
+
+    public VirtualInstanceBuilder() {
+    }
+    
+    public VirtualInstanceBuilder newRepository(String path, boolean resetRepo) throws Exception {
+        createNewRepository();
+        ownRepository = true;
+        this.resetRepo = resetRepo;
+        setPath(path);
+        return this;
+    }
+    
+    public abstract VirtualInstanceBuilder createNewRepository() throws Exception;
+    
+    public VirtualInstanceBuilder useRepositoryOf(VirtualInstance other) throws Exception {
+        return useRepositoryOf(other.getBuilder());
+    }
+    
+    public VirtualInstanceBuilder useRepositoryOf(VirtualInstanceBuilder other) throws Exception {
+        factory = other.factory;
+        hookedToBuilder = other;
+        ownRepository = false;
+        return this;
+    }
+
+    public VirtualInstanceBuilder setConnectorPingTimeout(int connectorPingTimeout) {
+        getConnectorConfig().setViewCheckTimeout(connectorPingTimeout);
+        return this;
+    }
+    
+    public VirtualInstanceBuilder setConnectorPingInterval(int connectorPingInterval) {
+        getConnectorConfig().setViewCheckInterval(connectorPingInterval);
+        return this;
+    }
+
+    public boolean isResetRepo() {
+        return resetRepo;
+    }
+
+    public String getSlingId() {
+        return slingId;
+    }
+
+    public String getDebugName() {
+        return debugName;
+    }
+
+    public ResourceResolverFactory getResourceResolverFactory() {
+        return factory;
+    }
+
+    public ClusterViewService getClusterViewService() {
+        if (clusterViewService==null) {
+            clusterViewService = createClusterViewService();
+        }
+        return clusterViewService;
+    }
+    
+    protected abstract ClusterViewService createClusterViewService();
+
+    public ViewChecker getViewChecker() throws Exception {
+        if (viewChecker==null) {
+            viewChecker = createViewChecker();
+        }
+        return viewChecker;
+    }
+    
+    public AnnouncementRegistry getAnnouncementRegistry() {
+        if (announcementRegistry==null) {
+            announcementRegistry = createAnnouncementRegistry();
+        }
+        return announcementRegistry;
+    }
+    
+    protected AnnouncementRegistry createAnnouncementRegistry() {
+        return AnnouncementRegistryImpl.testConstructor( 
+                getResourceResolverFactory(), getSlingSettingsService(), getConnectorConfig());
+    }
+
+    public ConnectorRegistry getConnectorRegistry() {
+        if (connectorRegistry==null) {
+            connectorRegistry = createConnectorRegistry();
+        }
+        return connectorRegistry;
+    }
+    
+    protected ConnectorRegistry createConnectorRegistry() {
+        return ConnectorRegistryImpl.testConstructor(announcementRegistry, getConnectorConfig());
+    }
+
+    protected abstract ViewChecker createViewChecker() throws Exception;
+
+    protected abstract VirtualInstanceBuilder setPath(String string);
+
+    public VirtualInstanceBuilder setDebugName(String debugName) {
+        this.debugName = debugName;
+        return this;
+    }
+
+    public abstract ModifiableTestBaseConfig getConnectorConfig();
+
+    public void setScheduler(Scheduler singletonScheduler) {
+        this.scheduler = singletonScheduler;
+    }
+    
+    public Scheduler getScheduler() throws Exception {
+        if (scheduler == null) {
+            scheduler = getSingletonScheduler();
+        }
+        return scheduler;
+    }
+
+    public BaseDiscoveryService getDiscoverService() throws Exception {
+        if (discoveryService==null) {
+            discoveryService = createDiscoveryService();
+        }
+        return discoveryService;
+    }
+
+    protected abstract BaseDiscoveryService createDiscoveryService() throws Exception;
+    
+    protected SlingSettingsService getSlingSettingsService() {
+        if (slingSettingsService==null) {
+            slingSettingsService = createSlingSettingsService();
+        }
+        return slingSettingsService;
+    }
+
+    protected SlingSettingsService createSlingSettingsService() {
+        return new DummySlingSettingsService(getSlingId());
+    }
+
+    public abstract Object[] getAdditionalServices(VirtualInstance instance) throws Exception;
+
+    public VirtualInstanceBuilder setMinEventDelay(int minEventDelay) {
+        this.minEventDelay = minEventDelay;
+        return this;
+    }
+
+    public int getMinEventDelay() {
+        return minEventDelay;
+    }
+
+    public VirtualInstance build() throws Exception {
+        return new VirtualInstance(this);
+    }
+
+    public VirtualInstanceBuilder setSlingId(String slingId) {
+        this.slingId = slingId;
+        return this;
+    }
+    
+    public VirtualInstanceBuilder withFailingScheduler(boolean useFailingScheduler) {
+        if (useFailingScheduler) {
+            this.scheduler = new FailingScheduler();
+        }
+        return this;
+    }
+
+    protected abstract void resetRepo() throws Exception;
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceHelper.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceHelper.java
new file mode 100644
index 0000000..ffbaff4
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualInstanceHelper.java
@@ -0,0 +1,88 @@
+/*
+ * 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.sling.discovery.base.its.setup;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class VirtualInstanceHelper {
+
+    private final static Logger logger = LoggerFactory.getLogger(VirtualInstanceHelper.class);
+
+    public static void dumpRepo(ResourceResolverFactory resourceResolverFactory) throws Exception {
+        Session session = resourceResolverFactory
+                .getAdministrativeResourceResolver(null).adaptTo(Session.class);
+        logger.info("dumpRepo: ====== START =====");
+        logger.info("dumpRepo: repo = " + session.getRepository());
+    
+        dump(session.getRootNode());
+    
+        // session.logout();
+        logger.info("dumpRepo: ======  END  =====");
+    
+        session.logout();
+    }
+
+    public static void dump(Node node) throws RepositoryException {
+        if (node.getPath().equals("/jcr:system")
+                || node.getPath().equals("/rep:policy")) {
+            // ignore that one
+            return;
+        }
+    
+        PropertyIterator pi = node.getProperties();
+        StringBuilder sb = new StringBuilder();
+        while (pi.hasNext()) {
+            Property p = pi.nextProperty();
+            sb.append(" ");
+            sb.append(p.getName());
+            sb.append("=");
+            if (p.getType() == PropertyType.BOOLEAN) {
+                sb.append(p.getBoolean());
+            } else if (p.getType() == PropertyType.STRING) {
+                sb.append(p.getString());
+            } else if (p.getType() == PropertyType.DATE) {
+                sb.append(p.getDate().getTime());
+            } else {
+                sb.append("<unknown type=" + p.getType() + "/>");
+            }
+        }
+    
+        StringBuffer depth = new StringBuffer();
+        for(int i=0; i<node.getDepth(); i++) {
+            depth.append(" ");
+        }
+        logger.info(depth + "/" + node.getName() + " -- " + sb);
+        NodeIterator it = node.getNodes();
+        while (it.hasNext()) {
+            Node child = it.nextNode();
+            dump(child);
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualRepository.java b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualRepository.java
new file mode 100644
index 0000000..958f90b
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/VirtualRepository.java
@@ -0,0 +1,27 @@
+/*
+ * 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.sling.discovery.base.its.setup;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+
+public interface VirtualRepository {
+
+    ResourceResolverFactory getResourceResolverFactory();
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/WithholdingAppender.java b/src/test/java/org/apache/sling/discovery/base/its/setup/WithholdingAppender.java
new file mode 100644
index 0000000..a294be6
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/WithholdingAppender.java
@@ -0,0 +1,82 @@
+/*
+ * 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 SF 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.sling.discovery.base.its.setup;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.URL;
+
+import org.apache.log4j.Layout;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.PropertyConfigurator;
+import org.apache.log4j.WriterAppender;
+
+public class WithholdingAppender extends WriterAppender {
+
+    private final ByteArrayOutputStream baos;
+    private final Writer writer;
+
+    /**
+     * Install the WithholdingAppender, essentially muting all logging 
+     * and withholding it until release() is called
+     * @return the WithholdingAppender that can be used to get the 
+     * withheld log output
+     */
+    public static WithholdingAppender install() {
+        LogManager.getRootLogger().removeAllAppenders();
+        final WithholdingAppender withholdingAppender = new WithholdingAppender(
+                new PatternLayout("%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m\n"));
+        LogManager.getRootLogger().addAppender(withholdingAppender);
+        return withholdingAppender;
+    }
+    
+    /**
+     * Release this WithholdingAppender and optionally dump what was
+     * withheld (eg in case of an exception)
+     * @param dumpToSysout
+     */
+    public void release(boolean dumpToSysout) {
+        LogManager.resetConfiguration();
+        URL log4jPropertiesFile = getClass().getResource("/log4j.properties");
+        PropertyConfigurator.configure(log4jPropertiesFile);
+        if (dumpToSysout) {
+            String withheldLogoutput = getBuffer();
+            System.out.println(withheldLogoutput);
+        }
+    }
+    
+    public WithholdingAppender(Layout layout) {
+        this.layout = layout;
+        this.baos = new ByteArrayOutputStream();
+        this.writer = new BufferedWriter(new OutputStreamWriter(baos));
+        this.setWriter(writer);
+    }
+    
+    public String getBuffer() {
+        try{
+            writer.flush();
+        } catch(IOException e) {
+            // ignore
+        }
+        return baos.toString();
+    }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsMultiple.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsMultiple.java
new file mode 100644
index 0000000..0562555
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsMultiple.java
@@ -0,0 +1,65 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+
+public class AcceptsMultiple implements TopologyEventAsserter {
+
+    private final Type[] acceptedTypes;
+
+    private final Map<Type, Integer> counts = new HashMap<Type, Integer>();
+
+    public AcceptsMultiple(Type... acceptedTypes) {
+        this.acceptedTypes = acceptedTypes;
+    }
+
+    public synchronized void assertOk(TopologyEvent event) {
+        for (int i = 0; i < acceptedTypes.length; i++) {
+            Type aType = acceptedTypes[i];
+            if (aType == event.getType()) {
+                // perfect
+                Integer c = counts.remove(aType);
+                if (c == null) {
+                    counts.put(aType, new Integer(1));
+                } else {
+                    counts.put(aType, new Integer(c + 1));
+                }
+                return;
+            }
+        }
+
+        throw new IllegalStateException("Got an Event which I did not expect: "
+                + event.getType());
+    }
+
+    public synchronized int getEventCnt(Type type) {
+        Integer i = counts.get(type);
+        if (i!=null) {
+            return i;
+        } else {
+            return 0;
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsParticularTopologyEvent.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsParticularTopologyEvent.java
new file mode 100644
index 0000000..1d3cbda
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AcceptsParticularTopologyEvent.java
@@ -0,0 +1,50 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+
+public class AcceptsParticularTopologyEvent implements TopologyEventAsserter {
+
+    private final Type particularType;
+
+    private int eventCnt = 0;
+
+    /**
+     * @param singleInstanceTest
+     */
+    public AcceptsParticularTopologyEvent(Type particularType) {
+        this.particularType = particularType;
+    }
+
+    public void assertOk(TopologyEvent event) {
+        if (event.getType() == particularType) {
+            // fine
+            eventCnt++;
+        } else {
+            throw new IllegalStateException("expected " + particularType
+                    + ", got " + event.getType());
+        }
+    }
+
+    public int getEventCnt() {
+        return eventCnt;
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AssertingTopologyEventListener.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AssertingTopologyEventListener.java
new file mode 100644
index 0000000..d5c912f
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/AssertingTopologyEventListener.java
@@ -0,0 +1,160 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sling.discovery.TopologyEvent;
+import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.TopologyEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AssertingTopologyEventListener implements TopologyEventListener {
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+    private final List<TopologyEventAsserter> expectedEvents = new LinkedList<TopologyEventAsserter>();
+
+    private String debugInfo = null;
+    
+    private String errorMsg = null;
+    
+    public AssertingTopologyEventListener() {
+    }
+
+    public AssertingTopologyEventListener(String debugInfo) {
+        this.debugInfo = debugInfo;
+    }
+    
+    @Override
+    public String toString() {
+        return super.toString()+"-[debugInfo="+debugInfo+"]";
+    }
+    
+    private List<TopologyEvent> events_ = new LinkedList<TopologyEvent>();
+
+    private List<TopologyEvent> unexpectedEvents_ = new LinkedList<TopologyEvent>();
+
+    public void handleTopologyEvent(TopologyEvent event) {
+        final String logPrefix = "handleTopologyEvent["+(debugInfo!=null ? debugInfo : "this="+this) +"] ";
+        logger.info(logPrefix + "got event=" + event);
+        TopologyEventAsserter asserter = null;
+        synchronized (expectedEvents) {
+            if (expectedEvents.size() == 0) {
+                unexpectedEvents_.add(event);
+                throw new IllegalStateException(
+                        "no expected events anymore. But got: " + event);
+            }
+            asserter = expectedEvents.remove(0);
+        }
+        if (asserter == null) {
+            throw new IllegalStateException("this should not occur");
+        }
+        try{
+            asserter.assertOk(event);
+            logger.info(logPrefix + "event matched expectations (" + event+")");
+        } catch(RuntimeException re) {
+            synchronized(expectedEvents) {
+                unexpectedEvents_.add(event);
+            }
+            throw re;
+        } catch(Error er) {
+            synchronized(expectedEvents) {
+                unexpectedEvents_.add(event);
+            }
+            throw er;
+        }
+        try{
+        switch(event.getType()) {
+        case PROPERTIES_CHANGED: {
+            assertNotNull(event.getOldView());
+            assertNotNull(event.getNewView());
+            assertTrue(event.getNewView().isCurrent());
+            assertFalse(event.getOldView().isCurrent());
+            break;
+        }
+        case TOPOLOGY_CHANGED: {
+            assertNotNull(event.getOldView());
+            assertNotNull(event.getNewView());
+            assertTrue(event.getNewView().isCurrent());
+            assertFalse(event.getOldView().isCurrent());
+            break;
+        }
+        case TOPOLOGY_CHANGING: {
+            assertNotNull(event.getOldView());
+            assertNull(event.getNewView());
+            assertFalse(event.getOldView().isCurrent());
+            break;
+        }
+        case TOPOLOGY_INIT: {
+            assertNull(event.getOldView());
+            assertNotNull(event.getNewView());
+            // cannot make any assertions on event.getNewView().isCurrent()
+            // as that can be true or false
+            break;
+        }
+        }
+        } catch(RuntimeException re) {
+            logger.error("RuntimeException: "+re, re);
+            throw re;
+        } catch(AssertionError e) {
+            logger.error("AssertionError: "+e, e);
+            throw e;
+        }
+        events_.add(event);
+    }
+
+    public List<TopologyEvent> getEvents() {
+        return events_;
+    }
+
+    public void addExpected(Type expectedType) {
+        addExpected(new AcceptsParticularTopologyEvent(expectedType));
+    }
+
+    public void addExpected(TopologyEventAsserter topologyEventAsserter) {
+        expectedEvents.add(topologyEventAsserter);
+    }
+
+    public int getRemainingExpectedCount() {
+        return expectedEvents.size();
+    }
+    
+    public int getUnexpectedCount() {
+        return unexpectedEvents_.size();
+    }
+
+    public void dump() {
+        StringBuffer ue = new StringBuffer();
+        if (unexpectedEvents_.size()>0) {
+            for (Iterator<TopologyEvent> it = unexpectedEvents_.iterator(); it.hasNext();) {
+                TopologyEvent topologyEvent = it.next();
+                ue.append(topologyEvent+", ");
+            }
+            unexpectedEvents_.iterator();
+        }
+        logger.info("dump: got "+events_.size()+" events, "+unexpectedEvents_.size()+" (details: "+ue+") thereof unexpected. My list of expected events contains "+expectedEvents.size());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/DummyViewChecker.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/DummyViewChecker.java
new file mode 100644
index 0000000..e66ec09
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/DummyViewChecker.java
@@ -0,0 +1,48 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.discovery.base.commons.BaseViewChecker;
+import org.apache.sling.discovery.base.connectors.BaseConfig;
+import org.apache.sling.discovery.base.connectors.announcement.AnnouncementRegistry;
+import org.apache.sling.discovery.base.connectors.ping.ConnectorRegistry;
+import org.apache.sling.settings.SlingSettingsService;
+
+public class DummyViewChecker extends BaseViewChecker {
+    
+    public static DummyViewChecker testConstructor(
+            SlingSettingsService slingSettingsService,
+            ResourceResolverFactory resourceResolverFactory,
+            ConnectorRegistry connectorRegistry,
+            AnnouncementRegistry announcementRegistry,
+            Scheduler scheduler,
+            BaseConfig connectorConfig) {
+        DummyViewChecker pinger = new DummyViewChecker();
+        pinger.slingSettingsService = slingSettingsService;
+        pinger.resourceResolverFactory = resourceResolverFactory;
+        pinger.connectorRegistry = connectorRegistry;
+        pinger.announcementRegistry = announcementRegistry;
+        pinger.scheduler = scheduler;
+        pinger.connectorConfig = connectorConfig;
+        return pinger;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/FailingScheduler.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/FailingScheduler.java
new file mode 100644
index 0000000..726c713
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/FailingScheduler.java
@@ -0,0 +1,72 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.apache.sling.commons.scheduler.Scheduler;
+
+public class FailingScheduler implements Scheduler {
+    
+    @Override
+    public void removeJob(String name) throws NoSuchElementException {
+        // nothing to do here
+    }
+    
+    @Override
+    public boolean fireJobAt(String name, Object job, Map<String, Serializable> config, Date date, int times, long period) {
+        return false;
+    }
+    
+    @Override
+    public void fireJobAt(String name, Object job, Map<String, Serializable> config, Date date) throws Exception {
+        throw new Exception("cos you are really worth it");
+    }
+    
+    @Override
+    public boolean fireJob(Object job, Map<String, Serializable> config, int times, long period) {
+        return false;
+    }
+    
+    @Override
+    public void fireJob(Object job, Map<String, Serializable> config) throws Exception {
+        throw new Exception("cos you are really worth it");
+    }
+    
+    @Override
+    public void addPeriodicJob(String name, Object job, Map<String, Serializable> config, long period, boolean canRunConcurrently,
+            boolean startImmediate) throws Exception {
+        throw new Exception("cos you are really worth it");
+    }
+    
+    @Override
+    public void addPeriodicJob(String name, Object job, Map<String, Serializable> config, long period, boolean canRunConcurrently)
+            throws Exception {
+        throw new Exception("cos you are really worth it");
+    }
+    
+    @Override
+    public void addJob(String name, Object job, Map<String, Serializable> config, String schedulingExpression,
+            boolean canRunConcurrently) throws Exception {
+        throw new Exception("cos you are really worth it");
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockFactory.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockFactory.java
new file mode 100644
index 0000000..f4e2404
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockFactory.java
@@ -0,0 +1,126 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import java.util.Dictionary;
+import java.util.Properties;
+
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.jcr.api.SlingRepository;
+import org.apache.sling.settings.SlingSettingsService;
+import org.hamcrest.Description;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.api.Action;
+import org.jmock.api.Invocation;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.jmock.lib.action.ReturnValueAction;
+import org.jmock.lib.action.VoidAction;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+
+public class MockFactory {
+
+    public final Mockery context = new JUnit4Mockery();
+
+    public static ResourceResolverFactory mockResourceResolverFactory()
+            throws Exception {
+    	return mockResourceResolverFactory(null);
+    }
+
+    public static ResourceResolverFactory mockResourceResolverFactory(final SlingRepository repositoryOrNull)
+            throws Exception {
+        Mockery context = new JUnit4Mockery();
+
+        final ResourceResolverFactory resourceResolverFactory = context
+                .mock(ResourceResolverFactory.class);
+        // final ResourceResolver resourceResolver = new MockResourceResolver();
+        // final ResourceResolver resourceResolver = new
+        // MockedResourceResolver();
+
+        context.checking(new Expectations() {
+            {
+                allowing(resourceResolverFactory)
+                        .getAdministrativeResourceResolver(null);
+                will(new Action() {
+
+                    public Object invoke(Invocation invocation)
+                            throws Throwable {
+                    	return new MockedResourceResolver(repositoryOrNull);
+                    }
+
+                    public void describeTo(Description arg0) {
+                        arg0.appendText("whateva - im going to create a new mockedresourceresolver");
+                    }
+                });
+            }
+        });
+        return resourceResolverFactory;
+    }
+
+    public static SlingSettingsService mockSlingSettingsService(
+            final String slingId) {
+        Mockery context = new JUnit4Mockery();
+
+        final SlingSettingsService settingsService = context
+                .mock(SlingSettingsService.class);
+        context.checking(new Expectations() {
+            {
+                allowing(settingsService).getSlingId();
+                will(returnValue(slingId));
+                
+                allowing(settingsService).getSlingHomePath();
+                will(returnValue("/n/a"));
+            }
+        });
+        return settingsService;
+    }
+
+    public static ComponentContext mockComponentContext() {
+        Mockery context = new JUnit4Mockery();
+        final BundleContext bc = context.mock(BundleContext.class);
+        context.checking(new Expectations() {
+            {
+                allowing(bc).registerService(with(any(String.class)),
+                        with(any(Object.class)), with(any(Dictionary.class)));
+                will(VoidAction.INSTANCE);
+                
+                allowing(bc).getProperty(with(any(String.class)));
+                will(new ReturnValueAction("foo"));
+            }
+        });
+
+        final ComponentContext cc = context.mock(ComponentContext.class);
+        context.checking(new Expectations() {
+            {
+                allowing(cc).getProperties();
+                will(returnValue(new Properties()));
+
+                allowing(cc).getBundleContext();
+                will(returnValue(bc));
+            }
+        });
+
+        return cc;
+    }
+
+    public static BundleContext mockBundleContext() {
+        return mockComponentContext().getBundleContext();
+    }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResource.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResource.java
new file mode 100644
index 0000000..040842b
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResource.java
@@ -0,0 +1,297 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MockedResource extends SyntheticResource {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final MockedResourceResolver mockedResourceResolver;
+    private Session session;
+
+    public MockedResource(MockedResourceResolver resourceResolver, String path,
+            String resourceType) {
+        super(resourceResolver, path, resourceType);
+        mockedResourceResolver = resourceResolver;
+
+        resourceResolver.register(this);
+    }
+
+    private Session getSession() {
+        synchronized (this) {
+            if (session == null) {
+                try {
+                    session = mockedResourceResolver.getSession();
+                } catch (RepositoryException e) {
+                    throw new RuntimeException("RepositoryException: " + e, e);
+                }
+            }
+            return session;
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+//        close();
+        super.finalize();
+    }
+
+    public void close() {
+        synchronized (this) {
+            if (session != null) {
+                if (session.isLive()) {
+                    session.logout();
+                }
+                session = null;
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        if (type.equals(Node.class)) {
+            try {
+                return (AdapterType) getSession().getNode(getPath());
+            } catch (Exception e) {
+                logger.error("Exception occurred: "+e, e);
+                throw new RuntimeException("Exception occurred: " + e, e);
+            }
+        } else if (type.equals(ValueMap.class)) {
+            try {
+                Session session = getSession();
+                Node node = session.getNode(getPath());
+                HashMap<String, Object> map = new HashMap<String, Object>();
+
+                PropertyIterator properties = node.getProperties();
+                while (properties.hasNext()) {
+                    Property p = properties.nextProperty();
+                    if (p.getType() == PropertyType.BOOLEAN) {
+                        map.put(p.getName(), p.getBoolean());
+                    } else if (p.getType() == PropertyType.STRING) {
+                        map.put(p.getName(), p.getString());
+                    } else if (p.getType() == PropertyType.DATE) {
+                        map.put(p.getName(), p.getDate().getTime());
+                    } else if (p.getType() == PropertyType.NAME) {
+                        map.put(p.getName(), p.getName());
+                    } else if (p.getType() == PropertyType.LONG) {
+                        map.put(p.getName(), p.getLong());
+                    } else {
+                        throw new RuntimeException(
+                                "Unsupported property type: " + p.getType());
+                    }
+                }
+                ValueMap valueMap = new ValueMapDecorator(map);
+                return (AdapterType) valueMap;
+            } catch (Exception e) {
+                e.printStackTrace();
+                return null;
+            }
+        } else if (type.equals(ModifiableValueMap.class)) {
+            return (AdapterType) new ModifiableValueMap() {
+                
+                public Collection<Object> values() {
+                    throw new UnsupportedOperationException();
+                }
+                
+                public int size() {
+                    throw new UnsupportedOperationException();
+                }
+                
+                public Object remove(Object arg0) {
+                    Session session = getSession();
+                    try{
+                        final Node node = session.getNode(getPath());
+                        final Property p = node.getProperty(String.valueOf(arg0));
+                        if (p!=null) {
+                        	p.remove();
+                        }
+                        // this is not according to the spec - but OK for tests since
+                        // the return value is never used
+                        return null;
+                    } catch(PathNotFoundException pnfe) {
+                    	// perfectly fine
+                    	return null;
+                    } catch(RepositoryException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+                
+                public void putAll(Map<? extends String, ? extends Object> arg0) {
+                    throw new UnsupportedOperationException();
+                }
+                
+                public Object put(String arg0, Object arg1) {
+                    Session session = getSession();
+                    try{
+                        final Node node = session.getNode(getPath());
+                        Object result = null;
+                        if (node.hasProperty(arg0)) {
+                            final Property previous = node.getProperty(arg0);
+                            if (previous==null) {
+                                // null
+                            } else if (previous.getType() == PropertyType.STRING) {
+                                result = previous.getString();
+                            } else if (previous.getType() == PropertyType.DATE) {
+                                result = previous.getDate();
+                            } else if (previous.getType() == PropertyType.BOOLEAN) {
+                                result = previous.getBoolean();
+                            } else if (previous.getType() == PropertyType.LONG) {
+                                result = previous.getLong();
+                            } else {
+                                throw new UnsupportedOperationException();
+                            }
+                        }
+                        if (arg1 instanceof String) {
+                            node.setProperty(arg0, (String)arg1);
+                        } else if (arg1 instanceof Calendar) {
+                            node.setProperty(arg0, (Calendar)arg1);
+                        } else if (arg1 instanceof Boolean) {
+                            node.setProperty(arg0, (Boolean)arg1);
+                        } else if (arg1 instanceof Date) {
+                            final Calendar c = Calendar.getInstance();
+                            c.setTime((Date)arg1);
+                            node.setProperty(arg0, c);
+                        } else if (arg1 instanceof Long) {
+                            node.setProperty(arg0, (Long)arg1);
+                        } else {
+                            throw new UnsupportedOperationException();
+                        }
+                        return result;
+                    } catch(RepositoryException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+                
+                public Set<String> keySet() {
+                    Session session = getSession();
+                    try {
+                        final Node node = session.getNode(getPath());
+                        final PropertyIterator pi = node.getProperties();
+                        final Set<String> result = new HashSet<String>();
+                        while(pi.hasNext()) {
+                            final Property p = pi.nextProperty();
+                            result.add(p.getName());
+                        }
+                        return result;
+                    } catch (RepositoryException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+                
+                public boolean isEmpty() {
+                    throw new UnsupportedOperationException();
+                }
+                
+                public Object get(Object arg0) {
+                    Session session = getSession();
+                    try{
+                        final Node node = session.getNode(getPath());
+                        final String key = String.valueOf(arg0);
+                        if (node.hasProperty(key)) {
+                            return node.getProperty(key);
+                        } else {
+                            return null;
+                        }
+                    } catch(RepositoryException re) {
+                        throw new RuntimeException(re);
+                    }
+                }
+                
+                public Set<Entry<String, Object>> entrySet() {
+                    throw new UnsupportedOperationException();
+                }
+                
+                public boolean containsValue(Object arg0) {
+                    throw new UnsupportedOperationException();
+                }
+                
+                public boolean containsKey(Object arg0) {
+                    Session session = getSession();
+                    try{
+                        final Node node = session.getNode(getPath());
+                        return node.hasProperty(String.valueOf(arg0));
+                    } catch(RepositoryException re) {
+                        throw new RuntimeException(re);
+                    }
+                }
+                
+                public void clear() {
+                    throw new UnsupportedOperationException();
+                }
+                
+                public <T> T get(String name, T defaultValue) {
+                    throw new UnsupportedOperationException();
+                }
+                
+                public <T> T get(String name, Class<T> type) {
+                    Session session = getSession();
+                    try{
+                        final Node node = session.getNode(getPath());
+                        if (node==null) {
+                        	return null;
+                        }
+                        if (!node.hasProperty(name)) {
+                            return null;
+                        }
+                        Property p = node.getProperty(name);
+                        if (p==null) {
+                        	return null;
+                        }
+                        if (type.equals(Calendar.class)) {
+                        	return (T) p.getDate();
+                        } else if (type.equals(String.class)) {
+                        	return (T) p.getString();
+                        } else {
+                            throw new UnsupportedOperationException();
+                        }
+                    } catch(RepositoryException e) {
+                    	throw new RuntimeException(e);
+                    }
+                }
+            };
+        } else {
+            return super.adaptTo(type);
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResourceResolver.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResourceResolver.java
new file mode 100644
index 0000000..9d9fd13
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/MockedResourceResolver.java
@@ -0,0 +1,335 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.jcr.Credentials;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.commons.testing.jcr.RepositoryProvider;
+import org.apache.sling.commons.testing.jcr.RepositoryUtil;
+import org.apache.sling.jcr.api.SlingRepository;
+import org.slf4j.LoggerFactory;
+
+public class MockedResourceResolver implements ResourceResolver {
+
+	private final SlingRepository repository;
+
+	private Session session;
+
+    private List<MockedResource> resources = new LinkedList<MockedResource>();
+
+    public MockedResourceResolver() throws RepositoryException {
+    	this(null);
+    }
+
+    public MockedResourceResolver(SlingRepository repositoryOrNull) throws RepositoryException {
+    	if (repositoryOrNull==null) {
+    		this.repository = RepositoryProvider.instance().getRepository();
+    		Session adminSession = null;
+    		try {
+    		    adminSession = this.repository.loginAdministrative(null);
+                RepositoryUtil.registerSlingNodeTypes(adminSession);
+    		} catch ( final IOException ioe ) {
+    		    throw new RepositoryException(ioe);
+    		} finally {
+    		    if ( adminSession != null ) {
+    		        adminSession.logout();
+    		    }
+    		}
+    	} else {
+    		this.repository = repositoryOrNull;
+    	}
+    }
+
+    public Session getSession() throws RepositoryException {
+        synchronized (this) {
+            if (session != null) {
+                return session;
+            }
+            session = createSession();
+            return session;
+        }
+    }
+
+    private Repository getRepository() {
+    	return repository;
+    }
+
+    private Session createSession() throws RepositoryException {
+        final Credentials credentials = new SimpleCredentials("admin",
+                "admin".toCharArray());
+        return repository.login(credentials, "default");
+    }
+
+
+    @SuppressWarnings("unchecked")
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        if (type.equals(Session.class)) {
+            try {
+                return (AdapterType) getSession();
+            } catch (RepositoryException e) {
+                throw new RuntimeException("RepositoryException: " + e, e);
+            }
+        } else if (type.equals(Repository.class)) {
+        	return (AdapterType) getRepository();
+        }
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public Resource resolve(HttpServletRequest request, String absPath) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public Resource resolve(String absPath) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Deprecated
+    public Resource resolve(HttpServletRequest request) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public String map(String resourcePath) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public String map(HttpServletRequest request, String resourcePath) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public Resource getResource(String path) {
+        Session session;
+        try {
+            session = getSession();
+            session.getNode(path);
+        } catch (PathNotFoundException e) {
+            return null;
+        } catch (RepositoryException e) {
+            throw new RuntimeException("RepositoryException: " + e, e);
+        }
+        return new MockedResource(this, path, "nt:unstructured");
+    }
+
+    public Resource getResource(Resource base, String path) {
+        if (base.getPath().equals("/")) {
+            return getResource("/" + path);
+        } else {
+            return getResource(base.getPath() + "/" + path);
+        }
+    }
+
+    public String[] getSearchPath() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public Iterator<Resource> listChildren(Resource parent) {
+        try {
+            Node node = parent.adaptTo(Node.class);
+            final NodeIterator nodes = node.getNodes();
+            return new Iterator<Resource>() {
+
+                public void remove() {
+                    throw new UnsupportedOperationException();
+                }
+
+                public Resource next() {
+                    Node next = nodes.nextNode();
+                    try {
+                        return new MockedResource(MockedResourceResolver.this,
+                                next.getPath(), "nt:unstructured");
+                    } catch (RepositoryException e) {
+                        throw new RuntimeException("RepositoryException: " + e,
+                                e);
+                    }
+                }
+
+                public boolean hasNext() {
+                    return nodes.hasNext();
+                }
+            };
+        } catch (RepositoryException e) {
+            throw new RuntimeException("RepositoryException: " + e, e);
+        }
+    }
+
+    public Iterable<Resource> getChildren(Resource parent) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public Iterator<Resource> findResources(String query, String language) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public Iterator<Map<String, Object>> queryResources(String query,
+            String language) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public ResourceResolver clone(Map<String, Object> authenticationInfo)
+            throws LoginException {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public boolean isLive() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public void close() {
+        Iterator<MockedResource> it = resources.iterator();
+        while (it.hasNext()) {
+            MockedResource r = it.next();
+            r.close();
+        }
+        if (session != null) {
+            if (session.isLive()) {
+                session.logout();
+            }
+            session = null;
+        }
+    }
+
+    public void register(MockedResource mockedResource) {
+        resources.add(mockedResource);
+    }
+
+    public String getUserID() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public Iterator<String> getAttributeNames() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public Object getAttribute(String name) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public void delete(Resource resource) throws PersistenceException {
+        if (resources.contains(resource)) {
+            resources.remove(resource);
+            Node node = resource.adaptTo(Node.class);
+            try {
+                node.remove();
+            } catch (RepositoryException e) {
+                throw new PersistenceException("RepositoryException: "+e, e);
+            }
+        } else {
+            throw new UnsupportedOperationException("Not implemented");
+        }
+    }
+
+    public Resource create(Resource parent, String name,
+            Map<String, Object> properties) throws PersistenceException {
+        final Node parentNode = parent.adaptTo(Node.class);
+        boolean success = false;
+        try {
+            final Node child;
+            if (properties!=null && properties.containsKey("jcr:primaryType")) {
+                child = parentNode.addNode(name, (String) properties.get("jcr:primaryType"));
+            } else {
+                child = parentNode.addNode(name);
+            }
+            if (properties!=null) {
+                final Iterator<Entry<String, Object>> it = properties.entrySet().iterator();
+                while(it.hasNext()) {
+                    final Entry<String, Object> entry = it.next();
+                    if (entry.getKey().equals("jcr:primaryType")) {
+                        continue;
+                    }
+                    if (entry.getValue() instanceof String) {
+                        child.setProperty(entry.getKey(), (String)entry.getValue());
+                    } else if (entry.getValue() instanceof Boolean) {
+                        child.setProperty(entry.getKey(), (Boolean)entry.getValue());
+                    } else if (entry.getValue() instanceof Calendar) {
+                        child.setProperty(entry.getKey(), (Calendar)entry.getValue());
+                    } else {
+                        throw new UnsupportedOperationException("Not implemented (entry.getValue(): "+entry.getValue()+")");
+                    }
+                }
+            }
+            Resource result = getResource(parent, name);
+            success = true;
+            return result;
+        } catch (RepositoryException e) {
+            throw new RuntimeException(e);
+        } finally {
+            LoggerFactory.getLogger(this.getClass()).info("create: creating of "+name+" under "+parent+" was successful="+success);
+        }
+    }
+
+    public void revert() {
+        try {
+            this.session.refresh(false);
+        } catch (final RepositoryException re) {
+            throw new RuntimeException("Unable to commit changes.", re);
+        }
+    }
+
+    public void commit() throws PersistenceException {
+        try {
+            this.session.save();
+        } catch (final RepositoryException re) {
+            throw new PersistenceException("Unable to commit changes.", re);
+        }
+    }
+
+    public boolean hasChanges() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public String getParentResourceType(Resource resource) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public String getParentResourceType(String resourceType) {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public boolean isResourceType(Resource resource, String resourceType) {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public void refresh() {
+        // TODO Auto-generated method stub
+
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/PropertyProviderImpl.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/PropertyProviderImpl.java
new file mode 100644
index 0000000..3f1f0c9
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/PropertyProviderImpl.java
@@ -0,0 +1,58 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.discovery.PropertyProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PropertyProviderImpl implements PropertyProvider {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private final Map<String, String> properties = new HashMap<String, String>();
+
+    private int getCnt = 0;
+
+    public PropertyProviderImpl() {
+        // nothing so far
+    }
+
+    public String getProperty(String name) {
+        getCnt++;
+        logger.warn("getProperty: name="+name+", new getCnt="+getCnt, new Exception("getProperty-stacktrace"));
+        return properties.get(name);
+    }
+
+    public void setProperty(String name, String value) {
+        properties.put(name, value);
+    }
+
+    public void setGetCnt(int getCnt) {
+        this.getCnt = getCnt;
+    }
+
+    public int getGetCnt() {
+        return getCnt;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleClusterViewService.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleClusterViewService.java
new file mode 100644
index 0000000..e397d2e
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleClusterViewService.java
@@ -0,0 +1,59 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import java.util.HashMap;
+import java.util.UUID;
+
+import org.apache.sling.discovery.base.commons.ClusterViewService;
+import org.apache.sling.discovery.base.commons.UndefinedClusterViewException;
+import org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+
+public class SimpleClusterViewService implements ClusterViewService {
+
+    private LocalClusterView clusterView;
+    
+    private final String slingId;
+
+    public SimpleClusterViewService(String slingId) {
+        this.slingId = slingId;
+        LocalClusterView clusterView = new LocalClusterView(UUID.randomUUID().toString(), null);
+        new DefaultInstanceDescription(clusterView, true, true, slingId, new HashMap<String, String>());
+        this.clusterView = clusterView;
+    }
+    
+    @Override
+    public String getSlingId() {
+        return slingId;
+    }
+
+    @Override
+    public LocalClusterView getLocalClusterView() throws UndefinedClusterViewException {
+        if (clusterView==null) {
+            throw new IllegalStateException("no clusterView set");
+        }
+        return clusterView;
+    }
+    
+    public void setClusterView(LocalClusterView clusterView) {
+        this.clusterView = clusterView;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java
new file mode 100644
index 0000000..5bffff4
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/SimpleConnectorConfig.java
@@ -0,0 +1,215 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import java.net.URL;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sling.discovery.base.its.setup.ModifiableTestBaseConfig;
+
+public class SimpleConnectorConfig implements ModifiableTestBaseConfig {
+
+    private int connectionTimeout;
+    private int soTimeout;
+    private URL[] topologyConnectorURLs;
+    private List<String> topologyConnectorWhitelist;
+    private String clusterInstancesPath = "/var/discovery/impl/clusterInstances";
+    private boolean hmacEnabled;
+    private String sharedKey;
+    private long keyInterval;
+    private boolean encryptionEnabled;
+    private boolean gzipConnectorRequestsEnabled;
+    private boolean autoStopLocalLoopEnabled;
+    private int backoffStandbyFactor;
+    private int backoffStableFactor;
+    private long backoffStandbyInterval;
+    private long announcementInterval = 20;
+    private long announcementTimeout = 20;
+    private int minEventDelay;
+
+    @Override
+    public int getSocketConnectionTimeout() {
+        return connectionTimeout;
+    }
+    
+    public void setConnectionTimeout(int connectionTimeout) {
+        this.connectionTimeout = connectionTimeout;
+    }
+
+    @Override
+    public int getSoTimeout() {
+        return soTimeout;
+    }
+    
+    public void setSoTimeout(int soTimeout) {
+        this.soTimeout = soTimeout;
+    }
+
+    @Override
+    public URL[] getTopologyConnectorURLs() {
+        return topologyConnectorURLs;
+    }
+    
+    public void setTopologyConnectorURLs(URL[] topologyConnectorURLs) {
+        this.topologyConnectorURLs = topologyConnectorURLs;
+    }
+
+    @Override
+    public String[] getTopologyConnectorWhitelist() {
+        if (topologyConnectorWhitelist==null) {
+            return null;
+        }
+        return topologyConnectorWhitelist.toArray(new String[topologyConnectorWhitelist.size()]);
+    }
+
+    public void addTopologyConnectorWhitelistEntry(String whitelistEntry) {
+        if (topologyConnectorWhitelist==null) {
+            topologyConnectorWhitelist = new LinkedList<String>();
+        }
+        topologyConnectorWhitelist.add(whitelistEntry);
+    }
+
+    @Override
+    public String getClusterInstancesPath() {
+        return clusterInstancesPath;
+    }
+    
+    public void setClusterInstancesPath(String clusterInstancesPath) {
+        this.clusterInstancesPath = clusterInstancesPath;
+    }
+
+    @Override
+    public boolean isHmacEnabled() {
+        return hmacEnabled;
+    }
+    
+    public void setHmacEnabled(boolean hmacEnabled) {
+        this.hmacEnabled = hmacEnabled;
+    }
+
+    @Override
+    public String getSharedKey() {
+        return sharedKey;
+    }
+    
+    public void setSharedKey(String sharedKey) {
+        this.sharedKey = sharedKey;
+    }
+
+    @Override
+    public long getKeyInterval() {
+        return keyInterval;
+    }
+    
+    public void setKeyInterval(int keyInterval) {
+        this.keyInterval = keyInterval;
+    }
+
+    @Override
+    public boolean isEncryptionEnabled() {
+        return encryptionEnabled;
+    }
+    
+    public void setEncryptionEnabled(boolean encryptionEnabled) {
+        this.encryptionEnabled = encryptionEnabled;
+    }
+
+    @Override
+    public boolean isGzipConnectorRequestsEnabled() {
+        return gzipConnectorRequestsEnabled;
+    }
+    
+    public void setGzipConnectorRequestsEnabled(boolean gzipConnectorRequestsEnabled) {
+        this.gzipConnectorRequestsEnabled = gzipConnectorRequestsEnabled;
+    }
+
+    @Override
+    public boolean isAutoStopLocalLoopEnabled() {
+        return autoStopLocalLoopEnabled;
+    }
+    
+    public void setAutoStopLocalLoopEnabled(boolean autoStopLocalLoopEnabled) {
+        this.autoStopLocalLoopEnabled = autoStopLocalLoopEnabled;
+    }
+
+    @Override
+    public int getBackoffStandbyFactor() {
+        return backoffStandbyFactor;
+    }
+    
+    public void setBackoffStandbyFactor(int backoffStandbyFactor) {
+        this.backoffStandbyFactor = backoffStandbyFactor;
+    }
+
+    @Override
+    public int getBackoffStableFactor() {
+        return backoffStableFactor;
+    }
+    
+    public void setBackoffStableFactor(int backoffStableFactor) {
+        this.backoffStableFactor = backoffStableFactor;
+    }
+
+    @Override
+    public long getBackoffStandbyInterval() {
+        return backoffStandbyInterval;
+    }
+    
+    public void setBackoffStandbyInterval(long backoffStandbyInterval) {
+        this.backoffStandbyInterval = backoffStandbyInterval;
+    }
+
+    @Override
+    public long getConnectorPingInterval() {
+        return announcementInterval;
+    }
+    
+    public void setAnnouncementInterval(long announcementInterval) {
+        this.announcementInterval = announcementInterval;
+    }
+
+    @Override
+    public long getConnectorPingTimeout() {
+        return announcementTimeout;
+    }
+    
+    public void setAnnouncementTimeout(long announcementTimeout) {
+        this.announcementTimeout = announcementTimeout;
+    }
+
+    @Override
+    public int getMinEventDelay() {
+        return minEventDelay;
+    }
+    
+    public void setMinEventDelay(int minEventDelay) {
+        this.minEventDelay = minEventDelay;
+    }
+
+    @Override
+    public void setViewCheckTimeout(int viewCheckTimeout) {
+        announcementTimeout = viewCheckTimeout;
+    }
+
+    @Override
+    public void setViewCheckInterval(int viewCheckInterval) {
+        announcementInterval = viewCheckInterval;
+    }
+}
diff --git a/src/test/java/org/apache/sling/discovery/base/its/setup/mock/TopologyEventAsserter.java b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/TopologyEventAsserter.java
new file mode 100644
index 0000000..15a1409
--- /dev/null
+++ b/src/test/java/org/apache/sling/discovery/base/its/setup/mock/TopologyEventAsserter.java
@@ -0,0 +1,25 @@
+/*
+ * 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.sling.discovery.base.its.setup.mock;
+
+import org.apache.sling.discovery.TopologyEvent;
+
+public interface TopologyEventAsserter {
+    public void assertOk(TopologyEvent event);
+}
\ No newline at end of file
diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties
new file mode 100644
index 0000000..7db291c
--- /dev/null
+++ b/src/test/resources/log4j.properties
@@ -0,0 +1,26 @@
+# 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.
+
+log4j.rootLogger=INFO, stdout
+
+log4j.logger.org.apache.jackrabbit.core.TransientRepository=WARN
+#log4j.logger.org.apache.sling.discovery.impl=DEBUG
+
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+#log4j.appender.stdout.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n
+log4j.appender.stdout.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m\n

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.