/*
* Copyright (C) 2011 Red Hat, Inc. and/or its affiliates.
*
* Licensed 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.jboss.errai.common.client.api.extension;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
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 org.jboss.errai.common.client.api.tasks.AsyncTask;
import org.jboss.errai.common.client.api.tasks.TaskManagerFactory;
import org.jboss.errai.common.client.util.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gwt.core.client.GWT;
/**
* <p>
* The <tt>InitVotes</tt> class provides the central algorithm around which disparate services within the Errai
* Framework can elect to prevent initialization and be notified when initialization occurs. This is required internally
* to ensure that services such as RPC proxies have been properly bound prior to any remote calls being made. This API
* also makes it possible for user-defined services and extensions to Errai to participate in the startup contract.
*
* <p>
* Initialization fails if there are any services still waiting after the timeout duration has elapsed. By default the
* timeout is 90 seconds for Development Mode and 45 seconds for production mode, but it can be adjusted by setting the
* Javascript variable <code>erraiInitTimeout</code> in the GWT Host Page.
*
* @author Mike Brock
*/
public final class InitVotes {
private InitVotes() {}
private static final List<Runnable> preInitCallbacks = new ArrayList<Runnable>();
private static final Map<String, List<Runnable>> dependencyCallbacks = new HashMap<String, List<Runnable>>();
private static final List<Runnable> initCallbacks = new ArrayList<Runnable>();
private static final List<InitFailureListener> initFailureListeners = new ArrayList<InitFailureListener>();
private static boolean armed = false;
private static boolean init = false;
private static final Set<String> waitForSet = new HashSet<String>();
// a list of both strings and runnable references that are marked done.
private static final Set<Object> completedSet = new HashSet<Object>();
private static int timeoutMillis = !GWT.isProdMode() ? 90000 : 45000;
private static volatile AsyncTask initTimeout;
private static volatile AsyncTask initDelay;
private static boolean _initWait = false;
private static final Object lock = new Object();
private static final Logger logger = LoggerFactory.getLogger(InitVotes.class);
/**
* Resets the state, clearing all current waiting votes and disarming the startup process. Calling
* <tt>reset()</tt> does not however clear out any initialization callbacks registered with
* {@link #registerPersistentInitCallback(Runnable)}.
*/
public static void reset() {
synchronized (lock) {
logger.info("init polling system reset ...");
idempotentStopFailTimer();
idempotentStopDelayTimer();
_clearOneTimeRunnables(preInitCallbacks);
_clearOneTimeRunnables(initCallbacks);
for (final Map.Entry<String, List<Runnable>> entry : dependencyCallbacks.entrySet()) {
_clearOneTimeRunnables(entry.getValue());
}
waitForSet.clear();
completedSet.clear();
armed = false;
init = false;
}
}
private static native int getConfiguredTimeoutOrElse(final int fallback) /*-{
var configuredValue = $wnd.erraiInitTimeout;
return (configuredValue == undefined || configuredValue <= 0) ?
fallback :
configuredValue;
}-*/;
/**
* Specifies the number of milliseconds that will be permitted to transpire until dependencies are
* assumed to have failed to satisfy, and thus an error is rendered to the browser console.
*
* @param millis
* milliseconds.
*/
public static void setTimeoutMillis(final int millis) {
timeoutMillis = millis;
};
/**
* Declares a startup dependency on the specified class. By doing so, initialization of the
* framework services will be blocked until a {@link #voteFor(Class)} is called with the same
* <tt>Class</tt> reference passed to this method.
* <p/>
* If no dependencies have previously been declared, then the first caller to invoke this method
* arms and begins the startup process. This starts the timer window (see
* {@link #setTimeoutMillis(int)}) for which all components being waited on are expected to report
* back that they're ready.
*
* @param clazz
* a class reference.
*
* @see #voteFor(Class)
*/
public static void waitFor(final Class<?> clazz) {
waitFor(clazz.getName());
}
private static void waitFor(final String topic) {
synchronized (lock) {
if (completedSet.contains(topic)) {
// throw new RuntimeException("cannot declare a wait on '" + topic +
// "' as it is already marked completed!");
return;
}
if (waitForSet.contains(topic))
return;
logger.info("wait for: " + topic);
if (!armed && waitForSet.isEmpty()) {
beginInit();
}
waitForSet.add(topic);
}
}
public static boolean isInitialized() {
return init;
}
/**
* Votes for initialization and removes a lock on the initialization of framework services. If the
* initialization process has been armed and this vote releases the final dependency, the
* initialization process will be triggered, calling all the registered initialization callbacks.
* See: {@link #registerPersistentInitCallback(Runnable)}
*
* @param clazz
* a class reference
*/
public static void voteFor(final Class<?> clazz) {
voteFor(clazz.getName());
}
private static void voteFor(final String topic) {
synchronized (lock) {
if (waitForSet.remove(topic)) {
logger.info("vote for: " + topic);
completedSet.add(topic);
}
_runAllRunnables(dependencyCallbacks.get(topic));
if (!waitForSet.isEmpty())
logger.info(" still waiting for -> " + waitForSet);
if (armed && waitForSet.isEmpty()) {
scheduleFinish();
}
}
}
private static void scheduleFinish() {
if (_initWait)
return;
_initWait = true;
_scheduleFinish(new Runnable() {
@Override
public void run() {
if (armed && waitForSet.isEmpty()) {
idempotentStopFailTimer();
finishInit();
_initWait = false;
}
else {
_scheduleFinish(this);
}
}
});
}
private static void _scheduleFinish(final Runnable runnable) {
initDelay = TaskManagerFactory.get().schedule(TimeUnit.MILLISECONDS, 250, runnable);
}
public static void registerPersistentDependencyCallback(final Class clazz, final Runnable runnable) {
_registerDependencyCallback(clazz.getName(), runnable);
}
public static void registerOneTimeDependencyCallback(final Class clazz, final Runnable runnable) {
registerPersistentDependencyCallback(clazz, new OneTimeRunnable(runnable));
}
private static void _registerDependencyCallback(final String topic, final Runnable runnable) {
synchronized (lock) {
List<Runnable> callbacks = dependencyCallbacks.get(topic);
if (callbacks == null) {
dependencyCallbacks.put(topic, callbacks = new ArrayList<Runnable>());
}
if (!callbacks.contains(runnable)) {
callbacks.add(runnable);
}
if (completedSet.contains(topic) && !completedSet.contains(runnable)) {
runnable.run();
}
}
}
public static void registerPersistentPreInitCallback(final Runnable runnable) {
synchronized (lock) {
if (!preInitCallbacks.contains(runnable)) {
preInitCallbacks.add(runnable);
if (armed) {
runnable.run();
}
}
}
}
public static void registerOneTimePreInitCallback(final Runnable runnable) {
registerPersistentPreInitCallback(new OneTimeRunnable(runnable));
}
/**
* Registers a callback task to be executed once initialization occurs. Callbacks registered with
* this method will be persistent <em>across</em> multiple initializations, and will not be
* cleared out even if {@link #reset()} is called. If this is not desirable, see:
* {@link #registerOneTimeInitCallback};
* <p>
* As of Errai 3.0, the callback list is de-duped based on instance to simplify initialization
* code in modules. You can now safely re-add a Runnable in initialization code as long as it is
* always guaranteed to be the same instance.*
*
* @param runnable
* a callback to execute
*/
public static void registerPersistentInitCallback(final Runnable runnable) {
synchronized (lock) {
if (!initCallbacks.contains(runnable)) {
initCallbacks.add(runnable);
}
if (init) {
_runAllRunnables(Arrays.asList(runnable), initCallbacks);
}
}
}
/**
* Registers a one-time callback task to be executed once initialization occurs. Unlike callbacks
* registered with {@link #registerPersistentInitCallback(Runnable)} Callback(Runnable)},
* callbacks registered with this method will only be executed once and will never be used again
* if framework services are re-initialized.
*
* @param runnable
* a callback to execute
*/
public static void registerOneTimeInitCallback(final Runnable runnable) {
registerPersistentInitCallback(new OneTimeRunnable(runnable));
}
/**
* Registers an {@link InitFailureListener} to monitor for initialization failures of the
* framework or its components.
*
* @param failureListener
* the instance of the {@link InitFailureListener} to be registered.
*/
public static void registerInitFailureListener(final InitFailureListener failureListener) {
initFailureListeners.add(failureListener);
}
public static void startInitPolling() {
if (armed) {
logger.warn("did not start polling. already armed.");
return;
}
timeoutMillis = getConfiguredTimeoutOrElse(timeoutMillis);
beginInit();
}
private static void beginInit() {
synchronized (lock) {
if (armed) {
throw new RuntimeException("attempt to arm voting process more than once.");
}
armed = true;
_initWait = false;
idempotentStopFailTimer();
initTimeout = TaskManagerFactory.get().schedule(TimeUnit.MILLISECONDS, timeoutMillis, new Runnable() {
@Override
public void run() {
synchronized (lock) {
if (waitForSet.isEmpty() || !armed)
return;
idempotentStopDelayTimer();
final Set<String> failedTopics = Collections.unmodifiableSet(new HashSet<String>(waitForSet));
_fireFailedInit(failedTopics);
logger.error("components failed to initialize");
for (final String comp : waitForSet) {
logger.error(" [failed] -> " + comp);
}
}
}
});
_runAllRunnables(preInitCallbacks);
}
}
private static void _fireFailedInit(final Set<String> failedTopics) {
for (final InitFailureListener initFailureListener : initFailureListeners) {
initFailureListener.onInitFailure(failedTopics);
}
}
private static void finishInit() {
synchronized (lock) {
armed = false;
init = true;
idempotentStopFailTimer();
_runAllRunnables(initCallbacks);
}
}
private static void idempotentStopFailTimer() {
synchronized (lock) {
if (initTimeout != null && !initTimeout.isFinished()) {
initTimeout.cancel(true);
}
}
}
private static void idempotentStopDelayTimer() {
synchronized (lock) {
if (initDelay != null && !initDelay.isFinished()) {
initDelay.cancel(true);
}
}
}
private static void _runAllRunnables(final List<Runnable> runnables) {
if (runnables == null || runnables.isEmpty())
return;
_runAllRunnables(new ArrayList<Runnable>(runnables), runnables);
}
private static void _runAllRunnables(final List<Runnable> curRunnables, final List<Runnable> allRunnables) {
for (Runnable runnable : curRunnables) {
if (completedSet.contains(runnable)) {
continue;
}
completedSet.add(runnable);
if (runnable instanceof OneTimeRunnable) {
allRunnables.remove(runnable);
}
int expectedSize = allRunnables.size();
runnable.run();
if (expectedSize < allRunnables.size()) {
// this runnable added more runnables that need to be executed
List<Runnable> moreRunnables = new ArrayList<Runnable>(allRunnables).subList(expectedSize, allRunnables.size());
_runAllRunnables(moreRunnables, allRunnables);
}
}
}
private static void _clearOneTimeRunnables(final List<Runnable> runnables) {
final Iterator<Runnable> runnableIterable = runnables.iterator();
while (runnableIterable.hasNext()) {
final Runnable runnable = runnableIterable.next();
if (runnable instanceof OneTimeRunnable) {
runnableIterable.remove();
}
}
}
private static class OneTimeRunnable implements Runnable {
private final Runnable delegate;
private OneTimeRunnable(Runnable delegate) {
this.delegate = delegate;
}
@Override
public void run() {
delegate.run();
}
}
}