/**
* Copyright 2014-2017 Linagora, Université Joseph Fourier, Floralis
*
* The present code is developed in the scope of the joint LINAGORA -
* Université Joseph Fourier - Floralis research program and is designated
* as a "Result" pursuant to the terms and conditions of the LINAGORA
* - Université Joseph Fourier - Floralis research program. Each copyright
* holder of Results enumerated here above fully & independently holds complete
* ownership of the complete Intellectual Property rights applicable to the whole
* of said Results, and may freely exploit it in any manner which does not infringe
* the moral rights of the other copyright holders.
*
* 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 net.roboconf.agent.internal.lifecycle;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import net.roboconf.agent.internal.misc.AgentUtils;
import net.roboconf.core.model.beans.Import;
import net.roboconf.core.model.beans.Instance;
import net.roboconf.core.model.beans.Instance.InstanceStatus;
import net.roboconf.core.model.helpers.ImportHelpers;
import net.roboconf.core.model.helpers.InstanceHelpers;
import net.roboconf.core.utils.Utils;
import net.roboconf.messaging.api.business.IAgentClient;
import net.roboconf.messaging.api.business.ListenerCommand;
import net.roboconf.messaging.api.messages.from_agent_to_dm.MsgNotifInstanceChanged;
import net.roboconf.plugin.api.PluginException;
import net.roboconf.plugin.api.PluginInterface;
/**
* The super class which deal with the life cycle of instances.
* <p>
* This class must make sure no instance remains in a transitive state
* (starting, deploying, etc).
* </p>
*
* @author Vincent Zurczak - Linagora
*/
public abstract class AbstractLifeCycleManager {
private static final String FORCE = "force";
private final Logger logger = Logger.getLogger( getClass().getName());
protected final String appName;
protected final IAgentClient messagingClient;
/**
* Constructor.
* @param appName
*/
protected AbstractLifeCycleManager( String appName, IAgentClient messagingClient ) {
this.appName = appName;
this.messagingClient = messagingClient;
}
/**
* Builds the right handler depending on the current instance's state.
* @param instance an instance
* @param appName the application name
* @param messagingClient the messaging client
* @return a non-null manager to update the instance's life cycle
*/
public static AbstractLifeCycleManager build( Instance instance, String appName, IAgentClient messagingClient ) {
AbstractLifeCycleManager result;
switch( instance.getStatus()) {
case DEPLOYED_STARTED:
result = new DeployedStarted( appName, messagingClient );
break;
case DEPLOYED_STOPPED:
result = new DeployedStopped( appName, messagingClient );
break;
case NOT_DEPLOYED:
result = new NotDeployed( appName, messagingClient );
break;
case UNRESOLVED:
result = new Unresolved( appName, messagingClient );
break;
case WAITING_FOR_ANCESTOR:
result = new WaitingForAncestor( appName, messagingClient );
break;
default:
result = new TransitiveStates( appName, messagingClient );
break;
}
return result;
}
/**
* Undertakes the appropriate actions to change the state of an instance.
* @param instance the instance to work on
* @param plugin the plug-in associated with the instance's installer
* @param newStatus the target state to reach
* @param fileNameToFileContent the recipe resources (for deployment only)
* @throws IOException if something went wrong
* @throws PluginException if something went wrong
*/
public abstract void changeInstanceState(
Instance instance, PluginInterface plugin,
InstanceStatus newStatus , Map<String,byte[]> fileNameToFileContent )
throws IOException, PluginException;
/**
* Updates the status of an instance based on the imports.
* @param impactedInstance the instance whose imports may have changed
* @param plugin the plug-in to use to apply a concrete modification
* @param statusChanged The changed status of the instance that changed (e.g. that provided new imports)
* @param importChanged The individual imports that changed
*/
public void updateStateFromImports( Instance impactedInstance, PluginInterface plugin, Import importChanged, InstanceStatus statusChanged )
throws IOException, PluginException {
// Do we have all the imports we need?
boolean haveAllImports = ImportHelpers.hasAllRequiredImports( impactedInstance, this.logger );
// Update the life cycle of this instance if necessary
// Maybe we have something to start
if( haveAllImports ) {
if( impactedInstance.getStatus() == InstanceStatus.UNRESOLVED
|| impactedInstance.data.remove( FORCE ) != null ) {
InstanceStatus oldState = impactedInstance.getStatus();
impactedInstance.setStatus( InstanceStatus.STARTING );
try {
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, impactedInstance ));
plugin.start( impactedInstance );
impactedInstance.setStatus( InstanceStatus.DEPLOYED_STARTED );
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, impactedInstance ));
this.messagingClient.publishExports( impactedInstance );
this.messagingClient.listenToRequestsFromOtherAgents( ListenerCommand.START, impactedInstance );
} catch( Exception e ) {
this.logger.severe( "An error occured while starting " + InstanceHelpers.computeInstancePath( impactedInstance ));
Utils.logException( this.logger, e );
impactedInstance.setStatus( oldState );
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, impactedInstance ));
}
} else if( impactedInstance.getStatus() == InstanceStatus.DEPLOYED_STARTED ) {
plugin.update( impactedInstance, importChanged, statusChanged );
} else {
this.logger.fine( InstanceHelpers.computeInstancePath( impactedInstance ) + " checked import changes but has nothing to update (1)." );
}
}
// Or maybe we have something to stop
else {
if( impactedInstance.getStatus() == InstanceStatus.DEPLOYED_STARTED ) {
stopInstance( impactedInstance, plugin, true );
} else {
this.logger.fine( InstanceHelpers.computeInstancePath( impactedInstance ) + " checked import changes but has nothing to update (2)." );
}
}
}
/**
* Deploys an instance (prerequisite: NOT_DEPLOYED).
* @param instance the instance
* @param plugin the associated plug-in
* @param fileNameToFileContent a map containing resources for the plug-in
* @throws IOException if something went wrong
*/
void deploy( Instance instance, PluginInterface plugin, Map<String,byte[]> fileNameToFileContent )
throws IOException {
String instancePath = InstanceHelpers.computeInstancePath( instance );
if( instance.getStatus() != InstanceStatus.NOT_DEPLOYED ) {
this.logger.fine( instancePath + " cannot be deployed. Prerequisite status: NOT_DEPLOYED (but was " + instance.getStatus() + ")." );
} else if( instance.getParent() != null
&& instance.getParent().getStatus() != InstanceStatus.DEPLOYED_STARTED
&& instance.getParent().getStatus() != InstanceStatus.DEPLOYED_STOPPED
&& instance.getParent().getStatus() != InstanceStatus.UNRESOLVED
&& instance.getParent().getStatus() != InstanceStatus.WAITING_FOR_ANCESTOR ) {
this.logger.fine( instancePath + " cannot be deployed because its parent is not deployed. Parent status: " + instance.getParent().getStatus() + "." );
} else {
this.logger.fine( "Deploying instance " + instancePath );
// User reporting => deploying...
instance.setStatus( InstanceStatus.DEPLOYING );
try {
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, instance ));
// Clean up the potential remains of a previous installation
AgentUtils.deleteInstanceResources( instance );
// Copy the resources
AgentUtils.copyInstanceResources( instance, fileNameToFileContent );
// Initialize the plugin
plugin.initialize( instance );
// Invoke the plug-in
plugin.deploy( instance );
instance.setStatus( InstanceStatus.DEPLOYED_STOPPED );
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, instance ));
} catch( Exception e ) {
this.logger.severe( "An error occured while deploying " + instancePath );
Utils.logException( this.logger, e );
instance.setStatus( InstanceStatus.NOT_DEPLOYED );
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, instance ));
}
}
}
/**
* Undeploys an instance (prerequisite: DEPLOYED_STOPPED).
* @param instance the instance
* @param plugin the associated plug-in
* @throws IOException if something went wrong
*/
void undeploy( Instance instance, PluginInterface plugin )
throws IOException {
// Preliminary check
String instancePath = InstanceHelpers.computeInstancePath( instance );
if( instance.getStatus() != InstanceStatus.DEPLOYED_STOPPED
&& instance.getStatus() != InstanceStatus.UNRESOLVED
&& instance.getStatus() != InstanceStatus.WAITING_FOR_ANCESTOR ) {
this.logger.fine( instancePath + " cannot be undeployed. Prerequisite status: DEPLOYED_STOPPED or UNRESOLVED or WAITING_FOR_ANCESTOR (but was " + instance.getStatus() + ")." );
return;
}
// Children may have to be marked as stopped.
// From a plug-in point of view, we only use the one for the given instance.
// Children are SUPPOSED to be stopped immediately.
List<Instance> instancesToUndeploy = InstanceHelpers.buildHierarchicalList( instance );
Collections.reverse( instancesToUndeploy );
InstanceStatus newStatus = InstanceStatus.NOT_DEPLOYED;
try {
// Update the statuses if necessary
for( Instance i : instancesToUndeploy ) {
if( i.getStatus() == InstanceStatus.NOT_DEPLOYED )
continue;
i.setStatus( InstanceStatus.UNDEPLOYING );
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, i ));
this.messagingClient.unpublishExports( i );
}
// Hypothesis: undeploying an instance undeploys its children too.
try {
plugin.undeploy( instance );
// Delete files for undeployed instances
for( Instance i : instancesToUndeploy )
AgentUtils.deleteInstanceResources( i );
} catch( PluginException e ) {
this.logger.severe( "An error occured while undeploying " + InstanceHelpers.computeInstancePath( instance ));
Utils.logException( this.logger, e );
newStatus = InstanceStatus.DEPLOYED_STOPPED;
}
} catch( Exception e ) {
newStatus = InstanceStatus.DEPLOYED_STOPPED;
Utils.logException( this.logger, e );
} finally {
// Update the status of all the instances
for( Instance i : instancesToUndeploy ) {
i.setStatus( newStatus );
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, i ));
}
}
}
/**
* Starts an instance (prerequisite: DEPLOYED_STOPPED).
* @param instance the instance
* @param plugin the associated plug-in
* @throws IOException if something went wrong
*/
void start( Instance instance, PluginInterface plugin )
throws IOException {
String instancePath = InstanceHelpers.computeInstancePath( instance );
if( instance.getStatus() != InstanceStatus.DEPLOYED_STOPPED
&& instance.getStatus() != InstanceStatus.WAITING_FOR_ANCESTOR ) {
this.logger.fine( instancePath + " cannot be started. Prerequisite status: DEPLOYED_STOPPED or WAITING_FOR_ANCESTOR (but was " + instance.getStatus() + ")." );
} else if( instance.getParent() != null
&& instance.getParent().getStatus() != InstanceStatus.DEPLOYED_STARTED ) {
// An element cannot start if its parent is not started.
// However, if the parent is unresolved (or waiting for its parent to start),
// then we can mark this instance as ready to start with its parent.
this.logger.fine( instancePath + " cannot be started because its parent is not started. Parent status: " + instance.getParent().getStatus() + "." );
if( instance.getParent().getStatus() == InstanceStatus.UNRESOLVED
|| instance.getParent().getStatus() == InstanceStatus.WAITING_FOR_ANCESTOR ) {
instance.setStatus( InstanceStatus.WAITING_FOR_ANCESTOR );
this.logger.fine( instancePath + " will start as soon as its parent starts." );
}
} else {
// Otherwise, start it
try {
if( ImportHelpers.hasAllRequiredImports( instance, this.logger )) {
instance.data.put( FORCE, "whatever" );
updateStateFromImports( instance, plugin, null, null );
} else {
this.logger.fine(
"Instance " + InstanceHelpers.computeInstancePath( instance )
+ " cannot be started, dependencies are missing. Requesting exports from other agents." );
instance.setStatus( InstanceStatus.UNRESOLVED );
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, instance ));
this.messagingClient.requestExportsFromOtherAgents( instance );
}
} catch( PluginException e ) {
this.logger.severe( "An error occured while starting " + InstanceHelpers.computeInstancePath( instance ));
Utils.logException( this.logger, e );
instance.setStatus( InstanceStatus.DEPLOYED_STOPPED );
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, instance ));
}
}
}
/**
* Stops an instance (prerequisite: DEPLOYED_STARTED).
* @param instance the instance
* @param plugin the associated plug-in
* @throws IOException if something went wrong
*/
void stop( Instance instance, PluginInterface plugin ) throws IOException {
String instancePath = InstanceHelpers.computeInstancePath( instance );
if( instance.getStatus() != InstanceStatus.DEPLOYED_STARTED )
this.logger.fine( instancePath + " cannot be stopped. Prerequisite status: DEPLOYED_STARTED (but was " + instance.getStatus() + ")." );
else
stopInstance( instance, plugin, false );
}
/**
* Stops an instance.
* <p>
* If necessary, it will stop its children.
* </p>
*
* @param instance an instance (must be deployed and started)
* @param plugin the plug-in to use for concrete modifications
* @param isDueToImportsChange true if this method is called because an import changed, false it matches a manual life cycle change
* @throws IOException if something went wrong
*/
private void stopInstance( Instance instance, PluginInterface plugin, boolean isDueToImportsChange )
throws IOException {
this.logger.fine( "Stopping instance " + InstanceHelpers.computeInstancePath( instance ) + "..." );
// Children may have to be stopped too.
// From a plug-in point of view, we only use the one for the given instance.
// Children are SUPPOSED to be stopped immediately.
List<Instance> instancesToStop = InstanceHelpers.buildHierarchicalList( instance );
Collections.reverse( instancesToStop );
try {
// Update the statuses if necessary
for( Instance i : instancesToStop ) {
if( i.getStatus() != InstanceStatus.DEPLOYED_STARTED )
continue;
i.setStatus( InstanceStatus.STOPPING );
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, i ));
this.messagingClient.listenToRequestsFromOtherAgents( ListenerCommand.STOP, i );
this.messagingClient.unpublishExports( i );
}
// Stop the initial instance.
// Even if the plugin invocation fails, we cannot consider these instances
// to be reliable anymore. So, we keep on as if the operation went well.
try {
plugin.stop( instance );
} catch( PluginException e ) {
this.logger.severe( "An error occured while stopping " + InstanceHelpers.computeInstancePath( instance ));
Utils.logException( this.logger, e );
}
} finally {
List<Instance> forNotifications = new ArrayList<> ();
for( Instance i : instancesToStop ) {
if( i.getStatus() != InstanceStatus.STOPPING
&& i.getStatus() != InstanceStatus.UNRESOLVED )
continue;
// Not due to import change? => stopped.
InstanceStatus newStatus;
if( ! isDueToImportsChange )
newStatus = InstanceStatus.DEPLOYED_STOPPED;
// If we deal with the instance whose dependencies changed => unresolved.
else if( i == instance )
newStatus = InstanceStatus.UNRESOLVED;
// Otherwise, we deal with a child instance.
else
newStatus = InstanceStatus.WAITING_FOR_ANCESTOR;
i.setStatus( newStatus );
forNotifications.add( i );
}
// Notify at the end
for( Instance i : forNotifications )
this.messagingClient.sendMessageToTheDm( new MsgNotifInstanceChanged( this.appName, i ));
}
}
}