/*
* $Id$
*
* SARL is an general-purpose agent programming language.
* More details on http://www.sarl.io
*
* Copyright (C) 2014-2017 the original authors or authors.
*
* 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 io.janusproject.kernel.bic.internaleventdispatching;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.stream.StreamSupport;
import javax.inject.Inject;
import com.google.common.collect.Lists;
import org.arakhne.afc.util.MultiCollection;
import org.arakhne.afc.util.OutputParameter;
import org.eclipse.xtext.xbase.lib.Functions.Function1;
import org.eclipse.xtext.xbase.lib.Procedures.Procedure1;
import io.janusproject.services.executor.EarlyExitException;
import io.janusproject.services.executor.ExecutorService;
import io.janusproject.services.executor.JanusRunnable;
import io.sarl.eventdispatching.BehaviorGuardEvaluator;
import io.sarl.eventdispatching.BehaviorGuardEvaluatorRegistry;
import io.sarl.lang.core.Event;
/**
* The class in charge of dispatching every single events coming from the outside of this agent (i.e. from a space) or from an
* agent's behavior.
*
* @author $Author: ngaud$
* @author $Author: sgalland$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
*
*/
public class AgentInternalEventsDispatcher {
/**
* The registry of all {@code BehaviorGuardEvaluator} classes containing a method to evaluate the guard of a given behavior
* (on clause in SARL behavior). This class has been inspired by the com.google.common.eventbus.SuscriberRegistry class of
* Google Guava library.
*/
private final BehaviorGuardEvaluatorRegistry behaviorGuardEvaluatorRegistry;
/**
* The executor used to execute behavior methods in dedicated thread.
*/
private final ExecutorService executor;
/**
* Instantiates a dispatcher.
*
* @param executor the executor service.
*/
@Inject
public AgentInternalEventsDispatcher(ExecutorService executor) {
this.executor = executor;
this.behaviorGuardEvaluatorRegistry = new BehaviorGuardEvaluatorRegistry();
}
/** Replies if a listener with the given type is registered.
*
* @param type the type of listener.
* @return {@code true} if a listener of the given type is registered.
* @since 0.5
*/
public boolean hasRegisteredEventListener(Class<?> type) {
synchronized (this.behaviorGuardEvaluatorRegistry) {
return this.behaviorGuardEvaluatorRegistry.hasRegisteredEventListener(type);
}
}
/** Extract the registered listeners with the given type.
*
* @param <T> the type of the listeners.
* @param type the type of the listeners.
* @param collection the collection of listeners that is filled by this function.
* @return the number of listeners added to the collection.
* @since 0.5
*/
public <T> int getRegisteredEventListeners(Class<T> type, Collection<? super T> collection) {
synchronized (this.behaviorGuardEvaluatorRegistry) {
return this.behaviorGuardEvaluatorRegistry.getRegisteredEventListeners(type, collection);
}
}
/**
* Registers all {@code PerceptGuardEvaluator} methods on {@code object} to receive events.
*
* <p>If the filter is provided, it will be used for determining if the given behavior accepts a specific event.
* If the filter function replies {@code true} for a specific event as argument, the event is fired in the
* behavior context. If the filter function replies {@code false}, the event is not fired in the behavior context.
*
* @param object object whose {@code PerceptGuardEvaluator} methods should be registered.
* @param filter - the filter function. It could be {@code null}.
* @param callback function which is invoked just after the first registration of the object. It could be {@code null}.
*/
public void register(Object object, Function1<? super Event, ? extends Boolean> filter, Procedure1<Object> callback) {
synchronized (this.behaviorGuardEvaluatorRegistry) {
this.behaviorGuardEvaluatorRegistry.register(object, filter, callback);
}
}
/**
* Unregisters all {@code PerceptGuardEvaluator} methods on a registered {@code object}.
*
* @param object object whose {@code PerceptGuardEvaluator} methods should be unregistered.
* @param callback function which is invoked just before the object is unregistered.
* @throws IllegalArgumentException if the object was not previously registered.
*/
public void unregister(Object object, Procedure1<Object> callback) {
synchronized (this.behaviorGuardEvaluatorRegistry) {
this.behaviorGuardEvaluatorRegistry.unregister(object, callback);
}
}
/**
* Unregisters all {@code PerceptGuardEvaluator} methods on all registered objects.
*
* @param callback function which is invoked just before the object is unregistered.
* @throws IllegalArgumentException if the object was not previously registered.
*/
public void unregisterAll(Procedure1<Object> callback) {
synchronized (this.behaviorGuardEvaluatorRegistry) {
this.behaviorGuardEvaluatorRegistry.unregisterAll(callback);
}
}
/**
* Posts an event to all registered {@code BehaviorGuardEvaluator}.
* The dispatch of this event will be done synchronously.
* This method will return successfully after the event has been posted to all {@code BehaviorGuardEvaluator}, and regardless
* of any exceptions thrown by {@code BehaviorGuardEvaluator}.
*
* @param event - an event to dispatch synchronously.
*/
public void immediateDispatch(Event event) {
assert event != null;
Iterable<BehaviorGuardEvaluator> behaviorGuardEvaluators = null;
synchronized (this.behaviorGuardEvaluatorRegistry) {
behaviorGuardEvaluators = AgentInternalEventsDispatcher.this.behaviorGuardEvaluatorRegistry
.getBehaviorGuardEvaluators(event);
}
if (behaviorGuardEvaluators != null) {
final Collection<Runnable> behaviorsMethodsToExecute;
try {
behaviorsMethodsToExecute = evaluateGuards(event, behaviorGuardEvaluators);
executeBehaviorMethodsInParalellWithSynchroAtTheEnd(behaviorsMethodsToExecute);
} catch (RuntimeException exception) {
throw exception;
} catch (InterruptedException | ExecutionException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
/*
* <p>If no {@code BehaviorGuardEvaluator} have been subscribed for {@code event}'s class, and {@code event} is not already a
* {@link DeadEvent}, it will be wrapped in a DeadEvent and reposted.
*/
// XXX: not in the SARL specifications. Should we fire the DeadEvent?
/*else if (!(event instanceof DeadEvent)) {
// the event had no subscribers and was not itself a DeadEvent
immediateDispatch(new DeadEvent(event));
}*/
}
/**
* Posts an event to the registered {@code BehaviorGuardEvaluator} of the given listener only.
* The dispatch of this event will be done synchronously.
* This method will return successfully after the event has been posted to all {@code BehaviorGuardEvaluator}, and regardless
* of any exceptions thrown by {@code BehaviorGuardEvaluator}.
*
* @param listener - the listener to dispatch to.
* @param event - an event to dispatch synchronously.
*/
public void immediateDispatchTo(Object listener, Event event) {
assert event != null;
Iterable<BehaviorGuardEvaluator> behaviorGuardEvaluators = null;
synchronized (this.behaviorGuardEvaluatorRegistry) {
behaviorGuardEvaluators = AgentInternalEventsDispatcher.this.behaviorGuardEvaluatorRegistry
.getBehaviorGuardEvaluatorsFor(event, listener);
}
if (behaviorGuardEvaluators != null) {
final Collection<Runnable> behaviorsMethodsToExecute;
try {
behaviorsMethodsToExecute = evaluateGuards(event, behaviorGuardEvaluators);
executeBehaviorMethodsInParalellWithSynchroAtTheEnd(behaviorsMethodsToExecute);
} catch (RuntimeException exception) {
throw exception;
} catch (InterruptedException | ExecutionException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
/*
* <p>If no {@code BehaviorGuardEvaluator} have been subscribed for {@code event}'s class, and {@code event} is not already a
* {@link DeadEvent}, it will be wrapped in a DeadEvent and reposted.
*/
// XXX: not in the SARL specifications. Should we fire the DeadEvent?
/*else if (!(event instanceof DeadEvent)) {
// the event had no subscribers and was not itself a DeadEvent
immediateDispatch(new DeadEvent(event));
}*/
}
/**
* Posts an event to all registered {@code BehaviorGuardEvaluator}.
* The dispatch of this event will be done asynchronously.
* This method will return successfully after the event has been posted to all {@code BehaviorGuardEvaluator}, and regardless
* of any exceptions thrown by {@code BehaviorGuardEvaluator}.
*
* @param event - an event to dispatch asynchronously.
*/
public void asyncDispatch(Event event) {
assert event != null;
this.executor.execute(() -> {
Iterable<BehaviorGuardEvaluator> behaviorGuardEvaluators = null;
synchronized (AgentInternalEventsDispatcher.this.behaviorGuardEvaluatorRegistry) {
behaviorGuardEvaluators = AgentInternalEventsDispatcher.this.behaviorGuardEvaluatorRegistry
.getBehaviorGuardEvaluators(event);
}
if (behaviorGuardEvaluators != null) {
final Collection<Runnable> behaviorsMethodsToExecute;
try {
behaviorsMethodsToExecute = evaluateGuards(event, behaviorGuardEvaluators);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
executeAsynchronouslyBehaviorMethods(behaviorsMethodsToExecute);
}
// XXX: Not in the SAR specification, should we fire the DeadEvent?
/*else if (!(event instanceof DeadEvent)) {
// the event had no subscribers and was not itself a DeadEvent
asyncDispatch(new DeadEvent(event));
}*/
});
}
/**
* Evaluate the guard associated to the specified {@code event} and returns the list of behaviors methods that must be
* executed.
*
* @param event - the event triggering behaviors
* @param behaviorGuardEvaluators - the list of class containing a {@code PerceptGuardEvaluator} method
* @return the collection of couple associating a object and its collection of behavior methods that must be executed
* @throws InvocationTargetException - exception when you try to execute a method by reflection and this method doesn't exist.
*/
private static Collection<Runnable> evaluateGuards(final Event event,
final Iterable<BehaviorGuardEvaluator> behaviorGuardEvaluators) throws InvocationTargetException {
final MultiCollection<Runnable> behaviorsMethodsToExecute = new MultiCollection<>();
try {
StreamSupport.stream(behaviorGuardEvaluators.spliterator(), true).forEach((evaluator) -> {
final Collection<Runnable> behaviorsMethodsToExecutePerTarget = Lists.newLinkedList();
evaluator.evaluateGuard(event, behaviorsMethodsToExecutePerTarget);
synchronized (behaviorsMethodsToExecute) {
behaviorsMethodsToExecute.addCollection(behaviorsMethodsToExecutePerTarget);
}
});
} catch (Exception exception) {
if (exception instanceof InvocationTargetException) {
throw (InvocationTargetException) exception;
}
final Throwable t = exception.getCause();
if (t instanceof InvocationTargetException) {
throw (InvocationTargetException) t;
}
throw exception;
}
return behaviorsMethodsToExecute;
}
/**
* Execute every single Behaviors runnable, a dedicated thread will created by the executor local to this class and be used to
* execute each runnable in parallel, and this method waits until its future has been completed before leaving.
*
* <p>This function may fail if one of the called handlers has failed. Errors are logged by the executor service too.
*
* @param behaviorsMethodsToExecute - the collection of Behaviors runnable that must be executed.
* @throws InterruptedException - something interrupt the waiting of the event handler terminations.
* @throws ExecutionException - when the event handlers cannot be called; or when one of the event handler has failed during
* its run.
*/
private void executeBehaviorMethodsInParalellWithSynchroAtTheEnd(Collection<Runnable> behaviorsMethodsToExecute)
throws InterruptedException, ExecutionException {
final CountDownLatch doneSignal = new CountDownLatch(behaviorsMethodsToExecute.size());
final OutputParameter<Throwable> runException = new OutputParameter<>();
for (final Runnable runnable : behaviorsMethodsToExecute) {
this.executor.execute(new JanusRunnable() {
@Override
public void run() {
try {
runnable.run();
} catch (EarlyExitException e) {
// Ignore this exception
} catch (RuntimeException e) {
// Catch exception for notifying the caller
runException.set(e);
// Do the standard behavior too -> logging
throw e;
} catch (Exception e) {
// Catch exception for notifying the caller
runException.set(e);
// Do the standard behavior too -> logging
throw new RuntimeException(e);
} finally {
doneSignal.countDown();
}
}
});
}
// Wait for all Behaviors runnable to complete before continuing
try {
doneSignal.await();
} catch (InterruptedException ex) {
// XXX: Improve because:
// This exception occurs when one of the launched task kills the agent before all the
// submitted tasks are finished. Keep in mind that killing an agent should kill the
// launched tasks.
// Example of code that is generating this issue:
//
// on Initialize {
// in (100) [
// killMe
// ]
// }
//
// In this example, the killMe is launched before the Initialize code is finished;
// and because the Initialize event is fired through the current function, it
// causes the InterruptedException.
}
// Re-throw the run-time exception
if (runException.get() != null) {
throw new ExecutionException(runException.get());
}
}
/**
* Execute every single Behaviors runnable, a dedicated thread will created by the executor local to this class and be used to
* execute each runnable in parallel.
*
* <p>This function never fails. Errors in the event handlers are logged by the executor service.
*
* @param behaviorsMethodsToExecute - the collection of Behaviors runnable that must be executed.
*/
private void executeAsynchronouslyBehaviorMethods(Collection<Runnable> behaviorsMethodsToExecute) {
for (final Runnable runnable : behaviorsMethodsToExecute) {
this.executor.execute(runnable);
}
}
}