/*************************************************************************
* Copyright 2009-2015 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* 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/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.simpleworkflow.common.client;
import java.beans.Introspector;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.net.ConnectException;
import java.security.SecureRandom;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nonnull;
import org.apache.http.NoHttpResponseException;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.log4j.Logger;
import com.amazonaws.AbortedException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.Request;
import com.amazonaws.Response;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.handlers.RequestHandler2;
import com.amazonaws.http.AmazonHttpClient;
import com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflow;
import com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflowClient;
import com.amazonaws.services.simpleworkflow.flow.ActivityWorker;
import com.amazonaws.services.simpleworkflow.flow.JsonDataConverter;
import com.amazonaws.services.simpleworkflow.flow.WorkerBase;
import com.amazonaws.services.simpleworkflow.flow.WorkflowWorker;
import com.eucalyptus.auth.AuthException;
import com.eucalyptus.auth.principal.User;
import com.eucalyptus.auth.tokens.SecurityTokenAWSCredentialsProvider;
import com.eucalyptus.bootstrap.Bootstrap;
import com.eucalyptus.component.ComponentId;
import com.eucalyptus.component.ServiceUris;
import com.eucalyptus.component.Topology;
import com.eucalyptus.configurable.ConfigurableProperty;
import com.eucalyptus.configurable.ConfigurablePropertyException;
import com.eucalyptus.configurable.PropertyChangeListener;
import com.eucalyptus.crypto.Crypto;
import com.eucalyptus.simpleworkflow.common.SimpleWorkflow;
import com.eucalyptus.simpleworkflow.common.model.SimpleWorkflowMessage;
import com.eucalyptus.util.Exceptions;
import com.eucalyptus.util.Internets;
import com.eucalyptus.util.Pair;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.Lists;
import com.google.common.reflect.AbstractInvocationHandler;
/**
*
*/
@SuppressWarnings( { "Guava", "WeakerAccess" } )
public class Config {
private static final ObjectMapper mapper = new ObjectMapper( )
.setPropertyNamingStrategy( PropertyNamingStrategy.PASCAL_CASE_TO_CAMEL_CASE );
static {
mapper.addMixIn( ClientConfiguration.class, ClientConfigurationMixin.class );
}
private static final ObjectMapper workerObjectMapper = buildWorkerObjectMapper();
private static final AtomicReference<String> clientConfig = new AtomicReference<>( "" );
/**
* Parse a JSON format string for AWS SDK for Java ClientConfiguration.
*
* @param text The configuration in JSON
* @return The configuration object
*/
public static ClientConfiguration buildConfiguration( final String text ) {
try {
// create configuration with default values that can be overwritten by given config string
final ClientConfiguration config = new ClientConfiguration( );
config.setCacheResponseMetadata( false );
config.setConnectionMaxIdleMillis( 45_000 );
config.setConnectionTimeout( 10_000 );
config.setMaxConnections( 100 );
config.setSecureRandom( Crypto.getSecureRandomSupplier( ).get( ) );
config.setUseThrottleRetries( false );
return Strings.isNullOrEmpty( text ) ?
config :
mapper.readerForUpdating( config ).readValue( source( text ) );
} catch ( final IOException e ) {
throw new IllegalArgumentException( "Invalid configuration: " + e.getMessage( ), e );
}
}
public static AmazonSimpleWorkflow buildClient( final Supplier<User> user ) throws AuthException {
final AWSCredentialsProvider credentialsProvider = new SecurityTokenAWSCredentialsProvider( user );
final AmazonSimpleWorkflowClient client = new EucaSimpleWorkflowClient(
credentialsProvider,
clientConfig.get( )
);
client.setEndpoint( ServiceUris.remote( Topology.lookup( SimpleWorkflow.class ) ).toString( ) );
client.addRequestHandler( new RequestHandler2( ) {
private volatile String endpointHost;
private final Supplier<Boolean> failbackChecker = Suppliers.memoizeWithExpiration( () -> {
final String currentEndpointHost = endpointHost;
if ( currentEndpointHost!=null &&
!Internets.testLocal( currentEndpointHost ) &&
Topology.isEnabledLocally( SimpleWorkflow.class ) ) {
resetEndpoint( );
}
return true;
}, 15, TimeUnit.SECONDS );
@Override
public void beforeRequest( final Request<?> request ) {
if ( Bootstrap.isShuttingDown( ) &&
request.getHeaders( ).getOrDefault( "X-Amz-Target", "" ).contains( "Poll" ) ) {
Thread.currentThread( ).interrupt( );
}
}
@Override
public void afterResponse( final Request<?> request, final Response<?> response ) {
// Check and failover (failback) to local swf service if available
endpointHost = request.getEndpoint( ).getHost( );
failbackChecker.get( );
}
@Override
public void afterError( final Request<?> request, final Response<?> response, final Exception e ) {
final String errorMessage = Strings.nullToEmpty( e.getMessage( ) );
boolean resetEndpoint = false;
if ( errorMessage.contains( "Response Code: 404" ) || errorMessage.contains( "Response Code: 503" ) ) {
resetEndpoint = true;
} else if ( Exceptions.isCausedBy( e, ConnectException.class ) ) {
resetEndpoint = true;
} else if ( Exceptions.isCausedBy( e, ConnectTimeoutException.class ) ) {
resetEndpoint = true;
} else if ( Exceptions.isCausedBy( e, NoHttpResponseException.class ) ) {
resetEndpoint = true;
}
if ( e instanceof AmazonServiceException ) {
final int status = ( (AmazonServiceException) e ).getStatusCode( );
if ( status == 403 ) {
credentialsProvider.refresh( );
} else if ( status == 404 || status == 503 ) {
resetEndpoint = true;
}
}
if ( resetEndpoint ) {
resetEndpoint( );
// pause and then trigger an SdkInterruptedException
if ( Bootstrap.isShuttingDown( ) ) {
shutdownSleep( );
if ( request.getHeaders( ).getOrDefault( "X-Amz-Target", "" ).contains( "Poll" ) ) {
// interrupt triggers SdkInterruptedException handled in PollServiceTask (aws sdk)
Thread.currentThread( ).interrupt( );
}
}
}
}
private void resetEndpoint( ) {
try {
client.setEndpoint( ServiceUris.remote( Topology.lookup( SimpleWorkflow.class ) ).toString( ) );
} catch ( final Exception e ) {
// retry on next failure or failback attempt
}
}
} );
return client;
}
public static WorkflowWorker buildWorkflowWorker(
final Class<? extends ComponentId> componentIdClass,
final AmazonSimpleWorkflow client,
final String domain,
final String taskList,
final String text ) {
final List<Class<?>> workflowImpl = Lists.newArrayList(WorkflowRegistry.lookupWorkflows( componentIdClass ));
return buildWorkflowWorker(workflowImpl.toArray(new Class<?>[workflowImpl.size()]),
client, domain, taskList, text);
}
public static WorkflowWorker buildWorkflowWorker(
final Class<?>[] workflowImpl,
final AmazonSimpleWorkflow client,
final String domain,
final String taskList,
final String text ) {
final WorkflowWorker workflowWorker = new WorkflowWorker( client, domain, taskList);
workflowWorker.setRegisterDomain( true );
workflowWorker.setDefaultConverter( new JsonDataConverter( workerObjectMapper ) );
configure( workflowWorker, text );
Package workerPackage = null;
for ( final Class<?> workflowImplementation : workflowImpl ) {
try {
if ( workerPackage == null ) {
workerPackage = workflowImplementation.getPackage( );
}
workflowWorker.addWorkflowImplementationType( workflowImplementation );
} catch ( InstantiationException | IllegalAccessException e ) {
throw new IllegalArgumentException( "Invalid workflow implementation: " + workflowImplementation, e );
}
}
perhapsSetExceptionHandler( workerPackage, "decision", workflowWorker );
return workflowWorker;
}
public static ActivityWorker buildActivityWorker(
final Class<? extends ComponentId> componentIdClass,
final AmazonSimpleWorkflow client,
final String domain,
final String taskList,
final String text ) {
final List<Class<?>> activitiesImpl =
Lists.newArrayList(WorkflowRegistry.lookupActivities( componentIdClass ));
return buildActivityWorker(activitiesImpl.toArray(new Class<?>[activitiesImpl.size()]),
client, domain, taskList, text);
}
public static ActivityWorker buildActivityWorker(
final Class<?>[] activitiesImpl,
final AmazonSimpleWorkflow client,
final String domain,
final String taskList,
final String text
) {
final ActivityWorker activityWorker = configure( new ActivityWorker( client, domain, taskList), text );
activityWorker.setDataConverter( new JsonDataConverter( workerObjectMapper ) );
Package workerPackage = null;
for ( final Class<?> activitiesImplementation : activitiesImpl ) {
try {
if ( workerPackage == null ) {
workerPackage = activitiesImplementation.getPackage();
}
activityWorker.addActivitiesImplementation( activitiesImplementation.newInstance( ) );
} catch ( InstantiationException | IllegalAccessException | NoSuchMethodException e ) {
throw new IllegalArgumentException( "Invalid activities implementation: " + activitiesImplementation, e );
}
}
perhapsSetExceptionHandler( workerPackage, "activity", activityWorker );
return activityWorker;
}
private static <T extends WorkerBase> T configure( final T worker, final String text ) {
try {
return Strings.isNullOrEmpty( text ) ?
worker :
mapper.readerForUpdating( worker ).<T>readValue( source( text ) );
} catch ( IOException e ) {
throw new IllegalArgumentException( "Invalid configuration: " + e.getMessage( ), e );
}
}
private static void perhapsSetExceptionHandler( final Package logPackage,
final String type,
final WorkerBase worker ) {
if ( logPackage != null ) {
final Logger logger = Logger.getLogger( logPackage.getName() );
//noinspection Convert2Lambda
worker.setUncaughtExceptionHandler( new Thread.UncaughtExceptionHandler() {
@SuppressWarnings( { "ThrowableResultOfMethodCallIgnored", "ConstantConditions" } )
@Override
public void uncaughtException( final Thread t, final Throwable e ) {
if ( Exceptions.isCausedBy( e, ConnectException.class ) ) {
logger.warn( "Connection error (retrying) for " + type + " worker " + t.getName() + "/" + t.getId() );
} else if ( Exceptions.isCausedBy( e, ConnectTimeoutException.class ) ) {
logger.warn( "Connection timeout (retrying) for " + type + " worker " + t.getName() + "/" + t.getId() );
} else if ( Exceptions.isCausedBy( e, NoHttpResponseException.class ) ) {
logger.warn( "No response (retrying) for " + type + " worker " + t.getName() + "/" + t.getId() );
} else if ( Exceptions.isCausedBy( e, AmazonServiceException.class ) &&
403 == (Exceptions.findCause( e, AmazonServiceException.class )).getStatusCode( ) ) {
logger.warn( "Authentication failure (retrying) for " + type + " worker " + t.getName() + "/" + t.getId() );
} else if ( Bootstrap.isShuttingDown( ) && ( Exceptions.isCausedBy( e, InterruptedException.class ) ||
Exceptions.isCausedBy( e, AbortedException.class ) ) ) {
logger.trace( "Interrupted during shutdown" );
shutdownSleep( );
} else {
logger.error( "Error in " + type + " worker " + t.getName() + "/" + t.getId(), e );
}
}
} );
}
}
private static void shutdownSleep( ) {
try {
Thread.sleep( 2000 );
} catch ( InterruptedException e1 ) {
}
}
private static StringReader source( final String text ) {
return new StringReader( text ) {
@Override public String toString( ) { return "property"; } // overridden for better source in error message
};
}
public static final class NameValidatingChangeListener implements PropertyChangeListener {
@Override
public void fireChange( final ConfigurableProperty t, final Object newValue ) throws ConfigurablePropertyException {
if ( newValue == null || !SimpleWorkflowMessage.FieldRegexValue.NAME_256.pattern( ).matcher( newValue.toString( ) ).matches( ) ) {
throw new ConfigurablePropertyException( "Value length must be 1 - 256 characters" );
}
}
}
public static final class ClientConfigurationValidatingChangeListener implements PropertyChangeListener {
@Override
public void fireChange( final ConfigurableProperty t, final Object newValue ) throws ConfigurablePropertyException {
if ( newValue != null && !newValue.toString( ).trim( ).isEmpty( ) ) try {
Config.buildConfiguration( newValue.toString( ).trim( ) );
} catch ( final IllegalArgumentException e ) {
throw new ConfigurablePropertyException( e.getMessage( ) );
}
clientConfig.set( Objects.toString( newValue, "" ).trim( ) );
}
}
public static final class ActivityWorkerConfigurationValidatingChangeListener implements PropertyChangeListener {
@Override
public void fireChange( final ConfigurableProperty t, final Object newValue ) throws ConfigurablePropertyException {
if ( newValue != null && !newValue.toString( ).trim( ).isEmpty( ) ) try {
// dummy values used for validating JSON configuration
Config.buildActivityWorker( SimpleWorkflow.class, client( ), "domain", "task-list", newValue.toString( ).trim( ) );
} catch ( final IllegalArgumentException e ) {
throw new ConfigurablePropertyException( e.getMessage( ) );
}
}
}
public static final class WorkflowWorkerConfigurationValidatingChangeListener implements PropertyChangeListener {
@Override
public void fireChange( final ConfigurableProperty t, final Object newValue ) throws ConfigurablePropertyException {
if ( newValue != null && !newValue.toString( ).trim( ).isEmpty( ) ) try {
// dummy values used for validating JSON configuration
Config.buildWorkflowWorker( SimpleWorkflow.class, client( ), "domain", "task-list", newValue.toString( ).trim( ) );
} catch ( final IllegalArgumentException e ) {
throw new ConfigurablePropertyException( e.getMessage( ) );
}
}
}
/**
* Extension of AmazonSimpleWorkflowClient to use a shared HTTP client.
*/
private static final class EucaSimpleWorkflowClient extends AmazonSimpleWorkflowClient {
private static final AtomicReference<Pair<String,EucaHttpClient>> clientPairRef = new AtomicReference<>( );
EucaSimpleWorkflowClient(
final AWSCredentialsProvider credentialsProvider,
final String config
) {
super( credentialsProvider, buildConfiguration( config ) );
this.client.shutdown( );
final Pair<String,EucaHttpClient> clientPair = clientPairRef.get( );
if ( clientPair != null && clientPair.getLeft( ).equals( config ) ) {
this.client = clientPair.getRight( ).ref( );
} else {
final EucaHttpClient eucaClient = new EucaHttpClient( clientConfiguration ).ref( );
if ( clientPairRef.compareAndSet( clientPair, Pair.of( config, eucaClient ) ) ) {
// unref/ref for atomic reference
eucaClient.ref( );
if ( clientPair != null ) {
clientPair.getRight( ).unref( );
}
}
this.client = eucaClient;
}
}
}
/**
* Extension of AmazonHttpClient with reference tracking
*/
private static final class EucaHttpClient extends AmazonHttpClient {
private final AtomicInteger refs = new AtomicInteger( 0 );
EucaHttpClient(
final ClientConfiguration configuration
) {
super( configuration, null, true, false );
}
public EucaHttpClient ref( ) {
refs.incrementAndGet( );
return this;
}
public EucaHttpClient unref( ) {
if ( refs.decrementAndGet( ) <= 0 ) {
super.shutdown( );
}
return this;
}
@Override
public void shutdown( ) {
unref( );
}
}
private static AmazonSimpleWorkflow client( ) {
return (AmazonSimpleWorkflow) Proxy.newProxyInstance( AmazonSimpleWorkflow.class.getClassLoader(), new Class<?>[]{ AmazonSimpleWorkflow.class }, new AbstractInvocationHandler() {
@Override
protected Object handleInvocation(
@Nonnull final Object o,
@Nonnull final Method method,
@Nonnull final Object[] objects
) throws Throwable {
throw new Exception( "dummy-client" );
}
} );
}
private static ObjectMapper buildWorkerObjectMapper( ) {
final ObjectMapper workerObjectMapper = new ObjectMapper( );
workerObjectMapper.setAnnotationIntrospector(
new JacksonAnnotationIntrospector( ) {
private static final long serialVersionUID = 1L;
@Override
public boolean hasIgnoreMarker( final AnnotatedMember m ) {
return isMethodBackedByTransientField( m ) || super.hasIgnoreMarker( m );
}
}
);
workerObjectMapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false );
workerObjectMapper.configure( SerializationFeature.FAIL_ON_EMPTY_BEANS, false );
workerObjectMapper.enableDefaultTyping( ObjectMapper.DefaultTyping.NON_FINAL );
return workerObjectMapper;
}
private static boolean isMethodBackedByTransientField( final AnnotatedMember m ) {
boolean isMethodBackedByTransientField = false;
if (m instanceof AnnotatedMethod ) {
final String fieldName = Introspector.decapitalize( com.eucalyptus.util.Strings.trimPrefix( "get", m.getName() ) );
for (final Field field : m.getMember().getDeclaringClass().getDeclaredFields()) {
if (fieldName.equals(field.getName())) {
isMethodBackedByTransientField = Modifier.isTransient(field.getModifiers());
break;
}
}
}
return isMethodBackedByTransientField;
}
@SuppressWarnings( "unused" )
private interface ClientConfigurationMixin {
@JsonIgnore SecureRandom getSecureRandom();
@JsonIgnore void setSecureRandom(SecureRandom secureRandom);
}
}