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

stefanegli pushed a commit to branch master
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-discovery-commons.git


The following commit(s) were added to refs/heads/master by this push:
     new d62c7da  SLING-10489 : ignore partially started instances : (#4)
d62c7da is described below

commit d62c7da07909373098c6ad4c2bc54124b0b4017f
Author: stefan-egli <[email protected]>
AuthorDate: Tue Sep 14 16:40:38 2021 +0200

    SLING-10489 : ignore partially started instances : (#4)
    
    * SLING-10489 : ignore partially started instances : a. skipping activeIds 
in OakBacklogClusterSyncService if they are partially started, b. ignore 
syncToken for view change checks if there are partially started instances plus 
bonus c. LogSilencer introduced, which reduces log.info spam caused by discovery
    
    * SLING-10489 : removed a noisy log.info
    
    * SLING-10489 : lowered a noisy log.info
    
    * SLING-10489 : lowered a noisy log.info
    
    * SLING-10489 : lowered a noisy log.info
    
    * SLING-10489: Ignore partially started, newly joining instances to avoid 
disturbing discovery (for a while)
    
    Unit test for LocalClusterView
    
    * Update 
src/main/java/org/apache/sling/discovery/commons/providers/util/LogSilencer.java
    
    Co-authored-by: Marcel Reutegger <[email protected]>
    
    * Update 
src/main/java/org/apache/sling/discovery/commons/providers/util/LogSilencer.java
    
    Co-authored-by: Marcel Reutegger <[email protected]>
    
    * SLING-10489 : javadoc updated
    
    * SLING-10489 : copy partiallyStartedClusterNodeIds on clone as well
    
    * SLING-10489 : simplified equalsIgnoreSyncToken - plus a unit test added 
for it
    
    Co-authored-by: Marcel Reutegger <[email protected]>
    Co-authored-by: Marcel Reutegger <[email protected]>
---
 .../providers/base/MinEventDelayHandler.java       |  37 +++--
 .../providers/base/ViewStateManagerImpl.java       |  45 +++++-
 .../commons/providers/base/package-info.java       |   4 +-
 .../commons/providers/spi/LocalClusterView.java    |  27 ++++
 .../base/AbstractServiceWithBackgroundCheck.java   |  16 +-
 .../spi/base/OakBacklogClusterSyncService.java     |  40 +++--
 .../providers/spi/base/SyncTokenService.java       |  29 ++--
 .../commons/providers/spi/package-info.java        |   4 +-
 .../commons/providers/util/LogSilencer.java        |  97 ++++++++++++
 .../commons/providers/util/package-info.java       |   4 +-
 .../commons/providers/DummyTopologyView.java       |  11 +-
 .../providers/base/TestViewStateManager.java       | 173 +++++++++++++++++++++
 .../providers/spi/LocalClusterViewTest.java        |  46 ++++++
 .../spi/base/TestOakSyncTokenService.java          |  84 +++++++++-
 14 files changed, 566 insertions(+), 51 deletions(-)

diff --git 
a/src/main/java/org/apache/sling/discovery/commons/providers/base/MinEventDelayHandler.java
 
b/src/main/java/org/apache/sling/discovery/commons/providers/base/MinEventDelayHandler.java
index 43093ad..12a4dfe 100644
--- 
a/src/main/java/org/apache/sling/discovery/commons/providers/base/MinEventDelayHandler.java
+++ 
b/src/main/java/org/apache/sling/discovery/commons/providers/base/MinEventDelayHandler.java
@@ -25,6 +25,7 @@ import org.apache.sling.commons.scheduler.Scheduler;
 import org.apache.sling.discovery.DiscoveryService;
 import org.apache.sling.discovery.TopologyView;
 import org.apache.sling.discovery.commons.providers.BaseTopologyView;
+import org.apache.sling.discovery.commons.providers.util.LogSilencer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -51,6 +52,8 @@ class MinEventDelayHandler {
     
     private volatile int cancelCnt = 0;
     
+    private final LogSilencer logSilencer = new LogSilencer(logger);
+
     MinEventDelayHandler(ViewStateManagerImpl viewStateManager, Lock lock,
             DiscoveryService discoveryService, Scheduler scheduler,
             long minEventDelaySecs) {
@@ -74,23 +77,33 @@ class MinEventDelayHandler {
      * Asks the MinEventDelayHandler to handle the new view
      * and return true if the caller shouldn't worry about any follow-up 
action -
      * only if the method returns false should the caller do the usual 
-     * handleNewView action
+     * handleNewView action.
+     * This caller of this method must ensure to be in a lock.lock() block
      */
     boolean handlesNewView(BaseTopologyView newView) {
         if (isDelaying) {
             // already delaying, so we'll soon ask the DiscoveryServiceImpl 
for the
             // latest view and go ahead then
-            logger.info("handleNewView: already delaying, ignoring new view 
meanwhile");
+            logSilencer.infoOrDebug("handlesNewView-" + 
newView.getLocalClusterSyncTokenId(),
+                    "handleNewView: already delaying, ignoring new view 
meanwhile");
             return true;
         }
         
         if (!viewStateManager.hadPreviousView()) {
-            logger.info("handlesNewView: never had a previous view, hence no 
delaying applicable");
+            logSilencer.infoOrDebug("handlesNewView-" + 
newView.getLocalClusterSyncTokenId(),
+                    "handlesNewView: never had a previous view, hence no 
delaying applicable");
+            return false;
+        }
+
+        if (viewStateManager.equalsIgnoreSyncToken(newView)) {
+            // this is a frequent case, hence only log.debug
+            logger.debug("handlesNewView: equalsIgnoreSyncToken, hence no 
delaying applicable");
             return false;
         }
         
         if (viewStateManager.onlyDiffersInProperties(newView)) {
-            logger.info("handlesNewView: only properties differ, hence no 
delaying applicable");
+            logSilencer.infoOrDebug("handlesNewView-" + 
newView.getLocalClusterSyncTokenId(),
+                    "handlesNewView: only properties differ, hence no delaying 
applicable");
             return false;
         }
         
@@ -103,7 +116,8 @@ class MinEventDelayHandler {
         
         // thanks to force==true this will always return true
         if (!triggerAsyncDelaying(newView)) {
-            logger.info("handleNewView: could not trigger async delaying, 
sending new view now.");
+            logSilencer.infoOrDebug("handlesNewView-" + 
newView.getLocalClusterSyncTokenId(),
+                    "handleNewView: could not trigger async delaying, sending 
new view now.");
             viewStateManager.handleNewViewNonDelayed(newView);
         } else {
             // if triggering the async event was successful, then we should 
also
@@ -118,7 +132,7 @@ class MinEventDelayHandler {
         return true;
     }
     
-    private boolean triggerAsyncDelaying(BaseTopologyView newView) {
+    private boolean triggerAsyncDelaying(final BaseTopologyView newView) {
         final int validCancelCnt = cancelCnt;
         final boolean triggered = runAfter(minEventDelaySecs /*seconds*/ , new 
Runnable() {
     
@@ -127,7 +141,8 @@ class MinEventDelayHandler {
                 lock.lock();
                 try{
                     if (cancelCnt!=validCancelCnt) {
-                        logger.info("asyncDelay.run: got cancelled 
(validCancelCnt="+validCancelCnt+", cancelCnt="+cancelCnt+"), quitting.");
+                        logSilencer.infoOrDebug("asyncDelay.run-cancel-" + 
newView.getLocalClusterSyncTokenId(),
+                                "asyncDelay.run: got cancelled 
(validCancelCnt="+validCancelCnt+", cancelCnt="+cancelCnt+"), quitting.");
                         return;
                     }
                     
@@ -144,10 +159,12 @@ class MinEventDelayHandler {
                     BaseTopologyView topology = (BaseTopologyView) t;
                     
                     if (topology.isCurrent()) {
-                        logger.info("asyncDelay.run: done delaying. got new 
view: "+ topology.toShortString());
+                        logSilencer.infoOrDebug("asyncDelay.run-done-" + 
newView.getLocalClusterSyncTokenId(),
+                                "asyncDelay.run: done delaying. got new view: 
"+ topology.toShortString());
                         viewStateManager.handleNewViewNonDelayed(topology);
                     } else {
-                        logger.info("asyncDelay.run: done delaying. new view 
(still/again) not current, delaying again");
+                        logSilencer.infoOrDebug("asyncDelay.run-done-" + 
newView.getLocalClusterSyncTokenId(),
+                                "asyncDelay.run: done delaying. new view 
(still/again) not current, delaying again");
                         triggerAsyncDelaying(topology);
                         // we're actually not interested in the result here
                         // if the async part failed, then we have to rely
@@ -168,7 +185,7 @@ class MinEventDelayHandler {
             }
         });
             
-        logger.info("triggerAsyncDelaying: asynch delaying of 
"+minEventDelaySecs+" triggered: "+triggered);
+        logSilencer.infoOrDebug("triggerAsyncDelaying", "triggerAsyncDelaying: 
asynch delaying of "+minEventDelaySecs+" triggered: "+triggered);
         if (triggered) {
             isDelaying = true;
         }
diff --git 
a/src/main/java/org/apache/sling/discovery/commons/providers/base/ViewStateManagerImpl.java
 
b/src/main/java/org/apache/sling/discovery/commons/providers/base/ViewStateManagerImpl.java
index b9f9fc7..da47195 100644
--- 
a/src/main/java/org/apache/sling/discovery/commons/providers/base/ViewStateManagerImpl.java
+++ 
b/src/main/java/org/apache/sling/discovery/commons/providers/base/ViewStateManagerImpl.java
@@ -28,6 +28,7 @@ import java.util.Set;
 import java.util.concurrent.locks.Lock;
 
 import org.apache.sling.commons.scheduler.Scheduler;
+import org.apache.sling.discovery.ClusterView;
 import org.apache.sling.discovery.DiscoveryService;
 import org.apache.sling.discovery.InstanceDescription;
 import org.apache.sling.discovery.TopologyEvent;
@@ -37,6 +38,8 @@ import 
org.apache.sling.discovery.commons.providers.BaseTopologyView;
 import org.apache.sling.discovery.commons.providers.EventHelper;
 import org.apache.sling.discovery.commons.providers.ViewStateManager;
 import org.apache.sling.discovery.commons.providers.spi.ClusterSyncService;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+import org.apache.sling.discovery.commons.providers.util.LogSilencer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -137,6 +140,8 @@ public class ViewStateManagerImpl implements 
ViewStateManager {
 
     private MinEventDelayHandler minEventDelayHandler;
 
+    private final LogSilencer logSilencer = new LogSilencer(logger);
+
     /**
      * Creates a new ViewStateManager which synchronizes each method with the 
given
      * lock and which optionally uses the given ClusterSyncService to sync the 
repository
@@ -456,7 +461,7 @@ public class ViewStateManagerImpl implements 
ViewStateManager {
                 // verify if there is actually a change between previousView 
and newView
                 // if there isn't, then there is not much point in sending a 
CHANGING/CHANGED tuple
                 // at all
-                if (previousView!=null && previousView.equals(newView)) {
+                if (previousView!=null && (previousView.equals(newView) || 
equalsIgnoreSyncToken(newView))) {
                     // then nothing to send - the view has not changed, and we 
haven't
                     // sent the CHANGING event - so we should not do anything 
here
                     logger.debug("handleNewViewNonDelayed: we were not in 
changing state and new view matches old, so - ignoring");
@@ -486,7 +491,7 @@ public class ViewStateManagerImpl implements 
ViewStateManager {
             if (!isChanging && onlyDiffersInProperties(newView)) {
                 // well then send a properties changed event only
                 // and that one does not go via consistencyservice
-                logger.info("handleNewViewNonDelayed: properties changed to: 
"+newView);
+                
logSilencer.infoOrDebug("handleNewViewNonDelayed-propsChanged", 
"handleNewViewNonDelayed: properties changed to: "+newView);
                 previousView.setNotCurrent();
                 enqueueForAll(eventListeners, 
EventHelper.newPropertiesChangedEvent(previousView, newView));
                 logger.trace("handleNewViewNonDelayed: setting previousView to 
{}", newView);
@@ -496,7 +501,7 @@ public class ViewStateManagerImpl implements 
ViewStateManager {
             
             final boolean invokeClusterSyncService;
             if (consistencyService==null) {
-                logger.info("handleNewViewNonDelayed: no ClusterSyncService 
set - continuing directly.");
+                
logSilencer.infoOrDebug("handleNewViewNonDelayed-noSyncService", 
"handleNewViewNonDelayed: no ClusterSyncService set - continuing directly.");
                 invokeClusterSyncService = false;
             } else {
                 // there used to be a distinction between:
@@ -511,7 +516,7 @@ public class ViewStateManagerImpl implements 
ViewStateManager {
                 //
                 // which is a long way of saying: if the consistencyService is 
configured,
                 // then we always use it, hence:
-                logger.info("handleNewViewNonDelayed: ClusterSyncService set - 
invoking...");
+                
logSilencer.infoOrDebug("handleNewViewNonDelayed-invokeSyncService", 
"handleNewViewNonDelayed: ClusterSyncService set - invoking...");
                 invokeClusterSyncService = true;
             }
                         
@@ -520,7 +525,7 @@ public class ViewStateManagerImpl implements 
ViewStateManager {
                 // then:
                 // run the set consistencyService
                 final int lastModCnt = modCnt;
-                logger.info("handleNewViewNonDelayed: invoking 
waitForAsyncEvents, then clusterSyncService (modCnt={})", modCnt);
+                
logSilencer.infoOrDebug("handleNewViewNonDelayed-invokeWaitAsync", 
"handleNewViewNonDelayed: invoking waitForAsyncEvents, then 
clusterSyncService");
                 asyncEventSender.enqueue(new AsyncEvent() {
                     
                     @Override
@@ -544,7 +549,7 @@ public class ViewStateManagerImpl implements 
ViewStateManager {
                                         lastModCnt, modCnt);
                                 return;
                             }
-                            
logger.info("handleNewViewNonDelayed/waitForAsyncEvents.run: done, now invoking 
consistencyService (modCnt={})", modCnt);
+                            
logSilencer.infoOrDebug("waitForAsyncEvents-asyncRun", 
"handleNewViewNonDelayed/waitForAsyncEvents.run: done, now invoking 
consistencyService");
                             consistencyService.sync(newView,
                                     new Runnable() {
                                 
@@ -558,7 +563,7 @@ public class ViewStateManagerImpl implements 
ViewStateManager {
                                                     lastModCnt, modCnt);
                                             return;
                                         }
-                                        
logger.info("consistencyService.callback.run: invoking doHandleConsistent.");
+                                        
logSilencer.infoOrDebug("consistencyService-callBackRun", 
"consistencyService.callback.run: invoking doHandleConsistent.");
                                         // else:
                                         doHandleConsistent(newView);
                                     } finally {
@@ -579,7 +584,7 @@ public class ViewStateManagerImpl implements 
ViewStateManager {
                 // or using it is not applicable at this stage - so continue
                 // with sending the TOPOLOGY_CHANGED (or TOPOLOGY_INIT if there
                 // are any newly bound topology listeners) directly
-                logger.info("handleNewViewNonDelayed: not invoking 
consistencyService, considering consistent now");
+                
logSilencer.infoOrDebug("handleNewViewNonDelayed-noSyncService-ignore", 
"handleNewViewNonDelayed: not invoking consistencyService, considering 
consistent now");
                 doHandleConsistent(newView);
             }
             logger.debug("handleNewViewNonDelayed: end");
@@ -590,6 +595,30 @@ public class ViewStateManagerImpl implements 
ViewStateManager {
         }
     }
 
+    /**
+     * Checks if the previouesView is equal to the newView, ignoring the
+     * syncToken (but only if the newView has partially started instances).
+     * <p/>
+     * This caller of this method must ensure to be in a lock.lock() block
+     */
+    protected boolean equalsIgnoreSyncToken(BaseTopologyView newView) {
+        if (previousView==null) {
+            return false;
+        }
+        if (newView==null) {
+            throw new IllegalArgumentException("newView must not be null");
+        }
+        final ClusterView cluster = 
newView.getLocalInstance().getClusterView();
+        if (cluster instanceof LocalClusterView) {
+            final LocalClusterView local = (LocalClusterView)cluster;
+            if (!local.hasPartiallyStartedInstances()) {
+                // then we should not ignore the syncToken I'm afraid
+                return previousView.equals(newView);
+            }
+        }
+        return previousView.getInstances().equals(newView.getInstances());
+    }
+
     protected boolean onlyDiffersInProperties(BaseTopologyView newView) {
         if (previousView==null) {
             return false;
diff --git 
a/src/main/java/org/apache/sling/discovery/commons/providers/base/package-info.java
 
b/src/main/java/org/apache/sling/discovery/commons/providers/base/package-info.java
index 9d9bd43..fd0915d 100644
--- 
a/src/main/java/org/apache/sling/discovery/commons/providers/base/package-info.java
+++ 
b/src/main/java/org/apache/sling/discovery/commons/providers/base/package-info.java
@@ -20,9 +20,9 @@
 /**
  * Provides commons implementations for providers of the Discovery API.
  *
- * @version 1.0.0
+ * @version 1.1.0
  */
-@Version("1.0.0")
+@Version("1.1.0")
 package org.apache.sling.discovery.commons.providers.base;
 
 import org.osgi.annotation.versioning.Version;
diff --git 
a/src/main/java/org/apache/sling/discovery/commons/providers/spi/LocalClusterView.java
 
b/src/main/java/org/apache/sling/discovery/commons/providers/spi/LocalClusterView.java
index 12363af..29663c0 100644
--- 
a/src/main/java/org/apache/sling/discovery/commons/providers/spi/LocalClusterView.java
+++ 
b/src/main/java/org/apache/sling/discovery/commons/providers/spi/LocalClusterView.java
@@ -18,11 +18,17 @@
  */
 package org.apache.sling.discovery.commons.providers.spi;
 
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
 import org.apache.sling.discovery.commons.providers.DefaultClusterView;
 
 public class LocalClusterView extends DefaultClusterView {
 
     private final String localClusterSyncTokenId;
+    private Set<Integer> partiallyStartedClusterNodeIds;
 
     public LocalClusterView(String id, String localClusterSyncTokenId) {
         super(id);
@@ -33,4 +39,25 @@ public class LocalClusterView extends DefaultClusterView {
         return localClusterSyncTokenId;
     }
 
+    public Set<Integer> getPartiallyStartedClusterNodeIds() {
+        return Collections.unmodifiableSet(partiallyStartedClusterNodeIds);
+    }
+
+    public void setPartiallyStartedClusterNodeIds(Collection<Integer> 
clusterNodeIds) {
+        this.partiallyStartedClusterNodeIds = new 
HashSet<Integer>(clusterNodeIds);
+    }
+
+    public boolean isPartiallyStarted(Integer clusterNodeId) {
+        if (partiallyStartedClusterNodeIds == null || clusterNodeId == null) {
+            return false;
+        }
+        return partiallyStartedClusterNodeIds.contains(clusterNodeId);
+    }
+
+    public boolean hasPartiallyStartedInstances() {
+        if (partiallyStartedClusterNodeIds == null) {
+            return false;
+        }
+        return !partiallyStartedClusterNodeIds.isEmpty();
+    }
 }
diff --git 
a/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/AbstractServiceWithBackgroundCheck.java
 
b/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/AbstractServiceWithBackgroundCheck.java
index 84f2f76..e7a2e5c 100644
--- 
a/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/AbstractServiceWithBackgroundCheck.java
+++ 
b/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/AbstractServiceWithBackgroundCheck.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sling.discovery.commons.providers.spi.base;
 
+import org.apache.sling.discovery.commons.providers.util.LogSilencer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -32,6 +33,8 @@ public abstract class AbstractServiceWithBackgroundCheck {
 
     protected String slingId;
 
+    private final LogSilencer logSilencer = new LogSilencer(logger);
+
     /**
      * The BackgroundCheckRunnable implements the details of
      * calling BackgroundCheck.check and looping until it 
@@ -83,9 +86,9 @@ public abstract class AbstractServiceWithBackgroundCheck {
                     if (timeoutMillis != -1 && 
                             (System.currentTimeMillis() > start + 
timeoutMillis)) {
                         if (callback == null) {
-                            logger.info("backgroundCheck.run: timeout hit (no 
callback to invoke)");
+                            logSilencer.infoOrDebug("backgroundCheck.run", 
"backgroundCheck.run: timeout hit (no callback to invoke)");
                         } else {
-                            logger.info("backgroundCheck.run: timeout hit, 
invoking callback.");
+                            logSilencer.infoOrDebug("backgroundCheck.run", 
"backgroundCheck.run: timeout hit, invoking callback.");
                             callback.run();
                         }
                         return;
@@ -131,7 +134,7 @@ public abstract class AbstractServiceWithBackgroundCheck {
 
         void cancel() {
             if (!done) {
-                logger.info("cancel: "+threadName);
+                logSilencer.infoOrDebug("cancel-" + threadName, "cancel: 
"+threadName);
             }
             cancelled = true;
         }
@@ -200,14 +203,15 @@ public abstract class AbstractServiceWithBackgroundCheck {
             // then we're not even going to start the background-thread
             // we're already done
             if (callback!=null) {
-                logger.info("backgroundCheck: already done, backgroundCheck 
successful, invoking callback");
+                logSilencer.infoOrDebug("backgroundCheck", "backgroundCheck: 
already done, backgroundCheck successful, invoking callback");
                 callback.run();
             } else {
-                logger.info("backgroundCheck: already done, backgroundCheck 
successful. no callback to invoke.");
+                logSilencer.infoOrDebug("backgroundCheck", "backgroundCheck: 
already done, backgroundCheck successful. no callback to invoke.");
             }
             return;
         }
-        logger.info("backgroundCheck: spawning background-thread for 
'"+threadName+"'");
+        logSilencer.infoOrDebug("backgroundCheck-" + threadName,
+                "backgroundCheck: spawning background-thread for 
'"+threadName+"'");
         backgroundCheckRunnable = new BackgroundCheckRunnable(callback, check, 
timeoutMillis, waitMillis, threadName);
         Thread th = new Thread(backgroundCheckRunnable);
         th.setName(threadName);
diff --git 
a/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/OakBacklogClusterSyncService.java
 
b/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/OakBacklogClusterSyncService.java
index f171728..abd94fb 100644
--- 
a/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/OakBacklogClusterSyncService.java
+++ 
b/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/OakBacklogClusterSyncService.java
@@ -30,6 +30,8 @@ import org.apache.sling.discovery.ClusterView;
 import org.apache.sling.discovery.InstanceDescription;
 import org.apache.sling.discovery.commons.providers.BaseTopologyView;
 import org.apache.sling.discovery.commons.providers.spi.ClusterSyncService;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
+import org.apache.sling.discovery.commons.providers.util.LogSilencer;
 import org.apache.sling.settings.SlingSettingsService;
 import org.osgi.framework.Constants;
 import org.osgi.service.component.annotations.Activate;
@@ -67,6 +69,8 @@ public class OakBacklogClusterSyncService extends 
AbstractServiceWithBackgroundC
 
     private ClusterSyncHistory consistencyHistory = new ClusterSyncHistory();
 
+    private final LogSilencer logSilencer = new LogSilencer(logger);
+
     public static OakBacklogClusterSyncService testConstructorAndActivate(
             final DiscoveryLiteConfig commonsConfig,
             final IdMapService idMapService,
@@ -132,20 +136,21 @@ public class OakBacklogClusterSyncService extends 
AbstractServiceWithBackgroundC
         cancelPreviousBackgroundCheck();
 
         // first do the wait-for-backlog part
-        logger.info("sync: doing wait-for-backlog part for 
view="+view.toShortString());
+        logSilencer.infoOrDebug("sync", "sync: doing wait-for-backlog part for 
view="+view.toShortString());
         waitWhileBacklog(view, callback);
     }
 
     private void waitWhileBacklog(final BaseTopologyView view, final Runnable 
runnable) {
         // start backgroundChecking until the backlogStatus
         // is NO_BACKLOG
-        startBackgroundCheck("OakBacklogClusterSyncService-backlog-waiting", 
new BackgroundCheck() {
+        startBackgroundCheck("OakBacklogClusterSyncService-backlog-waiting-" + 
view.getLocalClusterSyncTokenId(), new BackgroundCheck() {
 
             @Override
             public boolean check() {
                 try {
                     if (!idMapService.isInitialized()) {
-                        logger.info("waitWhileBacklog: could not 
initialize...");
+                        logSilencer.infoOrDebug("waitWhileBacklog-" + 
view.toShortString(),
+                                "waitWhileBacklog: could not initialize...");
                         consistencyHistory.addHistoryEntry(view, "could not 
initialize idMapService");
                         return false;
                     }
@@ -156,11 +161,13 @@ public class OakBacklogClusterSyncService extends 
AbstractServiceWithBackgroundC
                 }
                 BacklogStatus backlogStatus = getBacklogStatus(view);
                 if (backlogStatus == BacklogStatus.NO_BACKLOG) {
-                    logger.info("waitWhileBacklog: no backlog (anymore), 
done.");
+                    logSilencer.infoOrDebug("waitWhileBacklog-" + 
view.toShortString(),
+                            "waitWhileBacklog: no backlog (anymore), done.");
                     consistencyHistory.addHistoryEntry(view, "no backlog 
(anymore)");
                     return true;
                 } else {
-                    logger.info("waitWhileBacklog: backlogStatus still 
"+backlogStatus);
+                    logSilencer.infoOrDebug("waitWhileBacklog-" + 
view.toShortString(),
+                            "waitWhileBacklog: backlogStatus still 
"+backlogStatus);
                     // clear the cache to make sure to get the latest version 
in case something changed
                     idMapService.clearCache();
                     consistencyHistory.addHistoryEntry(view, "backlog status 
"+backlogStatus);
@@ -201,11 +208,16 @@ public class OakBacklogClusterSyncService extends 
AbstractServiceWithBackgroundC
 
             // 1) 'deactivating' must be empty
             if (deactivatingIds.length!=0) {
-                logger.info("getBacklogStatus: there are deactivating 
instances: "+Arrays.toString(deactivatingIds));
+                logSilencer.infoOrDebug("getBacklogStatus-hasBacklog-" + 
view.toShortString(),
+                        "getBacklogStatus: there are deactivating instances: 
"+Arrays.toString(deactivatingIds));
                 return BacklogStatus.HAS_BACKLOG;
             }
 
             ClusterView cluster = view.getLocalInstance().getClusterView();
+            LocalClusterView localCluster = null;
+            if (cluster instanceof LocalClusterView) {
+                localCluster = (LocalClusterView)cluster;
+            }
             Set<String> slingIds = new HashSet<>();
             for (InstanceDescription instance : cluster.getInstances()) {
                 slingIds.add(instance.getSlingId());
@@ -213,23 +225,31 @@ public class OakBacklogClusterSyncService extends 
AbstractServiceWithBackgroundC
 
             for(int i=0; i<activeIds.length; i++) {
                 int activeId = activeIds[i];
+                if (localCluster != null && 
localCluster.isPartiallyStarted(activeId)) {
+                    // ignore this one then
+                    continue;
+                }
                 String slingId = idMapService.toSlingId(activeId, 
resourceResolver);
                 // 2) all ids of the descriptor must have a mapping to slingIds
                 if (slingId == null) {
-                    logger.info("getBacklogStatus: no slingId found for active 
id: "+activeId);
+                    logSilencer.infoOrDebug("getBacklogStatus-undefined-" + 
view.toShortString(),
+                            "getBacklogStatus: no slingId found for active id: 
"+activeId);
                     return BacklogStatus.UNDEFINED;
                 }
                 // 3) all 'active' instances must be in the view
                 if (!slingIds.contains(slingId)) {
-                    logger.info("getBacklogStatus: active instance's 
("+activeId+") slingId ("+slingId+") not found in cluster ("+cluster+")");
+                    logSilencer.infoOrDebug("getBacklogStatus-hasBacklog-" + 
view.toShortString(),
+                            "getBacklogStatus: active instance's 
("+activeId+") slingId ("+slingId+") not found in cluster ("+cluster+")");
                     return BacklogStatus.HAS_BACKLOG;
                 }
             }
 
-            logger.info("getBacklogStatus: no backlog (anymore)");
+            logSilencer.infoOrDebug("getBacklogStatus-" + view.toShortString(),
+                    "getBacklogStatus: no backlog (anymore)");
             return BacklogStatus.NO_BACKLOG;
         } catch(Exception e) {
-            logger.info("getBacklogStatus: failed to determine backlog status: 
"+e);
+            logSilencer.infoOrDebug("getBacklogStatus-undefined-" + 
view.toShortString(),
+                    "getBacklogStatus: failed to determine backlog status: 
"+e);
             return BacklogStatus.UNDEFINED;
         } finally {
             logger.trace("getBacklogStatus: end");
diff --git 
a/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/SyncTokenService.java
 
b/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/SyncTokenService.java
index c5bc4d9..0fe5209 100644
--- 
a/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/SyncTokenService.java
+++ 
b/src/main/java/org/apache/sling/discovery/commons/providers/spi/base/SyncTokenService.java
@@ -28,6 +28,7 @@ import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.discovery.InstanceDescription;
 import org.apache.sling.discovery.commons.providers.BaseTopologyView;
 import org.apache.sling.discovery.commons.providers.spi.ClusterSyncService;
+import org.apache.sling.discovery.commons.providers.util.LogSilencer;
 import org.apache.sling.discovery.commons.providers.util.ResourceHelper;
 import org.apache.sling.settings.SlingSettingsService;
 import org.osgi.framework.Constants;
@@ -63,6 +64,8 @@ public class SyncTokenService extends 
AbstractServiceWithBackgroundCheck impleme
 
     protected ClusterSyncHistory clusterSyncHistory = new ClusterSyncHistory();
 
+    private final LogSilencer logSilencer = new LogSilencer(logger);
+
     public static SyncTokenService testConstructorAndActivate(
             DiscoveryLiteConfig commonsConfig,
             ResourceResolverFactory resourceResolverFactory,
@@ -128,7 +131,7 @@ public class SyncTokenService extends 
AbstractServiceWithBackgroundCheck impleme
 
     protected void syncToken(final BaseTopologyView view, final Runnable 
callback) {
 
-        startBackgroundCheck("SyncTokenService", new BackgroundCheck() {
+        startBackgroundCheck("SyncTokenService-" + 
view.getLocalClusterSyncTokenId(), new BackgroundCheck() {
 
             @Override
             public boolean check() {
@@ -172,9 +175,11 @@ public class SyncTokenService extends 
AbstractServiceWithBackgroundCheck impleme
             if (updateToken) {
                 syncTokens.put(slingId, syncTokenId);
                 resourceResolver.commit();
-                logger.info("storeMySyncToken: stored syncToken of 
slingId="+slingId+" as="+syncTokenId);
+                logSilencer.infoOrDebug("storeMySyncToken-" + syncTokenId,
+                        "storeMySyncToken: stored syncToken of 
slingId="+slingId+" as="+syncTokenId);
             } else {
-                logger.info("storeMySyncToken: syncToken was left unchanged 
for slingId="+slingId+" at="+syncTokenId);
+                logSilencer.infoOrDebug("storeMySyncToken-" + syncTokenId,
+                        "storeMySyncToken: syncToken was left unchanged for 
slingId="+slingId+" at="+syncTokenId);
             }
             return true;
         } catch (LoginException e) {
@@ -208,10 +213,12 @@ public class SyncTokenService extends 
AbstractServiceWithBackgroundCheck impleme
             boolean success = true;
             StringBuffer historyEntry = new StringBuffer();
             for (InstanceDescription instance : 
view.getLocalInstance().getClusterView().getInstances()) {
-                Object currentValue = syncTokens.get(instance.getSlingId());
+                String instanceSlingId = instance.getSlingId();
+                Object currentValue = syncTokens.get(instanceSlingId);
                 if (currentValue == null) {
                     String msg = "no syncToken yet of "+instance.getSlingId();
-                    logger.info("seenAllSyncTokens: " + msg);
+                    logSilencer.infoOrDebug("seenAllSyncToken-" + syncToken + 
"-no-" + instanceSlingId,
+                            "seenAllSyncTokens: " + msg);
                     if (historyEntry.length() != 0) {
                         historyEntry.append(",");
                     }
@@ -221,7 +228,8 @@ public class SyncTokenService extends 
AbstractServiceWithBackgroundCheck impleme
                     String msg = "syncToken of " + instance.getSlingId()
                                                 + " is " + currentValue
                                                 + " waiting for " + syncToken;
-                    logger.info("seenAllSyncTokens: " + msg);
+                    logSilencer.infoOrDebug("seenAllSyncToken-" + syncToken + 
"-wait-"+instanceSlingId,
+                            "seenAllSyncTokens: " + msg);
                     if (historyEntry.length() != 0) {
                         historyEntry.append(",");
                     }
@@ -230,15 +238,18 @@ public class SyncTokenService extends 
AbstractServiceWithBackgroundCheck impleme
                 }
             }
             if (!success) {
-                logger.info("seenAllSyncTokens: not yet seen all expected 
syncTokens (see above for details)");
+                logSilencer.infoOrDebug("seenAllSyncToken-result-" + syncToken,
+                        "seenAllSyncTokens: not yet seen all expected 
syncTokens (see above for details)");
                 clusterSyncHistory.addHistoryEntry(view, 
historyEntry.toString());
                 return false;
             } else {
-                clusterSyncHistory.addHistoryEntry(view, "seen all 
syncTokens");
+                clusterSyncHistory.addHistoryEntry(view,
+                        "seen all syncTokens");
             }
 
             resourceResolver.commit();
-            logger.info("seenAllSyncTokens: seen all syncTokens!");
+            logSilencer.infoOrDebug("seenAllSyncToken-result-" + syncToken,
+                    "seenAllSyncTokens: seen all syncTokens!");
             return true;
         } catch (LoginException e) {
             logger.error("seenAllSyncTokens: could not login: "+e, e);
diff --git 
a/src/main/java/org/apache/sling/discovery/commons/providers/spi/package-info.java
 
b/src/main/java/org/apache/sling/discovery/commons/providers/spi/package-info.java
index df9d9f2..6d1dbe0 100644
--- 
a/src/main/java/org/apache/sling/discovery/commons/providers/spi/package-info.java
+++ 
b/src/main/java/org/apache/sling/discovery/commons/providers/spi/package-info.java
@@ -20,9 +20,9 @@
 /**
  * Provides an SPI for providers, used by discovery.commons.providers.impl
  *
- * @version 1.0.0
+ * @version 1.1.0
  */
-@Version("1.0.0")
+@Version("1.1.0")
 package org.apache.sling.discovery.commons.providers.spi;
 
 import org.osgi.annotation.versioning.Version;
diff --git 
a/src/main/java/org/apache/sling/discovery/commons/providers/util/LogSilencer.java
 
b/src/main/java/org/apache/sling/discovery/commons/providers/util/LogSilencer.java
new file mode 100644
index 0000000..9b9ee0f
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/discovery/commons/providers/util/LogSilencer.java
@@ -0,0 +1,97 @@
+/*
+ * 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.commons.providers.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+
+/**
+ * Helper class to help reduce log.info output. It will avoid repetitive
+ * log.info calls and instead log future occurrences to log.debug
+ */
+public class LogSilencer {
+
+    private static final long DEFAULT_AUTO_RESET_DELAY_MINUTES = 10;
+
+    private final Logger logger;
+
+    private final Object syncObj = new Object();
+
+    private final long autoResetDelayMillis;
+
+    private Map<String, String> lastMsgPerCategory;
+
+    private long autoResetTime = 0;
+
+    public LogSilencer(Logger logger, long autoResetDelayMinutes) {
+        this.logger = logger;
+        if (autoResetDelayMinutes > 0) {
+            autoResetDelayMillis = 
TimeUnit.MINUTES.toMillis(autoResetDelayMinutes);
+        } else {
+            autoResetDelayMillis = 0;
+        }
+    }
+
+    public LogSilencer(Logger logger) {
+        this(logger, DEFAULT_AUTO_RESET_DELAY_MINUTES);
+    }
+
+    public void infoOrDebug(String category, String msg) {
+        final boolean doLogInfo;
+        synchronized (syncObj) {
+            if (autoResetTime == 0 || System.currentTimeMillis() > 
autoResetTime) {
+                reset();
+            }
+            if (lastMsgPerCategory == null) {
+                lastMsgPerCategory = new HashMap<>();
+            }
+            final String localLastMsg = lastMsgPerCategory.get(category);
+            if (localLastMsg == null || !localLastMsg.equals(msg)) {
+                doLogInfo = true;
+                lastMsgPerCategory.put(category, msg);
+            } else {
+                doLogInfo = false;
+            }
+        }
+        if (doLogInfo) {
+            logger.info("{} {}", msg, "(future identical logs go to debug)");
+        } else {
+            logger.debug(msg);
+        }
+    }
+
+    public void infoOrDebug(String msg) {
+        infoOrDebug(null, msg);
+    }
+
+    public void reset() {
+        synchronized (syncObj) {
+            lastMsgPerCategory = null;
+            if (autoResetDelayMillis == 0) {
+                autoResetTime = Long.MAX_VALUE;
+            } else {
+                autoResetTime = System.currentTimeMillis() + 
autoResetDelayMillis;
+            }
+        }
+    }
+
+}
diff --git 
a/src/main/java/org/apache/sling/discovery/commons/providers/util/package-info.java
 
b/src/main/java/org/apache/sling/discovery/commons/providers/util/package-info.java
index b4778b2..cbe2750 100644
--- 
a/src/main/java/org/apache/sling/discovery/commons/providers/util/package-info.java
+++ 
b/src/main/java/org/apache/sling/discovery/commons/providers/util/package-info.java
@@ -20,9 +20,9 @@
 /**
  * Provides some static helpers for providers of the Discovery API.
  *
- * @version 1.0.0
+ * @version 1.1.0
  */
-@Version("1.0.0")
+@Version("1.1.0")
 package org.apache.sling.discovery.commons.providers.util;
 
 import org.osgi.annotation.versioning.Version;
diff --git 
a/src/test/java/org/apache/sling/discovery/commons/providers/DummyTopologyView.java
 
b/src/test/java/org/apache/sling/discovery/commons/providers/DummyTopologyView.java
index e12b24d..140b747 100644
--- 
a/src/test/java/org/apache/sling/discovery/commons/providers/DummyTopologyView.java
+++ 
b/src/test/java/org/apache/sling/discovery/commons/providers/DummyTopologyView.java
@@ -30,6 +30,7 @@ import java.util.UUID;
 import org.apache.sling.discovery.ClusterView;
 import org.apache.sling.discovery.InstanceDescription;
 import org.apache.sling.discovery.InstanceFilter;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
 
 public class DummyTopologyView extends BaseTopologyView {
 
@@ -199,7 +200,15 @@ public class DummyTopologyView extends BaseTopologyView {
             String clusterId = id.getClusterView().getId();
             DefaultClusterView cluster = clusters.get(clusterId);
             if (cluster==null) {
-                cluster = new DefaultClusterView(clusterId);
+                final ClusterView origCluster = id.getClusterView();
+                if (origCluster instanceof LocalClusterView) {
+                    final LocalClusterView localOrigCluster = 
(LocalClusterView) origCluster;
+                    final LocalClusterView clonedCluster = new 
LocalClusterView(origCluster.getId(), 
localOrigCluster.getLocalClusterSyncTokenId());
+                    
clonedCluster.setPartiallyStartedClusterNodeIds(localOrigCluster.getPartiallyStartedClusterNodeIds());
+                    cluster = clonedCluster;
+                } else {
+                    cluster = new DefaultClusterView(clusterId);
+                }
                 clusters.put(clusterId, cluster);
             }
             DefaultInstanceDescription clone = clone(cluster, id);
diff --git 
a/src/test/java/org/apache/sling/discovery/commons/providers/base/TestViewStateManager.java
 
b/src/test/java/org/apache/sling/discovery/commons/providers/base/TestViewStateManager.java
index 3b29202..c48a115 100644
--- 
a/src/test/java/org/apache/sling/discovery/commons/providers/base/TestViewStateManager.java
+++ 
b/src/test/java/org/apache/sling/discovery/commons/providers/base/TestViewStateManager.java
@@ -21,12 +21,15 @@ package org.apache.sling.discovery.commons.providers.base;
 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.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Random;
 import java.util.UUID;
 import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -43,6 +46,7 @@ import 
org.apache.sling.discovery.commons.providers.DefaultInstanceDescription;
 import org.apache.sling.discovery.commons.providers.DummyTopologyView;
 import org.apache.sling.discovery.commons.providers.EventHelper;
 import org.apache.sling.discovery.commons.providers.spi.ClusterSyncService;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -716,4 +720,173 @@ public class TestViewStateManager {
         assertFalse(mgr.onlyDiffersInProperties(view6));
     }
 
+    @Test
+    public void testSuppression_withDelay() throws Exception {
+        doTestSuppression(true);
+    }
+
+    @Test
+    public void testSuppression_withoutDelay() throws Exception {
+        doTestSuppression(false);
+    }
+
+    private void doTestSuppression(boolean minEventDelayHelepr) throws 
Exception {
+        logger.info("testSuppression: start");
+
+        final AtomicReference<TopologyView> topologyRef = new 
AtomicReference<>();
+        if (minEventDelayHelepr) {
+            mgr.installMinEventDelayHandler(new DiscoveryService() {
+
+                @Override
+                public TopologyView getTopology() {
+                    return topologyRef.get();
+                }
+            }, new DummyScheduler(), 1);
+        }
+
+        final String slingId1 = UUID.randomUUID().toString();
+        final String slingId2 = UUID.randomUUID().toString();
+        final String clusterId = UUID.randomUUID().toString();
+        final String syncToken1 = "s1";
+        final LocalClusterView cluster1 = new LocalClusterView(clusterId, 
syncToken1);
+        final DummyTopologyView view1 = new DummyTopologyView(syncToken1)
+                .addInstance(slingId1, cluster1, true, true);
+        final String syncToken4 = "s4";
+        final LocalClusterView cluster4 = new LocalClusterView(clusterId, 
syncToken4);
+        final DummyTopologyView view4 = new DummyTopologyView(syncToken4)
+                .addInstance(slingId1, cluster4, true, true)
+                .addInstance(slingId2, cluster4, false, false);
+
+        final DummyListener listener = new DummyListener();
+        mgr.bind(listener);
+        TestHelper.assertNoEvents(listener);
+        mgr.handleActivated();
+        TestHelper.assertNoEvents(listener);
+
+        logger.info("testSuppression: handleNewView(view1)");
+        mgr.handleNewView(view1);
+        topologyRef.set(view1);
+        assertEquals(0, mgr.waitForAsyncEvents(5000));
+        TopologyEvent initEvent = EventHelper.newInitEvent(view1);
+        assertEvents(listener, initEvent);
+
+        // for a view change to "go undetected" ie not trigger any topology 
change,
+        // the list of instances must remain the same
+        // (but the syncToken can differ and it can have suppressed 
clusterNodeIds)
+        for(int i = 0; i < 100; i++) {
+            final String syncToken2SameAsS1 = "s1";
+            final LocalClusterView cluster1Suppressed = new 
LocalClusterView(clusterId, syncToken2SameAsS1);
+            final DummyTopologyView view1Suppressed = new 
DummyTopologyView(syncToken2SameAsS1)
+                    .addInstance(slingId1, cluster1Suppressed, true, true);
+            
cluster1Suppressed.setPartiallyStartedClusterNodeIds(Arrays.asList(1));
+            logger.info("testSuppression: handleNewView(view2[a])");
+            mgr.handleNewView(view1Suppressed);
+            topologyRef.set(view1Suppressed);
+            assertEquals(0, mgr.waitForAsyncEvents(5000));
+            TestHelper.assertNoEvents(listener);
+        }
+        for(int i = 0; i < 100; i++) {
+            final String syncToken2Different = "s1Suppressed";
+            final LocalClusterView cluster1Suppressed = new 
LocalClusterView(clusterId, syncToken2Different);
+            final DummyTopologyView view1Suppressed = new 
DummyTopologyView(syncToken2Different)
+                    .addInstance(slingId1, cluster1Suppressed, true, true);
+            
cluster1Suppressed.setPartiallyStartedClusterNodeIds(Arrays.asList(1));
+            logger.info("testSuppression: handleNewView(view2[b])");
+            mgr.handleNewView(view1Suppressed);
+            topologyRef.set(view1Suppressed);
+            assertEquals(0, mgr.waitForAsyncEvents(5000));
+            TestHelper.assertNoEvents(listener);
+        }
+
+        logger.info("testSuppression: handleNewView(view4)");
+        mgr.handleNewView(view4);
+        topologyRef.set(view4);
+        assertEquals(0, mgr.waitForAsyncEvents(5000));
+        assertEvents(listener, EventHelper.newChangingEvent(view1), 
EventHelper.newChangedEvent(view1, view4));
+    }
+
+    @Test
+    public void testEqualsIgnoreSyncToken() throws Exception {
+        // no previous view, so returns false
+        assertFalse(mgr.equalsIgnoreSyncToken(null));
+
+        final String clusterId = UUID.randomUUID().toString();
+        final String syncToken1 = "s1";
+        final DummyTopologyView view1 = createTopology(clusterId, syncToken1, 
1);
+        assertTrue(mgr.handleNewViewNonDelayed(view1));
+        try {
+            mgr.equalsIgnoreSyncToken(null);
+            fail("should have thrown a NPE");
+        } catch(RuntimeException e) {
+            // ok
+        }
+        assertTrue(mgr.equalsIgnoreSyncToken(view1));
+
+        DummyTopologyView view;
+        for(int i = 1; i < 10; i++) {
+            // same instances, same syncToken, no partiallyStartedInstances => 
true
+            view = createTopology(view1, syncToken1, 0);
+            assertTrue(mgr.equalsIgnoreSyncToken(view));
+            // same instances, same syncToken, with partiallyStartedInstances 
=> true
+            addPartiallyStartedInstance(view, 1);
+            assertTrue(mgr.equalsIgnoreSyncToken(view));
+
+            // different instances, same syncToken, no 
partiallyStartedInstances => false
+            view = createTopology(view1, syncToken1, i);
+            assertFalse(mgr.equalsIgnoreSyncToken(view));
+            // different instances, same syncToken, with 
partiallyStartedInstances => false
+            addPartiallyStartedInstance(view, 1);
+            assertFalse(mgr.equalsIgnoreSyncToken(view));
+
+            // same instances, different syncToken, no 
partiallyStartedInstances => false
+            final String differentSyncToken = "s2";
+            view = createTopology(view1, differentSyncToken, 0);
+            assertFalse(mgr.equalsIgnoreSyncToken(view));
+            // same instances, different syncToken, with 
partiallyStartedInstances => true
+            view = createTopology(view1, differentSyncToken, 0);
+            addPartiallyStartedInstance(view, 1);
+            assertTrue(mgr.equalsIgnoreSyncToken(view));
+
+            // different instances, different syncToken, no 
partiallyStartedInstances => false
+            view = createTopology(view1, differentSyncToken, i);
+            assertFalse(mgr.equalsIgnoreSyncToken(view));
+            // different instances, different syncToken, with 
partiallyStartedInstances => false
+            view = createTopology(view1, differentSyncToken, i);
+            addPartiallyStartedInstance(view, 1);
+            assertFalse(mgr.equalsIgnoreSyncToken(view));
+        }
+    }
+
+    private void addPartiallyStartedInstance(TopologyView view, Integer... 
clusterNodeIds) {
+        LocalClusterView local = (LocalClusterView) 
view.getLocalInstance().getClusterView();
+        local.setPartiallyStartedClusterNodeIds(Arrays.asList(clusterNodeIds));
+    }
+
+    private DummyTopologyView createTopology(String clusterId, String 
syncToken, int numInstances) {
+        final LocalClusterView cluster = new LocalClusterView(clusterId, 
syncToken);
+        final DummyTopologyView view = new DummyTopologyView(syncToken);
+        for(int i = 0; i < numInstances; i++) {
+            view.addInstance(UUID.randomUUID().toString(), cluster, i == 0, i 
== 0);
+        }
+        return view;
+    }
+
+    private DummyTopologyView createTopology(DummyTopologyView base, String 
syncToken,
+            int numAdditionalInstances) {
+        return createTopology(base.getLocalInstance().getClusterView(), 
syncToken,
+                numAdditionalInstances);
+    }
+
+    private DummyTopologyView createTopology(ClusterView baseCluster, String 
syncToken,
+            int numAdditionalInstances) {
+        final LocalClusterView cluster = new 
LocalClusterView(baseCluster.getId(), syncToken);
+        final DummyTopologyView view2 = new DummyTopologyView(syncToken);
+        for (InstanceDescription inst : baseCluster.getInstances()) {
+            view2.addInstance(inst.getSlingId(), cluster, inst.isLeader(), 
inst.isLocal());
+        }
+        for (int i = 0; i < numAdditionalInstances; i++) {
+            view2.addInstance(UUID.randomUUID().toString(), cluster, false, 
false);
+        }
+        return view2;
+    }
 }
\ No newline at end of file
diff --git 
a/src/test/java/org/apache/sling/discovery/commons/providers/spi/LocalClusterViewTest.java
 
b/src/test/java/org/apache/sling/discovery/commons/providers/spi/LocalClusterViewTest.java
new file mode 100644
index 0000000..243f0db
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/discovery/commons/providers/spi/LocalClusterViewTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.commons.providers.spi;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class LocalClusterViewTest {
+
+    private final LocalClusterView view = new LocalClusterView("id", "token");
+
+    @Test
+    public void partiallyStarted() {
+        assertFalse(view.hasPartiallyStartedInstances());
+        assertFalse(view.isPartiallyStarted(42));
+
+        
view.setPartiallyStartedClusterNodeIds(Collections.<Integer>emptyList());
+        assertFalse(view.hasPartiallyStartedInstances());
+        assertFalse(view.isPartiallyStarted(42));
+
+        view.setPartiallyStartedClusterNodeIds(Arrays.asList(1, 2, 3));
+        assertTrue(view.hasPartiallyStartedInstances());
+        assertFalse(view.isPartiallyStarted(42));
+        assertTrue(view.isPartiallyStarted(1));
+        assertFalse(view.isPartiallyStarted(null));
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/discovery/commons/providers/spi/base/TestOakSyncTokenService.java
 
b/src/test/java/org/apache/sling/discovery/commons/providers/spi/base/TestOakSyncTokenService.java
index ce8b31a..6f99021 100644
--- 
a/src/test/java/org/apache/sling/discovery/commons/providers/spi/base/TestOakSyncTokenService.java
+++ 
b/src/test/java/org/apache/sling/discovery/commons/providers/spi/base/TestOakSyncTokenService.java
@@ -24,6 +24,7 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import java.lang.reflect.Field;
+import java.util.Arrays;
 import java.util.UUID;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
@@ -36,11 +37,11 @@ import 
org.apache.sling.discovery.commons.providers.ViewStateManager;
 import org.apache.sling.discovery.commons.providers.base.DummyListener;
 import org.apache.sling.discovery.commons.providers.base.TestHelper;
 import 
org.apache.sling.discovery.commons.providers.base.ViewStateManagerFactory;
+import org.apache.sling.discovery.commons.providers.spi.LocalClusterView;
 import 
org.apache.sling.discovery.commons.providers.spi.base.AbstractServiceWithBackgroundCheck.BackgroundCheckRunnable;
 import org.apache.sling.jcr.api.SlingRepository;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -223,4 +224,85 @@ public class TestOakSyncTokenService {
         Object backgroundCheckRunnable = field.get(idMapService);
         return (BackgroundCheckRunnable) backgroundCheckRunnable;
     }
+
+    @Test
+    public void testPartiallyStartedInstance() throws Exception {
+        logger.info("testPartiallyStartedInstance: start");
+        OakBacklogClusterSyncService cs = 
OakBacklogClusterSyncService.testConstructorAndActivate(new 
SimpleCommonsConfig(), idMapService1, new DummySlingSettingsService(slingId1), 
factory1);
+        Lock lock = new ReentrantLock();
+        ViewStateManager vsm = 
ViewStateManagerFactory.newViewStateManager(lock, cs);
+        DummyListener l = new DummyListener();
+        vsm.bind(l);
+        vsm.handleActivated();
+
+        final DummyTopologyView view1 = TestHelper.newView(true, slingId1, 
slingId1, slingId1);
+        {
+            // simulate a view with just itself (slingId1 / 1)
+            vsm.handleNewView(view1);
+            cs.triggerBackgroundCheck();
+            assertEquals(0, vsm.waitForAsyncEvents(1000));
+            assertEquals(0, l.countEvents());
+            DescriptorHelper.setDiscoveryLiteDescriptor(factory1, new 
DiscoveryLiteDescriptorBuilder().me(1).seq(1).activeIds(1).setFinal(true));
+            assertTrue(idMapService1.waitForInit(5000));
+            cs.triggerBackgroundCheck();
+            assertEquals(0, vsm.waitForAsyncEvents(1000));
+            assertEquals(1, l.countEvents());
+        }
+
+        assertTrue(idMapService1.waitForInit(5000));
+
+        {
+            // simulate a new instance coming up - first it will show up in 
oak leases/lite-view
+            DescriptorHelper.setDiscoveryLiteDescriptor(factory1, new 
DiscoveryLiteDescriptorBuilder().me(1).seq(2).activeIds(1, 2).setFinal(true));
+
+            // the view is still the same (only contains slingId1) - but it 
has the flag 'partial' set
+            final String syncToken2 = "s2";
+            final String clusterId = 
view1.getLocalInstance().getClusterView().getId();
+            final LocalClusterView cluster1Suppressed = new 
LocalClusterView(clusterId, syncToken2);
+            final DummyTopologyView view1Suppressed = new 
DummyTopologyView(syncToken2)
+                    .addInstance(slingId1, cluster1Suppressed, true, true);
+            
cluster1Suppressed.setPartiallyStartedClusterNodeIds(Arrays.asList(2));
+
+            vsm.handleNewView(view1Suppressed);
+            cs.triggerBackgroundCheck();
+            assertEquals(0, vsm.waitForAsyncEvents(1000));
+            assertEquals(1, l.countEvents());
+        }
+        final String slingId2 = UUID.randomUUID().toString();
+        {
+            // now define slingId for activeId == 2
+            IdMapService idMapService2 = IdMapService.testConstructor(
+                    new SimpleCommonsConfig(), new 
DummySlingSettingsService(slingId2), factory2);
+            DescriptorHelper.setDiscoveryLiteDescriptor(factory2, new 
DiscoveryLiteDescriptorBuilder().setFinal(true).me(2).seq(2).activeIds(1, 2));
+            assertTrue(idMapService2.waitForInit(5000));
+        }
+        {
+            // now that shouldn't have triggered anything yet towards the 
listeners
+            final String syncToken2 = "s2";
+            final String clusterId = 
view1.getLocalInstance().getClusterView().getId();
+            final LocalClusterView cluster1Suppressed = new 
LocalClusterView(clusterId, syncToken2);
+            final DummyTopologyView view1Suppressed = new 
DummyTopologyView(syncToken2)
+                    .addInstance(slingId1, cluster1Suppressed, true, true);
+            
cluster1Suppressed.setPartiallyStartedClusterNodeIds(Arrays.asList(2));
+
+            vsm.handleNewView(view1Suppressed);
+            cs.triggerBackgroundCheck();
+            assertEquals(0, vsm.waitForAsyncEvents(1000));
+            assertEquals(1, l.countEvents());
+        }
+        {
+            // now let's finish slingId2 startup - only this should trigger 
CHANGING/CHANGED
+            final String syncToken2 = "s2";
+            final String clusterId = 
view1.getLocalInstance().getClusterView().getId();
+            final LocalClusterView cluster1Suppressed = new 
LocalClusterView(clusterId, syncToken2);
+            final DummyTopologyView view2 = new DummyTopologyView(syncToken2)
+                    .addInstance(slingId1, cluster1Suppressed, true, true)
+                    .addInstance(slingId2, cluster1Suppressed, false, false);
+            vsm.handleNewView(view2);
+            cs.triggerBackgroundCheck();
+            assertEquals(0, vsm.waitForAsyncEvents(1000));
+            assertEquals(3, l.countEvents());
+        }
+        logger.info("testPartiallyStartedInstance: end");
+    }
 }

Reply via email to