package com.kryptnostic.rhizome.mapstores.cassandra; import java.util.Collection; import java.util.Map; import java.util.Map.Entry; import java.util.stream.Collectors; import com.datastax.driver.core.querybuilder.Update; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.datastax.driver.core.BoundStatement; import com.datastax.driver.core.PreparedStatement; import com.datastax.driver.core.RegularStatement; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; import com.datastax.driver.core.exceptions.NoHostAvailableException; import com.datastax.driver.core.exceptions.QueryExecutionException; import com.datastax.driver.core.exceptions.QueryValidationException; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import com.hazelcast.config.MapConfig; import com.hazelcast.config.MapStoreConfig; import com.kryptnostic.rhizome.cassandra.CassandraTableBuilder; import com.kryptnostic.rhizome.mapstores.TestableSelfRegisteringMapStore; public abstract class AbstractStructuredCassandraMapstoreBase<K, V> implements TestableSelfRegisteringMapStore<K, V> { private static final Logger logger = LoggerFactory .getLogger( AbstractStructuredCassandraMapstoreBase.class ); protected final Session session; protected final CassandraTableBuilder tableBuilder; private final String mapName; private final PreparedStatement allKeysQuery; private final PreparedStatement loadQuery; private final PreparedStatement storeQuery; private final PreparedStatement deleteQuery; public AbstractStructuredCassandraMapstoreBase( String mapName, Session session, CassandraTableBuilder tableBuilder ) { Preconditions.checkArgument( StringUtils.isNotBlank( mapName ), "Map name cannot be blank" ); this.mapName = mapName; this.session = Preconditions.checkNotNull( session, "Cassandra session cannot be null" ); this.tableBuilder = Preconditions.checkNotNull( tableBuilder, "Table builder is required" ); createCassandraSchemaIfNotExist( session, tableBuilder ); allKeysQuery = prepareLoadAllKeysQuery(); loadQuery = prepareLoadQuery(); storeQuery = prepareStoreQuery(); deleteQuery = prepareDeleteQuery(); } private void createCassandraSchemaIfNotExist( Session session, CassandraTableBuilder tableBuilder ) { session.execute( String.format( "CREATE KEYSPACE IF NOT EXISTS %s WITH REPLICATION={ 'class' : 'SimpleStrategy', 'replication_factor' : %d } AND DURABLE_WRITES=true", tableBuilder.getKeyspace().or( "sparks" ), tableBuilder.getReplicationFactor() ) ); tableBuilder.build().forEach( stmt -> session.execute( stmt ) ); } @Override public V load( K key ) { return safeTransform( asyncLoad( key ) ); } @Override public Map<K, V> loadAll( Collection<K> keys ) { return keys.parallelStream() .map( k -> Pair.of( k, asyncLoad( k ) ) ) .map( p -> Pair.of( p.getLeft(), safeTransform( p.getRight() ) ) ) .filter( p -> p.getRight() != null ) .collect( Collectors.toMap( p -> p.getLeft(), p -> p.getRight() ) ); } @Override public Iterable<K> loadAllKeys() { /* * One limitation of this is that if key stream isn't unique then values may get loaded into the map multiple times. */ ResultSet rs = session.execute( getLoadAllKeysQuery().bind() ); return Iterables.transform( rs, this::mapKey )::iterator; } @Override public void store( K key, V value ) { asyncStore( key, value ).getUninterruptibly(); } @Override public void storeAll( Map<K, V> map ) { map.entrySet().parallelStream().map( this::asyncStoreEntry ).forEach( ResultSetFuture::getUninterruptibly ); } @Override public void delete( K key ) { asyncDelete( key ).getUninterruptibly(); } @Override public void deleteAll( Collection<K> keys ) { keys.parallelStream().map( this::asyncDelete ).forEach( ResultSetFuture::getUninterruptibly ); } protected ResultSetFuture asyncLoad( K key ) { try { return session.executeAsync( bind( key, getLoadQuery().bind() ) ); } catch ( NoHostAvailableException | QueryExecutionException | QueryValidationException e ) { logger.error( "Unable to perform query to load key {}", key, e ); return null; } } protected V safeTransform( ResultSetFuture rsf ) { ResultSet rs = rsf.getUninterruptibly(); return rs == null ? null : mapValue( rs ); } protected ResultSetFuture asyncStore( K key, V value ) { try { return session.executeAsync( bind( key, value, getStoreQuery().bind() ) ); } catch ( NoHostAvailableException | QueryExecutionException | QueryValidationException e ) { logger.error( "Unable to perform query to store key {}", key, e ); return null; } } protected ResultSetFuture asyncDelete( K key ) { try { return session.executeAsync( bind( key, getDeleteQuery().bind() ) ); } catch ( NoHostAvailableException | QueryExecutionException | QueryValidationException e ) { logger.error( "Unable to perform query to delete key {}", key, e ); return null; } } private ResultSetFuture asyncStoreEntry( Entry<K, V> entry ) { return asyncStore( entry.getKey(), entry.getValue() ); } protected RegularStatement loadAllKeysQuery() { return tableBuilder.buildLoadAllPrimaryKeysQuery(); } protected PreparedStatement prepareLoadAllKeysQuery() { return session.prepare( loadAllKeysQuery() ); } protected PreparedStatement prepareLoadQuery() { return session.prepare( loadQuery() ); } protected RegularStatement storeQuery() { return tableBuilder.buildStoreQuery(); } protected PreparedStatement prepareStoreQuery() { return session.prepare( storeQuery() ); } protected PreparedStatement prepareDeleteQuery() { return session.prepare( deleteQuery() ); } protected PreparedStatement getLoadAllKeysQuery() { return allKeysQuery; } protected PreparedStatement getLoadQuery() { return loadQuery; } protected PreparedStatement getStoreQuery() { return storeQuery; } protected PreparedStatement getDeleteQuery() { return deleteQuery; } @Override public MapStoreConfig getMapStoreConfig() { return new MapStoreConfig().setImplementation( this ).setEnabled( true ) .setWriteDelaySeconds( 0 ); } @Override public MapConfig getMapConfig() { return new MapConfig( mapName ).setBackupCount( this.tableBuilder.getReplicationFactor() ) .setMapStoreConfig( getMapStoreConfig() ); } @Override public String getMapName() { return mapName; } @Override public String getTable() { return tableBuilder.getName(); } protected abstract RegularStatement loadQuery(); protected abstract RegularStatement deleteQuery(); protected abstract BoundStatement bind( K key, BoundStatement bs ); protected abstract BoundStatement bind( K key, V value, BoundStatement bs ); protected abstract K mapKey( Row rs ); protected abstract V mapValue( ResultSet rs ); }