/* * Copyright (C) 2011 Laurent Caillette * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.novelang.nhovestone; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.commons.lang.StringUtils; import org.novelang.KnownVersions; import org.novelang.Version; import org.novelang.common.FileTools; import org.novelang.logger.Logger; import org.novelang.logger.LoggerFactory; import org.novelang.nhovestone.driver.HttpDaemonDriver; import org.novelang.nhovestone.scenario.Measurer; import org.novelang.nhovestone.scenario.Upsizer; import org.novelang.outfit.shell.ProcessCreationException; import org.novelang.outfit.shell.ProcessInitializationException; /** * Starts and queries several {@link org.novelang.nhovestone.driver.HttpDaemonDriver}s against an * evolving source document. * * @author Laurent Caillette */ public class Scenario< UPSIZING, MEASUREMENT > { private static final Logger LOGGER = LoggerFactory.getLogger( Scenario.class ); private final String name ; private final int warmupIterations ; private final Integer maximumIterations ; /** * Keeps the monitoring state for one given {@link org.novelang.Version}. * Not static as it saves additional generic declaration. */ private class Monitoring { public final HttpDaemonDriver driver ; public Termination termination = null ; public final List< MEASUREMENT > measurements = Lists.newArrayList() ; public Monitoring( final HttpDaemonDriver driver ) { this.driver = driver ; } } /** * Mutable object: contains list of non-null {@link Scenario.Monitoring} objects. */ private final Map< Version, Monitoring > monitorings = Maps.newHashMap() ; private final String documentRequest ; private final Upsizer< UPSIZING > upsizer ; private final Measurer< MEASUREMENT > measurer ; public Scenario( final Configuration< ? extends Configuration, UPSIZING, MEASUREMENT > configuration ) throws IOException { name = ( configuration.getScenarioName() == null ) ? getClass().getSimpleName() : configuration.getScenarioName() ; this.warmupIterations = configuration.getWarmupIterationCount() > 0 ? configuration.getWarmupIterationCount() : DEFAULT_WARMUP_ITERATIONS ; this.maximumIterations = configuration.getMaximumIterations() > 0 ? configuration.getMaximumIterations() : DEFAULT_MAXIMUM_ITERATIONS ; final File scenarioDirectory = FileTools.createFreshDirectory( configuration.getScenariiDirectory(), FileTools.sanitizeFileName( name ) ) ; final File contentDirectory = FileTools.createFreshDirectory( scenarioDirectory, "content" ) ; upsizer = configuration.getUpsizerFactory().create( contentDirectory ) ; documentRequest = configuration.getUpsizerFactory().getDocumentRequest() ; Preconditions.checkArgument( ! StringUtils.isBlank( documentRequest ) ) ; this.measurer = Preconditions.checkNotNull( configuration.getMeasurer() ) ; Preconditions.checkArgument( configuration.getJvmHeapSizeMegabytes() > 0 ) ; int tcpPort = configuration.getFirstTcpPort() ; Preconditions.checkArgument( tcpPort > 0 ) ; for( final Version version : configuration.getVersions() ) { final File versionWorkingDirectory = FileTools.createFreshDirectory( scenarioDirectory, version.getName() ) ; final HttpDaemonDriver httpDaemonDriver = new HttpDaemonDriver( org.novelang.outfit.Husk.create( HttpDaemonDriver.Configuration.class ) .withContentRootDirectory( contentDirectory ) .withJvmHeapSizeMegabytes( configuration.getJvmHeapSizeMegabytes() ) .withJavaClasses( KnownVersions.asJavaClasses( configuration.getInstallationsDirectory(), version ) ) .withLogDirectory( versionWorkingDirectory ) .withVersion( version ) .withHttpPort( tcpPort ) .withWorkingDirectory( versionWorkingDirectory ) ) ; final Monitoring monitoring = new Monitoring( httpDaemonDriver ) ; monitorings.put( version, monitoring ) ; tcpPort ++ ; } } private static final int DEFAULT_WARMUP_ITERATIONS = 100 ; private static final Integer DEFAULT_MAXIMUM_ITERATIONS = 1000 ; private static final long LAUNCH_TIMEOUT_SECONDS = 20L ; /** * Call this once only. */ public void run() throws InterruptedException, IOException, ProcessCreationException, ProcessInitializationException { try { startDaemons() ; warmup( warmupIterations ) ; int activeCount = monitorings.size() ; int iterationCount = 1 ; for( ; isBelowMaximumIterations( iterationCount ) && activeCount > 0 ; iterationCount ++ ) { logPassCount( "Querying " + activeCount + " daemon(s), pass %d...", iterationCount ) ; runOnceWithMeasurementsOnEveryDaemon() ; final int updatedActiveCount = countActive( monitorings.values() ) ; final int difference = activeCount - updatedActiveCount ; final String message = "{" + name + "} pass " + iterationCount + " on " + activeCount + " daemons. " ; if( difference > 0 ) { LOGGER.info( message, ( difference + " daemon(s) terminated." ) ) ; } else { LOGGER.debug( message ) ; } activeCount = updatedActiveCount ; } if( hasReachedMaximumIterations( iterationCount ) ) { for( final Monitoring monitoring : monitorings.values() ) { if( monitoring.termination != null ) { monitoring.termination = Terminations.ITERATION_COUNT_EXCEEDED ; } } } } finally { shutdownDaemons() ; } } private boolean isBelowMaximumIterations( final int iterationCount ) { return ( maximumIterations == null || iterationCount <= maximumIterations ); } private boolean hasReachedMaximumIterations( final int iterationCount ) { return ( maximumIterations != null && iterationCount >= maximumIterations ); } private int countActive( final Collection< Monitoring > monitorings ) { int count = 0 ; for( final Monitoring monitoring : monitorings ) { if( monitoring.termination == null ) { count ++ ; } } return count ; } public Map< Version, MeasurementBundle< MEASUREMENT > > getMeasurements() { final ImmutableMap.Builder< Version, MeasurementBundle< MEASUREMENT > > builder = new ImmutableMap.Builder< Version, MeasurementBundle< MEASUREMENT > >() ; for( final Map.Entry< Version, Monitoring > entry : monitorings.entrySet() ) { final MeasurementBundle< MEASUREMENT > measurementBundle = new MeasurementBundle< MEASUREMENT >( entry.getValue().measurements, entry.getValue().termination ) ; builder.put( entry.getKey(), measurementBundle ) ; } return builder.build() ; } public List< UPSIZING > getUpsizings() { return upsizer.getUpsizings() ; } private void startDaemons() throws IOException, InterruptedException, ProcessCreationException, ProcessInitializationException { for( final Monitoring monitoring : monitorings.values() ) { final HttpDaemonDriver driver = monitoring.driver ; driver.start( LAUNCH_TIMEOUT_SECONDS, TimeUnit.SECONDS ) ; } } /** * Normally, all daemons got strained and there are none to stop. * But if we get something like a limit on run count this might become useful. */ private void shutdownDaemons() { for( final Monitoring monitoring : monitorings.values() ) { final HttpDaemonDriver driver = monitoring.driver ; if( monitoring.termination == null ) { try { driver.shutdown( true ) ; } catch( InterruptedException e ) { LOGGER.error( e, "Could not stop " + driver ) ; } catch( IOException e ) { LOGGER.error( e, "Could not stop " + driver ) ; } monitoring.termination = Terminations.LAST_CLEANUP ; } } } public interface Terminations { Termination LAST_CLEANUP = new Termination( "Last cleanup" ) ; Termination ITERATION_COUNT_EXCEEDED = new Termination( "Iteration count exceeded" ) ; } private void warmup( final int passCount ) throws IOException { LOGGER.info( "Warming up " + name + ", " + passCount + " iterations..." ) ; upsizer.upsize() ; for( int pass = 1 ; pass <= passCount ; pass ++ ) { for( final Monitoring monitoring : monitorings.values() ) { final HttpDaemonDriver driver = monitoring.driver ; final URL url = createRequestUrl( driver.getTcpPort() ) ; measurer.runDry( url ) ; } logPassCount( "Performed warmup pass %d.", pass ) ; } LOGGER.info( "Warmup of ", name, " complete." ) ; } private static void logPassCount( final String message, final int pass ) { if( pass == 1 || pass == 2 || pass == 10 || pass % 100 == 0 ) { LOGGER.info( String.format( message, pass ) ) ; } } private void runOnceWithMeasurementsOnEveryDaemon() throws InterruptedException, IOException { upsizer.upsize() ; for( final Monitoring monitoring : monitorings.values() ) { final URL url = createRequestUrl( monitoring.driver.getTcpPort() ) ; if( monitoring.termination == null ) { final List< MEASUREMENT > measurementHistory = monitoring.measurements ; final Measurer.Result< MEASUREMENT > result = measurer.run( measurementHistory, url ) ; if( result.hasTermination() ) { monitoring.driver.shutdown( true ) ; monitoring.termination = result.getTermination() ; } else { measurementHistory.add( result.getMeasurement() ) ; } } } } private URL createRequestUrl( final int tcpPort ) { try { return new URL( "http", "localhost", tcpPort, documentRequest ) ; } catch( MalformedURLException e ) { throw new RuntimeException( e ) ; } } public interface Configuration< CONFIGURATION extends Configuration, UPSIZING, MEASUREMENT > { String getScenarioName() ; CONFIGURATION withScenarioName( String name ) ; int getWarmupIterationCount() ; CONFIGURATION withWarmupIterationCount( int count ) ; Integer getMaximumIterations() ; CONFIGURATION withMaximumIterations( Integer maximumOrNullForNoLimit ) ; File getScenariiDirectory() ; CONFIGURATION withScenariiDirectory( File scenariiDirectory ) ; Upsizer.Factory< UPSIZING > getUpsizerFactory() ; CONFIGURATION withUpsizerFactory( Upsizer.Factory< UPSIZING > factory ) ; File getInstallationsDirectory() ; CONFIGURATION withInstallationsDirectory( File directory ) ; Iterable< Version > getVersions() ; CONFIGURATION withVersions( Iterable< Version > versions ) ; int getFirstTcpPort() ; CONFIGURATION withFirstTcpPort( int firstTcpPort ) ; int getJvmHeapSizeMegabytes() ; CONFIGURATION withJvmHeapSizeMegabytes( int megabytes ) ; Measurer< MEASUREMENT > getMeasurer() ; CONFIGURATION withMeasurer( Measurer< MEASUREMENT > measurer ) ; } }