You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by st...@apache.org on 2015/01/26 09:58:22 UTC

svn commit: r1654742 - in /sling/trunk/bundles/extensions/discovery/impl/src: main/java/org/apache/sling/discovery/impl/common/heartbeat/ main/java/org/apache/sling/discovery/impl/topology/announcement/ test/java/org/apache/sling/discovery/impl/cluster...

Author: stefanegli
Date: Mon Jan 26 08:58:22 2015
New Revision: 1654742

URL: http://svn.apache.org/r1654742
Log:
SLING-4139 : fixing regression introduced with SLING-3389 : topology announcements that are no longer active (thus no longer in the AnnouncementRegistryImpl's cache) will be cleaned up from the repository. They used carry a heartbeat which made them invalid automatically, but SLING-3389 removed writing that heartbeat. Added a number of tests to reproduce what has been reported in both SLING-4139 and SLING-3726.

Modified:
    sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/common/heartbeat/HeartbeatHandler.java
    sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/topology/announcement/AnnouncementRegistryImpl.java
    sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/topology/announcement/CachedAnnouncement.java
    sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/cluster/ClusterTest.java
    sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/setup/Instance.java
    sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/setup/MockedResource.java

Modified: sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/common/heartbeat/HeartbeatHandler.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/common/heartbeat/HeartbeatHandler.java?rev=1654742&r1=1654741&r2=1654742&view=diff
==============================================================================
--- sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/common/heartbeat/HeartbeatHandler.java (original)
+++ sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/common/heartbeat/HeartbeatHandler.java Mon Jan 26 08:58:22 2015
@@ -19,6 +19,7 @@
 package org.apache.sling.discovery.impl.common.heartbeat;
 
 import java.util.Calendar;
+import java.util.Date;
 import java.util.Iterator;
 import java.util.Set;
 import java.util.UUID;
@@ -68,7 +69,7 @@ public class HeartbeatHandler implements
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
     /** the name used for the period job with the scheduler **/
-    private static final String NAME = "discovery.impl.heartbeat.runner";
+    private String NAME = "discovery.impl.heartbeat.runner.";
 
     @Reference
     private SlingSettingsService slingSettingsService;
@@ -150,6 +151,7 @@ public class HeartbeatHandler implements
     		this.context = context;
 
 	        slingId = slingSettingsService.getSlingId();
+	        NAME = "discovery.impl.heartbeat.runner." + slingId;
 	        // on activate the resetLeaderElectionId is set to true to ensure that
 	        // the 'leaderElectionId' property is reset on next heartbeat issuance.
 	        // the idea being that a node which leaves the cluster should not
@@ -232,7 +234,10 @@ public class HeartbeatHandler implements
         forcePing = true;
         try {
             // then fire a job immediately
-            scheduler.fireJob(this, null);
+            // 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
+            scheduler.fireJobAt(NAME+UUID.randomUUID(), this, null, new Date(System.currentTimeMillis()-1000 /* make sure it gets triggered immediately*/));
         } catch (Exception e) {
             logger.info("triggerHeartbeat: Could not trigger heartbeat: " + e);
         }
@@ -429,7 +434,7 @@ public class HeartbeatHandler implements
     private void doCheckView(final ResourceResolver resourceResolver) throws PersistenceException {
 
         if (votingHandler==null) {
-            logger.info("doCheckView: votingHandler is null!");
+            logger.info("doCheckView: votingHandler is null! slingId="+slingId);
         } else {
             votingHandler.analyzeVotings(resourceResolver);
             try{

Modified: sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/topology/announcement/AnnouncementRegistryImpl.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/topology/announcement/AnnouncementRegistryImpl.java?rev=1654742&r1=1654741&r2=1654742&view=diff
==============================================================================
--- sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/topology/announcement/AnnouncementRegistryImpl.java (original)
+++ sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/topology/announcement/AnnouncementRegistryImpl.java Mon Jan 26 08:58:22 2015
@@ -444,10 +444,65 @@ public class AnnouncementRegistryImpl im
                 it.remove();
                 
                 final String instanceId = entry.getKey();
-                logger.info("checkExpiredAnnouncements: topology connector of "+instanceId+" has expired.");
+                logger.info("checkExpiredAnnouncements: topology connector of "+instanceId+
+                        " (to me="+settingsService.getSlingId()+
+                        ", 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
+        final String instanceId = settingsService.getSlingId();
+        ResourceResolver resourceResolver = null;
+        try {
+            resourceResolver = resourceResolverFactory
+                    .getAdministrativeResourceResolver(null);
+            final Resource announcementsResource = ResourceHelper
+                    .getOrCreateResource(
+                            resourceResolver,
+                            config.getClusterInstancesPath()
+                                    + "/"
+                                    + instanceId
+                                    + "/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 instanceId="+instanceId+": " + e, e);
+        } catch (PersistenceException e) {
+            logger.error(
+                    "checkExpiredAnnouncements: got PersistenceException when checking "
+                    + "for expired announcements of instanceId="+instanceId+": " + e, e);
+        } finally {
+            if (resourceResolver!=null) {
+                resourceResolver.revert();
+                resourceResolver.close();
+                resourceResolver = null;
+            }
+        }
     }
 
     private final void deleteAnnouncementsOf(final String instanceId) {

Modified: sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/topology/announcement/CachedAnnouncement.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/topology/announcement/CachedAnnouncement.java?rev=1654742&r1=1654741&r2=1654742&view=diff
==============================================================================
--- sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/topology/announcement/CachedAnnouncement.java (original)
+++ sling/trunk/bundles/extensions/discovery/impl/src/main/java/org/apache/sling/discovery/impl/topology/announcement/CachedAnnouncement.java Mon Jan 26 08:58:22 2015
@@ -41,14 +41,19 @@ public class CachedAnnouncement {
 
     private long backoffIntervalSeconds = -1;
 
-    private final long configuredHeartbeatTimeout;
-
-    private final long configuredHeartbeatInterval;
+    private final Config config;
     
     CachedAnnouncement(final Announcement announcement, final Config config) {
         this.announcement = announcement;
-        this.configuredHeartbeatTimeout = config.getHeartbeatTimeout();
-        this.configuredHeartbeatInterval = config.getHeartbeatInterval();
+        this.config = config;
+    }
+    
+    private long getConfiguredHeartbeatTimeout() {
+        return config.getHeartbeatTimeout();
+    }
+    
+    private long getConfiguredHeartbeatInterval() {
+        return config.getHeartbeatInterval();
     }
 
     public final boolean hasExpired() {
@@ -75,8 +80,8 @@ public class CachedAnnouncement {
     
     
     private final long getEffectiveHeartbeatTimeout() {
-        final long configuredGoodwill = configuredHeartbeatTimeout - configuredHeartbeatInterval;
-        return Math.max(configuredHeartbeatTimeout, backoffIntervalSeconds + configuredGoodwill);
+        final long configuredGoodwill = getConfiguredHeartbeatTimeout() - getConfiguredHeartbeatInterval();
+        return Math.max(getConfiguredHeartbeatTimeout(), backoffIntervalSeconds + configuredGoodwill);
     }
 
     /** Registers a heartbeat event, and returns the new resulting backoff interval -

Modified: sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/cluster/ClusterTest.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/cluster/ClusterTest.java?rev=1654742&r1=1654741&r2=1654742&view=diff
==============================================================================
--- sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/cluster/ClusterTest.java (original)
+++ sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/cluster/ClusterTest.java Mon Jan 26 08:58:22 2015
@@ -19,21 +19,29 @@
 package org.apache.sling.discovery.impl.cluster;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 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.Set;
 import java.util.UUID;
 
+import org.apache.sling.discovery.ClusterView;
 import org.apache.sling.discovery.InstanceDescription;
 import org.apache.sling.discovery.TopologyEvent.Type;
+import org.apache.sling.discovery.TopologyView;
 import org.apache.sling.discovery.impl.cluster.helpers.AcceptsMultiple;
 import org.apache.sling.discovery.impl.cluster.helpers.AssertingTopologyEventListener;
 import org.apache.sling.discovery.impl.setup.Instance;
 import org.apache.sling.discovery.impl.setup.PropertyProviderImpl;
+import org.apache.sling.discovery.impl.topology.announcement.Announcement;
+import org.apache.sling.discovery.impl.topology.announcement.AnnouncementFilter;
+import org.apache.sling.discovery.impl.topology.announcement.AnnouncementRegistry;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -41,8 +49,26 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class ClusterTest {
-
+	
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
+    
+    private class SimpleClusterView {
+    	
+    	private Instance[] instances;
+
+    	SimpleClusterView(Instance... 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;
+    	}
+    }
 
     Instance instance1;
     Instance instance2;
@@ -55,6 +81,9 @@ public class ClusterTest {
     private String property1Name;
 
     private String property2Name;
+    Instance instance4;
+    Instance instance5;
+    Instance instance1Restarted;
 
     @Before
     public void setup() throws Exception {
@@ -66,14 +95,33 @@ public class ClusterTest {
 
     @After
     public void tearDown() throws Exception {
+        if (instance5 != null) {
+            instance5.stop();
+        }
+        if (instance4 != null) {
+            instance4.stop();
+        }
         if (instance3 != null) {
             instance3.stop();
         }
-        instance2.stop();
-        instance1.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;
     }
     
     /** test leader behaviour with ascending slingIds, SLING-3253 **/
@@ -136,7 +184,715 @@ public class ClusterTest {
         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 = Instance.newClusterInstance("/var/discovery/impl/", "firstInstance", instance2,
+                false, Integer.MAX_VALUE /* no timeout */, 1, instance1SlingId);
+        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));
+        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 = Instance.newClusterInstance("/var/discovery/impl/", "firstInstance", instance2,
+                false, Integer.MAX_VALUE /* no timeout */, 1, instance1SlingId);
+        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));
+
+        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
+        
+        instance1 = Instance.newStandaloneInstance("/var/discovery/clusterA/", "instance1", true, 8 /* sec*/, MIN_EVENT_DELAY);
+        instance2 = Instance.newClusterInstance("/var/discovery/clusterA/", "instance2", instance1,
+                false, 8 /* sec*/, MIN_EVENT_DELAY);
+        // now launch the remote instance
+        instance3 = Instance.newStandaloneInstance("/var/discovery/clusterB/", "instance3", false, 8 /* sec*/, MIN_EVENT_DELAY);
+        instance4 = Instance.newClusterInstance("/var/discovery/clusterB/", "instance4", instance3,
+                false, 8 /* sec*/, MIN_EVENT_DELAY);
+        instance5 = Instance.newStandaloneInstance("/var/discovery/clusterC/", "instance5", false, 8 /* sec*/, MIN_EVENT_DELAY);
+
+        // join the instances to form a cluster by sending out heartbeats
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+
+        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);
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance4, instance5);
+        pingConnector(instance3, instance1);
+        pingConnector(instance5, instance1);
+        
+        // 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 10 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.");
+                break;
+            }
+            logger.info("testConnectorSwitching4139: looping");
+            Thread.sleep(500); // 25x500ms = 12.5sec max - (vs 8sec 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
+        instance1Restarted = Instance.newClusterInstance("/var/discovery/clusterA/", "instance1", instance2,
+                false, Integer.MAX_VALUE /* no timeout */, 1, instance1.slingId);
+        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);
+            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) {
+                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));
+    }
+
     @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
