/*
* 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.scenario;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import org.apache.commons.math.stat.regression.SimpleRegression;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.novelang.nhovestone.Termination;
/**
* @author Laurent Caillette
*/
public class TimeMeasurer implements Measurer< TimeMeasurement > {
public static final int TIMEOUT_MILLISECONDS = 5 * 60 * 1000 ;
private static final int MINIMUM_MEASUREMENT_COUNT_FOR_REGRESSION = 50 ;
private static final double GUARD_FACTOR = 10.0 ;
private static final int BUFFER_SIZE = 1024 * 1024 ;
private final byte[] buffer = new byte[ BUFFER_SIZE ] ;
@Override
public Result< TimeMeasurement > run(
final List< TimeMeasurement > previousMeasurements,
final URL url
) throws IOException {
// HttpKit avoids including creation time of HttpClient stuff into measurement. Yeah!
final HttpKit httpKit = createHttpKit( url ) ;
final long startTime = System.currentTimeMillis() ;
final Termination termination = run( httpKit ) ;
final long endTime = System.currentTimeMillis() ;
if( termination == null ) {
final TimeMeasurement lastMeasurement = new TimeMeasurement( endTime - startTime ) ;
if( detectStrain( previousMeasurements, lastMeasurement ) ) {
return Result.create( Terminations.STRAIN ) ;
} else {
return Result.create( lastMeasurement ) ;
}
} else {
return Result.create( termination ) ;
}
}
@Override
public Termination runDry( final URL url ) {
return run( createHttpKit( url ) ) ;
}
/**
* Strain means last request exceeded of more than {@value #GUARD_FACTOR} times the time
* extrapolated from first half of measurements, using linear regression.
*
* @param previousMeasurements a non-null object, contains no null.
* @param lastMeasurement a non-null object.
* @return true if strain detected, false otherwise.
*/
private static boolean detectStrain(
final List< TimeMeasurement > previousMeasurements,
final TimeMeasurement lastMeasurement
) {
final int measurementCount = previousMeasurements.size() ;
if( measurementCount < MINIMUM_MEASUREMENT_COUNT_FOR_REGRESSION ) {
return false ;
} else {
final SimpleRegression simpleRegression = new SimpleRegression() ;
for( int i = 0 ; i < measurementCount / 2 ; i ++ ) {
final TimeMeasurement measurement = previousMeasurements.get( i ) ;
simpleRegression.addData( ( double ) i, ( double ) measurement.getTimeMilliseconds() ) ;
}
final double extrapolated = simpleRegression.predict( ( double ) measurementCount ) ;
if( Double.isNaN( extrapolated ) ) {
return false ;
} else {
final double increasing = extrapolated - simpleRegression.getIntercept() ;
final double highLimit = simpleRegression.getIntercept() + increasing * GUARD_FACTOR ;
return lastMeasurement.getTimeMilliseconds() > ( long ) highLimit ;
}
}
}
// =================
// Shared HTTP stuff
// =================
private HttpKit createHttpKit( final URL url ) {
final AbstractHttpClient httpClient = new DefaultHttpClient() ;
final HttpParams parameters = new BasicHttpParams() ;
parameters.setIntParameter(
CoreConnectionPNames.SO_TIMEOUT, TimeMeasurer.TIMEOUT_MILLISECONDS ) ;
final HttpGet httpGet = new HttpGet( asUri( url ) ) ;
httpGet.setParams( parameters ) ;
return new HttpKit( httpClient, httpGet, buffer ) ;
}
private static Termination run( final HttpKit httpKit ) {
final HttpResponse httpResponse;
try {
httpResponse = httpKit.httpClient.execute( httpKit.httpGet );
final int statusCode = httpResponse.getStatusLine().getStatusCode();
if( statusCode == HttpStatus.SC_OK ) {
final InputStream inputStream = httpResponse.getEntity().getContent() ;
try {
for( int read = 0 ; read > -1 ; read = inputStream.read( httpKit.buffer ) ) { }
return null ;
} finally {
inputStream.close() ;
}
} else {
return Terminations.HTTP_CODE;
}
} catch( IOException e ) {
return Terminations.CALLER_EXCEPTION ;
}
}
private static class HttpKit {
public final AbstractHttpClient httpClient ;
public final HttpGet httpGet ;
public final byte[] buffer ;
private HttpKit(
final AbstractHttpClient httpClient,
final HttpGet httpGet,
final byte[] buffer
) {
this.httpClient = httpClient;
this.httpGet = httpGet;
this.buffer = buffer;
}
}
private static URI asUri( final URL url ) {
try {
return url.toURI() ;
} catch( URISyntaxException e ) {
throw new RuntimeException( e ) ;
}
}
// ==================
// Termination causes
// ==================
public interface Terminations {
Termination HTTP_CODE = new Termination( "HTTP response code not OK" ) ;
Termination CALLER_EXCEPTION = new Termination( "Exception occured" ) ;
Termination STRAIN = new Termination( "Daemon strained") ;
}
}