/************************************************************************* * (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 ) { } } }