/* * $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.services.jdk.spawn; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; import javax.inject.Inject; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Service; import com.google.inject.AbstractModule; import com.google.inject.Injector; import com.google.inject.Provider; import com.google.inject.Singleton; import io.janusproject.kernel.bic.BuiltinCapacityUtil; import io.janusproject.services.AbstractDependentService; import io.janusproject.services.contextspace.ContextSpaceService; import io.janusproject.services.executor.ExecutorService; import io.janusproject.services.logging.LogService; import io.janusproject.services.spawn.KernelAgentSpawnListener; import io.janusproject.services.spawn.SpawnService; import io.janusproject.services.spawn.SpawnServiceListener; import io.janusproject.util.ListenerCollection; import io.sarl.core.AgentKilled; import io.sarl.core.AgentSpawned; import io.sarl.core.Logging; import io.sarl.lang.core.Address; import io.sarl.lang.core.Agent; import io.sarl.lang.core.AgentContext; import io.sarl.lang.core.BuiltinCapacitiesProvider; import io.sarl.lang.core.EventSpace; import io.sarl.lang.core.Skill; import io.sarl.lang.util.SynchronizedCollection; import io.sarl.lang.util.SynchronizedSet; import io.sarl.sarlspecification.SarlSpecificationChecker; import io.sarl.util.Collections3; /** * Implementation of a spawning service that is based on the other services of the Janus platform. * * @author $Author: srodriguez$ * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ @Singleton public class StandardSpawnService extends AbstractDependentService implements SpawnService { /** Maximum number of agents to be launch by a single thread. */ private static final int CREATION_POOL_SIZE = 128; /** Static reference to the private function for setting the build-in capacities. */ private static final Method MAP_CAPACITY_FUNCTION; /** Static reference to the private function for setting the build-in capacities. */ private static final Method GET_SKILL_FUNCTION; static { try { MAP_CAPACITY_FUNCTION = Agent.class.getDeclaredMethod("mapCapacity", Class.class, Skill.class); //$NON-NLS-1$ MAP_CAPACITY_FUNCTION.setAccessible(true); } catch (NoSuchMethodException | SecurityException e) { throw new Error(Messages.StandardSpawnService_4, e); } try { GET_SKILL_FUNCTION = Agent.class.getDeclaredMethod("getSkill", Class.class); //$NON-NLS-1$ GET_SKILL_FUNCTION.setAccessible(true); } catch (NoSuchMethodException | SecurityException e) { throw new Error(Messages.StandardSpawnService_6, e); } } private final ListenerCollection<?> globalListeners = new ListenerCollection<>(); // TODO The use of two maps is slowly the platform private final Map<UUID, ListenerCollection<SpawnServiceListener>> agentLifecycleListeners = new TreeMap<>(); // TODO The use of two maps is slowly the platform private final Map<UUID, Agent> agents = new TreeMap<>(); private final Injector injector; private final SarlSpecificationChecker sarlSpecificationChecker; @Inject private ExecutorService executor; @Inject private LogService logger; @Inject private BuiltinCapacitiesProvider builtinCapacityProvider; /** * Constructs the service with the given (injected) injector. * * @param injector * the injector that should be used by this service for creating the agents. * @param sarlSpecificationChecker the tool for checking the validity of the SARL specification supported by * the agents to launch. */ @Inject public StandardSpawnService(Injector injector, SarlSpecificationChecker sarlSpecificationChecker) { this.injector = injector; this.sarlSpecificationChecker = sarlSpecificationChecker; } /** Replies the mutex for synchronizing on agent repository. * * @return the mutex. */ protected final Object getAgentRepositoryMutex() { return this.agents; } /** Replies the mutex for synchronizing on agent-lifecycle listeners. * * @return the mutex. */ protected final Object getAgentLifecycleListenerMutex() { return this.agentLifecycleListeners; } @Override public final Class<? extends Service> getServiceType() { return SpawnService.class; } @Override public Collection<Class<? extends Service>> getServiceDependencies() { return Arrays.<Class<? extends Service>>asList(ContextSpaceService.class); } private void ensureSarlSpecificationVersion(Class<? extends Agent> agentClazz) { if (!this.sarlSpecificationChecker.isValidSarlElement(agentClazz)) { throw new InvalidSarlSpecificationException(agentClazz); } } @Override public List<UUID> spawn(int nbAgents, UUID spawningAgent, AgentContext parent, UUID agentID, Class<? extends Agent> agentClazz, Object... params) { if (isRunning() && nbAgents > 0) { try { // Check if the version of the SARL agent class is compatible. ensureSarlSpecificationVersion(agentClazz); // Create the shared injector that is also able to create the agent instance. final JustInTimeAgentInjectionModule agentInjectionModule = new JustInTimeAgentInjectionModule( agentClazz, parent.getID(), agentID); final Injector agentInjector = this.injector.createChildInjector(agentInjectionModule); // Create the list of the spawned agents during this function execution final List<Agent> agents = new ArrayList<>(nbAgents); // Create the block of code for creating a single agent final Runnable agentCreator = () -> { final Agent agent = agentInjector.getInstance(Agent.class); assert agent != null; // Create the builtin capacities / skill installation will be done later in the life cycle. StandardSpawnService.this.builtinCapacityProvider.builtinCapacities(agent, (capacity, skill) -> { try { MAP_CAPACITY_FUNCTION.invoke(agent, capacity, skill); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new Error(Messages.StandardSpawnService_5, e); } }); // Add the agent in the system synchronized (this.agents) { this.agents.put(agent.getID(), agent); } synchronized (agents) { agents.add(agent); } fireAgentSpawnedInAgent(spawningAgent, parent, agent, params); }; // Create a single agent with a sequential call; or multiple agents in parallel if (nbAgents > 1) { this.executor.executeMultipleTimesInParallelAndWaitForTermination( agentCreator, nbAgents, CREATION_POOL_SIZE); } else { agentCreator.run(); } // Fire the general spawning event fireAgentSpawnedOutsideAgent(spawningAgent, parent, agentClazz, agents, params); return Collections.unmodifiableList(Lists.transform(agents, (it) -> it.getID())); } catch (Throwable e) { throw new CannotSpawnException(agentClazz, e); } } throw new SpawnDisabledException(parent.getID(), agentClazz); } /** Notify the listeners about the agents' spawning. * * @param spawningAgent the spawning agent. * @param context the context in which the agents were spawned. * @param agentClazz the type of the spwnaed agents. * @param agents the spawned agents. * @param initializationParameters the initialization parameters. */ protected void fireAgentSpawnedOutsideAgent(UUID spawningAgent, AgentContext context, Class<? extends Agent> agentClazz, List<Agent> agents, Object... initializationParameters) { // Notify the listeners on the spawn events (not restricted to a single agent) for (final SpawnServiceListener l : this.globalListeners.getListeners(SpawnServiceListener.class)) { l.agentSpawned(spawningAgent, context, agents, initializationParameters); } // Send the event in the default space. final EventSpace defSpace = context.getDefaultSpace(); assert defSpace != null : "A context does not contain a default space"; //$NON-NLS-1$ final Address source = new Address(defSpace.getSpaceID(), spawningAgent == null ? context.getID() : spawningAgent); assert source != null; final AgentSpawned event = new AgentSpawned(source, agentClazz.getName(), Collections2.transform(agents, (it) -> it.getID())); defSpace.emit(event); } /** Notify the agent's listeners about its spawning. * * @param spawningAgent the spawning agent. * @param context the context in which the agent was spawned. * @param agent the spawned agent. * @param initializationParameters the initialization parameters. */ protected void fireAgentSpawnedInAgent(UUID spawningAgent, AgentContext context, Agent agent, Object... initializationParameters) { // Notify the listeners on the lifecycle events on // the just spawned agent. // Usually, only BICs and the AgentLifeCycleSupport in // io.janusproject.kernel.bic.StandardBuiltinCapacitiesProvider // is invoked. final ListenerCollection<SpawnServiceListener> list; synchronized (this.agentLifecycleListeners) { list = this.agentLifecycleListeners.get(agent.getID()); } if (list != null) { final List<Agent> singleton = Collections.singletonList(agent); for (final SpawnServiceListener l : list.getListeners(SpawnServiceListener.class)) { l.agentSpawned(spawningAgent, context, singleton, initializationParameters); } } } @Override public boolean killAgent(UUID agentID) { final boolean error = !isRunning(); // We should check if it is possible to kill the agent BEFORE killing it. final boolean foundAgent; boolean isLast = false; Agent killAgent = null; final String warningMessage; synchronized (getAgentRepositoryMutex()) { final Agent agent = this.agents.get(agentID); foundAgent = agent != null; if (foundAgent) { if (canKillAgent(agent)) { this.agents.remove(agentID); isLast = this.agents.isEmpty(); killAgent = agent; warningMessage = null; } else { warningMessage = Messages.StandardSpawnService_7; } } else { warningMessage = Messages.StandardSpawnService_8; } } if (warningMessage == null) { assert killAgent != null; fireAgentDestroyed(killAgent); if (isLast) { fireKernelAgentDestroy(); } if (error) { throw new SpawnServiceStopException(agentID); } return true; } if (killAgent != null) { try { final Logging skill = (Logging) GET_SKILL_FUNCTION.invoke(killAgent, Logging.class); skill.warning(warningMessage); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new Error(Messages.StandardSpawnService_9, e); } } else { this.logger.warning(warningMessage); } return false; } /** * Replies the registered agents. * * @return the registered agents. */ public SynchronizedSet<UUID> getAgents() { final Object mutex = getAgentRepositoryMutex(); synchronized (mutex) { return Collections3.synchronizedSet(this.agents.keySet(), mutex); } } /** * Replies the registered agent. * * @param id * is the identifier of the agent. * @return the registered agent, or <code>null</code>. */ Agent getAgent(UUID id) { assert id != null; synchronized (getAgentRepositoryMutex()) { return this.agents.get(id); } } @Override public void addKernelAgentSpawnListener(KernelAgentSpawnListener listener) { this.globalListeners.add(KernelAgentSpawnListener.class, listener); } @Override public void removeKernelAgentSpawnListener(KernelAgentSpawnListener listener) { this.globalListeners.remove(KernelAgentSpawnListener.class, listener); } /** * Notifies the listeners about the kernel agent creation. */ protected void fireKernelAgentSpawn() { for (final KernelAgentSpawnListener l : this.globalListeners.getListeners(KernelAgentSpawnListener.class)) { l.kernelAgentSpawn(); } } /** * Notifies the listeners about the kernel agent destruction. */ protected void fireKernelAgentDestroy() { for (final KernelAgentSpawnListener l : this.globalListeners.getListeners(KernelAgentSpawnListener.class)) { l.kernelAgentDestroy(); } } @Override public void addSpawnServiceListener(UUID id, SpawnServiceListener agentLifecycleListener) { synchronized (getAgentLifecycleListenerMutex()) { ListenerCollection<SpawnServiceListener> listeners = this.agentLifecycleListeners.get(id); if (listeners == null) { listeners = new ListenerCollection<>(); this.agentLifecycleListeners.put(id, listeners); } listeners.add(SpawnServiceListener.class, agentLifecycleListener); } } @Override public void addSpawnServiceListener(SpawnServiceListener agentLifecycleListener) { this.globalListeners.add(SpawnServiceListener.class, agentLifecycleListener); } @Override public void removeSpawnServiceListener(UUID id, SpawnServiceListener agentLifecycleListener) { synchronized (getAgentLifecycleListenerMutex()) { final ListenerCollection<SpawnServiceListener> listeners = this.agentLifecycleListeners.get(id); if (listeners != null) { listeners.remove(SpawnServiceListener.class, agentLifecycleListener); if (listeners.isEmpty()) { this.agentLifecycleListeners.remove(id); } } } } @Override public void removeSpawnServiceListener(SpawnServiceListener agentLifecycleListener) { this.globalListeners.remove(SpawnServiceListener.class, agentLifecycleListener); } /** * Replies if the given agent can be killed. * * @param agent * - agent to test. * @return <code>true</code> if the given agent can be killed, otherwise <code>false</code>. */ @SuppressWarnings("static-method") public boolean canKillAgent(Agent agent) { try { final AgentContext ac = BuiltinCapacityUtil.getContextIn(agent); if (ac != null) { final SynchronizedSet<UUID> participants = ac.getDefaultSpace().getParticipants(); if (participants != null) { synchronized (participants.mutex()) { if (participants.size() > 1 || (participants.size() == 1 && !participants.contains(agent.getID()))) { return false; } } } } return true; } catch (Throwable exception) { return false; } } /** * Notifies the listeners about the agent destruction. * * @param agent * - the destroyed agent. */ protected void fireAgentDestroyed(Agent agent) { final ListenerCollection<SpawnServiceListener> list; synchronized (getAgentLifecycleListenerMutex()) { list = this.agentLifecycleListeners.get(agent.getID()); } final SpawnServiceListener[] ilisteners; if (list != null) { ilisteners = list.getListeners(SpawnServiceListener.class); } else { ilisteners = null; } final SpawnServiceListener[] ilisteners2 = this.globalListeners.getListeners(SpawnServiceListener.class); try { final SynchronizedCollection<AgentContext> sc = BuiltinCapacityUtil.getContextsOf(agent); synchronized (sc.mutex()) { for (final AgentContext context : sc) { final EventSpace defSpace = context.getDefaultSpace(); defSpace.emit(new AgentKilled(defSpace.getAddress(agent.getID()), agent.getID(), agent.getClass().getName())); } } } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } if (ilisteners != null) { for (final SpawnServiceListener l : ilisteners) { l.agentDestroy(agent); } } for (final SpawnServiceListener l : ilisteners2) { l.agentDestroy(agent); } } @Override protected void doStart() { // Assume that when the service is starting, the kernel agent is up. fireKernelAgentSpawn(); notifyStarted(); } @Override protected void doStop() { synchronized (getAgentLifecycleListenerMutex()) { this.agentLifecycleListeners.clear(); } notifyStopped(); } /** * This exception is thrown when the spawning service of agents is disabled. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ public static class SpawnDisabledException extends RuntimeException { private static final long serialVersionUID = -380402400888610762L; /** * @param parentID * - the identifier of the parent entity that is creating the agent. * @param agentClazz * - the type of the agent to spawn. */ public SpawnDisabledException(UUID parentID, Class<? extends Agent> agentClazz) { super(MessageFormat.format(Messages.StandardSpawnService_0, parentID, agentClazz)); } } /** * This exception is thrown when the spawning service is not running when the killing function on an agent is called. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ public static class SpawnServiceStopException extends RuntimeException { private static final long serialVersionUID = 8104012713598435249L; /** * @param agentID * - the identifier of the agent. */ public SpawnServiceStopException(UUID agentID) { super(MessageFormat.format(Messages.StandardSpawnService_1, agentID)); } } /** * This exception is thrown when the agent to spawn is not generated according to a valid SARL specification version. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ public static class InvalidSarlSpecificationException extends RuntimeException { private static final long serialVersionUID = -3194494637438344108L; /** * @param agentType * the invalid type of agent. */ public InvalidSarlSpecificationException(Class<? extends Agent> agentType) { super(MessageFormat.format(Messages.StandardSpawnService_2, agentType.getName())); } } /** * This exception is thrown when an agent cannot be spawned. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ public static class CannotSpawnException extends RuntimeException { private static final long serialVersionUID = -380402400888610762L; /** * @param agentClazz * - the type of the agent to spawn. * @param cause * - the cause of the exception. */ public CannotSpawnException(Class<? extends Agent> agentClazz, Throwable cause) { super(MessageFormat.format(Messages.StandardSpawnService_3, agentClazz, (cause == null) ? null : cause.getLocalizedMessage()), cause); } } /** * An injection module that is able to inject the parent ID and agent ID when creating an agent. * * @author $Author: sgalland$ * @version $FullVersion$ * @mavengroupid $GroupId$ * @mavenartifactid $ArtifactId$ */ private static class JustInTimeAgentInjectionModule extends AbstractModule implements Provider<Agent> { private final Class<? extends Agent> agentType; private final Constructor<? extends Agent> constructor1; private final Constructor<? extends Agent> constructor2; private final UUID parentID; private final UUID agentID; JustInTimeAgentInjectionModule(Class<? extends Agent> agentType, UUID parentID, UUID agentID) { assert agentType != null; assert parentID != null; this.agentType = agentType; this.parentID = parentID; this.agentID = (agentID == null) ? UUID.randomUUID() : agentID; Constructor<? extends Agent> cons; Exception e1 = null; try { cons = this.agentType.getConstructor(UUID.class, UUID.class); } catch (NoSuchMethodException | SecurityException | IllegalArgumentException exception) { cons = null; e1 = exception; } this.constructor1 = cons; Exception e2 = null; try { cons = this.agentType.getConstructor(BuiltinCapacitiesProvider.class, UUID.class, UUID.class); } catch (NoSuchMethodException | SecurityException | IllegalArgumentException exception) { cons = null; e2 = exception; } this.constructor2 = cons; if (this.constructor1 == null && this.constructor2 == null) { throw new CannotSpawnException(this.agentType, e1 == null ? e2 : e1); } } @Override public void configure() { bind(Agent.class).toProvider(this); } @Override public Agent get() { assert this.constructor1 != null || this.constructor2 != null; try { if (this.constructor1 != null) { return this.constructor1.newInstance(this.parentID, this.agentID); } return this.constructor2.newInstance(null, this.parentID, this.agentID); } catch (InstantiationException | IllegalAccessException | InvocationTargetException exception) { throw new CannotSpawnException(this.agentType, exception); } } } }