+        
+        instance1 = Instance.newStandaloneInstance("/var/discovery/clusterA/", "instance1", true, 5 /* sec*/, MIN_EVENT_DELAY);
+        instance2 = Instance.newClusterInstance("/var/discovery/clusterA/", "instance2", instance1,
+                false, 5 /* sec*/, MIN_EVENT_DELAY);
+        // now launch the remote instance
+        instance3 = Instance.newStandaloneInstance("/var/discovery/clusterB/", "instance3", false, 5 /* sec*/, MIN_EVENT_DELAY);
+        instance5 = Instance.newStandaloneInstance("/var/discovery/clusterC/", "instance5", false, 5 /* sec*/, MIN_EVENT_DELAY);
+
+        // join the instances to form a cluster by sending out heartbeats
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance5);
+        runHeartbeatOnceWith(instance1, instance2, instance3, instance5);
+
+        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
+        boolean success = false;
+        for(int i=0; i<25; i++) {
+            // loop for max 10 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.");
+                break;
+            }
+            logger.info("testDuplicateInstance3726: looping");
+            Thread.sleep(500); // 25x500ms = 12.5sec max - (vs 5sec 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 = Instance.newClusterInstance("/var/discovery/clusterA/", "instance1", instance2,
+                false, Integer.MAX_VALUE /* no timeout */, 1, instance1.slingId);
+        instance4 = Instance.newClusterInstance("/var/discovery/clusterB/", "instance4", instance3,
+                false, 5 /* sec*/, MIN_EVENT_DELAY);
+        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));
+        logger.info("testDuplicateInstance3726: end");
+    }
+
+    private void assertSameTopology(SimpleClusterView... clusters) {
+        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 Instance 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(Instance... instances) {
+        if (instances==null) {
+            return;
+        }
+        for(int i=0; i<instances.length; i++) {
+            instances[i].runHeartbeatOnce();
+        }
+    }
+
+    /**
+     * 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 = Instance.newStandaloneInstance("/var/discovery/implremote4/", "remoteInstance4", false, Integer.MAX_VALUE /* no timeout */, 1);
+        assertNotSameClusterIds(instance2, instance4);
+        assertNotSameClusterIds(instance3, instance4);
+        
+        // instead, now start a connector from instance3 to instance2
+        pingConnector(instance3, instance4);
+        
+        // start instance 1
+        instance1Restarted = Instance.newClusterInstance("/var/discovery/impl/", "firstInstance", instance2,
+                false, Integer.MAX_VALUE /* no timeout */, 1, instance1SlingId);
+        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));
+        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 = Instance.newStandaloneInstance("/var/discovery/impl/", "firstInstance", true, Integer.MAX_VALUE /* no timeout */, 1);
+        instance2 = Instance.newClusterInstance("/var/discovery/impl/", "secondInstance", instance1,
+                false, Integer.MAX_VALUE /* no timeout */, 1);
+        // 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 = Instance.newStandaloneInstance("/var/discovery/implremote/", "remoteInstance", false, Integer.MAX_VALUE /* no timeout */, 1);
+        assertSameClusterIds(instance1, instance2);
+        assertNotSameClusterIds(instance1, instance3);
+        
+        // create a topology connector from instance3 to instance1
+        // -> corresponds to starting to ping
+        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.stopHeartbeats(); // and have instance3 no longer pinging instance1
+        instance1 = null; // set to null to early fail if anyone still assumes (original) instance1 is up form now on
+        instance2.getConfig().setHeartbeatTimeout(1); // set instance2's heartbeatTimeout to 1 sec to time out instance1 quickly!
+        instance3.getConfig().setHeartbeatTimeout(1); // set instance3's heartbeatTimeout to 1 sec to time out instance1 quickly!
+        Thread.sleep(100);
+        runHeartbeatOnceWith(instance2, instance3);
+        Thread.sleep(100);
+        runHeartbeatOnceWith(instance2, instance3);
+        Thread.sleep(100);
+        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().setHeartbeatTimeout(Integer.MAX_VALUE /* no timeout */); // set instance2's heartbeatTimeout back to Integer.MAX_VALUE /* no timeout */
+        instance3.getConfig().setHeartbeatTimeout(Integer.MAX_VALUE /* no timeout */); // set instance3's heartbeatTimeout back to Integer.MAX_VALUE /* no timeout */
+		return instance1SlingId;
+	}
+    
+    private void assertNotSameClusterIds(Instance... instances) {
+    	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()
+                .getClusterView().getId();
+        for(int i=1; i<instances.length; i++) {
+	        final String otherClusterId = instances[i].getClusterViewService()
+	                .getClusterView().getId();
+	        // cluster ids must NOT be the same
+	        assertNotEquals(clusterId1, otherClusterId);
+        }
+        if (instances.length>2) {
+        	final Instance[] subset = new Instance[instances.length-1];
+        	System.arraycopy(instances, 0, subset, 1, instances.length-1);
+        	assertNotSameClusterIds(subset);
+        }
+	}
+
+	private void assertSameClusterIds(Instance... instances) {
+    	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()
+                .getClusterView().getId();
+        for(int i=1; i<instances.length; i++) {
+	        final String otherClusterId = instances[i].getClusterViewService()
+	                .getClusterView().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(Instance instance, SimpleClusterView... assertedClusterViews) {
+    	final TopologyView topology = instance.getDiscoveryService().getTopology();
+    	assertNotNull(topology);
+    	if (assertedClusterViews.length!=topology.getClusterViews().size()) {
+            dumpFailureDetails(topology, assertedClusterViews);
+    	    fail("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("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 Instance 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 Instance from, final Instance to) {
+	    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;
+	    }
+        registerReplyAnnouncement(from, replyAnnouncement);
+        return true;
+    }
+
+	private void registerReplyAnnouncement(Instance 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(Instance to, final Announcement incomingTopologyAnnouncement) {
+		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;
+        if (!incomingTopologyAnnouncement.isCorrectVersion()) {
+        	fail("incorrect version");
+            return null; // never reached
+        } else if (clusterViewService.contains(incomingTopologyAnnouncement
+                .getOwnerId())) {
+        	fail("loop=true");
+            return null; // never reached
+        } else if (clusterViewService.containsAny(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
+                final ClusterView clusterView = clusterViewService
+                        .getClusterView();
+                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 Instance from) {
+		// 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().getClusterView();
+        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 = 
+                        from.getClusterViewService().getClusterView().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

Modified: sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/setup/Instance.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/setup/Instance.java?rev=1654742&r1=1654741&r2=1654742&view=diff
==============================================================================
--- sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/setup/Instance.java (original)
+++ sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/setup/Instance.java Mon Jan 26 08:58:22 2015
@@ -348,6 +348,11 @@ public class Instance {
 
         osgiMock.activateAll(resetRepo);
     }
+    
+    @Override
+    public String toString() {
+        return "a [Test]Instance[slingId="+slingId+", debugName="+debugName+"]";
+    }
 
     public static Instance newStandaloneInstance(String debugName,
             SlingRepository repository) throws Exception {

Modified: sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/setup/MockedResource.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/setup/MockedResource.java?rev=1654742&r1=1654741&r2=1654742&view=diff
==============================================================================
--- sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/setup/MockedResource.java (original)
+++ sling/trunk/bundles/extensions/discovery/impl/src/test/java/org/apache/sling/discovery/impl/setup/MockedResource.java Mon Jan 26 08:58:22 2015
@@ -70,7 +70,7 @@ public class MockedResource extends Synt
 
     @Override
     protected void finalize() throws Throwable {
-        close();
+//        close();
         super.finalize();
     }