/* * 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.base; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; import org.apache.sling.commons.scheduler.Scheduler; import org.apache.sling.discovery.DiscoveryService; import org.apache.sling.discovery.InstanceDescription; import org.apache.sling.discovery.TopologyEvent; import org.apache.sling.discovery.TopologyEvent.Type; import org.apache.sling.discovery.TopologyEventListener; import org.apache.sling.discovery.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.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The ViewStateManager is at the core of managing TopologyEventListeners, * the 'view state' (changing vs changed) and sending out the appropriate * and according TopologyEvents to the registered listeners. * <p> * Note re synchronization: this class rquires a lock object to be passed * in the constructor - this will be applied to all public methods * appropriately. Additionally, the ClusterSyncService callback will * also be locked using the provided lock object. */ public class ViewStateManagerImpl implements ViewStateManager { private static final Logger logger = LoggerFactory.getLogger(ViewStateManagerImpl.class); /** * List of bound listeners that have already received their INIT event - others are in unInitializedEventListeners. * @see ViewStateManagerImpl#unInitializedEventListeners */ private List<TopologyEventListener> eventListeners = new ArrayList<TopologyEventListener>(); /** * List of bound listeners that have not yet received their TOPOLOGY_INIT event - * once they are sent the TOPOLOGY_INIT event they are moved to eventListeners (and stay there). * <p> * This list becomes necessary for cases where the bind() happens before activate, or after activate but at a time * when the topology is TOPOLOGY_CHANGING - at which point an TOPOLOGY_INIT event can not yet be sent. * @see ViewStateManagerImpl#eventListeners */ private List<TopologyEventListener> unInitializedEventListeners = new ArrayList<TopologyEventListener>(); /** * Set true when the bundle.activate() was called, false if not yet or when deactivate() is called. * <p> * This controls whether handleChanging() and handleNewView() should cause any events * to be sent - which they do not if called before handleActivated() (or after handleDeactivated()) * @see ViewStateManagerImpl#handleActivated() * @see ViewStateManagerImpl#handleChanging() * @see ViewStateManagerImpl#handleNewView(BaseTopologyView) * @see ViewStateManagerImpl#handleDeactivated() */ private boolean activated; /** * Represents the 'newView' passed to handleNewTopologyView at the most recent invocation. * <p> * This is used for: * <ul> * <li>sending with the TOPOLOGY_INIT event to newly bound listeners or at activate time</li> * <li>sending as oldView (marked not current) with the TOPOLOGY_CHANGING event</li> * <li>sending as oldView (marked not current in case handleChanging() was not invoked) with the TOPOLOGY_CHANGED event</li> * </ul> */ private BaseTopologyView previousView; /** * Set to true when handleChanging is called - set to false in handleNewView. * When this goes true, a TOPOLOGY_CHANGING is sent. * When this goes false, a TOPOLOGY_CHANGED is sent. */ private boolean isChanging; /** * The lock object with which all public methods are guarded - to be provided in the constructor. */ protected final Lock lock; /** * An optional ClusterSyncService can be provided in the constructor which, when set, will * be invoked upon a new view becoming available (in handleNewView) and the actual * TOPOLOGY_CHANGED event will only be sent once the ClusterSyncService.sync method * does the according callback (which can be synchronous or asynchronous again). */ private final ClusterSyncService consistencyService; /** * A modification counter that increments on each of the following methods: * <ul> * <li>handleActivated()</li> * <li>handleDeactivated()</li> * <li>handleChanging()</li> * <li>handleNewView()</li> * </ul> * with the intent that - when a consistencyService is set - the callback from the * ClusterSyncService can check if any of the above methods was invoked - and if so, * it does not send the TOPOLOGY_CHANGED event due to those new facts that happened * while it was synching with the repository. */ private int modCnt = 0; /** SLING-4755 : reference to the background AsyncEventSender. Started/stopped in activate/deactivate **/ private AsyncEventSender asyncEventSender; /** SLING-5030 : this map contains the event last sent to each listener to prevent duplicate CHANGING events when scheduler is broken**/ private Map<TopologyEventListener,TopologyEvent.Type> lastEventMap = new HashMap<TopologyEventListener, TopologyEvent.Type>(); private MinEventDelayHandler minEventDelayHandler; /** * Creates a new ViewStateManager which synchronizes each method with the given * lock and which optionally uses the given ClusterSyncService to sync the repository * upon handling a new view where an instances leaves the local cluster. * @param lock the lock to be used - must not be null * @param consistencyService optional (ie can be null) - the ClusterSyncService to * sync the repository upon handling a new view where an instances leaves the local cluster. */ ViewStateManagerImpl(Lock lock, ClusterSyncService consistencyService) { if (lock==null) { throw new IllegalArgumentException("lock must not be null"); } this.lock = lock; this.consistencyService = consistencyService; } @Override public void installMinEventDelayHandler(DiscoveryService discoveryService, Scheduler scheduler, long minEventDelaySecs) { this.minEventDelayHandler = new MinEventDelayHandler(this, lock, discoveryService, scheduler, minEventDelaySecs); } protected boolean hadPreviousView() { return previousView!=null; } protected boolean unchanged(BaseTopologyView newView) { if (isChanging) { return false; } if (previousView==null) { return false; } return previousView.equals(newView); } /* (non-Javadoc) * @see org.apache.sling.discovery.commons.providers.impl.ViewStateManager#bind(org.apache.sling.discovery.TopologyEventListener) */ @Override public void bind(final TopologyEventListener eventListener) { logger.trace("bind: start {}", eventListener); lock.lock(); try{ logger.debug("bind: Binding TopologyEventListener {}", eventListener); if (eventListeners.contains(eventListener) || unInitializedEventListeners.contains(eventListener)) { logger.info("bind: TopologyEventListener already registered: "+eventListener); return; } if (activated) { // check to see in which state we are if (isChanging || (previousView==null)) { // then we cannot send the TOPOLOGY_INIT at this point - need to delay this if (logger.isDebugEnabled()) { logger.debug("bind: view is not yet/currently not defined (isChanging: "+isChanging+ ", previousView==null: "+(previousView==null)+ ", delaying INIT to "+eventListener); } unInitializedEventListeners.add(eventListener); } else { // otherwise we can send the TOPOLOGY_INIT now logger.debug("bind: view is defined, sending INIT now to {}", eventListener); enqueue(eventListener, EventHelper.newInitEvent(previousView), true); eventListeners.add(eventListener); } } else { logger.debug("bind: not yet activated, delaying INIT to {}", eventListener); unInitializedEventListeners.add(eventListener); } } finally { lock.unlock(); logger.trace("bind: end"); } } /* (non-Javadoc) * @see org.apache.sling.discovery.commons.providers.impl.ViewStateManager#unbind(org.apache.sling.discovery.TopologyEventListener) */ @Override public boolean unbind(final TopologyEventListener eventListener) { logger.trace("unbind: start {}", eventListener); lock.lock(); try{ logger.debug("unbind: Releasing TopologyEventListener {}", eventListener); // even though a listener must always only ever exist in one of the two, // the unbind we do - for safety-paranoia-reasons - remove them from both final boolean a = eventListeners.remove(eventListener); final boolean b = unInitializedEventListeners.remove(eventListener); return a || b; } finally { lock.unlock(); logger.trace("unbind: end"); } } private void enqueue(final TopologyEventListener da, final TopologyEvent event, boolean logInfo) { logger.trace("enqueue: start: topologyEvent {}, to {}", event, da); if (asyncEventSender==null) { // this should never happen - sendTopologyEvent should only be called // when activated logger.warn("enqueue: asyncEventSender is null, cannot send event ({}, {})!", da, event); return; } if (lastEventMap.get(da)==event.getType() && event.getType()==Type.TOPOLOGY_CHANGING) { // don't sent TOPOLOGY_CHANGING twice if (logInfo) { logger.info("enqueue: listener already got TOPOLOGY_CHANGING: {}", da); } else { logger.debug("enqueue: listener already got TOPOLOGY_CHANGING: {}", da); } return; } if (logInfo) { logger.info("enqueue: enqueuing topologyEvent {}, to {}", EventHelper.toShortString(event), da); } else { logger.debug("enqueue: enqueuing topologyEvent {}, to {}", event, da); } asyncEventSender.enqueue(da, event); lastEventMap.put(da, event.getType()); logger.trace("enqueue: sending topologyEvent {}, to {}", event, da); } /** Internal helper method that sends a given event to a list of listeners **/ private void enqueueForAll(final List<TopologyEventListener> audience, final TopologyEvent event) { logger.info("enqueueForAll: sending topologyEvent {}, to all ({}) listeners", EventHelper.toShortString(event), audience.size()); for (Iterator<TopologyEventListener> it = audience.iterator(); it.hasNext();) { TopologyEventListener topologyEventListener = it.next(); enqueue(topologyEventListener, event, false); } logger.trace("enqueueForAll: sent topologyEvent {}, to all ({}) listeners", event, audience.size()); } /* (non-Javadoc) * @see org.apache.sling.discovery.commons.providers.impl.ViewStateManager#handleActivated() */ @Override public void handleActivated() { logger.trace("handleActivated: start"); lock.lock(); try{ logger.debug("handleActivated: activating the ViewStateManager"); activated = true; modCnt++; // SLING-4755 : start the asyncEventSender in the background // will be stopped in deactivate (at which point // all pending events will still be sent but no // new events can be enqueued) asyncEventSender = new AsyncEventSender(); Thread th = new Thread(asyncEventSender); th.setName("Discovery-AsyncEventSender"); th.setDaemon(true); th.start(); if (previousView!=null && !isChanging) { enqueueForAll(unInitializedEventListeners, EventHelper.newInitEvent(previousView)); eventListeners.addAll(unInitializedEventListeners); unInitializedEventListeners.clear(); } logger.debug("handleActivated: activated the ViewStateManager"); } finally { lock.unlock(); logger.trace("handleActivated: finally"); } } /* (non-Javadoc) * @see org.apache.sling.discovery.commons.providers.impl.ViewStateManager#handleDeactivated() */ @Override public void handleDeactivated() { logger.trace("handleDeactivated: start"); lock.lock(); try{ logger.debug("handleDeactivated: deactivating the ViewStateManager"); activated = false; modCnt++; if (asyncEventSender!=null) { // it should always be not-null though asyncEventSender.flushThenStop(); asyncEventSender = null; } if (previousView!=null) { previousView.setNotCurrent(); logger.trace("handleDeactivated: setting previousView to null"); previousView = null; } if (consistencyService!=null) { consistencyService.cancelSync(); } if (minEventDelayHandler!=null) { minEventDelayHandler.cancelDelaying(); } logger.trace("handleDeactivated: setting isChanging to false"); isChanging = false; eventListeners.clear(); unInitializedEventListeners.clear(); logger.debug("handleDeactivated: deactivated the ViewStateManager"); } finally { lock.unlock(); logger.trace("handleDeactivated: finally"); } } /* (non-Javadoc) * @see org.apache.sling.discovery.commons.providers.impl.ViewStateManager#handleChanging() */ @Override public void handleChanging() { logger.trace("handleChanging: start"); lock.lock(); try{ if (isChanging) { // if isChanging: then this is no news // hence: return asap logger.debug("handleChanging: was already changing - ignoring."); return; } modCnt++; // whether activated or not: set isChanging to true now logger.trace("handleChanging: setting isChanging to true"); isChanging = true; if (!activated) { // if not activated: we can only start sending events once activated // hence returning here - after isChanging was set to true accordingly // note however, that if !activated, there should be no eventListeners yet // all of them should be in unInitializedEventListeners at the moment // waiting for activate() and handleNewTopologyView logger.debug("handleChanging: not yet activated - ignoring."); return; } if (previousView==null) { // then nothing further to do - this is a very early changing event // before even the first view was available logger.debug("handleChanging: no previousView set - ignoring."); return; } logger.debug("handleChanging: sending TOPOLOGY_CHANGING to initialized listeners"); previousView.setNotCurrent(); enqueueForAll(eventListeners, EventHelper.newChangingEvent(previousView)); } finally { lock.unlock(); logger.trace("handleChanging: finally"); } } /* (non-Javadoc) * @see org.apache.sling.discovery.commons.providers.impl.ViewStateManager#handleNewView(org.apache.sling.discovery.commons.providers.BaseTopologyView) */ @Override public void handleNewView(final BaseTopologyView newView) { logger.trace("handleNewView: start, newView={}", newView); if (newView==null) { throw new IllegalArgumentException("newView must not be null"); } if (!newView.isCurrent()) { logger.debug("handleNewView: newView is not current - calling handleChanging."); handleChanging(); return;// false; } // paranoia-testing: InstanceDescription localInstance = newView.getLocalInstance(); if (localInstance==null) { throw new IllegalStateException("newView does not contain the local instance - hence cannot be current"); } if (!localInstance.isLocal()) { throw new IllegalStateException("newView's local instance is not isLocal - very unexpected - hence cannot be current"); } // cancel any potentially ongoing sync if (consistencyService != null) { consistencyService.cancelSync(); } logger.debug("handleNewView: newView is current, so trying with minEventDelayHandler..."); if (minEventDelayHandler!=null) { if (minEventDelayHandler.handlesNewView(newView)) { return;// true; } else { logger.debug("handleNewView: event delaying not applicable this time, invoking hanldeNewViewNonDelayed next."); } } else { logger.debug("handleNewView: minEventDelayHandler not set, invoking hanldeNewViewNonDelayed..."); } handleNewViewNonDelayed(newView); } boolean handleNewViewNonDelayed(final BaseTopologyView newView) { logger.trace("handleNewViewNonDelayed: start"); lock.lock(); try{ logger.debug("handleNewViewNonDelayed: start, newView={}", newView); if (!newView.isCurrent()) { logger.error("handleNewViewNonDelayed: newView must be current"); throw new IllegalArgumentException("newView must be current"); } modCnt++; if (!isChanging) { // 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)) { // 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"); return false; } if (previousView==null || !onlyDiffersInProperties(newView)) { logger.debug("handleNewViewNonDelayed: implicitly triggering a handleChanging as we were not in changing state"); handleChanging(); logger.debug("handleNewViewNonDelayed: implicitly triggering of a handleChanging done"); } } if (!activated) { // then all we can do is to pass this on to previoueView logger.trace("handleNewViewNonDelayed: setting previousView to {}", newView); previousView = newView; // plus set the isChanging flag to false logger.trace("handleNewViewNonDelayed: setting isChanging to false"); isChanging = false; // other than that, we can't currently send any event, before activate logger.debug("handleNewViewNonDelayed: not yet activated - ignoring"); return true; } // now check if the view indeed changed or if it was just the properties 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); previousView.setNotCurrent(); enqueueForAll(eventListeners, EventHelper.newPropertiesChangedEvent(previousView, newView)); logger.trace("handleNewViewNonDelayed: setting previousView to {}", newView); previousView = newView; return true; } final boolean invokeClusterSyncService; if (consistencyService==null) { logger.info("handleNewViewNonDelayed: no ClusterSyncService set - continuing directly."); invokeClusterSyncService = false; } else { // there used to be a distinction between: // * if no previousView is set, then we should invoke the consistencyService // * if one was set, then we only invoke it if any instance left the cluster // this algorithm would not work though, as the newly joining instance // would always have (previousView==null) - thus would always do the syncToken // thingy - while the existing instances would think: ah, no instance left, // so it is not so urgent to do the syncToken. // at which point the joining instance would wait forever for a syncToken // to arrive. // // which is a long way of saying: if the consistencyService is configured, // then we always use it, hence: logger.info("handleNewViewNonDelayed: ClusterSyncService set - invoking..."); invokeClusterSyncService = true; } if (invokeClusterSyncService) { // if "instances from the local cluster have been removed" // then: // run the set consistencyService final int lastModCnt = modCnt; logger.info("handleNewViewNonDelayed: invoking waitForAsyncEvents, then clusterSyncService (modCnt={})", modCnt); asyncEventSender.enqueue(new AsyncEvent() { @Override public String toString() { return "the waitForAsyncEvents-flush-token-"+hashCode(); } @Override public void trigger() { // when this event is triggered we're guaranteed to have // no more async events - cos the async events are handled // in a queue and this AsyncEvent was put at the end of the // queue at enqueue time. So now e can go ahead. // the plus using such a token event is that others when // calling waitForAsyncEvent() will get blocked while this // 'token async event' is handled. Which is what we explicitly want. lock.lock(); try{ if (modCnt!=lastModCnt) { logger.info("handleNewViewNonDelayed/waitForAsyncEvents.run: modCnt changed (from {} to {}) - ignoring", lastModCnt, modCnt); return; } logger.info("handleNewViewNonDelayed/waitForAsyncEvents.run: done, now invoking consistencyService (modCnt={})", modCnt); consistencyService.sync(newView, new Runnable() { public void run() { logger.trace("consistencyService.callback.run: start. acquiring lock..."); lock.lock(); try{ logger.debug("consistencyService.callback.run: lock aquired. (modCnt should be {}, is {})", lastModCnt, modCnt); if (modCnt!=lastModCnt) { logger.info("consistencyService.callback.run: modCnt changed (from {} to {}) - ignoring", lastModCnt, modCnt); return; } logger.info("consistencyService.callback.run: invoking doHandleConsistent."); // else: doHandleConsistent(newView); } finally { lock.unlock(); logger.trace("consistencyService.callback.run: end."); } } }); } finally { lock.unlock(); } } }); } else { // otherwise we're either told not to use any ClusterSyncService // 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"); doHandleConsistent(newView); } logger.debug("handleNewViewNonDelayed: end"); return true; } finally { lock.unlock(); logger.trace("handleNewViewNonDelayed: finally"); } } protected boolean onlyDiffersInProperties(BaseTopologyView newView) { if (previousView==null) { return false; } if (newView==null) { throw new IllegalArgumentException("newView must not be null"); } String previousSyncTokenId = null; String newSyncTokenId = null; try{ previousSyncTokenId = previousView.getLocalClusterSyncTokenId(); } catch(IllegalStateException re) { previousSyncTokenId = null; } try{ newSyncTokenId = newView.getLocalClusterSyncTokenId(); } catch(IllegalStateException re) { newSyncTokenId = null; } if ((previousSyncTokenId == null && newSyncTokenId != null) || (newSyncTokenId == null && previousSyncTokenId != null) || (previousSyncTokenId!=null && !previousSyncTokenId.equals(newSyncTokenId))) { return false; } if (previousView.getInstances().size()!=newView.getInstances().size()) { return false; } if (previousView.equals(newView)) { return false; } Set<String> newIds = new HashSet<String>(); for(InstanceDescription newInstance : newView.getInstances()) { newIds.add(newInstance.getSlingId()); } for(InstanceDescription oldInstance : previousView.getInstances()) { InstanceDescription newInstance = newView.getInstance(oldInstance.getSlingId()); if (newInstance == null) { return false; } if (oldInstance.isLeader() != newInstance.isLeader()) { return false; } if (!oldInstance.getClusterView().getId().equals(newInstance.getClusterView().getId())) { return false; } } return true; } private void doHandleConsistent(BaseTopologyView newView) { logger.trace("doHandleConsistent: start"); // unset the isChanging flag logger.trace("doHandleConsistent: setting isChanging to false"); isChanging = false; if (previousView==null) { // this is the first time handleNewTopologyView is called if (eventListeners.size()>0) { logger.info("doHandleConsistent: no previous view available even though listeners already got CHANGED event"); } else { logger.debug("doHandleConsistent: no previous view and there are no event listeners yet. very quiet."); } // otherwise this is the normal case where there are uninitialized event listeners waiting below } else { logger.debug("doHandleConsistent: sending TOPOLOGY_CHANGED to initialized listeners"); previousView.setNotCurrent(); enqueueForAll(eventListeners, EventHelper.newChangedEvent(previousView, newView)); } if (unInitializedEventListeners.size()>0) { // then there were bindTopologyEventListener calls coming in while // we were in CHANGING state - so we must send those the INIT they were // waiting for oh so long logger.debug("doHandleConsistent: sending TOPOLOGY_INIT to uninitialized listeners ({})", unInitializedEventListeners.size()); enqueueForAll(unInitializedEventListeners, EventHelper.newInitEvent(newView)); eventListeners.addAll(unInitializedEventListeners); unInitializedEventListeners.clear(); } logger.trace("doHandleConsistent: setting previousView to {}", newView); previousView = newView; logger.trace("doHandleConsistent: end"); } /** get-hook for testing only! **/ AsyncEventSender getAsyncEventSender() { return asyncEventSender; } @Override public int waitForAsyncEvents(long timeout) { long end = System.currentTimeMillis() + timeout; while(true) { int inFlightEventCnt = getInFlightAsyncEventCnt(); if (inFlightEventCnt==0) { // no in-flight events - return 0 return 0; } if (timeout==0) { // timeout is set to 'no-wait', but we have in-flight events, // return the actual cnt return inFlightEventCnt; } if (timeout<0 /*infinite waiting*/ || System.currentTimeMillis()<end) { try { Thread.sleep(50); } catch (InterruptedException e) { // ignore } } else { // timeout hit return inFlightEventCnt; } } } private int getInFlightAsyncEventCnt() { int cnt = asyncEventSender.getInFlightEventCnt(); if (minEventDelayHandler!=null && minEventDelayHandler.isDelaying()) { cnt++; } return cnt; } }