package com.netflix.astyanax.contrib.dualwrites; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.util.concurrent.ListenableFuture; import com.netflix.astyanax.AstyanaxConfiguration; import com.netflix.astyanax.ColumnMutation; import com.netflix.astyanax.Execution; import com.netflix.astyanax.Keyspace; import com.netflix.astyanax.MutationBatch; import com.netflix.astyanax.SerializerPackage; import com.netflix.astyanax.connectionpool.ConnectionPool; import com.netflix.astyanax.connectionpool.Operation; import com.netflix.astyanax.connectionpool.OperationResult; import com.netflix.astyanax.connectionpool.TokenRange; import com.netflix.astyanax.connectionpool.exceptions.ConnectionException; import com.netflix.astyanax.connectionpool.exceptions.OperationException; import com.netflix.astyanax.cql.CqlStatement; import com.netflix.astyanax.ddl.KeyspaceDefinition; import com.netflix.astyanax.ddl.SchemaChangeResult; import com.netflix.astyanax.model.ColumnFamily; import com.netflix.astyanax.partitioner.Partitioner; import com.netflix.astyanax.query.ColumnFamilyQuery; import com.netflix.astyanax.retry.RetryPolicy; import com.netflix.astyanax.serializers.UnknownComparatorException; /** * Main class that orchistrates all the dual writes. It wraps the 2 keyspaces (source and destination) * and appropriately forwards reads and writes to it as dictated by updates using the {@link DualWritesUpdateListener} * * Note that if dual writes are enabled then the writes are sent to the {@link DualWritesMutationBatch} or {@link DualWritesColumnMutation} * classes appropriately. * * The reads are always served from the primary data source. * * @author poberai * */ public class DualWritesKeyspace implements Keyspace, DualWritesUpdateListener { private static final Logger Logger = LoggerFactory.getLogger(DualWritesKeyspace.class); private final AtomicReference<KeyspacePair> ksPair = new AtomicReference<KeyspacePair>(null); private final AtomicBoolean dualWritesEnabled = new AtomicBoolean(false); private final DualWritesStrategy executionStrategy; public DualWritesKeyspace(DualKeyspaceMetadata dualKeyspaceSetup, Keyspace primaryKS, Keyspace secondaryKS, DualWritesStrategy execStrategy) { ksPair.set(new KeyspacePair(dualKeyspaceSetup, primaryKS, secondaryKS)); executionStrategy = execStrategy; } private Keyspace getPrimaryKS() { return ksPair.get().getPrimaryKS(); } public DualKeyspaceMetadata getDualKeyspaceMetadata() { return ksPair.get().getDualKSMetadata(); } @Override public AstyanaxConfiguration getConfig() { return getPrimaryKS().getConfig(); } @Override public String getKeyspaceName() { return getPrimaryKS().getKeyspaceName(); } @Override public Partitioner getPartitioner() throws ConnectionException { return getPrimaryKS().getPartitioner(); } @Override public String describePartitioner() throws ConnectionException { return getPrimaryKS().describePartitioner(); } @Override public List<TokenRange> describeRing() throws ConnectionException { return getPrimaryKS().describeRing(); } @Override public List<TokenRange> describeRing(String dc) throws ConnectionException { return getPrimaryKS().describeRing(dc); } @Override public List<TokenRange> describeRing(String dc, String rack) throws ConnectionException { return getPrimaryKS().describeRing(dc, rack); } @Override public List<TokenRange> describeRing(boolean cached) throws ConnectionException { return getPrimaryKS().describeRing(cached); } @Override public KeyspaceDefinition describeKeyspace() throws ConnectionException { return getPrimaryKS().describeKeyspace(); } @Override public Properties getKeyspaceProperties() throws ConnectionException { return getPrimaryKS().getKeyspaceProperties(); } @Override public Properties getColumnFamilyProperties(String columnFamily) throws ConnectionException { return getPrimaryKS().getColumnFamilyProperties(columnFamily); } @Override public SerializerPackage getSerializerPackage(String cfName, boolean ignoreErrors) throws ConnectionException, UnknownComparatorException { return getPrimaryKS().getSerializerPackage(cfName, ignoreErrors); } @Override public MutationBatch prepareMutationBatch() { if (dualWritesEnabled.get()) { KeyspacePair pair = ksPair.get(); return new DualWritesMutationBatch( pair.getDualKSMetadata(), pair.getPrimaryKS().prepareMutationBatch(), pair.getSecondaryKS().prepareMutationBatch(), executionStrategy); } else { return getPrimaryKS().prepareMutationBatch(); } } @Override public <K, C> ColumnFamilyQuery<K, C> prepareQuery(ColumnFamily<K, C> cf) { return getPrimaryKS().prepareQuery(cf); } @Override public <K, C> ColumnMutation prepareColumnMutation(ColumnFamily<K, C> columnFamily, K rowKey, C column) { KeyspacePair pair = ksPair.get(); if (dualWritesEnabled.get()) { WriteMetadata md = new WriteMetadata(pair.getDualKSMetadata(), columnFamily.getName(), rowKey.toString()); return new DualWritesColumnMutation(md, pair.getPrimaryKS() .prepareColumnMutation(columnFamily, rowKey, column), pair.getSecondaryKS().prepareColumnMutation(columnFamily, rowKey, column), executionStrategy); } else { return pair.getPrimaryKS().prepareColumnMutation(columnFamily, rowKey, column); } } @Override public <K, C> OperationResult<Void> truncateColumnFamily(final ColumnFamily<K, C> columnFamily) throws OperationException, ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<Void>() { @Override public OperationResult<Void> exec(Keyspace ks) throws ConnectionException { return ks.truncateColumnFamily(columnFamily); } }); } @Override public OperationResult<Void> truncateColumnFamily(final String columnFamily) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<Void>() { @Override public OperationResult<Void> exec(Keyspace ks) throws ConnectionException { return ks.truncateColumnFamily(columnFamily); } }); } @Override public OperationResult<Void> testOperation(final Operation<?, ?> operation) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<Void>() { @Override public OperationResult<Void> exec(Keyspace ks) throws ConnectionException { return ks.testOperation(operation); } }); } @Override public OperationResult<Void> testOperation(final Operation<?, ?> operation, RetryPolicy retry) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<Void>() { @Override public OperationResult<Void> exec(Keyspace ks) throws ConnectionException { return ks.testOperation(operation); } }); } @Override public <K, C> OperationResult<SchemaChangeResult> createColumnFamily(final ColumnFamily<K, C> columnFamily, final Map<String, Object> options) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.createColumnFamily(columnFamily, options); } }); } @Override public OperationResult<SchemaChangeResult> createColumnFamily(final Properties props) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.createColumnFamily(props); } }); } @Override public OperationResult<SchemaChangeResult> createColumnFamily(final Map<String, Object> options) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.createColumnFamily(options); } }); } @Override public <K, C> OperationResult<SchemaChangeResult> updateColumnFamily(final ColumnFamily<K, C> columnFamily, final Map<String, Object> options) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.updateColumnFamily(columnFamily, options); } }); } @Override public OperationResult<SchemaChangeResult> updateColumnFamily(final Properties props) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.updateColumnFamily(props); } }); } @Override public OperationResult<SchemaChangeResult> updateColumnFamily(final Map<String, Object> options) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.updateColumnFamily(options); } }); } @Override public OperationResult<SchemaChangeResult> dropColumnFamily(final String columnFamilyName) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.dropColumnFamily(columnFamilyName); } }); } @Override public <K, C> OperationResult<SchemaChangeResult> dropColumnFamily(final ColumnFamily<K, C> columnFamily) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.dropColumnFamily(columnFamily); } }); } @Override public OperationResult<SchemaChangeResult> createKeyspace(final Map<String, Object> options) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.createKeyspace(options); } }); } @Override public OperationResult<SchemaChangeResult> createKeyspaceIfNotExists(final Map<String, Object> options) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.createKeyspaceIfNotExists(options); } }); } @Override public OperationResult<SchemaChangeResult> createKeyspace(final Properties properties) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.createKeyspace(properties); } }); } @Override public OperationResult<SchemaChangeResult> createKeyspaceIfNotExists(final Properties properties) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.createKeyspaceIfNotExists(properties); } }); } @Override public OperationResult<SchemaChangeResult> createKeyspace(final Map<String, Object> options, final Map<ColumnFamily, Map<String, Object>> cfs) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.createKeyspace(options, cfs); } }); } @Override public OperationResult<SchemaChangeResult> createKeyspaceIfNotExists(final Map<String, Object> options, final Map<ColumnFamily, Map<String, Object>> cfs) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.createKeyspaceIfNotExists(options, cfs); } }); } @Override public OperationResult<SchemaChangeResult> updateKeyspace(final Map<String, Object> options) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.updateKeyspace(options); } }); } @Override public OperationResult<SchemaChangeResult> updateKeyspace(final Properties props) throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.updateKeyspace(props); } }); } @Override public OperationResult<SchemaChangeResult> dropKeyspace() throws ConnectionException { return execDualKeyspaceOperation(new KeyspaceOperation<SchemaChangeResult>() { @Override public OperationResult<SchemaChangeResult> exec(Keyspace ks) throws ConnectionException { return ks.dropKeyspace(); } }); } @Override public Map<String, List<String>> describeSchemaVersions() throws ConnectionException { return getPrimaryKS().describeSchemaVersions(); } @Override public CqlStatement prepareCqlStatement() { KeyspacePair pair = ksPair.get(); CqlStatement primaryStmt = pair.getPrimaryKS().prepareCqlStatement(); CqlStatement secondaryStmt = pair.getSecondaryKS().prepareCqlStatement(); return new DualWritesCqlStatement(primaryStmt, secondaryStmt, executionStrategy, pair.getDualKSMetadata()); } @Override public ConnectionPool<?> getConnectionPool() throws ConnectionException { return getPrimaryKS().getConnectionPool(); } private class KeyspacePair { private final DualKeyspaceMetadata dualKeyspaceMetadata; private final Keyspace ksPrimary; private final Keyspace ksSecondary; private KeyspacePair(final DualKeyspaceMetadata dualKeyspaceSetup, final Keyspace pKS, final Keyspace sKS) { dualKeyspaceMetadata = dualKeyspaceSetup; ksPrimary = pKS; ksSecondary = sKS; } private Keyspace getPrimaryKS() { return ksPrimary; } private Keyspace getSecondaryKS() { return ksSecondary; } private DualKeyspaceMetadata getDualKSMetadata() { return dualKeyspaceMetadata; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((ksPrimary == null) ? 0 : ksPrimary.hashCode()); result = prime * result + ((ksSecondary == null) ? 0 : ksSecondary.hashCode()); result = prime * result + ((dualKeyspaceMetadata == null) ? 0 : dualKeyspaceMetadata.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; KeyspacePair other = (KeyspacePair) obj; boolean equals = true; equals &= (ksPrimary == null) ? (other.ksPrimary == null) : (ksPrimary.equals(other.ksPrimary)); equals &= (ksSecondary == null) ? (other.ksSecondary == null) : (ksSecondary.equals(other.ksSecondary)); equals &= (dualKeyspaceMetadata == null) ? (other.dualKeyspaceMetadata == null) : (dualKeyspaceMetadata.equals(other.dualKeyspaceMetadata)); return equals; } } @Override public void dualWritesEnabled() { Logger.info("ENABLING dual writes for dual keyspace setup: " + ksPair.get().getDualKSMetadata()); dualWritesEnabled.set(true); } @Override public void dualWritesDisabled() { Logger.info("DISABLING dual writes for dual keyspace setup: " + ksPair.get().getDualKSMetadata()); dualWritesEnabled.set(false); } @Override public void flipPrimaryAndSecondary() { // Check that the expected state is actually reverse of what the destination state should be KeyspacePair currentPair = ksPair.get(); DualKeyspaceMetadata currentKeyspaceSetup = currentPair.getDualKSMetadata(); DualKeyspaceMetadata newDualKeyspaceSetup = new DualKeyspaceMetadata(currentKeyspaceSetup.getSecondaryCluster(), currentKeyspaceSetup.getSecondaryKeyspaceName(), currentKeyspaceSetup.getPrimaryCluster(), currentKeyspaceSetup.getPrimaryKeyspaceName()); KeyspacePair newPair = new KeyspacePair(newDualKeyspaceSetup, currentPair.getSecondaryKS(), currentPair.getPrimaryKS()); boolean success = ksPair.compareAndSet(currentPair, newPair); if (success) { Logger.info("Successfully flipped to new dual keyspace setup" + ksPair.get().getDualKSMetadata()); } else { Logger.info("Could not flip keyspace pair: " + currentPair + " to new pair: " + newPair); } } private abstract class SimpleSyncExec<R> implements Execution<R> { @Override public ListenableFuture<OperationResult<R>> executeAsync() throws ConnectionException { throw new RuntimeException("executeAsync not implemented for SimpleSyncExec"); } } private interface KeyspaceOperation<R> { OperationResult<R> exec(Keyspace ks) throws ConnectionException; } private <R> OperationResult<R> execDualKeyspaceOperation(final KeyspaceOperation<R> ksOperation) throws ConnectionException { final KeyspacePair pair = ksPair.get(); final Execution<R> exec1 = new SimpleSyncExec<R>() { @Override public OperationResult<R> execute() throws ConnectionException { return ksOperation.exec(pair.getPrimaryKS()); } }; final Execution<R> exec2 = new SimpleSyncExec<R>() { @Override public OperationResult<R> execute() throws ConnectionException { return ksOperation.exec(pair.getSecondaryKS()); } }; WriteMetadata writeMd = new WriteMetadata(pair.getDualKSMetadata(), null, null); return executionStrategy.wrapExecutions(exec1, exec2, Collections.singletonList(writeMd)).execute(); } }