/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.usergrid.chop.runner; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.usergrid.chop.runner.drivers.Driver; import org.apache.usergrid.chop.runner.drivers.TimeDriver; import org.reflections.Reflections; import org.apache.usergrid.chop.api.Project; import org.apache.usergrid.chop.api.Runner; import org.apache.usergrid.chop.api.Signal; import org.apache.usergrid.chop.api.StatsSnapshot; import org.apache.usergrid.chop.api.State; import org.apache.usergrid.chop.api.IterationChop; import org.apache.usergrid.chop.api.TimeChop; import org.apache.usergrid.chop.spi.RunManager; import org.apache.usergrid.chop.spi.RunnerRegistry; import org.apache.usergrid.chop.runner.drivers.IterationDriver; import org.apache.usergrid.chop.stack.ChopCluster; import org.apache.usergrid.chop.stack.ICoordinatedCluster; import org.apache.usergrid.chop.stack.Instance; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import com.google.inject.Inject; import com.google.inject.Singleton; /** * The Controller controls the process of executing chops on test classes. */ @Singleton public class Controller implements IController, Runnable { private static final Logger LOG = LoggerFactory.getLogger( Controller.class ); // @todo make this configurable and also put this into the project or runner fig private static final long DEFAULT_LAGER_WAIT_TIMEOUT_MILLIS = 120000; private final Object lock = new Object(); private Set<Class<?>> timeChopClasses; private Set<Class<?>> iterationChopClasses; private State state = State.INACTIVE; private Driver<?> currentDriver; private Map<String, ICoordinatedCluster> clusterMap = new HashMap<String, ICoordinatedCluster>(); private List<Runner> otherRunners; private RunManager runManager; private Project project; private int runNumber; @Inject Controller( Project project, RunnerRegistry registry, RunManager runManager, Runner me ) { setProject( project ); setRunManager( runManager ); if ( state != State.INACTIVE ) { runNumber = runManager.getNextRunNumber( project ); otherRunners = registry.getRunners( me ); List<ICoordinatedCluster> clusters = registry.getClusters(); if ( clusters == null ) { LOG.debug( "Returned clusters list is null" ); } else { LOG.info( "{} clusters total", clusters.size() ); for ( ICoordinatedCluster cluster : clusters ) { clusterMap.put( cluster.getName(), cluster ); LOG.info( "Cluster name {}, {} instances", cluster.getName(), cluster.getInstances().size() ); for ( Instance i : cluster.getInstances() ) { LOG.debug( "Public: {}, Private: {}", i.getPublicIpAddress(), i.getPrivateIpAddress() ); } } injectClusters(); } } } private void setProject( Project project ) { // if the project is null which should never really happen we just return // and stay in the INACTIVE state waiting for a load to activate this runner if ( project == null ) { return; } // setup the valid runner project this.project = project; LOG.info( "Controller injected with project properties: {}", project ); // if the project test package base is null there's nothing we can do but // return and stay in the inactive state waiting for a load to occur if ( project.getTestPackageBase() == null ) { return; } // reflect into package base looking for annotated classes Reflections reflections = new Reflections( project.getTestPackageBase() ); timeChopClasses = reflections.getTypesAnnotatedWith( TimeChop.class ); LOG.info( "TimeChop classes = {}", timeChopClasses ); iterationChopClasses = reflections.getTypesAnnotatedWith( IterationChop.class ); LOG.info( "IterationChop classes = {}", iterationChopClasses ); // if we don't have a valid project load key then this is bogus if ( project.getLoadKey() == null ) { state = State.INACTIVE; LOG.info( "Null loadKey: controller going into INACTIVE state." ); return; } if ( timeChopClasses.isEmpty() && iterationChopClasses.isEmpty() ) { state = State.INACTIVE; LOG.info( "Nothing to scan: controller going into INACTIVE state." ); return; } state = State.READY; LOG.info( "We have things to scan and a valid loadKey: controller going into READY state." ); } private void setRunManager( RunManager runManager ) { Preconditions.checkNotNull( runManager, "The RunManager cannot be null." ); this.runManager = runManager; } /** * Scans all @IterationChop and @TimeChop annotated test classes in code base * and sets @ChopCluster annotated fields in these classes to their runtime values. * <p> * For this to work properly, fields in test classes should be declared as follows: * <p> * <code>@ChopCluster( name = "ClusterName" )</code> * <code>public static ICoordinatedCluster clusterToBeInjected;</code> * </p> * In this case, <code>clusterToBeInjected</code> field will be set to the cluster object * taken from the coordinator, if indeed a cluster with a name of "ClusterName" exists. */ private void injectClusters() { Collection<Class<?>> testClasses = new LinkedList<Class<?>>(); testClasses.addAll( iterationChopClasses ); testClasses.addAll( timeChopClasses ); for( Class<?> iterationTest : testClasses ) { LOG.info( "Scanning test class {} for annotations", iterationTest.getName() ); for( Field f : iterationTest.getDeclaredFields() ) { if( f.getType().isAssignableFrom( ICoordinatedCluster.class ) ) { for( Annotation annotation : f.getDeclaredAnnotations() ) { if( annotation.annotationType().equals( ChopCluster.class ) ) { String clusterName = ( ( ChopCluster ) annotation).name(); ICoordinatedCluster cluster; if ( ! clusterMap.containsKey( clusterName ) || ( cluster = clusterMap.get( clusterName ) ) == null ) { LOG.warn( "No clusters found with name: {}", clusterName ); continue; } try { LOG.info( "Setting cluster {} on {} field", clusterName, f.getName() ); f.set( null, cluster ); } catch ( IllegalAccessException e ) { LOG.error( "Cannot access field {}", f.getName(), e ); } } } } } } } @Override public StatsSnapshot getCurrentChopStats() { return currentDriver != null ? currentDriver.getChopStats() : null; } @Override public State getState() { return state; } @Override public boolean isRunning() { return state == State.RUNNING; } @Override public boolean needsReset() { return state == State.STOPPED; } @Override public Project getProject() { return project; } @Override public void reset() { synchronized ( lock ) { Preconditions.checkState( state.accepts( Signal.RESET, State.READY ), "Cannot reset the controller in state: " + state ); state = state.next( Signal.RESET ); currentDriver = null; } } @Override public void start() { synchronized ( lock ) { Preconditions.checkState( state.accepts( Signal.START ), "Cannot start the controller in state: " + state ); runNumber = runManager.getNextRunNumber( project ); state = state.next( Signal.START ); new Thread( this ).start(); lock.notifyAll(); } } @Override public void stop() { synchronized ( lock ) { Preconditions.checkState( state.accepts( Signal.STOP ), "Cannot stop a controller in state: " + state ); state = state.next( Signal.STOP ); lock.notifyAll(); } } @Override public void send( final Signal signal ) { Preconditions.checkState( state.accepts( signal ), state.getMessage( signal ) ); switch ( signal ) { case STOP: stop(); break; case START: start(); break; case RESET: reset(); break; default: throw new IllegalStateException( "Just accepting start, stop, and reset." ); } } /** * Gets the collection of runners that are still executing a chop on a test class. * * @param runNumber the current run number * @param testClass the current chop test * @return the runners still executing a test class */ private Collection<Runner> getLagers( int runNumber, Class<?> testClass ) { Collection<Runner> lagers = new ArrayList<Runner>( otherRunners.size() ); for ( Runner runner : otherRunners ) { if ( runManager.hasCompleted( runner, project, runNumber, testClass ) ) { LOG.info( "Runner {} has completed test {}", runner.getHostname(), testClass.getName() ); } else { LOG.warn( "Waiting on runner {} to complete test {}", runner.getHostname(), testClass.getName() ); lagers.add( runner ); } } return lagers; } @Override public void run() { for ( Class<?> iterationTest : iterationChopClasses ) { synchronized ( lock ) { currentDriver = new IterationDriver( iterationTest ); currentDriver.setTimeout( project.getTestStopTimeout() ); currentDriver.start(); lock.notifyAll(); } LOG.info( "Started new IterationDriver driver: controller state = {}", state ); while ( currentDriver.blockTilDone( project.getTestStopTimeout() ) ) { if ( state == State.STOPPED ) { LOG.info( "Got the signal to stop running." ); synchronized ( lock ) { currentDriver.stop(); currentDriver = null; lock.notifyAll(); } return; } } LOG.info( "Out of while loop. controller state = {}, currentDriver is running = {}", state, currentDriver.isRunning()); if ( currentDriver.isComplete() ) { BasicSummary summary = new BasicSummary( runNumber ); summary.setIterationTracker( ( ( IterationDriver ) currentDriver ).getTracker() ); try { runManager.store( project, summary, currentDriver.getResultsFile(), currentDriver.getTracker().getTestClass() ); } catch ( Exception e ) { LOG.error( "Failed to store project results file " + currentDriver.getResultsFile() + " with runManager", e ); } long startWaitingForLagers = System.currentTimeMillis(); while ( state == State.RUNNING ) { Collection<Runner> lagers = getLagers( runNumber, iterationTest ); if ( lagers.size() > 0 ) { LOG.info( "IterationChop test {} completed but waiting on lagging runners:\n{}", iterationTest.getName(), lagers ); } else { LOG.info( "IterationChop test {} completed and there are NO lagging runners.", iterationTest.getName() ); break; } synchronized ( lock ) { try { lock.wait( project.getTestStopTimeout() ); } catch ( InterruptedException e ) { LOG.error( "Awe snap! Someone woke me up before it was time!" ); } } boolean waitTimeoutReached = ( System.currentTimeMillis() - startWaitingForLagers ) > DEFAULT_LAGER_WAIT_TIMEOUT_MILLIS; if ( waitTimeoutReached && ( lagers.size() > 0 ) ) { LOG.warn( "Timeout reached. Not waiting anymore for lagers: {}", lagers ); break; } } } } for ( Class<?> timeTest : timeChopClasses ) { synchronized ( lock ) { currentDriver = new TimeDriver( timeTest ); currentDriver.setTimeout( project.getTestStopTimeout() ); currentDriver.start(); lock.notifyAll(); } LOG.info( "Started new TimeDriver driver: controller state = {}", state ); while ( currentDriver.blockTilDone( project.getTestStopTimeout() ) ) { if ( state == State.STOPPED ) { LOG.info( "Got the signal to stop running." ); synchronized ( lock ) { currentDriver.stop(); currentDriver = null; lock.notifyAll(); } return; } } LOG.info( "Out of while loop. controller state = {}, currentDriver is running = {}", state, currentDriver.isRunning()); if ( currentDriver.isComplete() ) { BasicSummary summary = new BasicSummary( runNumber ); summary.setTimeTracker( ( ( TimeDriver ) currentDriver ).getTracker() ); try { runManager.store( project, summary, currentDriver.getResultsFile(), currentDriver.getTracker().getTestClass() ); } catch ( Exception e ) { LOG.error( "Failed to store project results file " + currentDriver.getResultsFile() + " with runManager", e ); } long startWaitingForLagers = System.currentTimeMillis(); while ( state == State.RUNNING ) { Collection<Runner> lagers = getLagers( runNumber, timeTest ); if ( lagers.size() > 0 ) { LOG.warn( "TimeChop test {} completed but waiting on lagging runners:\n{}", timeTest.getName(), lagers ); } else { LOG.info( "TimeChop test {} completed and there are NO lagging runners.", timeTest.getName() ); break; } synchronized ( lock ) { try { lock.wait( project.getTestStopTimeout() ); } catch ( InterruptedException e ) { LOG.error( "Awe snap! Someone woke me up before it was time!" ); } } boolean waitTimeoutReached = ( System.currentTimeMillis() - startWaitingForLagers ) > DEFAULT_LAGER_WAIT_TIMEOUT_MILLIS; if ( waitTimeoutReached && ( lagers.size() > 0 ) ) { LOG.warn( "Timeout reached. Not waiting anymore for lagers: {}", lagers ); break; } } } } LOG.info( "The controller has completed." ); currentDriver = null; state = state.next( Signal.COMPLETED ); } }