/*************************************************************************
* (c) Copyright 2017 Hewlett Packard Enterprise Development Company LP
*
* 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/.
************************************************************************/
package com.eucalyptus.cassandra.common;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.data.cassandra.repository.support.CassandraRepositoryFactory;
import org.springframework.retry.backoff.ExponentialRandomBackOffPolicy;
import org.springframework.retry.policy.ExceptionClassifierRetryPolicy;
import org.springframework.retry.policy.TimeoutRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.NettyOptions;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.ThreadingOptions;
import com.datastax.driver.core.exceptions.NoHostAvailableException;
import com.datastax.driver.core.policies.DefaultRetryPolicy;
import com.datastax.driver.core.policies.ExponentialReconnectionPolicy;
import com.eucalyptus.component.Components;
import com.eucalyptus.component.ServiceConfiguration;
import com.eucalyptus.component.Topology;
import com.eucalyptus.component.annotation.ComponentPart;
import com.eucalyptus.system.Ats;
import com.eucalyptus.util.LockResource;
import com.eucalyptus.util.Pair;
import com.eucalyptus.util.Parameters;
import com.eucalyptus.util.ThrowingFunction;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
/**
* API for accessing a cassandra store
*/
public class CassandraPersistence {
private static final ConcurrentMap<String,Session> sessionMap = Maps.newConcurrentMap( ); // sessions by keyspace
private static final ConcurrentMap<Pair<String,Class<? extends CassandraPersistenceRepository>>,
CassandraPersistenceRepository> repositoryMap = Maps.newConcurrentMap( ); // repositories by keyspace/type
private static final Lock sessionLock = new ReentrantLock( );
private static final RetryTemplate template =
buildRetryTemplate( NoSuchElementException.class, 15_000L, TimeUnit.MINUTES.toMillis( 5 ) );
private static final RetryTemplate startupRetryTemplate =
buildRetryTemplate( NoHostAvailableException.class, 15_000L, TimeUnit.MINUTES.toMillis( 1 ) );
/**
* Perform work using a datastax session in a callback.
*
* Using template or repository callbacks is preferred.
*
* @param keyspace The keyspace for the session
* @param callbackFunction The callback that will perform work
* @param <R> The result type
* @return The result from the callback which can be null
* @see #doWithTemplate(String, Function)
* @see #doWithRepository(Class, Function)
*/
public static <R> R doWithSession(
final String keyspace,
final Function<? super Session,? extends R> callbackFunction
) {
return doWithSession( SessionUsage.Service, keyspace, callbackFunction );
};
/**
* Perform work using a datastax session in a callback.
*
* Using template or repository callbacks is preferred.
*
* @param keyspace The keyspace for the session
* @param callbackFunction The callback that will perform work and may throw an exception
* @param <R> The result type
* @param <E> The exception type
* @return The result from the callback which can be null
* @throws E if thrown from the callback
* @see #doThrowsWithTemplate(String, ThrowingFunction)
* @see #doThrowsWithRepository(Class, ThrowingFunction)
*/
public static <R,E extends Throwable> R doThrowsWithSession(
final String keyspace,
final ThrowingFunction<? super Session,? extends R, ? extends E> callbackFunction
) throws E {
return doThrowsWithSession( SessionUsage.Service, keyspace, callbackFunction );
};
/**
* Perform work using a datastax session in a callback.
*
* Using template or repository callbacks is preferred.
*
* @param keyspace The keyspace for the session
* @param usage The session usage, typically SessionUsage.Service
* @param callbackFunction The callback that will perform work
* @param <R> The result type
* @return The result from the callback which can be null
* @see #doWithTemplate(String, Function)
* @see #doWithRepository(Class, Function)
*/
public static <R> R doWithSession(
final SessionUsage usage,
final String keyspace,
final Function<? super Session,? extends R> callbackFunction
) {
final Session session = getSession( usage, keyspace );
try {
return callbackFunction.apply( session );
} finally {
releaseSession( usage, session, keyspace );
}
};
/**
* Perform work using a datastax session in a callback.
*
* Using template or repository callbacks is preferred.
*
* @param keyspace The keyspace for the session
* @param usage The session usage, typically SessionUsage.Service
* @param callbackFunction The callback that will perform work and may throw an exception
* @param <R> The result type
* @param <E> The exception type
* @return The result from the callback which can be null
* @throws E if thrown from the callback
* @see #doThrowsWithTemplate(String, ThrowingFunction)
* @see #doThrowsWithRepository(Class, ThrowingFunction)
*/
public static <R,E extends Throwable> R doThrowsWithSession(
final SessionUsage usage,
final String keyspace,
final ThrowingFunction<? super Session,? extends R, ? extends E> callbackFunction
) throws E {
final Session session = getSession( usage, keyspace );
try {
return callbackFunction.apply( session );
} finally {
releaseSession( usage, session, keyspace );
}
};
/**
* Perform work using a spring data cassandra template in a callback.
*
* @param keyspace The keyspace for the underlying cassandra session
* @param callbackFunction The callback that will perform work
* @param <R> The result type
* @return The result from the callback which can be null
*/
public static <R> R doWithTemplate(
final String keyspace,
final Function<? super CassandraPersistenceTemplate,? extends R> callbackFunction
) {
return callbackFunction.apply(
new CassandraPersistenceTemplate( getSession( SessionUsage.Service, keyspace ), keyspace ) );
}
/**
* Perform work using a spring data cassandra template in a callback.
*
* @param keyspace The keyspace for the underlying cassandra session
* @param callbackFunction The callback that will perform work and may throw an exception
* @param <R> The result type
* @param <E> The exception type
* @return The result from the callback which can be null
* @throws E if thrown from the callback
*/
public static <R,E extends Throwable> R doThrowsWithTemplate(
final String keyspace,
final ThrowingFunction<? super CassandraPersistenceTemplate,? extends R, ? extends E> callbackFunction
) throws E {
return callbackFunction.apply(
new CassandraPersistenceTemplate( getSession( SessionUsage.Service, keyspace ), keyspace ) );
}
/**
* Perform work using the given service specific repository.
*
* @param repositoryType Class for the repository type
* @param callbackFunction The callback that will perform work
* @param <R> The result type
* @param <RT> The repository type
* @return The result from the callback which can be null
*/
public static <R,RT extends CassandraPersistenceRepository> R doWithRepository(
final Class<RT> repositoryType,
final Function<? super RT,? extends R> callbackFunction
) {
return callbackFunction.apply( getRepository( repositoryType ) );
}
/**
* Perform work using the given service specific repository.
*
* @param repositoryType Class for the repository type
* @param callbackFunction The callback that will perform work and may throw an exception
* @param <R> The result type
* @param <RT> The repository type
* @param <E> The exception type
* @return The result from the callback which can be null
* @throws E if thrown from the callback
*/
public static <R,RT extends CassandraPersistenceRepository,E extends Throwable> R doThrowsWithRepository(
final Class<RT> repositoryType,
final ThrowingFunction<? super RT,? extends R, ? extends E> callbackFunction
) throws E {
return callbackFunction.apply( getRepository( repositoryType ) );
}
private static String keyspace( final Class<?> repositoryType ) {
final Ats repositoryAts = Ats.from( repositoryType );
return
repositoryAts.getOption( CassandraKeyspace.class )
.orElse( repositoryAts.getOption( ComponentPart.class )
.flatMap( componentPart -> Ats.from( componentPart.value( ) ).getOption( CassandraKeyspace.class ) ) )
.map( CassandraKeyspace::value )
.getOrElse( (String)null );
}
@SuppressWarnings( "unchecked" )
private static <RT extends CassandraPersistenceRepository> RT getRepository( final Class<RT> repositoryType ) {
final String keyspace = keyspace( repositoryType );
final Pair<String,Class<? extends CassandraPersistenceRepository>> key = Pair.of( keyspace, repositoryType );
return (RT) repositoryMap.computeIfAbsent( key, keyPair -> {
final CassandraRepositoryFactory factory = new CassandraRepositoryFactory(
new CassandraPersistenceTemplate( getSession( SessionUsage.Service, keyspace ), keyspace ) );
factory.setRepositoryBaseClass( CassandraPersistenceRepositoryImpl.class );
return factory.getRepository( repositoryType );
} );
}
private static Session getSession( final SessionUsage usage, final String keyspace ) {
Parameters.checkParamNotNull( "usage", usage );
Session session = sessionMap.get( Parameters.checkParamNotNullOrEmpty( "keyspace", keyspace ) );
if ( session == null ) {
final List<ServiceConfiguration> configurations = usage.getCassandraServiceConfigurations( );
try ( final LockResource lockResource = LockResource.lock( sessionLock ) ) {
session = sessionMap.get( keyspace );
if ( session == null ) {
session = usage.buildSession( configurations, keyspace );
sessionMap.put( keyspace, session );
}
}
}
return session;
}
private static Session buildSession( final List<ServiceConfiguration> configurations, final String keyspace ) {
final Cluster cluster = Cluster.builder( )
.addContactPointsWithPorts( configurations.stream( ).map( ServiceConfiguration::getSocketAddress ).collect( Collectors.toList( ) ) )
//.withLoadBalancingPolicy( ) //TODO topology aware policy?
.withNettyOptions( new NettyOptions( ) {
@Override
public EventLoopGroup eventLoopGroup( final ThreadFactory threadFactory ) {
return new NioEventLoopGroup( 0, threadFactory );
}
} )
.withReconnectionPolicy( new ExponentialReconnectionPolicy( 1_000L, 60_000L ) )
.withRetryPolicy( DefaultRetryPolicy.INSTANCE )
//.withSSL( new NettySSLOptions( ) ) //TODO use ssl
.withThreadingOptions( new ThreadingOptions( ) {
@Override
public ThreadFactory createThreadFactory( final String clusterName, final String executorName ) {
return super.createThreadFactory( "cassandra-client", executorName );
}
} )
.withoutJMXReporting( )
.build( );
return cluster.connect( keyspace );
}
private static void releaseSession( final SessionUsage usage, final Session session, final String keyspace ) {
usage.releaseSession( session, keyspace );
}
private static RetryTemplate buildRetryTemplate(
final Class<? extends Throwable> thrownType,
final long maxBackoffInterval,
final long timeout
) {
// retry with timeout on expected exception
final TimeoutRetryPolicy timeoutRetryPolicy = new TimeoutRetryPolicy( );
timeoutRetryPolicy.setTimeout( timeout );
final ExceptionClassifierRetryPolicy exceptionRetryPolicy = new ExceptionClassifierRetryPolicy( );
exceptionRetryPolicy.setPolicyMap( Collections.singletonMap( thrownType, timeoutRetryPolicy ) );
// use exponential backoff
final ExponentialRandomBackOffPolicy backOffPolicy = new ExponentialRandomBackOffPolicy( );
backOffPolicy.setMaxInterval( maxBackoffInterval );
final RetryTemplate template = new RetryTemplate( );
template.setRetryPolicy( exceptionRetryPolicy );
template.setBackOffPolicy( backOffPolicy );
return template;
}
public enum SessionUsage {
/**
* Administrative session usage, should not be used by services
*/
Admin {
@Override
List<ServiceConfiguration> getCassandraServiceConfigurations( ) {
try {
return Lists.newArrayList( Topology.lookupAtLeastOne( Cassandra.class ) );
} catch ( final NoSuchElementException e ) {
return Collections.singletonList( Components.lookup( Cassandra.class ).getLocalServiceConfiguration( ) );
}
}
@Override
Session buildSession( final List<ServiceConfiguration> configurations, final String keyspace ) {
return startupRetryTemplate.execute( retryContext ->
CassandraPersistence.buildSession( configurations, null ) ); // keyspace may not be created at this point
}
@Override
void releaseSession( final Session session, final String keyspace ) {
session.execute( "USE " + keyspace );
}
},
/**
* General purpose session usage for services (etc)
*/
Service {
@Override
List<ServiceConfiguration> getCassandraServiceConfigurations( ) {
return template.execute( retryContext -> Lists.newArrayList( Topology.lookupAtLeastOne( Cassandra.class ) ) );
}
},
;
abstract List<ServiceConfiguration> getCassandraServiceConfigurations( );
Session buildSession( final List<ServiceConfiguration> configurations, final String keyspace ) {
return CassandraPersistence.buildSession( configurations, keyspace );
}
void releaseSession( final Session session, final String keyspace ) { }
}
}