blob: 53afa8b7c8484c22daf1fae6dc439c05b979a2c5 [file] [log] [blame]
/*
* 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.event.impl.discovery;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.sling.discovery.TopologyEvent;
import org.apache.sling.discovery.TopologyEvent.Type;
import org.apache.sling.discovery.TopologyEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This listener facade applies a 'startup delay' to a topology event handler,
* that is, it allows to specify a startup time during which any topology events
* will be queued and processing only starts after this time.
*
* What happens aFter the startup time depends on what was received during the delay:
* <ul>
* <li>if no events were received then this is a no-op</li>
* <li>if the last event received was a CHANGING then this facade
* waits until it receives the next non-CHANGING event (which should
* be the very next), to then simulate an INIT event
* (as the discovery API says the first event received is an INIT event).
* </li>
* <li>if the last event received was not a CHANGING event
* (ie it was an INIT, CHANGED or PROPERTIES), then
* as soon as the startup time passes this facade will simulate
* an INIT event
* (again, as the discovery API says the first event received is an INIT event)
* </li>
* </ul>
* Note that users of this facade must call dispose to avoid any async calls
* to the delegate after startup, in case they themselves are deactivated!
*
* This class is a cleaned copy of <code>InitDelayingTopologyEventListener</code> from discovery commons.
*
**/
public class InitDelayingTopologyEventListener implements TopologyEventListener {
/** the delegate which does the actual event processing after delaying **/
private final TopologyEventListener delegate;
/** the sync object used to guard asynchronous discovery events and scheduler callback **/
private final Object syncObj = new Object();
/** the guarded flag indicating whether this listener is active or disposed - used between scheduler callback and dispose **/
private final AtomicBoolean active = new AtomicBoolean(false);
/** flag indicating whether we're still delaying or not **/
private volatile boolean delaying = true;
/** the last pending delayed event - we only have to keep the last event to support all different cases **/
private volatile TopologyEvent pendingDelayedEvent;
private final Logger logger = LoggerFactory.getLogger(InitDelayingTopologyEventListener.class);
/**
* Creates a new init-delaying listener with the given delay and delegate.
* <p>
* For properly disposing the caller should use the dispose method!
* @param startupDelay The startup delay in seconds
* @param delegate The topology event listener
* @see #dispose()
*/
public InitDelayingTopologyEventListener(final long startupDelay, final TopologyEventListener delegate) {
if ( delegate == null ) {
throw new IllegalArgumentException("delegate must not be null");
}
if ( startupDelay <= 0) {
throw new IllegalArgumentException("startupDelay must be greater than 0, not " + startupDelay);
}
this.delegate = delegate;
final Runnable r = new Runnable() {
@Override
public void run() {
if (InitDelayingTopologyEventListener.this.active.get()) {
// only call afterStartupDelay if we're active
// (to avoid this call if disposed in the meantime)
// and since after disposing this listener is no longer
// used - ie it is a throw-away - you can create
// such a listener on each activate and dispose it on
// deactivate and you'll be fine.
// in any case - time for calling afterStartupDelay here:
afterStartupDelay();
}
}
};
// mark this listener as active
this.active.set(true);
// schedule me
final Timer timer = new Timer();
final TimerTask task = new TimerTask() {
@Override
public void run() {
r.run();
}
};
timer.schedule(task, startupDelay * 1000);
// SLING-5560 : at this point either r is invoked immediately or scheduled after the delay
}
@Override
public void handleTopologyEvent(TopologyEvent event) {
synchronized ( syncObj ) {
if ( this.delaying ) {
// when we're delaying, keep hold of the last event
// as action afterStartupDelay depends on this last
// event
this.logger.debug("handleTopologyEvent: delaying processing of received topology event (startup delay active) {}", event);
this.pendingDelayedEvent = event;
// and we're delaying - so stop processing now and return
return;
} else if ( this.pendingDelayedEvent != null ) {
// this means that while we're no longer delaying we still
// have a pending delayed event to process.
// more concretely, it means that this event is a CHANGING
// event (as the others are treated via an artificial INIT event
// in afterStartupDelay).
// which means that we must now convert the new event into an INIT
// to ensure our code gets an INIT first thing
// paranoia check:
if ( event.getType() == Type.TOPOLOGY_CHANGING ) {
// this should never happen - but if it does, rinse and repeat
this.pendingDelayedEvent = event;
this.logger.info("handleTopologyEvent: ignoring received topology event of type CHANGING {}", event);
return;
} else {
// otherwise we now **convert** the event to an init event
this.pendingDelayedEvent = null;
this.logger.debug("handleTopologyEvent: first stable topology event received after startup delaying. "
+ "Simulating an INIT event with this new view: {}", event);
event = new TopologyEvent(Type.TOPOLOGY_INIT, null, event.getNewView());
}
} else {
// otherwise we're no longer delaying nor do we have a pending-backlog.
// we can just pass the event through
this.logger.debug("handleTopologyEvent: received topology event {}", event);
}
}
// no delaying applicable - call delegate
this.delegate.handleTopologyEvent(event);
}
/**
* Marks this listener as no longer active - ensures that it doesn't call the delegate
* via any potentially pending scheduler callback.
* <p>
* Note that after dispose you can *still* call handleTopologyEvent and the events
* are passed to the delegate - but those are expected to be 'late' events and not
* really part of the normal game. Hence, the caller must also ensure that the
* handleTopologyEvent method isn't called anymore (which typically is automatically
* guaranteed since the caller is typically an osgi service that gets unregistered anyway)
*/
public void dispose() {
this.active.set(false);
}
/**
* Called via the scheduler callback when the startup delay has passed.
* <p>
* Hence only called once!
*/
private void afterStartupDelay() {
synchronized ( this.syncObj ) {
// stop any future delaying
this.delaying = false;
if ( this.pendingDelayedEvent == null ) {
// if no event received while we delayed,
// then we don't have to do anything later
this.logger.debug("afterStartupDelay: startup delay passed without any events delayed. "
+ "So, ready for first upcoming INIT event");
} else if ( this.pendingDelayedEvent.getType() == Type.TOPOLOGY_CHANGING ) {
// if the last delayed event was CHANGING
// then we must convert the next upcoming CHANGED, PROPERTIES
// into an INIT
// and the way this is done in this class is by leving this
// event sit in this.pendingDelayedEvent, for grabs in handleTopologyEvent later
this.logger.debug("afterStartupDelay: startup delay passed, pending delayed event was CHANGING. "
+ "Waiting for next stable topology event");
} else {
// otherwise the last delayed event was either an INIT, CHANGED or PROPERTIES
// - but in any case we definitely never
// processed any INIT - and our code expects an INIT
// as the first event ever..
// so we now convert the event into an INIT
final TopologyEvent artificialInitEvent =
new TopologyEvent(Type.TOPOLOGY_INIT, null, this.pendingDelayedEvent.getNewView());
this.logger.debug("afterStartupDelay: startup delay passed, last pending delayed event was stable ({}). "
+ "Simulating an INIT event with that view: {}", this.pendingDelayedEvent, artificialInitEvent);
this.pendingDelayedEvent = null;
// call the delegate.
// we must do this call in the synchronized block
// to ensure any concurrent new event waits properly
// before the INIT is done
delegate.handleTopologyEvent(artificialInitEvent);
}
}
}
}