/** * 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.target.api; import java.io.Closeable; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import net.roboconf.core.model.beans.Instance; import net.roboconf.core.model.beans.Instance.InstanceStatus; import net.roboconf.core.utils.Utils; /** * An abstract implementation of target handler that supports time-consuming configurations. * <p> * Creating a machine is generally straight-forward. However, configuring it can take time. * Rather than blocking a thread, this class provides a timer to schedule configuration steps. * </p> * <p> * To use this class, just extend it and implement {@link MachineConfigurator}.<br> * A machine configurator is in charge of configuring a machine. There will be one instance per * VM instance. This instance will be invoked periodically until the machine configuration is completed. * </p> * * @author Vincent Zurczak - Linagora */ public abstract class AbstractThreadedTargetHandler implements TargetHandler { protected static final int DEFAULT_DELAY = 1000; // Protected fields protected final Logger logger = Logger.getLogger( getClass().getName()); protected long delay = DEFAULT_DELAY; // Private fields private ScheduledThreadPoolExecutor timer; private final Map<String,MachineConfigurator> machineIdToConfigurators = new ConcurrentHashMap<> (); private final CancelledMachines cancelledMachineIds = new CancelledMachines(); /** * Starts a thread to periodically check machines under creation process. * <p> * The period is defined by {@link #delay} whose value is expressed in milliseconds * and whose default value is {@value #DEFAULT_DELAY}. * </p> * <p> * This method should be made invokable by iPojo. * </p> */ public void start() { this.timer = new ScheduledThreadPoolExecutor( 1 ); this.timer.scheduleWithFixedDelay( new CheckingRunnable( this.machineIdToConfigurators, this.cancelledMachineIds ), 0, this.delay, TimeUnit.MILLISECONDS ); } /** * Stops the background thread. * <p> * This method should be made invokable by iPojo. * </p> */ public void stop() { this.timer.shutdownNow(); this.timer = null; } @Override public final void configureMachine( TargetHandlerParameters parameters, String machineId, Instance scopedInstance ) throws TargetException { this.logger.fine( "Configuring machine '" + machineId + "'." ); this.machineIdToConfigurators.put( machineId, machineConfigurator( parameters, machineId, scopedInstance )); } /** * Gets or builds a machine configurator to (guess what!) configure a machine. * @param parameters the target parameters * @param machineId the ID machine of the machine to configure * @param scopedInstance the scoped instance * @return a machine configurator */ public abstract MachineConfigurator machineConfigurator( TargetHandlerParameters parameters, String machineId, Instance scopedInstance ); /** * Cancels a machine configurator. * <p> * This method is useful if we try to terminate a machine that is * still being configured. This make sure the configuration will stop * anyway. * </p> * <p> * If the configuration was already completed, this method does nothing. * Same thing if the machine ID is null, invalid or not found. * </p> * * @param machineId the machine ID */ protected void cancelMachineConfigurator( String machineId ) { this.cancelledMachineIds.addMachineId( machineId ); } /** * A class in charge of configuring a machine. * <p> * It is up to the implementation to handle states to resume the * configuration of a given machine. * </p> * * @author Vincent Zurczak - Linagora */ public interface MachineConfigurator extends Closeable { /** * Configures a machine. * @return true if the machine is completely configured, false otherwise * <p> * If <code>false</code> is returned, another invocation will be scheduled. * </p> * * @throws TargetException if something went wrong during the configuration */ boolean configure() throws TargetException; /** * @return the scoped instance (cannot be null) */ Instance getScopedInstance(); } /** * @author Vincent Zurczak - Linagora */ static class CheckingRunnable implements Runnable { private final CancelledMachines cancelledMachineIds; private final Map<String,MachineConfigurator> machineIdToConfigurators; private final Logger logger = Logger.getLogger( getClass().getName()); /** * Constructor. */ public CheckingRunnable( Map<String,MachineConfigurator> machineIdToConfigurators, CancelledMachines cancelledMachineIds ) { super(); this.machineIdToConfigurators = machineIdToConfigurators; this.cancelledMachineIds = cancelledMachineIds; } @Override public void run() { this.logger.finest( "Periodic check is running." ); // Deal with cancelled configurations for( String machineId : this.cancelledMachineIds.removeSnapshot()) { MachineConfigurator handler = this.machineIdToConfigurators.remove( machineId ); if( handler != null ) closeConfigurator( machineId, handler ); } // Check the state of all the launchers Set<String> keysToRemove = new HashSet<> (); for( Map.Entry<String,MachineConfigurator> entry : this.machineIdToConfigurators.entrySet()) { MachineConfigurator handler = entry.getValue(); try { if( handler.configure()) { // Configure is completed, remove it from the things to check keysToRemove.add( entry.getKey()); closeConfigurator( entry.getKey(), handler ); // It may require to be configured from the DM => add the right marker Instance scopedInstance = handler.getScopedInstance(); scopedInstance.data.put( Instance.READY_FOR_CFG_MARKER, "true" ); } } catch( Throwable t ) { // We need to catch ALL the exceptions. // Otherwise, the timer will stop scheduling this runnable, // and this may result in unpredictable behaviors in Roboconf deployments. // That would impact all the VMs on a given target. this.logger.severe( "An error occurred while configuring machine '" + entry.getKey() + "'. " + t.getMessage()); Utils.logException( this.logger, t ); keysToRemove.add( entry.getKey()); // If a problem occurs, try to close the handler anyway closeConfigurator( entry.getKey(), handler ); // Update the scoped instance Instance scopedInstance = handler.getScopedInstance(); if( scopedInstance.getStatus() != InstanceStatus.NOT_DEPLOYED ) { scopedInstance.setStatus( InstanceStatus.PROBLEM ); scopedInstance.data.put( Instance.LAST_PROBLEM, "Configuration failed. " + t.getMessage()); } } } // Remove old keys this.machineIdToConfigurators.keySet().removeAll( keysToRemove ); } /** * Closes the configurator. * @param machineId * @param handler */ private void closeConfigurator( String machineId, MachineConfigurator handler ) { try { this.logger.fine( "Closing the configurator for machine " + machineId ); handler.close(); } catch( Exception e ) { this.logger.warning( "An error occurred while closing the configurator for machine '" + machineId + "'. " + e.getMessage()); Utils.logException( this.logger, e ); } } } /** * A class that guarantees concurrent operations on a set of cancelled IDs. * @author Vincent Zurczak - Linagora */ static class CancelledMachines { private final Set<String> cancelledIds = new HashSet<> (); /** * Adds a machine ID. * @param machineId a machine ID (can be null) */ public synchronized void addMachineId( String machineId ) { if( machineId != null ) this.cancelledIds.add( machineId ); } /** * Returns and removes all the cancelled IDs. * @return a non-null set */ public synchronized Set<String> removeSnapshot() { Set<String> result = new HashSet<>( this.cancelledIds ); this.cancelledIds.clear(); return result; } } }