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:31 UTC

[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

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>.